From 25b4edfc1d226c156073a963561132f808a9d96d Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Mon, 8 Jul 2013 23:39:16 +0200 Subject: [PATCH] [gen] Moved Appy fields into appy/fields together with their PX. --- fields/__init__.py | 875 ++++++++++++ fields/action.py | 114 ++ fields/boolean.py | 99 ++ {gen => fields}/calendar.py | 247 +++- fields/computed.py | 118 ++ fields/date.py | 251 ++++ fields/file.py | 226 ++++ fields/float.py | 105 ++ fields/info.py | 39 + fields/integer.py | 80 ++ fields/list.py | 166 +++ {gen => fields}/ogone.py | 34 +- fields/pod.py | 226 ++++ fields/ref.py | 672 ++++++++++ fields/string.py | 506 +++++++ gen/__init__.py | 2496 +---------------------------------- gen/descriptors.py | 4 +- gen/generator.py | 4 +- gen/installer.py | 2 +- gen/mixins/ToolMixin.py | 7 +- gen/ui/widgets/calendar.pt | 2 +- gen/wrappers/__init__.py | 6 +- pod/buffers.py | 10 +- px/px_parser.py | 9 +- 24 files changed, 3795 insertions(+), 2503 deletions(-) create mode 100644 fields/__init__.py create mode 100644 fields/action.py create mode 100644 fields/boolean.py rename {gen => fields}/calendar.py (64%) create mode 100644 fields/computed.py create mode 100644 fields/date.py create mode 100644 fields/file.py create mode 100644 fields/float.py create mode 100644 fields/info.py create mode 100644 fields/integer.py create mode 100644 fields/list.py rename {gen => fields}/ogone.py (85%) create mode 100644 fields/pod.py create mode 100644 fields/ref.py create mode 100644 fields/string.py diff --git a/fields/__init__.py b/fields/__init__.py new file mode 100644 index 0000000..45b496e --- /dev/null +++ b/fields/__init__.py @@ -0,0 +1,875 @@ +# ------------------------------------------------------------------------------ +# This file is part of Appy, a framework for building applications in the Python +# language. Copyright (C) 2007 Gaetan Delannay + +# Appy is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. + +# Appy is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License along with +# Appy. If not, see . + +# ------------------------------------------------------------------------------ +import copy, types, re +from appy.gen.layout import Table, defaultFieldLayouts +from appy.gen import utils as gutils +from appy.shared import utils as sutils + +# ------------------------------------------------------------------------------ +nullValues = (None, '', []) +validatorTypes = (types.FunctionType, types.UnboundMethodType, + type(re.compile(''))) +labelTypes = ('label', 'descr', 'help') + +def initMasterValue(v): + '''Standardizes p_v as a list of strings.''' + if not isinstance(v, bool) and not v: res = [] + elif type(v) not in sutils.sequenceTypes: res = [v] + else: res = v + return [str(v) for v in res] + +class No: + '''When you write a workflow condition method and you want to return False + but you want to give to the user some explanations about why a transition + can't be triggered, do not return False, return an instance of No + instead. When creating such an instance, you can specify an error + message.''' + def __init__(self, msg): + self.msg = msg + def __nonzero__(self): + return False + +# ------------------------------------------------------------------------------ +# Page. Every field lives into a Page. +# ------------------------------------------------------------------------------ +class Page: + '''Used for describing a page, its related phase, show condition, etc.''' + subElements = ('save', 'cancel', 'previous', 'next', 'edit') + def __init__(self, name, phase='main', show=True, showSave=True, + showCancel=True, showPrevious=True, showNext=True, + showEdit=True): + self.name = name + self.phase = phase + self.show = show + # When editing the page, must I show the "save" button? + self.showSave = showSave + # When editing the page, must I show the "cancel" button? + self.showCancel = showCancel + # When editing the page, and when a previous page exists, must I show + # the "previous" button? + self.showPrevious = showPrevious + # When editing the page, and when a next page exists, must I show the + # "next" button? + self.showNext = showNext + # When viewing the page, must I show the "edit" button? + self.showEdit = showEdit + + @staticmethod + def get(pageData): + '''Produces a Page instance from p_pageData. User-defined p_pageData + can be: + (a) a string containing the name of the page; + (b) a string containing _; + (c) a Page instance. + This method returns always a Page instance.''' + res = pageData + if res and isinstance(res, basestring): + # Page data is given as a string. + pageElems = pageData.rsplit('_', 1) + if len(pageElems) == 1: # We have case (a) + res = Page(pageData) + else: # We have case (b) + res = Page(pageData[0], phase=pageData[1]) + return res + + def isShowable(self, obj, layoutType, elem='page'): + '''Must this page be shown for p_obj? "Show value" can be True, False + or 'view' (page is available only in "view" mode). + + If p_elem is not "page", this method returns the fact that a + sub-element is viewable or not (buttons "save", "cancel", etc).''' + # Define what attribute to test for "showability". + showAttr = 'show' + if elem != 'page': + showAttr = 'show%s' % elem.capitalize() + # Get the value of the show attribute as identified above. + show = getattr(self, showAttr) + if callable(show): + show = show(obj.appy()) + # Show value can be 'view', for example. Thanks to p_layoutType, + # convert show value to a real final boolean value. + res = show + if res == 'view': res = layoutType == 'view' + return res + + def getInfo(self, obj, layoutType): + '''Gets information about this page, for p_obj, as a dict.''' + res = {} + for elem in Page.subElements: + res['show%s' % elem.capitalize()] = self.isShowable(obj, layoutType, + elem=elem) + return res + +# ------------------------------------------------------------------------------ +# Group. Fields can be grouped. +# ------------------------------------------------------------------------------ +class Group: + '''Used for describing a group of widgets within a page.''' + def __init__(self, name, columns=['100%'], wide=True, style='section2', + hasLabel=True, hasDescr=False, hasHelp=False, + hasHeaders=False, group=None, colspan=1, align='center', + valign='top', css_class='', master=None, masterValue=None, + cellpadding=1, cellspacing=1, cellgap='0.6em', label=None, + translated=None): + self.name = name + # In its simpler form, field "columns" below can hold a list or tuple + # of column widths expressed as strings, that will be given as is in + # the "width" attributes of the corresponding "td" tags. Instead of + # strings, within this list or tuple, you may give Column instances + # (see below). + self.columns = columns + self._setColumns() + # If field "wide" below is True, the HTML table corresponding to this + # group will have width 100%. You can also specify some string value, + # which will be used for HTML param "width". + if wide == True: + self.wide = '100%' + elif isinstance(wide, basestring): + self.wide = wide + else: + self.wide = '' + # If style = 'fieldset', all widgets within the group will be rendered + # within an HTML fieldset. If style is 'section1' or 'section2', widgets + # will be rendered after the group title. + self.style = style + # If hasLabel is True, the group will have a name and the corresponding + # i18n label will be generated. + self.hasLabel = hasLabel + # If hasDescr is True, the group will have a description and the + # corresponding i18n label will be generated. + self.hasDescr = hasDescr + # If hasHelp is True, the group will have a help text associated and the + # corresponding i18n label will be generated. + self.hasHelp = hasHelp + # If hasheaders is True, group content will begin with a row of headers, + # and a i18n label will be generated for every header. + self.hasHeaders = hasHeaders + self.nbOfHeaders = len(columns) + # If this group is himself contained in another group, the following + # attribute is filled. + self.group = Group.get(group) + # If the group is rendered into another group, we can specify the number + # of columns that this group will span. + self.colspan = colspan + self.align = align + self.valign = valign + self.cellpadding = cellpadding + self.cellspacing = cellspacing + # Beyond standard cellpadding and cellspacing, cellgap can define an + # additional horizontal gap between cells in a row. So this value does + # not add space before the first cell or after the last one. + self.cellgap = cellgap + if style == 'tabs': + # Group content will be rendered as tabs. In this case, some + # param combinations have no sense. + self.hasLabel = self.hasDescr = self.hasHelp = False + # The rendering is forced to a single column + self.columns = self.columns[:1] + # Header labels will be used as labels for the tabs. + self.hasHeaders = True + self.css_class = css_class + self.master = master + self.masterValue = initMasterValue(masterValue) + if master: master.slaves.append(self) + self.label = label # See similar attr of Type class. + # If a translated name is already given here, we will use it instead of + # trying to translate the group label. + self.translated = translated + + def _setColumns(self): + '''Standardizes field "columns" as a list of Column instances. Indeed, + the initial value for field "columns" may be a list or tuple of + Column instances or strings.''' + for i in range(len(self.columns)): + columnData = self.columns[i] + if not isinstance(columnData, Column): + self.columns[i] = Column(self.columns[i]) + + @staticmethod + def get(groupData): + '''Produces a Group instance from p_groupData. User-defined p_groupData + can be a string or a Group instance; this method returns always a + Group instance.''' + res = groupData + if res and isinstance(res, basestring): + # Group data is given as a string. 2 more possibilities: + # (a) groupData is simply the name of the group; + # (b) groupData is of the form _. + groupElems = groupData.rsplit('_', 1) + if len(groupElems) == 1: + res = Group(groupElems[0]) + else: + try: + nbOfColumns = int(groupElems[1]) + except ValueError: + nbOfColumns = 1 + width = 100.0 / nbOfColumns + res = Group(groupElems[0], ['%.2f%%' % width] * nbOfColumns) + return res + + def getMasterData(self): + '''Gets the master of this group (and masterValue) or, recursively, of + containing groups when relevant.''' + if self.master: return (self.master, self.masterValue) + if self.group: return self.group.getMasterData() + + def generateLabels(self, messages, classDescr, walkedGroups, + forSearch=False): + '''This method allows to generate all the needed i18n labels related to + this group. p_messages is the list of i18n p_messages (a PoMessages + instance) that we are currently building; p_classDescr is the + descriptor of the class where this group is defined. If p_forSearch + is True, this group is used for grouping searches, and not fields.''' + # A part of the group label depends on p_forSearch. + if forSearch: gp = 'searchgroup' + else: gp = 'group' + if self.hasLabel: + msgId = '%s_%s_%s' % (classDescr.name, gp, self.name) + messages.append(msgId, self.name) + if self.hasDescr: + msgId = '%s_%s_%s_descr' % (classDescr.name, gp, self.name) + messages.append(msgId, ' ', nice=False) + if self.hasHelp: + msgId = '%s_%s_%s_help' % (classDescr.name, gp, self.name) + messages.append(msgId, ' ', nice=False) + if self.hasHeaders: + for i in range(self.nbOfHeaders): + msgId = '%s_%s_%s_col%d' % (classDescr.name, gp, self.name, i+1) + messages.append(msgId, ' ', nice=False) + walkedGroups.add(self) + if self.group and (self.group not in walkedGroups) and \ + not self.group.label: + # We remember walked groups for avoiding infinite recursion. + self.group.generateLabels(messages, classDescr, walkedGroups, + forSearch=forSearch) + + def insertInto(self, widgets, groupDescrs, page, metaType, forSearch=False): + '''Inserts the GroupDescr instance corresponding to this Group instance + into p_widgets, the recursive structure used for displaying all + widgets in a given p_page (or all searches), and returns this + GroupDescr instance.''' + # First, create the corresponding GroupDescr if not already in + # p_groupDescrs. + if self.name not in groupDescrs: + groupDescr = groupDescrs[self.name] = gutils.GroupDescr(\ + self, page, metaType, forSearch=forSearch).get() + # Insert the group at the higher level (ie, directly in p_widgets) + # if the group is not itself in a group. + if not self.group: + widgets.append(groupDescr) + else: + outerGroupDescr = self.group.insertInto(widgets, groupDescrs, + page, metaType, forSearch=forSearch) + gutils.GroupDescr.addWidget(outerGroupDescr, groupDescr) + else: + groupDescr = groupDescrs[self.name] + return groupDescr + +class Column: + '''Used for describing a column within a Group like defined above.''' + def __init__(self, width, align="left"): + self.width = width + self.align = align + +# ------------------------------------------------------------------------------ +# Abstract base class for every field. +# ------------------------------------------------------------------------------ +class Field: + '''Basic abstract class for defining any field.''' + # Those attributes can be overridden by subclasses for defining, + # respectively, names of CSS and Javascript files that are required by this + # field, keyed by layoutType. + cssFiles = {} + jsFiles = {} + dLayouts = 'lrv-d-f' + + def __init__(self, validator, multiplicity, default, show, page, group, + layouts, move, indexed, searchable, specificReadPermission, + specificWritePermission, width, height, maxChars, colspan, + master, masterValue, focus, historized, sync, mapping, label, + sdefault, scolspan, swidth, sheight): + # The validator restricts which values may be defined. It can be an + # interval (1,None), a list of string values ['choice1', 'choice2'], + # a regular expression, a custom function, a Selection instance, etc. + self.validator = validator + # Multiplicity is a 2-tuple indicating the minimum and maximum + # occurrences of values. + self.multiplicity = multiplicity + # Is the field required or not ? (derived from multiplicity) + self.required = self.multiplicity[0] > 0 + # Default value + self.default = default + # Must the field be visible or not? + self.show = show + # When displaying/editing the whole object, on what page and phase must + # this field value appear? + self.page = Page.get(page) + self.pageName = self.page.name + # Within self.page, in what group of fields must this one appear? + self.group = Group.get(group) + # The following attribute allows to move a field back to a previous + # position (useful for moving fields above predefined ones). + self.move = move + # If indexed is True, a database index will be set on the field for + # fast access. + self.indexed = indexed + # If specified "searchable", the field will be added to some global + # index allowing to perform application-wide, keyword searches. + self.searchable = searchable + # Normally, permissions to read or write every attribute in a type are + # granted if the user has the global permission to read or + # edit instances of the whole type. If you want a given attribute + # to be protected by specific permissions, set one or the 2 next boolean + # values to "True". In this case, you will create a new "field-only" + # read and/or write permission. If you need to protect several fields + # with the same read/write permission, you can avoid defining one + # specific permission for every field by specifying a "named" + # permission (string) instead of assigning "True" to the following + # arg(s). A named permission will be global to your whole Zope site, so + # take care to the naming convention. Typically, a named permission is + # of the form: ": Write|Read ---". If, for example, I want + # to define, for my application "MedicalFolder" a specific permission + # for a bunch of fields that can only be modified by a doctor, I can + # define a permission "MedicalFolder: Write medical information" and + # assign it to the "specificWritePermission" of every impacted field. + self.specificReadPermission = specificReadPermission + self.specificWritePermission = specificWritePermission + # Widget width and height + self.width = width + self.height = height + # While width and height refer to widget dimensions, maxChars hereafter + # represents the maximum number of chars that a given input field may + # accept (corresponds to HTML "maxlength" property). "None" means + # "unlimited". + self.maxChars = maxChars + # If the widget is in a group with multiple columns, the following + # attribute specifies on how many columns to span the widget. + self.colspan = colspan + # The list of slaves of this field, if it is a master + self.slaves = [] + # The behaviour of this field may depend on another, "master" field + self.master = master + if master: self.master.slaves.append(self) + # When master has some value(s), there is impact on this field. + self.masterValue = initMasterValue(masterValue) + # If a field must retain attention in a particular way, set focus=True. + # It will be rendered in a special way. + self.focus = focus + # If we must keep track of changes performed on a field, "historized" + # must be set to True. + self.historized = historized + # self.sync below determines if the field representations will be + # retrieved in a synchronous way by the browser or not (Ajax). + self.sync = self.formatSync(sync) + # Mapping is a dict of contexts that, if specified, are given when + # translating the label, descr or help related to this field. + self.mapping = self.formatMapping(mapping) + self.id = id(self) + self.type = self.__class__.__name__ + self.pythonType = None # The True corresponding Python type + # Get the layouts. Consult layout.py for more info about layouts. + self.layouts = self.formatLayouts(layouts) + # Can we filter this field? + self.filterable = False + # Can this field have values that can be edited and validated? + self.validable = True + # The base label for translations is normally generated automatically. + # It is made of 2 parts: the prefix, based on class name, and the name, + # which is the field name by default. You can change this by specifying + # a value for param "label". If this value is a string, it will be + # understood as a new prefix. If it is a tuple, it will represent the + # prefix and another name. If you want to specify a new name only, and + # not a prefix, write (None, newName). + self.label = label + # When you specify a default value "for search" (= "sdefault"), on a + # search screen, in the search field corresponding to this field, this + # default value will be present. + self.sdefault = sdefault + # Colspan for rendering the search widget corresponding to this field. + self.scolspan = scolspan + # Width and height for the search widget + self.swidth = swidth or width + self.sheight = sheight or height + + def init(self, name, klass, appName): + '''When the application server starts, this secondary constructor is + called for storing the name of the Appy field (p_name) and other + attributes that are based on the name of the Appy p_klass, and the + application name (p_appName).''' + if hasattr(self, 'name'): return # Already initialized + self.name = name + # Determine prefix for this class + if not klass: prefix = appName + else: prefix = gutils.getClassName(klass, appName) + # Recompute the ID (and derived attributes) that may have changed if + # we are in debug mode (because we recreate new Field instances). + self.id = id(self) + # Remember master name on every slave + for slave in self.slaves: slave.masterName = name + # Determine ids of i18n labels for this field + labelName = name + trPrefix = None + if self.label: + if isinstance(self.label, basestring): trPrefix = self.label + else: # It is a tuple (trPrefix, name) + if self.label[1]: labelName = self.label[1] + if self.label[0]: trPrefix = self.label[0] + if not trPrefix: + trPrefix = prefix + # Determine name to use for i18n + self.labelId = '%s_%s' % (trPrefix, labelName) + self.descrId = self.labelId + '_descr' + self.helpId = self.labelId + '_help' + # Determine read and write permissions for this field + rp = self.specificReadPermission + if rp and not isinstance(rp, basestring): + self.readPermission = '%s: Read %s %s' % (appName, prefix, name) + elif rp and isinstance(rp, basestring): + self.readPermission = rp + else: + self.readPermission = 'View' + wp = self.specificWritePermission + if wp and not isinstance(wp, basestring): + self.writePermission = '%s: Write %s %s' % (appName, prefix, name) + elif wp and isinstance(wp, basestring): + self.writePermission = wp + else: + self.writePermission = 'Modify portal content' + if (self.type == 'Ref') and not self.isBack: + # We must initialise the corresponding back reference + self.back.klass = klass + self.back.init(self.back.attribute, self.klass, appName) + if self.type == "List": + for subName, subField in self.fields: + fullName = '%s_%s' % (name, subName) + subField.init(fullName, klass, appName) + subField.name = '%s*%s' % (name, subName) + + def reload(self, klass, obj): + '''In debug mode, we want to reload layouts without restarting Zope. + So this method will prepare a "new", reloaded version of p_self, + that corresponds to p_self after a "reload" of its containing Python + module has been performed.''' + res = getattr(klass, self.name, None) + if not res: return self + if (self.type == 'Ref') and self.isBack: return self + res.init(self.name, klass, obj.getProductConfig().PROJECTNAME) + return res + + def isMultiValued(self): + '''Does this type definition allow to define multiple values?''' + res = False + maxOccurs = self.multiplicity[1] + if (maxOccurs == None) or (maxOccurs > 1): + res = True + return res + + def isSortable(self, usage): + '''Can fields of this type be used for sorting purposes (when sorting + search results (p_usage="search") or when sorting reference fields + (p_usage="ref")?''' + if usage == 'search': + return self.indexed and not self.isMultiValued() and not \ + ((self.type == 'String') and self.isSelection()) + elif usage == 'ref': + return self.type in ('Integer', 'Float', 'Boolean', 'Date') or \ + ((self.type == 'String') and (self.format == 0)) + + def isShowable(self, obj, layoutType): + '''When displaying p_obj on a given p_layoutType, must we show this + field?''' + # Check if the user has the permission to view or edit the field + if layoutType == 'edit': perm = self.writePermission + else: perm = self.readPermission + if not obj.allows(perm): return False + # Evaluate self.show + if callable(self.show): + res = self.callMethod(obj, self.show) + else: + res = self.show + # Take into account possible values 'view', 'edit', 'result'... + if type(res) in sutils.sequenceTypes: + for r in res: + if r == layoutType: return True + return False + elif res in ('view', 'edit', 'result'): + return res == layoutType + return bool(res) + + def isClientVisible(self, obj): + '''This method returns True if this field is visible according to + master/slave relationships.''' + masterData = self.getMasterData() + if not masterData: return True + else: + master, masterValue = masterData + reqValue = master.getRequestValue(obj.REQUEST) + # reqValue can be a list or not + if type(reqValue) not in sutils.sequenceTypes: + return reqValue in masterValue + else: + for m in masterValue: + for r in reqValue: + if m == r: return True + + def formatSync(self, sync): + '''Creates a dictionary indicating, for every layout type, if the field + value must be retrieved synchronously or not.''' + if isinstance(sync, bool): + sync = {'edit': sync, 'view': sync, 'cell': sync} + for layoutType in ('edit', 'view', 'cell'): + if layoutType not in sync: + sync[layoutType] = False + return sync + + def formatMapping(self, mapping): + '''Creates a dict of mappings, one entry by label type (label, descr, + help).''' + if isinstance(mapping, dict): + # Is it a dict like {'label':..., 'descr':...}, or is it directly a + # dict with a mapping? + for k, v in mapping.iteritems(): + if (k not in labelTypes) or isinstance(v, basestring): + # It is already a mapping + return {'label':mapping, 'descr':mapping, 'help':mapping} + # If we are here, we have {'label':..., 'descr':...}. Complete + # it if necessary. + for labelType in labelTypes: + if labelType not in mapping: + mapping[labelType] = None # No mapping for this value. + return mapping + else: + # Mapping is a method that must be applied to any i18n message. + return {'label':mapping, 'descr':mapping, 'help':mapping} + + def formatLayouts(self, layouts): + '''Standardizes the given p_layouts. .''' + # First, get the layouts as a dictionary, if p_layouts is None or + # expressed as a simple string. + areDefault = False + if not layouts: + # Get the default layouts as defined by the subclass + areDefault = True + layouts = self.computeDefaultLayouts() + else: + if isinstance(layouts, basestring): + # The user specified a single layoutString (the "edit" one) + layouts = {'edit': layouts} + elif isinstance(layouts, Table): + # Idem, but with a Table instance + layouts = {'edit': Table(other=layouts)} + else: + # Here, we make a copy of the layouts, because every layout can + # be different, even if the user decides to reuse one from one + # field to another. This is because we modify every layout for + # adding master/slave-related info, focus-related info, etc, + # which can be different from one field to the other. + layouts = copy.deepcopy(layouts) + if 'edit' not in layouts: + defEditLayout = self.computeDefaultLayouts() + if type(defEditLayout) == dict: + defEditLayout = defEditLayout['edit'] + layouts['edit'] = defEditLayout + # We have now a dict of layouts in p_layouts. Ensure now that a Table + # instance is created for every layout (=value from the dict). Indeed, + # a layout could have been expressed as a simple layout string. + for layoutType in layouts.iterkeys(): + if isinstance(layouts[layoutType], basestring): + layouts[layoutType] = Table(layouts[layoutType]) + # Derive "view" and "cell" layouts from the "edit" layout when relevant + if 'view' not in layouts: + layouts['view'] = Table(other=layouts['edit'], derivedType='view') + # Create the "cell" layout from the 'view' layout if not specified. + if 'cell' not in layouts: + layouts['cell'] = Table(other=layouts['view'], derivedType='cell') + # Put the required CSS classes in the layouts + layouts['cell'].addCssClasses('noStyle') + if self.focus: + # We need to make it flashy + layouts['view'].addCssClasses('focus') + layouts['edit'].addCssClasses('focus') + # If layouts are the default ones, set width=None instead of width=100% + # for the field if it is not in a group (excepted for rich texts). + if areDefault and not self.group and \ + not ((self.type == 'String') and (self.format == self.XHTML)): + for layoutType in layouts.iterkeys(): + layouts[layoutType].width = '' + # Remove letters "r" from the layouts if the field is not required. + if not self.required: + for layoutType in layouts.iterkeys(): + layouts[layoutType].removeElement('r') + # Derive some boolean values from the layouts. + self.hasLabel = self.hasLayoutElement('l', layouts) + self.hasDescr = self.hasLayoutElement('d', layouts) + self.hasHelp = self.hasLayoutElement('h', layouts) + # Store Table instance's dicts instead of instances: this way, they can + # be manipulated in ZPTs. + for layoutType in layouts.iterkeys(): + layouts[layoutType] = layouts[layoutType].get() + return layouts + + def hasLayoutElement(self, element, layouts): + '''This method returns True if the given layout p_element can be found + at least once among the various p_layouts defined for this field.''' + for layout in layouts.itervalues(): + if element in layout.layoutString: return True + return False + + def getDefaultLayouts(self): + '''Any subclass can define this for getting a specific set of + default layouts. If None is returned, a global set of default layouts + will be used.''' + + def getInputLayouts(self): + '''Gets, as a string, the layouts as could have been specified as input + value for the Field constructor.''' + res = '{' + for k, v in self.layouts.iteritems(): + res += '"%s":"%s",' % (k, v['layoutString']) + res += '}' + return res + + def computeDefaultLayouts(self): + '''This method gets the default layouts from an Appy type, or a copy + from the global default field layouts when they are not available.''' + res = self.getDefaultLayouts() + if not res: + # Get the global default layouts + res = copy.deepcopy(defaultFieldLayouts) + return res + + def getCss(self, layoutType, res): + '''This method completes the list p_res with the names of CSS files + that are required for displaying widgets of self's type on a given + p_layoutType. p_res is not a set because order of inclusion of CSS + files may be important and may be loosed by using sets.''' + if layoutType in self.cssFiles: + for fileName in self.cssFiles[layoutType]: + if fileName not in res: + res.append(fileName) + + def getJs(self, layoutType, res): + '''This method completes the list p_res with the names of Javascript + files that are required for displaying widgets of self's type on a + given p_layoutType. p_res is not a set because order of inclusion of + CSS files may be important and may be loosed by using sets.''' + if layoutType in self.jsFiles: + for fileName in self.jsFiles[layoutType]: + if fileName not in res: + res.append(fileName) + + def getValue(self, obj): + '''Gets, on_obj, the value conforming to self's type definition.''' + value = getattr(obj.aq_base, self.name, None) + if self.isEmptyValue(value): + # If there is no value, get the default value if any: return + # self.default, of self.default() if it is a method. + if callable(self.default): + try: + return self.callMethod(obj, self.default) + except Exception, e: + # Already logged. Here I do not raise the exception, + # because it can be raised as the result of reindexing + # the object in situations that are not foreseen by + # method in self.default. + return None + else: + return self.default + return value + + def getFormattedValue(self, obj, value, showChanges=False): + '''p_value is a real p_obj(ect) value from a field from this type. This + method returns a pretty, string-formatted version, for displaying + purposes. Needs to be overridden by some child classes. If + p_showChanges is True, the result must also include the changes that + occurred on p_value across the ages.''' + if self.isEmptyValue(value): return '' + return value + + def getIndexType(self): + '''Returns the name of the technical, Zope-level index type for this + field.''' + # Normally, self.indexed contains a Boolean. If a string value is given, + # we consider it to be an index type. It allows to bypass the standard + # way to decide what index type must be used. + if isinstance(self.indexed, str): return self.indexed + if self.name == 'title': return 'TextIndex' + return 'FieldIndex' + + def getIndexValue(self, obj, forSearch=False): + '''This method returns a version for this field value on p_obj that is + ready for indexing purposes. Needs to be overridden by some child + classes. + + If p_forSearch is True, it will return a "string" version of the + index value suitable for a global search.''' + value = self.getValue(obj) + if forSearch and (value != None): + if isinstance(value, unicode): + res = value.encode('utf-8') + elif type(value) in sutils.sequenceTypes: + res = [] + for v in value: + if isinstance(v, unicode): res.append(v.encode('utf-8')) + else: res.append(str(v)) + res = ' '.join(res) + else: + res = str(value) + return res + return value + + def getRequestValue(self, request, requestName=None): + '''Gets a value for this field as carried in the request object. In the + simplest cases, the request value is a single value whose name in the + request is the name of the field. + + Sometimes (ie: a Date: see the overriden method in the Date class), + several request values must be combined. + + Sometimes (ie, a field which is a sub-field in a List), the name of + the request value(s) representing the field value do not correspond + to the field name (ie: the request name includes information about + the container field). In this case, p_requestName must be used for + searching into the request, instead of the field name (self.name).''' + name = requestName or self.name + return request.get(name, None) + + def getStorableValue(self, value): + '''p_value is a valid value initially computed through calling + m_getRequestValue. So, it is a valid string (or list of strings) + representation of the field value coming from the request. + This method computes the real (potentially converted or manipulated + in some other way) value as can be stored in the database.''' + if self.isEmptyValue(value): return None + return value + + def getMasterData(self): + '''Gets the master of this field (and masterValue) or, recursively, of + containing groups when relevant.''' + if self.master: return (self.master, self.masterValue) + if self.group: return self.group.getMasterData() + + def isEmptyValue(self, value, obj=None): + '''Returns True if the p_value must be considered as an empty value.''' + return value in nullValues + + def validateValue(self, obj, value): + '''This method may be overridden by child classes and will be called at + the right moment by m_validate defined below for triggering + type-specific validation. p_value is never empty.''' + return None + + def securityCheck(self, obj, value): + '''This method performs some security checks on the p_value that + represents user input.''' + if not isinstance(value, basestring): return + # Search Javascript code in the value (prevent XSS attacks). + if '/onProcess.''' + return obj.goto(obj.absolute_url()) +# ------------------------------------------------------------------------------ diff --git a/fields/action.py b/fields/action.py new file mode 100644 index 0000000..f1385a2 --- /dev/null +++ b/fields/action.py @@ -0,0 +1,114 @@ +# ------------------------------------------------------------------------------ +# This file is part of Appy, a framework for building applications in the Python +# language. Copyright (C) 2007 Gaetan Delannay + +# Appy is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. + +# Appy is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License along with +# Appy. If not, see . + +# ------------------------------------------------------------------------------ +from appy.fields import Field +from appy.px import Px +from appy.shared import utils as sutils + +# ------------------------------------------------------------------------------ +class Action(Field): + '''An action is a Python method that can be triggered by the user on a + given gen-class. An action is rendered as a button.''' + + # PX for viewing the Action button. + pxView = pxCell = Px(''' +
+ + + + + + +
''') + + # It is not possible to edit an action, not to search it. + pxEdit = pxSearch = '' + + def __init__(self, validator=None, multiplicity=(1,1), default=None, + show=True, page='main', group=None, layouts=None, move=0, + indexed=False, searchable=False, specificReadPermission=False, + specificWritePermission=False, width=None, height=None, + maxChars=None, colspan=1, action=None, result='computation', + confirm=False, master=None, masterValue=None, focus=False, + historized=False, mapping=None, label=None): + # Can be a single method or a list/tuple of methods + self.action = action + # For the 'result' param: + # * value 'computation' means that the action will simply compute + # things and redirect the user to the same page, with some status + # message about execution of the action; + # * 'file' means that the result is the binary content of a file that + # the user will download. + # * 'filetmp' is similar to file, but the file is a temp file and Appy + # will delete it as soon as it will be served to the browser. + # * 'redirect' means that the action will lead to the user being + # redirected to some other page. + self.result = result + # If following field "confirm" is True, a popup will ask the user if + # she is really sure about triggering this action. + self.confirm = confirm + Field.__init__(self, None, (0,1), default, show, page, group, layouts, + move, indexed, False, specificReadPermission, + specificWritePermission, width, height, None, colspan, + master, masterValue, focus, historized, False, mapping, + label, None, None, None, None) + self.validable = False + + def getDefaultLayouts(self): return {'view': 'l-f', 'edit': 'lrv-f'} + + def __call__(self, obj): + '''Calls the action on p_obj.''' + if type(self.action) in sutils.sequenceTypes: + # There are multiple Python methods + res = [True, ''] + for act in self.action: + actRes = act(obj) + if type(actRes) in sutils.sequenceTypes: + res[0] = res[0] and actRes[0] + if self.result.startswith('file'): + res[1] = res[1] + actRes[1] + else: + res[1] = res[1] + '\n' + actRes[1] + else: + res[0] = res[0] and actRes + else: + # There is only one Python method + actRes = self.action(obj) + if type(actRes) in sutils.sequenceTypes: + res = list(actRes) + else: + res = [actRes, ''] + # If res is None (ie the user-defined action did not return anything), + # we consider the action as successfull. + if res[0] == None: res[0] = True + return res + + def isShowable(self, obj, layoutType): + if layoutType == 'edit': return False + else: return Field.isShowable(self, obj, layoutType) +# ------------------------------------------------------------------------------ diff --git a/fields/boolean.py b/fields/boolean.py new file mode 100644 index 0000000..fc20b12 --- /dev/null +++ b/fields/boolean.py @@ -0,0 +1,99 @@ +# ------------------------------------------------------------------------------ +# This file is part of Appy, a framework for building applications in the Python +# language. Copyright (C) 2007 Gaetan Delannay + +# Appy is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. + +# Appy is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License along with +# Appy. If not, see . + +# ------------------------------------------------------------------------------ +from appy.fields import Field +from appy.px import Px +from appy.gen.layout import Table + +# ------------------------------------------------------------------------------ +class Boolean(Field): + '''Field for storing boolean values.''' + + pxView = pxCell = Px(''' + :value + + ''') + + pxEdit = Px(''' + + + + ''') + + pxSearch = Px(''' + +
   + + + + + + + + + + + +
+
''') + + def __init__(self, validator=None, multiplicity=(0,1), default=None, + show=True, page='main', group=None, layouts = None, move=0, + indexed=False, searchable=False, specificReadPermission=False, + specificWritePermission=False, width=None, height=None, + maxChars=None, colspan=1, master=None, masterValue=None, + focus=False, historized=False, mapping=None, label=None, + sdefault=False, scolspan=1, swidth=None, sheight=None): + Field.__init__(self, validator, multiplicity, default, show, page, + group, layouts, move, indexed, searchable, + specificReadPermission, specificWritePermission, width, + height, None, colspan, master, masterValue, focus, + historized, True, mapping, label, sdefault, scolspan, + swidth, sheight) + self.pythonType = bool + + # Layout including a description + dLayouts = {'view': 'lf', 'edit': Table('flrv;=d', width=None)} + # Centered layout, no description + cLayouts = {'view': 'lf|', 'edit': 'flrv|'} + + def getDefaultLayouts(self): + return {'view': 'lf', 'edit': Table('f;lrv;=', width=None)} + + def getValue(self, obj): + '''Never returns "None". Returns always "True" or "False", even if + "None" is stored in the DB.''' + value = Field.getValue(self, obj) + if value == None: return False + return value + + def getFormattedValue(self, obj, value, showChanges=False): + if value: res = obj.translate('yes') + else: res = obj.translate('no') + return res + + def getStorableValue(self, value): + if not self.isEmptyValue(value): + exec 'res = %s' % value + return res +# ------------------------------------------------------------------------------ diff --git a/gen/calendar.py b/fields/calendar.py similarity index 64% rename from gen/calendar.py rename to fields/calendar.py index 7272a71..90f1273 100644 --- a/gen/calendar.py +++ b/fields/calendar.py @@ -1,17 +1,250 @@ # ------------------------------------------------------------------------------ import types from appy import Object -from appy.gen import Type +from appy.gen import Field +from appy.px import Px from DateTime import DateTime from BTrees.IOBTree import IOBTree from persistent.list import PersistentList # ------------------------------------------------------------------------------ -class Calendar(Type): +class Calendar(Field): '''This field allows to produce an agenda (monthly view) and view/edit events on it.''' jsFiles = {'view': ('widgets/calendar.js',)} + # Month view for a calendar. Called by pxView, and directly from the UI, + # via Ajax, when the user selects another month. + pxMonthView = Px(''' +
+ + + + +
+ + + + + + + + + :_('month_%s' % monthDayOne.aMonth()) + :month.split('/')[0] +
+ + + + + + + + + + + + + + + + + + + + +
:dayName
+ + :day + :_('month_%s_short' % date.aMonth())"> + + + + + + + + +
+ :contextObj.callField(fieldName, \ + 'getEventName', contextObj, eventType)"> +
+
+ + + +
:event['name']
+
+
+ + ::info +
+
+ + + + + + +
''') + + pxView = pxCell = Px(''' + + :widget['pxMonthView']> + + ''') + + pxEdit = pxSearch = '' + def __init__(self, eventTypes, eventNameMethod=None, validator=None, default=None, show='view', page='main', group=None, layouts=None, move=0, specificReadPermission=False, @@ -21,11 +254,11 @@ class Calendar(Type): otherCalendars=None, additionalInfo=None, startDate=None, endDate=None, defaultDate=None, preCompute=None, applicableEvents=None): - Type.__init__(self, validator, (0,1), default, show, page, group, - layouts, move, False, False, specificReadPermission, - specificWritePermission, width, height, None, colspan, - master, masterValue, focus, False, True, mapping, label, - None, None, None, None) + Field.__init__(self, validator, (0,1), default, show, page, group, + layouts, move, False, False, specificReadPermission, + specificWritePermission, width, height, None, colspan, + master, masterValue, focus, False, True, mapping, label, + None, None, None, None) # eventTypes can be a "static" list or tuple of strings that identify # the types of events that are supported by this calendar. It can also # be a method that computes such a "dynamic" list or tuple. When diff --git a/fields/computed.py b/fields/computed.py new file mode 100644 index 0000000..ae9c620 --- /dev/null +++ b/fields/computed.py @@ -0,0 +1,118 @@ +# ------------------------------------------------------------------------------ +# This file is part of Appy, a framework for building applications in the Python +# language. Copyright (C) 2007 Gaetan Delannay + +# Appy is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. + +# Appy is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License along with +# Appy. If not, see . + +# ------------------------------------------------------------------------------ +from appy.fields import Field +from appy.px import Px + +# ------------------------------------------------------------------------------ +class Computed(Field): + + # Ajax-called view content of a non sync Computed field. + pxViewContent = Px(''' + :widget['pxView']''') + + pxView = pxCell = pxEdit = Px(''' + + + :value + ::value> + + +
+ +
+
+
''') + + pxSearch = Px(''' + +
   + +
''') + + def __init__(self, validator=None, multiplicity=(0,1), default=None, + show='view', page='main', group=None, layouts=None, move=0, + indexed=False, searchable=False, specificReadPermission=False, + specificWritePermission=False, width=None, height=None, + maxChars=None, colspan=1, method=None, plainText=True, + master=None, masterValue=None, focus=False, historized=False, + sync=True, mapping=None, label=None, sdefault='', scolspan=1, + swidth=None, sheight=None, context={}): + # The Python method used for computing the field value + self.method = method + # Does field computation produce plain text or XHTML? + self.plainText = plainText + if isinstance(method, basestring): + # When field computation is done with a macro, we know the result + # will be HTML. + self.plainText = False + # The context is a dict (or method returning a dict) that will be given + # to the macro specified in self.method. If the dict contains key + # "someKey", it will be available to the macro as "options/someKey". + self.context = context + Field.__init__(self, None, multiplicity, default, show, page, group, + layouts, move, indexed, searchable, + specificReadPermission, specificWritePermission, width, + height, None, colspan, master, masterValue, focus, + historized, sync, mapping, label, sdefault, scolspan, + swidth, sheight) + self.validable = False + + def callMacro(self, obj, macroPath): + '''Returns the macro corresponding to p_macroPath. The base folder + where we search is "ui".''' + # Get the special page in Appy that allows to call a macro + macroPage = obj.ui.callMacro + # Get, from p_macroPath, the page where the macro lies, and the macro + # name. + names = self.method.split('/') + # Get the page where the macro lies + page = obj.ui + for name in names[:-1]: + page = getattr(page, name) + macroName = names[-1] + # Compute the macro context. + ctx = {'contextObj':obj, 'page':page, 'macroName':macroName} + if callable(self.context): + ctx.update(self.context(obj.appy())) + else: + ctx.update(self.context) + return macroPage(obj, **ctx) + + def getValue(self, obj): + '''Computes the value instead of getting it in the database.''' + if not self.method: return + if isinstance(self.method, basestring): + # self.method is a path to a macro that will produce the field value + return self.callMacro(obj, self.method) + else: + # self.method is a method that will return the field value + return self.callMethod(obj, self.method, cache=False) + + def getFormattedValue(self, obj, value, showChanges=False): + if not isinstance(value, basestring): return str(value) + return value +# ------------------------------------------------------------------------------ diff --git a/fields/date.py b/fields/date.py new file mode 100644 index 0000000..2a72735 --- /dev/null +++ b/fields/date.py @@ -0,0 +1,251 @@ +# ------------------------------------------------------------------------------ +# This file is part of Appy, a framework for building applications in the Python +# language. Copyright (C) 2007 Gaetan Delannay + +# Appy is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. + +# Appy is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License along with +# Appy. If not, see . + +# ------------------------------------------------------------------------------ +import time +from appy.fields import Field +from appy.px import Px + +# ------------------------------------------------------------------------------ +class Date(Field): + + pxView = pxCell = Px(''':value''') + pxEdit = Px(''' + + + + + + + + + + + + + + + + + + + + : + + + ''') + + pxSearch = Px(''' + + + + + + + + + + + + + + + + +
  + / + / + + + + + + + +
     + / + / + + + + + + + +
+
''') + + # Required CSS and Javascript files for this type. + cssFiles = {'edit': ('jscalendar/calendar-blue.css',)} + jsFiles = {'edit': ('jscalendar/calendar.js', + 'jscalendar/lang/calendar-en.js', + 'jscalendar/calendar-setup.js')} + # Possible values for "format" + WITH_HOUR = 0 + WITHOUT_HOUR = 1 + dateParts = ('year', 'month', 'day') + hourParts = ('hour', 'minute') + + def __init__(self, validator=None, multiplicity=(0,1), default=None, + format=WITH_HOUR, calendar=True, + startYear=time.localtime()[0]-10, + endYear=time.localtime()[0]+10, reverseYears=False, + show=True, page='main', group=None, layouts=None, move=0, + indexed=False, searchable=False, specificReadPermission=False, + specificWritePermission=False, width=None, height=None, + maxChars=None, colspan=1, master=None, masterValue=None, + focus=False, historized=False, mapping=None, label=None, + sdefault=None, scolspan=1, swidth=None, sheight=None): + self.format = format + self.calendar = calendar + self.startYear = startYear + self.endYear = endYear + # If reverseYears is True, in the selection box, available years, from + # self.startYear to self.endYear will be listed in reverse order. + self.reverseYears = reverseYears + Field.__init__(self, validator, multiplicity, default, show, page, + group, layouts, move, indexed, searchable, + specificReadPermission, specificWritePermission, width, + height, None, colspan, master, masterValue, focus, + historized, True, mapping, label, sdefault, scolspan, + swidth, sheight) + + def getCss(self, layoutType, res): + # CSS files are only required if the calendar must be shown. + if self.calendar: Field.getCss(self, layoutType, res) + + def getJs(self, layoutType, res): + # Javascript files are only required if the calendar must be shown. + if self.calendar: Field.getJs(self, layoutType, res) + + def getSelectableYears(self): + '''Gets the list of years one may select for this field.''' + res = range(self.startYear, self.endYear + 1) + if self.reverseYears: res.reverse() + return res + + def validateValue(self, obj, value): + DateTime = obj.getProductConfig().DateTime + try: + value = DateTime(value) + except DateTime.DateError, ValueError: + return obj.translate('bad_date') + + def getFormattedValue(self, obj, value, showChanges=False): + if self.isEmptyValue(value): return '' + tool = obj.getTool().appy() + # A problem may occur with some extreme year values. Replace the "year" + # part "by hand". + dateFormat = tool.dateFormat + if '%Y' in dateFormat: + dateFormat = dateFormat.replace('%Y', str(value.year())) + res = value.strftime(dateFormat) + if self.format == Date.WITH_HOUR: + res += ' %s' % value.strftime(tool.hourFormat) + return res + + def getRequestValue(self, request, requestName=None): + name = requestName or self.name + # Manage the "date" part + value = '' + for part in self.dateParts: + valuePart = request.get('%s_%s' % (name, part), None) + if not valuePart: return None + value += valuePart + '/' + value = value[:-1] + # Manage the "hour" part + if self.format == self.WITH_HOUR: + value += ' ' + for part in self.hourParts: + valuePart = request.get('%s_%s' % (name, part), None) + if not valuePart: return None + value += valuePart + ':' + value = value[:-1] + return value + + def getStorableValue(self, value): + if not self.isEmptyValue(value): + import DateTime + return DateTime.DateTime(value) + + def getIndexType(self): return 'DateIndex' +# ------------------------------------------------------------------------------ diff --git a/fields/file.py b/fields/file.py new file mode 100644 index 0000000..e0504d0 --- /dev/null +++ b/fields/file.py @@ -0,0 +1,226 @@ +# ------------------------------------------------------------------------------ +# This file is part of Appy, a framework for building applications in the Python +# language. Copyright (C) 2007 Gaetan Delannay + +# Appy is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. + +# Appy is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License along with +# Appy. If not, see . + +# ------------------------------------------------------------------------------ +import time, os.path, mimetypes +from appy.fields import Field +from appy.px import Px +from appy.shared import utils as sutils + +# ------------------------------------------------------------------------------ +class File(Field): + + pxView = pxCell = Px(''' + + + :info['filename']"  - + '%sKb' % (info['size'] / 1024)"> + + + - + ''') + + pxEdit = Px(''' + + + :widget['pxView']
+ + + +
+ + + +
+
+ + +
+
+ + + +
''') + + pxSearch = '' + + def __init__(self, validator=None, multiplicity=(0,1), default=None, + show=True, page='main', group=None, layouts=None, move=0, + indexed=False, searchable=False, specificReadPermission=False, + specificWritePermission=False, width=None, height=None, + maxChars=None, colspan=1, master=None, masterValue=None, + focus=False, historized=False, mapping=None, label=None, + isImage=False, sdefault='', scolspan=1, swidth=None, + sheight=None): + self.isImage = isImage + Field.__init__(self, validator, multiplicity, default, show, page, + group, layouts, move, indexed, False, + specificReadPermission, specificWritePermission, width, + height, None, colspan, master, masterValue, focus, + historized, True, mapping, label, sdefault, scolspan, + swidth, sheight) + + @staticmethod + def getFileObject(filePath, fileName=None, zope=False): + '''Returns a File instance as can be stored in the database or + manipulated in code, filled with content from a file on disk, + located at p_filePath. If you want to give it a name that is more + sexy than the actual basename of filePath, specify it in + p_fileName. + + If p_zope is True, it will be the raw Zope object = an instance of + OFS.Image.File. Else, it will be a FileWrapper instance from Appy.''' + f = file(filePath, 'rb') + if not fileName: + fileName = os.path.basename(filePath) + fileId = 'file.%f' % time.time() + import OFS.Image + res = OFS.Image.File(fileId, fileName, f) + res.filename = fileName + res.content_type = mimetypes.guess_type(fileName)[0] + f.close() + if not zope: res = sutils.FileWrapper(res) + return res + + def getValue(self, obj): + value = Field.getValue(self, obj) + if value: value = sutils.FileWrapper(value) + return value + + def getFormattedValue(self, obj, value, showChanges=False): + if not value: return value + return value._zopeFile + + def getRequestValue(self, request, requestName=None): + name = requestName or self.name + return request.get('%s_file' % name) + + def getDefaultLayouts(self): return {'view':'l-f','edit':'lrv-f'} + + def isEmptyValue(self, value, obj=None): + '''Must p_value be considered as empty?''' + if not obj: return Field.isEmptyValue(self, value) + if value: return False + # If "nochange", the value must not be considered as empty + return obj.REQUEST.get('%s_delete' % self.name) != 'nochange' + + imageExts = ('.jpg', '.jpeg', '.png', '.gif') + def validateValue(self, obj, value): + form = obj.REQUEST.form + action = '%s_delete' % self.name + if (not value or not value.filename) and form.has_key(action) and \ + not form[action]: + # If this key is present but empty, it means that the user selected + # "replace the file with a new one". So in this case he must provide + # a new file to upload. + return obj.translate('file_required') + # Check that, if self.isImage, the uploaded file is really an image + if value and value.filename and self.isImage: + ext = os.path.splitext(value.filename)[1].lower() + if ext not in File.imageExts: + return obj.translate('image_required') + + defaultMimeType = 'application/octet-stream' + def store(self, obj, value): + '''Stores the p_value that represents some file. p_value can be: + * an instance of Zope class ZPublisher.HTTPRequest.FileUpload. In + this case, it is file content coming from a HTTP POST; + * an instance of Zope class OFS.Image.File; + * an instance of appy.shared.utils.FileWrapper, which wraps an + instance of OFS.Image.File and adds useful methods for manipulating + it; + * a string. In this case, the string represents the path of a file + on disk; + * a 2-tuple (fileName, fileContent) where: + - fileName is the name of the file (ie "myFile.odt") + - fileContent is the binary or textual content of the file or an + open file handler. + * a 3-tuple (fileName, fileContent, mimeType) where + - fileName and fileContent have the same meaning than above; + - mimeType is the MIME type of the file. + ''' + if value: + ZFileUpload = obj.o.getProductConfig().FileUpload + OFSImageFile = obj.o.getProductConfig().File + if isinstance(value, ZFileUpload): + # The file content comes from a HTTP POST. + # Retrieve the existing value, or create one if None + existingValue = getattr(obj.aq_base, self.name, None) + if not existingValue: + existingValue = OFSImageFile(self.name, '', '') + # Set mimetype + if value.headers.has_key('content-type'): + mimeType = value.headers['content-type'] + else: + mimeType = File.defaultMimeType + existingValue.content_type = mimeType + # Set filename + fileName = value.filename + filename= fileName[max(fileName.rfind('/'),fileName.rfind('\\'), + fileName.rfind(':'))+1:] + existingValue.filename = fileName + # Set content + existingValue.manage_upload(value) + setattr(obj, self.name, existingValue) + elif isinstance(value, OFSImageFile): + setattr(obj, self.name, value) + elif isinstance(value, sutils.FileWrapper): + setattr(obj, self.name, value._zopeFile) + elif isinstance(value, basestring): + setattr(obj, self.name, File.getFileObject(value, zope=True)) + elif type(value) in sutils.sequenceTypes: + # It should be a 2-tuple or 3-tuple + fileName = None + mimeType = None + if len(value) == 2: + fileName, fileContent = value + elif len(value) == 3: + fileName, fileContent, mimeType = value + else: + raise WRONG_FILE_TUPLE + if fileName: + fileId = 'file.%f' % time.time() + zopeFile = OFSImageFile(fileId, fileName, fileContent) + zopeFile.filename = fileName + if not mimeType: + mimeType = mimetypes.guess_type(fileName)[0] + zopeFile.content_type = mimeType + setattr(obj, self.name, zopeFile) + else: + # I store value "None", excepted if I find in the request the desire + # to keep the file unchanged. + action = None + rq = getattr(obj, 'REQUEST', None) + if rq: action = rq.get('%s_delete' % self.name, None) + if action == 'nochange': pass + else: setattr(obj, self.name, None) +# ------------------------------------------------------------------------------ diff --git a/fields/float.py b/fields/float.py new file mode 100644 index 0000000..01ec8f1 --- /dev/null +++ b/fields/float.py @@ -0,0 +1,105 @@ +# ------------------------------------------------------------------------------ +# This file is part of Appy, a framework for building applications in the Python +# language. Copyright (C) 2007 Gaetan Delannay + +# Appy is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. + +# Appy is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License along with +# Appy. If not, see . + +# ------------------------------------------------------------------------------ +from appy.fields import Field +from appy.px import Px +from appy.shared import utils as sutils + +# ------------------------------------------------------------------------------ +class Float(Field): + allowedDecimalSeps = (',', '.') + allowedThousandsSeps = (' ', '') + + pxView = pxCell = Px(''' + :value + + ''') + + pxEdit = Px(''' + ''') + + pxSearch = Px(''' + +
   + + + + + + + + + +
+
''') + + def __init__(self, validator=None, multiplicity=(0,1), default=None, + show=True, page='main', group=None, layouts=None, move=0, + indexed=False, searchable=False, specificReadPermission=False, + specificWritePermission=False, width=5, height=None, + maxChars=13, colspan=1, master=None, masterValue=None, + focus=False, historized=False, mapping=None, label=None, + sdefault=('',''), scolspan=1, swidth=None, sheight=None, + precision=None, sep=(',', '.'), tsep=' '): + # The precision is the number of decimal digits. This number is used + # for rendering the float, but the internal float representation is not + # rounded. + self.precision = precision + # The decimal separator can be a tuple if several are allowed, ie + # ('.', ',') + if type(sep) not in sutils.sequenceTypes: + self.sep = (sep,) + else: + self.sep = sep + # Check that the separator(s) are among allowed decimal separators + for sep in self.sep: + if sep not in Float.allowedDecimalSeps: + raise Exception('Char "%s" is not allowed as decimal ' \ + 'separator.' % sep) + self.tsep = tsep + Field.__init__(self, validator, multiplicity, default, show, page, + group, layouts, move, indexed, False, + specificReadPermission, specificWritePermission, width, + height, maxChars, colspan, master, masterValue, focus, + historized, True, mapping, label, sdefault, scolspan, + swidth, sheight) + self.pythonType = float + + def getFormattedValue(self, obj, value, showChanges=False): + return sutils.formatNumber(value, sep=self.sep[0], + precision=self.precision, tsep=self.tsep) + + def validateValue(self, obj, value): + # Replace used separator with the Python separator '.' + for sep in self.sep: value = value.replace(sep, '.') + value = value.replace(self.tsep, '') + try: + value = self.pythonType(value) + except ValueError: + return obj.translate('bad_%s' % self.pythonType.__name__) + + def getStorableValue(self, value): + if not self.isEmptyValue(value): + for sep in self.sep: value = value.replace(sep, '.') + value = value.replace(self.tsep, '') + return self.pythonType(value) +# ------------------------------------------------------------------------------ diff --git a/fields/info.py b/fields/info.py new file mode 100644 index 0000000..527d305 --- /dev/null +++ b/fields/info.py @@ -0,0 +1,39 @@ +# ------------------------------------------------------------------------------ +# This file is part of Appy, a framework for building applications in the Python +# language. Copyright (C) 2007 Gaetan Delannay + +# Appy is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. + +# Appy is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License along with +# Appy. If not, see . + +# ------------------------------------------------------------------------------ +from appy.fields import Field + +# ------------------------------------------------------------------------------ +class Info(Field): + '''An info is a field whose purpose is to present information + (text, html...) to the user.''' + # An info only displays a label. So PX for showing content are empty. + pxView = pxEdit = pxCell = pxSearch = '' + + def __init__(self, validator=None, multiplicity=(1,1), default=None, + show='view', page='main', group=None, layouts=None, move=0, + indexed=False, searchable=False, specificReadPermission=False, + specificWritePermission=False, width=None, height=None, + maxChars=None, colspan=1, master=None, masterValue=None, + focus=False, historized=False, mapping=None, label=None): + Field.__init__(self, None, (0,1), default, show, page, group, layouts, + move, indexed, False, specificReadPermission, + specificWritePermission, width, height, None, colspan, + master, masterValue, focus, historized, False, mapping, + label, None, None, None, None) + self.validable = False +# ------------------------------------------------------------------------------ diff --git a/fields/integer.py b/fields/integer.py new file mode 100644 index 0000000..872d4a0 --- /dev/null +++ b/fields/integer.py @@ -0,0 +1,80 @@ +# ------------------------------------------------------------------------------ +# This file is part of Appy, a framework for building applications in the Python +# language. Copyright (C) 2007 Gaetan Delannay + +# Appy is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. + +# Appy is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License along with +# Appy. If not, see . + +# ------------------------------------------------------------------------------ +from appy.fields import Field +from appy.px import Px + +# ------------------------------------------------------------------------------ +class Integer(Field): + + pxView = pxCell = Px(''' + :value + + ''') + + pxEdit = Px(''' + ''') + + pxSearch = Px(''' + +
   + + + + + + + + + +
+
''') + + def __init__(self, validator=None, multiplicity=(0,1), default=None, + show=True, page='main', group=None, layouts=None, move=0, + indexed=False, searchable=False, specificReadPermission=False, + specificWritePermission=False, width=5, height=None, + maxChars=13, colspan=1, master=None, masterValue=None, + focus=False, historized=False, mapping=None, label=None, + sdefault=('',''), scolspan=1, swidth=None, sheight=None): + Field.__init__(self, validator, multiplicity, default, show, page, + group, layouts, move, indexed, searchable, + specificReadPermission, specificWritePermission, width, + height, maxChars, colspan, master, masterValue, focus, + historized, True, mapping, label, sdefault, scolspan, + swidth, sheight) + self.pythonType = long + + def validateValue(self, obj, value): + try: + value = self.pythonType(value) + except ValueError: + return obj.translate('bad_%s' % self.pythonType.__name__) + + def getStorableValue(self, value): + if not self.isEmptyValue(value): return self.pythonType(value) + + def getFormattedValue(self, obj, value, showChanges=False): + if self.isEmptyValue(value): return '' + return str(value) +# ------------------------------------------------------------------------------ diff --git a/fields/list.py b/fields/list.py new file mode 100644 index 0000000..f305a8c --- /dev/null +++ b/fields/list.py @@ -0,0 +1,166 @@ +# ------------------------------------------------------------------------------ +# This file is part of Appy, a framework for building applications in the Python +# language. Copyright (C) 2007 Gaetan Delannay + +# Appy is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. + +# Appy is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License along with +# Appy. If not, see . + +# ------------------------------------------------------------------------------ +from appy import Object +from appy.fields import Field +from appy.px import Px +from appy.gen.layout import Table + +# ------------------------------------------------------------------------------ +class List(Field): + '''A list.''' + + # PX for rendering a single row. + pxRow = Px(''' + + + + :widget['pxView'] + + + + + + + ''') + + # PX for rendering the list (shared between pxView and pxEdit). + pxTable = Px(''' + + + + + + + + + + :widget['pxRow'] + + + + + :widget['pxRow'] + +
::_(fieldInfo[1]['labelId']) + + +
''') + + pxView = pxCell = Px(''':widget['pxTable']''') + pxEdit = Px(''' + + + + :widget['pxTable'] + ''') + + pxSearch = '' + + def __init__(self, fields, validator=None, multiplicity=(0,1), default=None, + show=True, page='main', group=None, layouts=None, move=0, + indexed=False, searchable=False, specificReadPermission=False, + specificWritePermission=False, width=None, height=None, + maxChars=None, colspan=1, master=None, masterValue=None, + focus=False, historized=False, mapping=None, label=None, + subLayouts=Table('fv', width=None)): + Field.__init__(self, validator, multiplicity, default, show, page, + group, layouts, move, indexed, False, + specificReadPermission, specificWritePermission, width, + height, None, colspan, master, masterValue, focus, + historized, True, mapping, label, None, None, None, None) + self.validable = True + # Tuples of (names, Field instances) determining the format of every + # element in the list. + self.fields = fields + self.fieldsd = [(n, f.__dict__) for (n,f) in self.fields] + # Force some layouting for sub-fields, if subLayouts are given. So the + # one who wants freedom on tuning layouts at the field level must + # specify subLayouts=None. + if subLayouts: + for name, field in self.fields: + field.layouts = field.formatLayouts(subLayouts) + + def getField(self, name): + '''Gets the field definition whose name is p_name.''' + for n, field in self.fields: + if n == name: return field + + def getRequestValue(self, request, requestName=None): + '''Concatenates the list from distinct form elements in the request.''' + name = requestName or self.name # A List may be into another List (?) + prefix = name + '*' + self.fields[0][0] + '*' + res = {} + for key in request.keys(): + if not key.startswith(prefix): continue + # I have found a row. Gets its index + row = Object() + if '_' in key: key = key[:key.index('_')] + rowIndex = int(key.split('*')[-1]) + if rowIndex == -1: continue # Ignore the template row. + for subName, subField in self.fields: + keyName = '%s*%s*%s' % (name, subName, rowIndex) + v = subField.getRequestValue(request, requestName=keyName) + setattr(row, subName, v) + res[rowIndex] = row + # Produce a sorted list. + keys = res.keys() + keys.sort() + res = [res[key] for key in keys] + # I store in the request this computed value. This way, when individual + # subFields will need to get their value, they will take it from here, + # instead of taking it from the specific request key. Indeed, specific + # request keys contain row indexes that may be wrong after row deletions + # by the user. + request.set(name, res) + return res + + def getStorableValue(self, value): + '''Gets p_value in a form that can be stored in the database.''' + res = [] + for v in value: + sv = Object() + for name, field in self.fields: + setattr(sv, name, field.getStorableValue(getattr(v, name))) + res.append(sv) + return res + + def getInnerValue(self, outerValue, name, i): + '''Returns the value of inner field named p_name in row number p_i + within the whole list of values p_outerValue.''' + if i == -1: return '' + if not outerValue: return '' + if i >= len(outerValue): return '' + return getattr(outerValue[i], name, '') + + def getCss(self, layoutType, res): + '''Gets the CSS required by sub-fields if any.''' + for name, field in self.fields: + field.getCss(layoutType, res) + + def getJs(self, layoutType, res): + '''Gets the JS required by sub-fields if any.''' + for name, field in self.fields: + field.getJs(layoutType, res) +# ------------------------------------------------------------------------------ diff --git a/gen/ogone.py b/fields/ogone.py similarity index 85% rename from gen/ogone.py rename to fields/ogone.py index cb6c3b0..d589476 100644 --- a/gen/ogone.py +++ b/fields/ogone.py @@ -1,7 +1,8 @@ # ------------------------------------------------------------------------------ import sha from appy import Object -from appy.gen import Type +from appy.gen import Field +from appy.px import Px # ------------------------------------------------------------------------------ class OgoneConfig: @@ -25,20 +26,39 @@ class OgoneConfig: def __repr__(self): return str(self.__dict__) # ------------------------------------------------------------------------------ -class Ogone(Type): +class Ogone(Field): '''This field allows to perform payments with the Ogone (r) system.''' urlTypes = ('accept', 'decline', 'exception', 'cancel') + pxView = pxCell = Px(''' + + +

:value

+ +
+ + + + + +
+
''') + + pxEdit = pxSearch = '' + def __init__(self, orderMethod, responseMethod, show='view', page='main', group=None, layouts=None, move=0, specificReadPermission=False, specificWritePermission=False, width=None, height=None, colspan=1, master=None, masterValue=None, focus=False, mapping=None, label=None): - Type.__init__(self, None, (0,1), None, show, page, group, layouts, move, - False, False,specificReadPermission, - specificWritePermission, width, height, None, colspan, - master, masterValue, focus, False, True, mapping, label, - None, None, None, None) + Field.__init__(self, None, (0,1), None, show, page, group, layouts, + move, False, False,specificReadPermission, + specificWritePermission, width, height, None, colspan, + master, masterValue, focus, False, True, mapping, label, + None, None, None, None) # orderMethod must contain a method returning a dict containing info # about the order. Following keys are mandatory: # * orderID An identifier for the order. Don't use the object UID diff --git a/fields/pod.py b/fields/pod.py new file mode 100644 index 0000000..29ae00e --- /dev/null +++ b/fields/pod.py @@ -0,0 +1,226 @@ +# ------------------------------------------------------------------------------ +# This file is part of Appy, a framework for building applications in the Python +# language. Copyright (C) 2007 Gaetan Delannay + +# Appy is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. + +# Appy is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License along with +# Appy. If not, see . + +# ------------------------------------------------------------------------------ +import time, os, os.path, StringIO +from appy.fields import Field +from appy.px import Px +from file import File +from appy.gen.layout import Table +from appy.pod import PodError +from appy.pod.renderer import Renderer +from appy.shared import utils as sutils + +# ------------------------------------------------------------------------------ +class Pod(Field): + '''A pod is a field allowing to produce a (PDF, ODT, Word, RTF...) document + from data contained in Appy class and linked objects or anything you + want to put in it. It uses appy.pod.''' + # Layout for rendering a POD field for exporting query results. + rLayouts = {'view': Table('fl', width=None)} + POD_ERROR = 'An error occurred while generating the document. Please ' \ + 'contact the system administrator.' + DELETE_TEMP_DOC_ERROR = 'A temporary document could not be removed. %s.' + + pxView = pxCell = Px(''' + + + + + + + + + + ''') + + pxEdit = pxSearch = '' + + def __init__(self, validator=None, default=None, show=('view', 'result'), + page='main', group=None, layouts=None, move=0, indexed=False, + searchable=False, specificReadPermission=False, + specificWritePermission=False, width=None, height=None, + maxChars=None, colspan=1, master=None, masterValue=None, + focus=False, historized=False, mapping=None, label=None, + template=None, context=None, action=None, askAction=False, + stylesMapping={}, freezeFormat='pdf'): + # The following param stores the path to a POD template + self.template = template + # The context is a dict containing a specific pod context, or a method + # that returns such a dict. + self.context = context + # Next one is a method that will be triggered after the document has + # been generated. + self.action = action + # If askAction is True, the action will be triggered only if the user + # checks a checkbox, which, by default, will be unchecked. + self.askAction = askAction + # A global styles mapping that would apply to the whole template + self.stylesMapping = stylesMapping + # Freeze format is by PDF by default + self.freezeFormat = freezeFormat + Field.__init__(self, None, (0,1), default, show, page, group, layouts, + move, indexed, searchable, specificReadPermission, + specificWritePermission, width, height, None, colspan, + master, masterValue, focus, historized, False, mapping, + label, None, None, None, None) + self.validable = False + + def isFrozen(self, obj): + '''Is there a frozen document for p_self on p_obj?''' + value = getattr(obj.o.aq_base, self.name, None) + return isinstance(value, obj.o.getProductConfig().File) + + def getToolInfo(self, obj): + '''Gets information related to this field (p_self) that is available in + the tool: the POD template and the available output formats. If this + field is frozen, available output formats are not available anymore: + only the format of the frozen doc is returned.''' + tool = obj.tool + appyClass = tool.o.getAppyClass(obj.o.meta_type) + # Get the output format(s) + if self.isFrozen(obj): + # The only available format is the one from the frozen document + fileName = getattr(obj.o.aq_base, self.name).filename + formats = (os.path.splitext(fileName)[1][1:],) + else: + # Available formats are those which are selected in the tool. + name = tool.getAttributeName('formats', appyClass, self.name) + formats = getattr(tool, name) + # Get the POD template + name = tool.getAttributeName('podTemplate', appyClass, self.name) + template = getattr(tool, name) + return (template, formats) + + def getValue(self, obj): + '''Gets, on_obj, the value conforming to self's type definition. For a + Pod field, if a file is stored in the field, it means that the + field has been frozen. Else, it means that the value must be + retrieved by calling pod to compute the result.''' + rq = getattr(obj, 'REQUEST', None) + res = getattr(obj.aq_base, self.name, None) + if res and res.size: + # Return the frozen file. + return sutils.FileWrapper(res) + # If we are here, it means that we must call pod to compute the file. + # A Pod field differs from other field types because there can be + # several ways to produce the field value (ie: output file format can be + # odt, pdf,...; self.action can be executed or not...). We get those + # precisions about the way to produce the file from the request object + # and from the tool. If we don't find the request object (or if it does + # not exist, ie, when Zope runs in test mode), we use default values. + obj = obj.appy() + tool = obj.tool + # Get POD template and available formats from the tool. + template, availFormats = self.getToolInfo(obj) + # Get the output format + defaultFormat = 'pdf' + if defaultFormat not in availFormats: defaultFormat = availFormats[0] + outputFormat = getattr(rq, 'podFormat', defaultFormat) + # Get or compute the specific POD context + specificContext = None + if callable(self.context): + specificContext = self.callMethod(obj, self.context) + else: + specificContext = self.context + # Temporary file where to generate the result + tempFileName = '%s/%s_%f.%s' % ( + sutils.getOsTempFolder(), obj.uid, time.time(), outputFormat) + # Define parameters to give to the appy.pod renderer + podContext = {'tool': tool, 'user': obj.user, 'self': obj, 'field':self, + 'now': obj.o.getProductConfig().DateTime(), + '_': obj.translate, 'projectFolder': tool.getDiskFolder()} + # If the POD document is related to a query, get it from the request, + # execute it and put the result in the context. + isQueryRelated = rq.get('queryData', None) + if isQueryRelated: + # Retrieve query params from the request + cmd = ', '.join(tool.o.queryParamNames) + cmd += " = rq['queryData'].split(';')" + exec cmd + # (re-)execute the query, but without any limit on the number of + # results; return Appy objects. + objs = tool.o.executeQuery(obj.o.portal_type, searchName=search, + sortBy=sortKey, sortOrder=sortOrder, filterKey=filterKey, + filterValue=filterValue, maxResults='NO_LIMIT') + podContext['objects'] = [o.appy() for o in objs['objects']] + # Add the field-specific context if present. + if specificContext: + podContext.update(specificContext) + # If a custom param comes from the request, add it to the context. A + # custom param must have format "name:value". Custom params override any + # other value in the request, including values from the field-specific + # context. + customParams = rq.get('customParams', None) + if customParams: + paramsDict = eval(customParams) + podContext.update(paramsDict) + # Define a potential global styles mapping + if callable(self.stylesMapping): + stylesMapping = self.callMethod(obj, self.stylesMapping) + else: + stylesMapping = self.stylesMapping + rendererParams = {'template': StringIO.StringIO(template.content), + 'context': podContext, 'result': tempFileName, + 'stylesMapping': stylesMapping, + 'imageResolver': tool.o.getApp()} + if tool.unoEnabledPython: + rendererParams['pythonWithUnoPath'] = tool.unoEnabledPython + if tool.openOfficePort: + rendererParams['ooPort'] = tool.openOfficePort + # Launch the renderer + try: + renderer = Renderer(**rendererParams) + renderer.run() + except PodError, pe: + if not os.path.exists(tempFileName): + # In some (most?) cases, when OO returns an error, the result is + # nevertheless generated. + obj.log(str(pe), type='error') + return Pod.POD_ERROR + # Give a friendly name for this file + fileName = obj.translate(self.labelId) + if not isQueryRelated: + # This is a POD for a single object: personalize the file name with + # the object title. + fileName = '%s-%s' % (obj.title, fileName) + fileName = tool.normalize(fileName) + '.' + outputFormat + # Get a FileWrapper instance from the temp file on the filesystem + res = File.getFileObject(tempFileName, fileName) + # Execute the related action if relevant + doAction = getattr(rq, 'askAction', False) in ('True', True) + if doAction and self.action: self.action(obj, podContext) + # Returns the doc and removes the temp file + try: + os.remove(tempFileName) + except OSError, oe: + obj.log(Pod.DELETE_TEMP_DOC_ERROR % str(oe), type='warning') + except IOError, ie: + obj.log(Pod.DELETE_TEMP_DOC_ERROR % str(ie), type='warning') + return res + + def store(self, obj, value): + '''Stores (=freezes) a document (in p_value) in the field.''' + if isinstance(value, sutils.FileWrapper): + value = value._zopeFile + setattr(obj, self.name, value) +# ------------------------------------------------------------------------------ diff --git a/fields/ref.py b/fields/ref.py new file mode 100644 index 0000000..d526a8e --- /dev/null +++ b/fields/ref.py @@ -0,0 +1,672 @@ +# ------------------------------------------------------------------------------ +# This file is part of Appy, a framework for building applications in the Python +# language. Copyright (C) 2007 Gaetan Delannay + +# Appy is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. + +# Appy is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License along with +# Appy. If not, see . + +# ------------------------------------------------------------------------------ +import sys +from appy.fields import Field, No +from appy.px import Px +from appy.gen.layout import Table +from appy.gen import utils as gutils +from appy.shared import utils as sutils + +# ------------------------------------------------------------------------------ +class Ref(Field): + # Some default layouts. "w" stands for "wide": those layouts produce tables + # of Ref objects whose width is 100%. + wLayouts = Table('lrv-f', width='100%') + # "d" stands for "description": a description label is added. + wdLayouts = {'view': Table('l-d-f', width='100%')} + + # This PX displays the title of a referenced object, with a link on it to + # reach the consult view for this object. If we are on a back reference, the + # link allows to reach the correct page where the forward reference is + # defined. If we are on a forward reference, the "nav" parameter is added to + # the URL for allowing to navigate from one object to the next/previous on + # ui/view. + pxObjectTitle = Px(''' + + ::obj.getSupTitle(navInfo) + :(not includeShownInfo) and \ + obj.Title() or contextObj.getReferenceLabel(fieldName, obj.appy()) + ::obj.getSubTitle()" + ''') + + # This PX displays icons for triggering actions on a given referenced object + # (edit, delete, etc). + pxObjectActions = Px(''' + + + + + + + + + + + + + +
+ + + + + :targetObj.appy().pxTransitions + + + + + + + + +
''') + + # Displays the button allowing to add a new object through a Ref field, if + # it has been declared as addable and if multiplicities allow it. + pxAdd = Px(''' + + + ''') + + # This PX displays, in a cell header from a ref table, icons for sorting the + # ref field according to the field that corresponds to this column. + pxSortIcons = Px(''' + + + + ''') + + # This PX is called by a XmlHttpRequest (or directly by pxView) for + # displaying the referred objects of a reference field. + pxViewContent = Px(''' +
+ + + + + + + + + + + + + + + + + + +
:_('no_ref'):widget['pxAdd']:widget['pxObjectTitle']
+
+ + + +
+ (:totalNumber) + :widget['pxAdd'] + + +
+ + + :contextObj.appy().pxAppyNavigate + + +

:_('no_ref')

+ + + + + +
+ + + + + + + + + + +
+ + _(widget['labelId']) + :widget['pxSortIcons'] + :contextObj.appy(\ + ).pxShowDetails + +
+ + + + :widget['pxObjectTitle'] +
:widget['pxObjectActions']
+
+ + + + + + +
+
+
+ + + :contextObj.appy().pxAppyNavigate +
+
''') + + pxView = pxCell = Px(''' + :widget['pxViewContent'] + ''') + + pxEdit = Px(''' + + + ''') + + pxSearch = Px(''' + +
   + + + + + +
+
+ + +
''') + + def __init__(self, klass=None, attribute=None, validator=None, + multiplicity=(0,1), default=None, add=False, addConfirm=False, + delete=None, noForm=False, link=True, unlink=None, back=None, + show=True, page='main', group=None, layouts=None, + showHeaders=False, shownInfo=(), select=None, maxPerPage=30, + move=0, indexed=False, searchable=False, + specificReadPermission=False, specificWritePermission=False, + width=None, height=5, maxChars=None, colspan=1, master=None, + masterValue=None, focus=False, historized=False, mapping=None, + label=None, queryable=False, queryFields=None, queryNbCols=1, + navigable=False, searchSelect=None, changeOrder=True, + sdefault='', scolspan=1, swidth=None, sheight=None): + self.klass = klass + self.attribute = attribute + # May the user add new objects through this ref ? + self.add = add + # When the user adds a new object, must a confirmation popup be shown? + self.addConfirm = addConfirm + # May the user delete objects via this Ref? + self.delete = delete + if delete == None: + # By default, one may delete objects via a Ref for which one can + # add objects. + self.delete = bool(self.add) + # If noForm is True, when clicking to create an object through this ref, + # the object will be created automatically, and no creation form will + # be presented to the user. + self.noForm = noForm + # May the user link existing objects through this ref? + self.link = link + # May the user unlink existing objects? + self.unlink = unlink + if unlink == None: + # By default, one may unlink objects via a Ref for which one can + # link objects. + self.unlink = bool(self.link) + self.back = None + if back: + # It is a forward reference + self.isBack = False + # Initialise the backward reference + self.back = back + self.backd = back.__dict__ + back.isBack = True + back.back = self + back.backd = self.__dict__ + # klass may be None in the case we are defining an auto-Ref to the + # same class as the class where this field is defined. In this case, + # when defining the field within the class, write + # myField = Ref(None, ...) + # and, at the end of the class definition (name it K), write: + # K.myField.klass = K + # setattr(K, K.myField.back.attribute, K.myField.back) + if klass: setattr(klass, back.attribute, back) + # When displaying a tabular list of referenced objects, must we show + # the table headers? + self.showHeaders = showHeaders + # When displaying referenced object(s), we will display its title + all + # other fields whose names are listed in the following attribute. + self.shownInfo = list(shownInfo) + if not self.shownInfo: self.shownInfo.append('title') + # If a method is defined in this field "select", it will be used to + # filter the list of available tied objects. + self.select = select + # Maximum number of referenced objects shown at once. + self.maxPerPage = maxPerPage + # Specifies sync + sync = {'view': False, 'edit':True} + # If param p_queryable is True, the user will be able to perform queries + # from the UI within referenced objects. + self.queryable = queryable + # Here is the list of fields that will appear on the search screen. + # If None is specified, by default we take every indexed field + # defined on referenced objects' class. + self.queryFields = queryFields + # The search screen will have this number of columns + self.queryNbCols = queryNbCols + # Within the portlet, will referred elements appear ? + self.navigable = navigable + # The search select method is used if self.indexed is True. In this + # case, we need to know among which values we can search on this field, + # in the search screen. Those values are returned by self.searchSelect, + # which must be a static method accepting the tool as single arg. + self.searchSelect = searchSelect + # If changeOrder is False, it even if the user has the right to modify + # the field, it will not be possible to move objects or sort them. + self.changeOrder = changeOrder + Field.__init__(self, validator, multiplicity, default, show, page, + group, layouts, move, indexed, False, + specificReadPermission, specificWritePermission, width, + height, None, colspan, master, masterValue, focus, + historized, sync, mapping, label, sdefault, scolspan, + swidth, sheight) + self.validable = self.link + + def getDefaultLayouts(self): + return {'view': Table('l-f', width='100%'), 'edit': 'lrv-f'} + + def isShowable(self, obj, layoutType): + res = Field.isShowable(self, obj, layoutType) + if not res: return res + # We add here specific Ref rules for preventing to show the field under + # some inappropriate circumstances. + if (layoutType == 'edit') and \ + (self.mayAdd(obj) or not self.link): return False + if self.isBack: + if layoutType == 'edit': return False + else: return getattr(obj.aq_base, self.name, None) + return res + + def getValue(self, obj, type='objects', noListIfSingleObj=False, + startNumber=None, someObjects=False): + '''Returns the objects linked to p_obj through this Ref field. + - If p_type is "objects", it returns the Appy wrappers; + - If p_type is "zobjects", it returns the Zope objects; + - If p_type is "uids", it returns UIDs of objects (= strings). + + * If p_startNumber is None, it returns all referred objects. + * If p_startNumber is a number, it returns self.maxPerPage objects, + starting at p_startNumber. + + If p_noListIfSingleObj is True, it returns the single reference as + an object and not as a list. + + If p_someObjects is True, it returns an instance of SomeObjects + instead of returning a list of references.''' + uids = getattr(obj.aq_base, self.name, []) + if not uids: + # Maybe is there a default value? + defValue = Field.getValue(self, obj) + if defValue: + # I must prefix call to function "type" with "__builtins__" + # because this name was overridden by a method parameter. + if __builtins__['type'](defValue) in sutils.sequenceTypes: + uids = [o.o.UID() for o in defValue] + else: + uids = [defValue.o.UID()] + # Prepare the result: an instance of SomeObjects, that will be unwrapped + # if not required. + res = gutils.SomeObjects() + res.totalNumber = res.batchSize = len(uids) + batchNeeded = startNumber != None + if batchNeeded: + res.batchSize = self.maxPerPage + if startNumber != None: + res.startNumber = startNumber + # Get the objects given their uids + i = res.startNumber + while i < (res.startNumber + res.batchSize): + if i >= res.totalNumber: break + # Retrieve every reference in the correct format according to p_type + if type == 'uids': + ref = uids[i] + else: + ref = obj.getTool().getObject(uids[i]) + if type == 'objects': + ref = ref.appy() + res.objects.append(ref) + i += 1 + # Manage parameter p_noListIfSingleObj + if res.objects and noListIfSingleObj: + if self.multiplicity[1] == 1: + res.objects = res.objects[0] + if someObjects: return res + return res.objects + + def getFormattedValue(self, obj, value, showChanges=False): + return value + + def getIndexType(self): return 'ListIndex' + + def getIndexValue(self, obj, forSearch=False): + '''Value for indexing is the list of UIDs of linked objects. If + p_forSearch is True, it will return a list of the linked objects' + titles instead.''' + if not forSearch: + res = getattr(obj.aq_base, self.name, []) + if res: + # The index does not like persistent lists. + res = list(res) + else: + # Ugly catalog: if I return an empty list, the previous value + # is kept. + res.append('') + return res + else: + # For the global search: return linked objects' titles. + res = [o.title for o in self.getValue(type='objects')] + if not res: res.append('') + return res + + def validateValue(self, obj, value): + if not self.link: return None + # We only check "link" Refs because in edit views, "add" Refs are + # not visible. So if we check "add" Refs, on an "edit" view we will + # believe that that there is no referred object even if there is. + # If the field is a reference, we must ensure itself that multiplicities + # are enforced. + if not value: + nbOfRefs = 0 + elif isinstance(value, basestring): + nbOfRefs = 1 + else: + nbOfRefs = len(value) + minRef = self.multiplicity[0] + maxRef = self.multiplicity[1] + if maxRef == None: + maxRef = sys.maxint + if nbOfRefs < minRef: + return obj.translate('min_ref_violated') + elif nbOfRefs > maxRef: + return obj.translate('max_ref_violated') + + def linkObject(self, obj, value, back=False): + '''This method links p_value (which can be a list of objects) to p_obj + through this Ref field.''' + # p_value can be a list of objects + if type(value) in sutils.sequenceTypes: + for v in value: self.linkObject(obj, v, back=back) + return + # Gets the list of referred objects (=list of uids), or create it. + obj = obj.o + refs = getattr(obj.aq_base, self.name, None) + if refs == None: + refs = obj.getProductConfig().PersistentList() + setattr(obj, self.name, refs) + # Insert p_value into it. + uid = value.o.UID() + if uid not in refs: + # Where must we insert the object? At the start? At the end? + if callable(self.add): + add = self.callMethod(obj, self.add) + else: + add = self.add + if add == 'start': + refs.insert(0, uid) + else: + refs.append(uid) + # Update the back reference + if not back: self.back.linkObject(value, obj, back=True) + + def unlinkObject(self, obj, value, back=False): + '''This method unlinks p_value (which can be a list of objects) from + p_obj through this Ref field.''' + # p_value can be a list of objects + if type(value) in sutils.sequenceTypes: + for v in value: self.unlinkObject(obj, v, back=back) + return + obj = obj.o + refs = getattr(obj.aq_base, self.name, None) + if not refs: return + # Unlink p_value + uid = value.o.UID() + if uid in refs: + refs.remove(uid) + # Update the back reference + if not back: self.back.unlinkObject(value, obj, back=True) + + def store(self, obj, value): + '''Stores on p_obj, the p_value, which can be: + * None; + * an object UID (=string); + * a list of object UIDs (=list of strings). Generally, UIDs or lists + of UIDs come from Ref fields with link:True edited through the web; + * a Zope object; + * a Appy object; + * a list of Appy or Zope objects.''' + # Standardize p_value into a list of Zope objects + objects = value + if not objects: objects = [] + if type(objects) not in sutils.sequenceTypes: objects = [objects] + tool = obj.getTool() + for i in range(len(objects)): + if isinstance(objects[i], basestring): + # We have a UID here + objects[i] = tool.getObject(objects[i]) + else: + # Be sure to have a Zope object + objects[i] = objects[i].o + uids = [o.UID() for o in objects] + # Unlink objects that are not referred anymore + refs = getattr(obj.aq_base, self.name, None) + if refs: + i = len(refs)-1 + while i >= 0: + if refs[i] not in uids: + # Object having this UID must unlink p_obj + self.back.unlinkObject(tool.getObject(refs[i]), obj) + i -= 1 + # Link new objects + if objects: + self.linkObject(obj, objects) + + def mayAdd(self, obj): + '''May the user create a new referred object from p_obj via this Ref?''' + # We can't (yet) do that on back references. + if self.isBack: return No('is_back') + # Check if this Ref is addable + if callable(self.add): + add = self.callMethod(obj, self.add) + else: + add = self.add + if not add: return No('no_add') + # Have we reached the maximum number of referred elements? + if self.multiplicity[1] != None: + refCount = len(getattr(obj, self.name, ())) + if refCount >= self.multiplicity[1]: return No('max_reached') + # May the user edit this Ref field? + if not obj.allows(self.writePermission): return No('no_write_perm') + # Have the user the correct add permission? + tool = obj.getTool() + addPermission = '%s: Add %s' % (tool.getAppName(), + tool.getPortalType(self.klass)) + folder = obj.getCreateFolder() + if not obj.getUser().has_permission(addPermission, folder): + return No('no_add_perm') + return True + + def checkAdd(self, obj): + '''Compute m_mayAdd above, and raise an Unauthorized exception if + m_mayAdd returns False.''' + may = self.mayAdd(obj) + if not may: + from AccessControl import Unauthorized + raise Unauthorized("User can't write Ref field '%s' (%s)." % \ + (self.name, may.msg)) + + def changeOrderEnabled(self, obj): + '''Is changeOrder enabled?''' + if isinstance(self.changeOrder, bool): + return self.changeOrder + else: + return self.callMethod(obj, self.changeOrder) + +def autoref(klass, field): + '''klass.field is a Ref to p_klass. This kind of auto-reference can't be + declared in the "normal" way, like this: + + class A: + attr1 = Ref(A) + + because at the time Python encounters the static declaration + "attr1 = Ref(A)", class A is not completely defined yet. + + This method allows to overcome this problem. You can write such + auto-reference like this: + + class A: + attr1 = Ref(None) + autoref(A, A.attr1) + ''' + field.klass = klass + setattr(klass, field.back.attribute, field.back) +# ------------------------------------------------------------------------------ diff --git a/fields/string.py b/fields/string.py new file mode 100644 index 0000000..8e15253 --- /dev/null +++ b/fields/string.py @@ -0,0 +1,506 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# This file is part of Appy, a framework for building applications in the Python +# language. Copyright (C) 2007 Gaetan Delannay + +# Appy is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. + +# Appy is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License along with +# Appy. If not, see . + +# ------------------------------------------------------------------------------ +import re, random +from appy.gen.layout import Table +from appy.gen.indexer import XhtmlTextExtractor +from appy.fields import Field +from appy.shared.data import countries +from appy.shared.xml_parser import XhtmlCleaner +from appy.shared.diff import HtmlDiff +from appy.shared import utils as sutils + +# ------------------------------------------------------------------------------ +digit = re.compile('[0-9]') +alpha = re.compile('[a-zA-Z0-9]') +letter = re.compile('[a-zA-Z]') +digits = '0123456789' +letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' +emptyTuple = () + +# ------------------------------------------------------------------------------ +class Selection: + '''Instances of this class may be given as validator of a String, in order + to tell Appy that the validator is a selection that will be computed + dynamically.''' + def __init__(self, methodName): + # The p_methodName parameter must be the name of a method that will be + # called every time Appy will need to get the list of possible values + # for the related field. It must correspond to an instance method of + # the class defining the related field. This method accepts no argument + # and must return a list (or tuple) of pairs (lists or tuples): + # (id, text), where "id" is one of the possible values for the + # field, and "text" is the value as will be shown on the screen. + # You can use self.translate within this method to produce an + # internationalized version of "text" if needed. + self.methodName = methodName + + def getText(self, obj, value, appyType): + '''Gets the text that corresponds to p_value.''' + for v, text in appyType.getPossibleValues(obj, withTranslations=True): + if v == value: + return text + return value + +# ------------------------------------------------------------------------------ +class String(Field): + # Javascript files sometimes required by this type + jsFiles = {'edit': ('ckeditor/ckeditor.js',), + 'view': ('ckeditor/ckeditor.js',)} + + # Some predefined regular expressions that may be used as validators + c = re.compile + EMAIL = c('[a-zA-Z][\w\.-]*[a-zA-Z0-9]@[a-zA-Z0-9][\w\.-]*[a-zA-Z0-9]\.' \ + '[a-zA-Z][a-zA-Z\.]*[a-zA-Z]') + ALPHANUMERIC = c('[\w-]+') + URL = c('(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*(\.[a-z]{2,5})?' \ + '(([0-9]{1,5})?\/.*)?') + + # Some predefined functions that may also be used as validators + @staticmethod + def _MODULO_97(obj, value, complement=False): + '''p_value must be a string representing a number, like a bank account. + this function checks that the 2 last digits are the result of + computing the modulo 97 of the previous digits. Any non-digit + character is ignored. If p_complement is True, it does compute the + complement of modulo 97 instead of modulo 97. p_obj is not used; + it will be given by the Appy validation machinery, so it must be + specified as parameter. The function returns True if the check is + successful.''' + if not value: return True + # First, remove any non-digit char + v = '' + for c in value: + if digit.match(c): v += c + # There must be at least 3 digits for performing the check + if len(v) < 3: return False + # Separate the real number from the check digits + number = int(v[:-2]) + checkNumber = int(v[-2:]) + # Perform the check + if complement: + return (97 - (number % 97)) == checkNumber + else: + # The check number can't be 0. In this case, we force it to be 97. + # This is the way Belgian bank account numbers work. I hope this + # behaviour is general enough to be implemented here. + mod97 = (number % 97) + if mod97 == 0: return checkNumber == 97 + else: return checkNumber == mod97 + @staticmethod + def MODULO_97(obj, value): return String._MODULO_97(obj, value) + @staticmethod + def MODULO_97_COMPLEMENT(obj, value): + return String._MODULO_97(obj, value, True) + BELGIAN_ENTERPRISE_NUMBER = MODULO_97_COMPLEMENT + @staticmethod + def IBAN(obj, value): + '''Checks that p_value corresponds to a valid IBAN number. IBAN stands + for International Bank Account Number (ISO 13616). If the number is + valid, the method returns True.''' + if not value: return True + # First, remove any non-digit or non-letter char + v = '' + for c in value: + if alpha.match(c): v += c + # Maximum size is 34 chars + if (len(v) < 8) or (len(v) > 34): return False + # 2 first chars must be a valid country code + if not countries.exists(v[:2].upper()): return False + # 2 next chars are a control code whose value must be between 0 and 96. + try: + code = int(v[2:4]) + if (code < 0) or (code > 96): return False + except ValueError: + return False + # Perform the checksum + vv = v[4:] + v[:4] # Put the 4 first chars at the end. + nv = '' + for c in vv: + # Convert each letter into a number (A=10, B=11, etc) + # Ascii code for a is 65, so A=10 if we perform "minus 55" + if letter.match(c): nv += str(ord(c.upper()) - 55) + else: nv += c + return int(nv) % 97 == 1 + @staticmethod + def BIC(obj, value): + '''Checks that p_value corresponds to a valid BIC number. BIC stands + for Bank Identifier Code (ISO 9362). If the number is valid, the + method returns True.''' + if not value: return True + # BIC number must be 8 or 11 chars + if len(value) not in (8, 11): return False + # 4 first chars, representing bank name, must be letters + for c in value[:4]: + if not letter.match(c): return False + # 2 next chars must be a valid country code + if not countries.exists(value[4:6].upper()): return False + # Last chars represent some location within a country (a city, a + # province...). They can only be letters or figures. + for c in value[6:]: + if not alpha.match(c): return False + return True + + # Possible values for "format" + LINE = 0 + TEXT = 1 + XHTML = 2 + PASSWORD = 3 + CAPTCHA = 4 + def __init__(self, validator=None, multiplicity=(0,1), default=None, + format=LINE, show=True, page='main', group=None, layouts=None, + move=0, indexed=False, searchable=False, + specificReadPermission=False, specificWritePermission=False, + width=None, height=None, maxChars=None, colspan=1, master=None, + masterValue=None, focus=False, historized=False, mapping=None, + label=None, sdefault='', scolspan=1, swidth=None, sheight=None, + transform='none', styles=('p','h1','h2','h3','h4'), + allowImageUpload=True, inlineEdit=False): + # According to format, the widget will be different: input field, + # textarea, inline editor... Note that there can be only one String + # field of format CAPTCHA by page, because the captcha challenge is + # stored in the session at some global key. + self.format = format + self.isUrl = validator == String.URL + # When format is XHTML, the list of styles that the user will be able to + # select in the styles dropdown is defined hereafter. + self.styles = styles + # When format is XHTML, do we allow the user to upload images in it ? + self.allowImageUpload = allowImageUpload + # When format in XHTML, can the field be inline-edited (ckeditor)? + self.inlineEdit = inlineEdit + # The following field has a direct impact on the text entered by the + # user. It applies a transformation on it, exactly as does the CSS + # "text-transform" property. Allowed values are those allowed for the + # CSS property: "none" (default), "uppercase", "capitalize" or + # "lowercase". + self.transform = transform + Field.__init__(self, validator, multiplicity, default, show, page, + group, layouts, move, indexed, searchable, + specificReadPermission, specificWritePermission, width, + height, maxChars, colspan, master, masterValue, focus, + historized, True, mapping, label, sdefault, scolspan, + swidth, sheight) + self.isSelect = self.isSelection() + # If self.isSelect, self.sdefault must be a list of value(s). + if self.isSelect and not sdefault: + self.sdefault = [] + # Default width, height and maxChars vary according to String format + if width == None: + if format == String.TEXT: self.width = 60 + # This width corresponds to the standard width of an Appy page. + if format == String.XHTML: self.width = None + else: self.width = 30 + if height == None: + if format == String.TEXT: self.height = 5 + elif self.isSelect: self.height = 4 + else: self.height = 1 + if maxChars == None: + if self.isSelect: pass + elif format == String.LINE: self.maxChars = 256 + elif format == String.TEXT: self.maxChars = 9999 + elif format == String.XHTML: self.maxChars = 99999 + elif format == String.PASSWORD: self.maxChars = 20 + self.filterable = self.indexed and (self.format == String.LINE) and \ + not self.isSelect + self.swidth = self.swidth or self.width + self.sheight = self.sheight or self.height + + def isSelection(self): + '''Does the validator of this type definition define a list of values + into which the user must select one or more values?''' + res = True + if type(self.validator) in (list, tuple): + for elem in self.validator: + if not isinstance(elem, basestring): + res = False + break + else: + if not isinstance(self.validator, Selection): + res = False + return res + + def getDefaultLayouts(self): + '''Returns the default layouts for this type. Default layouts can vary + acccording to format, multiplicity or history.''' + if self.format == String.TEXT: + return {'view': 'l-f', 'edit': 'lrv-d-f'} + elif self.format == String.XHTML: + if self.historized: + # self.historized can be a method or a boolean. If it is a + # method, it means that under some condition, historization will + # be enabled. So we come here also in this case. + view = 'lc-f' + else: + view = 'l-f' + return {'view': Table(view, width='100%'), 'edit': 'lrv-d-f'} + elif self.isMultiValued(): + return {'view': 'l-f', 'edit': 'lrv-f'} + + def getValue(self, obj): + # Cheat if this field represents p_obj's state + if self.name == 'state': return obj.State() + value = Field.getValue(self, obj) + if not value: + if self.isMultiValued(): return emptyTuple + else: return value + if isinstance(value, basestring) and self.isMultiValued(): + value = [value] + elif isinstance(value, tuple): + value = list(value) + return value + + def store(self, obj, value): + '''When the value is XHTML, we perform some cleanup.''' + if (self.format == String.XHTML) and value: + # When image upload is allowed, ckeditor inserts some "style" attrs + # (ie for image size when images are resized). So in this case we + # can't remove style-related information. + try: + value = XhtmlCleaner(keepStyles=False).clean(value) + except XhtmlCleaner.Error, e: + # Errors while parsing p_value can't prevent the user from + # storing it. + obj.log('Unparsable XHTML content in field "%s".' % self.name, + type='warning') + Field.store(self, obj, value) + + def getDiffValue(self, obj, value): + '''Returns a version of p_value that includes the cumulative diffs + between successive versions.''' + res = None + lastEvent = None + for event in obj.workflow_history.values()[0]: + if event['action'] != '_datachange_': continue + if self.name not in event['changes']: continue + if res == None: + # We have found the first version of the field + res = event['changes'][self.name][0] or '' + else: + # We need to produce the difference between current result and + # this version. + iMsg, dMsg = obj.getHistoryTexts(lastEvent) + thisVersion = event['changes'][self.name][0] or '' + comparator = HtmlDiff(res, thisVersion, iMsg, dMsg) + res = comparator.get() + lastEvent = event + # Now we need to compare the result with the current version. + iMsg, dMsg = obj.getHistoryTexts(lastEvent) + comparator = HtmlDiff(res, value or '', iMsg, dMsg) + return comparator.get() + + def getFormattedValue(self, obj, value, showChanges=False): + if self.isEmptyValue(value): return '' + res = value + if self.isSelect: + if isinstance(self.validator, Selection): + # Value(s) come from a dynamic vocabulary + val = self.validator + if self.isMultiValued(): + return [val.getText(obj, v, self) for v in value] + else: + return val.getText(obj, value, self) + else: + # Value(s) come from a fixed vocabulary whose texts are in + # i18n files. + t = obj.translate + if self.isMultiValued(): + res = [t('%s_list_%s' % (self.labelId, v)) for v in value] + else: + res = t('%s_list_%s' % (self.labelId, value)) + elif (self.format == String.XHTML) and showChanges: + # Compute the successive changes that occurred on p_value. + res = self.getDiffValue(obj, res) + # If value starts with a carriage return, add a space; else, it will + # be ignored. + if isinstance(res, basestring) and \ + (res.startswith('\n') or res.startswith('\r\n')): res = ' ' + res + return res + + emptyStringTuple = ('',) + emptyValuesCatalogIgnored = (None, '') + def getIndexValue(self, obj, forSearch=False): + '''For indexing purposes, we return only strings, not unicodes.''' + res = Field.getIndexValue(self, obj, forSearch) + if isinstance(res, unicode): + res = res.encode('utf-8') + if res and forSearch and (self.format == String.XHTML): + # Convert the value to simple text. + extractor = XhtmlTextExtractor(raiseOnError=False) + res = extractor.parse('

%s

' % res) + # Ugly catalog: if I give an empty tuple as index value, it keeps the + # previous value. If I give him a tuple containing an empty string, it + # is ok. + if isinstance(res, tuple) and not res: res = self.emptyStringTuple + # Ugly catalog: if value is an empty string or None, it keeps the + # previous index value. + if res in self.emptyValuesCatalogIgnored: res = ' ' + return res + + def getPossibleValues(self,obj,withTranslations=False,withBlankValue=False): + '''Returns the list of possible values for this field if it is a + selection field. If p_withTranslations is True, + instead of returning a list of string values, the result is a list + of tuples (s_value, s_translation). If p_withBlankValue is True, a + blank value is prepended to the list, excepted if the type is + multivalued.''' + if not self.isSelect: raise 'This field is not a selection.' + if isinstance(self.validator, Selection): + # We need to call self.methodName for getting the (dynamic) values. + # If methodName begins with _appy_, it is a special Appy method: + # we will call it on the Mixin (=p_obj) directly. Else, it is a + # user method: we will call it on the wrapper (p_obj.appy()). Some + # args can be hidden into p_methodName, separated with stars, + # like in this example: method1*arg1*arg2. Only string params are + # supported. + methodName = self.validator.methodName + # Unwrap parameters if any. + if methodName.find('*') != -1: + elems = methodName.split('*') + methodName = elems[0] + args = elems[1:] + else: + args = () + # On what object must we call the method that will produce the + # values? + if methodName.startswith('tool:'): + obj = obj.getTool() + methodName = methodName[5:] + # Do we need to call the method on the object or on the wrapper? + if methodName.startswith('_appy_'): + exec 'res = obj.%s(*args)' % methodName + else: + exec 'res = obj.appy().%s(*args)' % methodName + if not withTranslations: res = [v[0] for v in res] + elif isinstance(res, list): res = res[:] + else: + # The list of (static) values is directly given in self.validator. + res = [] + for value in self.validator: + label = '%s_list_%s' % (self.labelId, value) + if withTranslations: + res.append( (value, obj.translate(label)) ) + else: + res.append(value) + if withBlankValue and not self.isMultiValued(): + # Create the blank value to insert at the beginning of the list + if withTranslations: + blankValue = ('', obj.translate('choose_a_value')) + else: + blankValue = '' + # Insert the blank value in the result + if isinstance(res, tuple): + res = (blankValue,) + res + else: + res.insert(0, blankValue) + return res + + def validateValue(self, obj, value): + if self.format == String.CAPTCHA: + challenge = obj.REQUEST.SESSION.get('captcha', None) + # Compute the challenge minus the char to remove + i = challenge['number']-1 + text = challenge['text'][:i] + challenge['text'][i+1:] + if value != text: + return obj.translate('bad_captcha') + elif self.isSelect: + # Check that the value is among possible values + possibleValues = self.getPossibleValues(obj) + if isinstance(value, basestring): + error = value not in possibleValues + else: + error = False + for v in value: + if v not in possibleValues: + error = True + break + if error: return obj.translate('bad_select_value') + + accents = {'é':'e','è':'e','ê':'e','ë':'e','à':'a','â':'a','ä':'a', + 'ù':'u','û':'u','ü':'u','î':'i','ï':'i','ô':'o','ö':'o', + 'ç':'c', 'Ç':'C', + 'Ù':'U','Û':'U','Ü':'U','Î':'I','Ï':'I','Ô':'O','Ö':'O', + 'É':'E','È':'E','Ê':'E','Ë':'E','À':'A','Â':'A','Ä':'A'} + def applyTransform(self, value): + '''Applies a transform as required by self.transform on single + value p_value.''' + if self.transform in ('uppercase', 'lowercase'): + # For those transforms, I will remove any accent, because + # (1) 'é'.upper() or 'Ê'.lower() has no effect; + # (2) most of the time, if the user wants to apply such effect, it + # is for ease of data manipulation, so I guess without accent. + for c, n in self.accents.iteritems(): + if c in value: value = value.replace(c, n) + # Apply the transform + if self.transform == 'lowercase': return value.lower() + elif self.transform == 'uppercase': return value.upper() + elif self.transform == 'capitalize': return value.capitalize() + return value + + def getStorableValue(self, value): + isString = isinstance(value, basestring) + # Apply transform if required + if isString and not self.isEmptyValue(value) and \ + (self.transform != 'none'): + value = self.applyTransform(value) + # Truncate the result if longer than self.maxChars + if isString and self.maxChars and (len(value) > self.maxChars): + value = value[:self.maxChars] + # Get a multivalued value if required. + if value and self.isMultiValued() and \ + (type(value) not in sutils.sequenceTypes): + value = [value] + return value + + def getIndexType(self): + '''Index type varies depending on String parameters.''' + # If String.isSelect, be it multivalued or not, we define a ListIndex: + # this way we can use AND/OR operator. + if self.isSelect: + return 'ListIndex' + elif self.format == String.TEXT: + return 'TextIndex' + elif self.format == String.XHTML: + return 'XhtmlIndex' + return Field.getIndexType(self) + + def getJs(self, layoutType, res): + if self.format == String.XHTML: Field.getJs(self, layoutType, res) + + def getCaptchaChallenge(self, session): + '''Returns a Captcha challenge in the form of a dict. At key "text", + value is a string that the user will be required to re-type, but + without 1 character whose position is at key "number". The challenge + is stored in the p_session, for the server-side subsequent check.''' + length = random.randint(5, 9) # The length of the challenge to encode + number = random.randint(1, length) # The position of the char to remove + text = '' # The challenge the user needs to type (minus one char) + for i in range(length): + j = random.randint(0, 1) + chars = (j == 0) and digits or letters + # Choose a char + text += chars[random.randint(0,len(chars)-1)] + res = {'text': text, 'number': number} + session['captcha'] = res + return res + + def generatePassword(self): + '''Generates a password (we recycle here the captcha challenge + generator).''' + return self.getCaptchaChallenge({})['text'] +# ------------------------------------------------------------------------------ diff --git a/gen/__init__.py b/gen/__init__.py index a37c3db..666b9f9 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -1,275 +1,32 @@ -# -*- coding: utf-8 -*- # ------------------------------------------------------------------------------ -import re, time, copy, sys, types, os, os.path, mimetypes, string, StringIO, \ - random -from appy import Object -from appy.gen.layout import Table -from appy.gen.layout import defaultFieldLayouts +import types, string from appy.gen.mail import sendNotification -from appy.gen.indexer import defaultIndexes, XhtmlTextExtractor +from appy.gen.indexer import defaultIndexes from appy.gen import utils as gutils -import appy.pod -from appy.pod.renderer import Renderer -from appy.shared.data import countries -from appy.shared.xml_parser import XhtmlCleaner -from appy.shared.diff import HtmlDiff from appy.shared import utils as sutils +# ------------------------------------------------------------------------------ +# Import stuff from appy.fields (and from a few other places too). +# This way, when an app gets "from appy.gen import *", everything is available. +# ------------------------------------------------------------------------------ +from appy.fields import Page, Group, Field, Column, No +from appy.fields.action import Action +from appy.fields.boolean import Boolean +from appy.fields.computed import Computed +from appy.fields.date import Date +from appy.fields.file import File +from appy.fields.float import Float +from appy.fields.info import Info +from appy.fields.integer import Integer +from appy.fields.list import List +from appy.fields.pod import Pod +from appy.fields.ref import Ref, autoref +from appy.fields.string import String, Selection +from appy.gen.layout import Table +from appy import Object + # Default Appy permissions ----------------------------------------------------- r, w, d = ('read', 'write', 'delete') -digit = re.compile('[0-9]') -alpha = re.compile('[a-zA-Z0-9]') -letter = re.compile('[a-zA-Z]') -nullValues = (None, '', []) -validatorTypes = (types.FunctionType, types.UnboundMethodType, - type(re.compile(''))) -emptyTuple = () -labelTypes = ('label', 'descr', 'help') - -def initMasterValue(v): - '''Standardizes p_v as a list of strings.''' - if not isinstance(v, bool) and not v: res = [] - elif type(v) not in sutils.sequenceTypes: res = [v] - else: res = v - return [str(v) for v in res] - -# Descriptor classes used for refining descriptions of elements in types -# (pages, groups,...) ---------------------------------------------------------- -class Page: - '''Used for describing a page, its related phase, show condition, etc.''' - subElements = ('save', 'cancel', 'previous', 'next', 'edit') - def __init__(self, name, phase='main', show=True, showSave=True, - showCancel=True, showPrevious=True, showNext=True, - showEdit=True): - self.name = name - self.phase = phase - self.show = show - # When editing the page, must I show the "save" button? - self.showSave = showSave - # When editing the page, must I show the "cancel" button? - self.showCancel = showCancel - # When editing the page, and when a previous page exists, must I show - # the "previous" button? - self.showPrevious = showPrevious - # When editing the page, and when a next page exists, must I show the - # "next" button? - self.showNext = showNext - # When viewing the page, must I show the "edit" button? - self.showEdit = showEdit - - @staticmethod - def get(pageData): - '''Produces a Page instance from p_pageData. User-defined p_pageData - can be: - (a) a string containing the name of the page; - (b) a string containing _; - (c) a Page instance. - This method returns always a Page instance.''' - res = pageData - if res and isinstance(res, basestring): - # Page data is given as a string. - pageElems = pageData.rsplit('_', 1) - if len(pageElems) == 1: # We have case (a) - res = Page(pageData) - else: # We have case (b) - res = Page(pageData[0], phase=pageData[1]) - return res - - def isShowable(self, obj, layoutType, elem='page'): - '''Must this page be shown for p_obj? "Show value" can be True, False - or 'view' (page is available only in "view" mode). - - If p_elem is not "page", this method returns the fact that a - sub-element is viewable or not (buttons "save", "cancel", etc).''' - # Define what attribute to test for "showability". - showAttr = 'show' - if elem != 'page': - showAttr = 'show%s' % elem.capitalize() - # Get the value of the show attribute as identified above. - show = getattr(self, showAttr) - if callable(show): - show = show(obj.appy()) - # Show value can be 'view', for example. Thanks to p_layoutType, - # convert show value to a real final boolean value. - res = show - if res == 'view': res = layoutType == 'view' - return res - - def getInfo(self, obj, layoutType): - '''Gets information about this page, for p_obj, as a dict.''' - res = {} - for elem in Page.subElements: - res['show%s' % elem.capitalize()] = self.isShowable(obj, layoutType, - elem=elem) - return res - -class Group: - '''Used for describing a group of widgets within a page.''' - def __init__(self, name, columns=['100%'], wide=True, style='section2', - hasLabel=True, hasDescr=False, hasHelp=False, - hasHeaders=False, group=None, colspan=1, align='center', - valign='top', css_class='', master=None, masterValue=None, - cellpadding=1, cellspacing=1, cellgap='0.6em', label=None, - translated=None): - self.name = name - # In its simpler form, field "columns" below can hold a list or tuple - # of column widths expressed as strings, that will be given as is in - # the "width" attributes of the corresponding "td" tags. Instead of - # strings, within this list or tuple, you may give Column instances - # (see below). - self.columns = columns - self._setColumns() - # If field "wide" below is True, the HTML table corresponding to this - # group will have width 100%. You can also specify some string value, - # which will be used for HTML param "width". - if wide == True: - self.wide = '100%' - elif isinstance(wide, basestring): - self.wide = wide - else: - self.wide = '' - # If style = 'fieldset', all widgets within the group will be rendered - # within an HTML fieldset. If style is 'section1' or 'section2', widgets - # will be rendered after the group title. - self.style = style - # If hasLabel is True, the group will have a name and the corresponding - # i18n label will be generated. - self.hasLabel = hasLabel - # If hasDescr is True, the group will have a description and the - # corresponding i18n label will be generated. - self.hasDescr = hasDescr - # If hasHelp is True, the group will have a help text associated and the - # corresponding i18n label will be generated. - self.hasHelp = hasHelp - # If hasheaders is True, group content will begin with a row of headers, - # and a i18n label will be generated for every header. - self.hasHeaders = hasHeaders - self.nbOfHeaders = len(columns) - # If this group is himself contained in another group, the following - # attribute is filled. - self.group = Group.get(group) - # If the group is rendered into another group, we can specify the number - # of columns that this group will span. - self.colspan = colspan - self.align = align - self.valign = valign - self.cellpadding = cellpadding - self.cellspacing = cellspacing - # Beyond standard cellpadding and cellspacing, cellgap can define an - # additional horizontal gap between cells in a row. So this value does - # not add space before the first cell or after the last one. - self.cellgap = cellgap - if style == 'tabs': - # Group content will be rendered as tabs. In this case, some - # param combinations have no sense. - self.hasLabel = self.hasDescr = self.hasHelp = False - # The rendering is forced to a single column - self.columns = self.columns[:1] - # Header labels will be used as labels for the tabs. - self.hasHeaders = True - self.css_class = css_class - self.master = master - self.masterValue = initMasterValue(masterValue) - if master: master.slaves.append(self) - self.label = label # See similar attr of Type class. - # If a translated name is already given here, we will use it instead of - # trying to translate the group label. - self.translated = translated - - def _setColumns(self): - '''Standardizes field "columns" as a list of Column instances. Indeed, - the initial value for field "columns" may be a list or tuple of - Column instances or strings.''' - for i in range(len(self.columns)): - columnData = self.columns[i] - if not isinstance(columnData, Column): - self.columns[i] = Column(self.columns[i]) - - @staticmethod - def get(groupData): - '''Produces a Group instance from p_groupData. User-defined p_groupData - can be a string or a Group instance; this method returns always a - Group instance.''' - res = groupData - if res and isinstance(res, basestring): - # Group data is given as a string. 2 more possibilities: - # (a) groupData is simply the name of the group; - # (b) groupData is of the form _. - groupElems = groupData.rsplit('_', 1) - if len(groupElems) == 1: - res = Group(groupElems[0]) - else: - try: - nbOfColumns = int(groupElems[1]) - except ValueError: - nbOfColumns = 1 - width = 100.0 / nbOfColumns - res = Group(groupElems[0], ['%.2f%%' % width] * nbOfColumns) - return res - - def getMasterData(self): - '''Gets the master of this group (and masterValue) or, recursively, of - containing groups when relevant.''' - if self.master: return (self.master, self.masterValue) - if self.group: return self.group.getMasterData() - - def generateLabels(self, messages, classDescr, walkedGroups, - forSearch=False): - '''This method allows to generate all the needed i18n labels related to - this group. p_messages is the list of i18n p_messages (a PoMessages - instance) that we are currently building; p_classDescr is the - descriptor of the class where this group is defined. If p_forSearch - is True, this group is used for grouping searches, and not fields.''' - # A part of the group label depends on p_forSearch. - if forSearch: gp = 'searchgroup' - else: gp = 'group' - if self.hasLabel: - msgId = '%s_%s_%s' % (classDescr.name, gp, self.name) - messages.append(msgId, self.name) - if self.hasDescr: - msgId = '%s_%s_%s_descr' % (classDescr.name, gp, self.name) - messages.append(msgId, ' ', nice=False) - if self.hasHelp: - msgId = '%s_%s_%s_help' % (classDescr.name, gp, self.name) - messages.append(msgId, ' ', nice=False) - if self.hasHeaders: - for i in range(self.nbOfHeaders): - msgId = '%s_%s_%s_col%d' % (classDescr.name, gp, self.name, i+1) - messages.append(msgId, ' ', nice=False) - walkedGroups.add(self) - if self.group and (self.group not in walkedGroups) and \ - not self.group.label: - # We remember walked groups for avoiding infinite recursion. - self.group.generateLabels(messages, classDescr, walkedGroups, - forSearch=forSearch) - - def insertInto(self, widgets, groupDescrs, page, metaType, forSearch=False): - '''Inserts the GroupDescr instance corresponding to this Group instance - into p_widgets, the recursive structure used for displaying all - widgets in a given p_page (or all searches), and returns this - GroupDescr instance.''' - # First, create the corresponding GroupDescr if not already in - # p_groupDescrs. - if self.name not in groupDescrs: - groupDescr = groupDescrs[self.name] = gutils.GroupDescr(\ - self, page, metaType, forSearch=forSearch).get() - # Insert the group at the higher level (ie, directly in p_widgets) - # if the group is not itself in a group. - if not self.group: - widgets.append(groupDescr) - else: - outerGroupDescr = self.group.insertInto(widgets, groupDescrs, - page, metaType, forSearch=forSearch) - gutils.GroupDescr.addWidget(outerGroupDescr, groupDescr) - else: - groupDescr = groupDescrs[self.name] - return groupDescr - -class Column: - '''Used for describing a column within a Group like defined above.''' - def __init__(self, width, align="left"): - self.width = width - self.align = align class Import: '''Used for describing the place where to find the data to use for creating @@ -298,7 +55,7 @@ class Import: self.sort = sort class Search: - '''Used for specifying a search for a given type.''' + '''Used for specifying a search for a given class.''' def __init__(self, name, group=None, sortBy='', sortOrder='asc', limit=None, default=False, colspan=1, translated=None, show=True, translatedDescr=None, **fields): @@ -415,2178 +172,6 @@ class Search: return gutils.callMethod(tool, self.show, klass=klass) return self.show -# ------------------------------------------------------------------------------ -class Type: - '''Basic abstract class for defining any appy type.''' - # Those attributes can be overridden by subclasses for defining, - # respectively, names of CSS and Javascript files that are required by this - # field, keyed by layoutType. - cssFiles = {} - jsFiles = {} - dLayouts = 'lrv-d-f' - - def __init__(self, validator, multiplicity, default, show, page, group, - layouts, move, indexed, searchable, specificReadPermission, - specificWritePermission, width, height, maxChars, colspan, - master, masterValue, focus, historized, sync, mapping, label, - sdefault, scolspan, swidth, sheight): - # The validator restricts which values may be defined. It can be an - # interval (1,None), a list of string values ['choice1', 'choice2'], - # a regular expression, a custom function, a Selection instance, etc. - self.validator = validator - # Multiplicity is a 2-tuple indicating the minimum and maximum - # occurrences of values. - self.multiplicity = multiplicity - # Is the field required or not ? (derived from multiplicity) - self.required = self.multiplicity[0] > 0 - # Default value - self.default = default - # Must the field be visible or not? - self.show = show - # When displaying/editing the whole object, on what page and phase must - # this field value appear? - self.page = Page.get(page) - self.pageName = self.page.name - # Within self.page, in what group of fields must this one appear? - self.group = Group.get(group) - # The following attribute allows to move a field back to a previous - # position (useful for moving fields above predefined ones). - self.move = move - # If indexed is True, a database index will be set on the field for - # fast access. - self.indexed = indexed - # If specified "searchable", the field will be added to some global - # index allowing to perform application-wide, keyword searches. - self.searchable = searchable - # Normally, permissions to read or write every attribute in a type are - # granted if the user has the global permission to read or - # edit instances of the whole type. If you want a given attribute - # to be protected by specific permissions, set one or the 2 next boolean - # values to "True". In this case, you will create a new "field-only" - # read and/or write permission. If you need to protect several fields - # with the same read/write permission, you can avoid defining one - # specific permission for every field by specifying a "named" - # permission (string) instead of assigning "True" to the following - # arg(s). A named permission will be global to your whole Zope site, so - # take care to the naming convention. Typically, a named permission is - # of the form: ": Write|Read ---". If, for example, I want - # to define, for my application "MedicalFolder" a specific permission - # for a bunch of fields that can only be modified by a doctor, I can - # define a permission "MedicalFolder: Write medical information" and - # assign it to the "specificWritePermission" of every impacted field. - self.specificReadPermission = specificReadPermission - self.specificWritePermission = specificWritePermission - # Widget width and height - self.width = width - self.height = height - # While width and height refer to widget dimensions, maxChars hereafter - # represents the maximum number of chars that a given input field may - # accept (corresponds to HTML "maxlength" property). "None" means - # "unlimited". - self.maxChars = maxChars - # If the widget is in a group with multiple columns, the following - # attribute specifies on how many columns to span the widget. - self.colspan = colspan - # The list of slaves of this field, if it is a master - self.slaves = [] - # The behaviour of this field may depend on another, "master" field - self.master = master - if master: self.master.slaves.append(self) - # When master has some value(s), there is impact on this field. - self.masterValue = initMasterValue(masterValue) - # If a field must retain attention in a particular way, set focus=True. - # It will be rendered in a special way. - self.focus = focus - # If we must keep track of changes performed on a field, "historized" - # must be set to True. - self.historized = historized - # self.sync below determines if the field representations will be - # retrieved in a synchronous way by the browser or not (Ajax). - self.sync = self.formatSync(sync) - # Mapping is a dict of contexts that, if specified, are given when - # translating the label, descr or help related to this field. - self.mapping = self.formatMapping(mapping) - self.id = id(self) - self.type = self.__class__.__name__ - self.pythonType = None # The True corresponding Python type - # Get the layouts. Consult layout.py for more info about layouts. - self.layouts = self.formatLayouts(layouts) - # Can we filter this field? - self.filterable = False - # Can this field have values that can be edited and validated? - self.validable = True - # The base label for translations is normally generated automatically. - # It is made of 2 parts: the prefix, based on class name, and the name, - # which is the field name by default. You can change this by specifying - # a value for param "label". If this value is a string, it will be - # understood as a new prefix. If it is a tuple, it will represent the - # prefix and another name. If you want to specify a new name only, and - # not a prefix, write (None, newName). - self.label = label - # When you specify a default value "for search" (= "sdefault"), on a - # search screen, in the search field corresponding to this field, this - # default value will be present. - self.sdefault = sdefault - # Colspan for rendering the search widget corresponding to this field. - self.scolspan = scolspan - # Width and height for the search widget - self.swidth = swidth or width - self.sheight = sheight or height - - def init(self, name, klass, appName): - '''When the application server starts, this secondary constructor is - called for storing the name of the Appy field (p_name) and other - attributes that are based on the name of the Appy p_klass, and the - application name (p_appName).''' - if hasattr(self, 'name'): return # Already initialized - self.name = name - # Determine prefix for this class - if not klass: prefix = appName - else: prefix = gutils.getClassName(klass, appName) - # Recompute the ID (and derived attributes) that may have changed if - # we are in debug mode (because we recreate new Type instances). - self.id = id(self) - # Remember master name on every slave - for slave in self.slaves: slave.masterName = name - # Determine ids of i18n labels for this field - labelName = name - trPrefix = None - if self.label: - if isinstance(self.label, basestring): trPrefix = self.label - else: # It is a tuple (trPrefix, name) - if self.label[1]: labelName = self.label[1] - if self.label[0]: trPrefix = self.label[0] - if not trPrefix: - trPrefix = prefix - # Determine name to use for i18n - self.labelId = '%s_%s' % (trPrefix, labelName) - self.descrId = self.labelId + '_descr' - self.helpId = self.labelId + '_help' - # Determine read and write permissions for this field - rp = self.specificReadPermission - if rp and not isinstance(rp, basestring): - self.readPermission = '%s: Read %s %s' % (appName, prefix, name) - elif rp and isinstance(rp, basestring): - self.readPermission = rp - else: - self.readPermission = 'View' - wp = self.specificWritePermission - if wp and not isinstance(wp, basestring): - self.writePermission = '%s: Write %s %s' % (appName, prefix, name) - elif wp and isinstance(wp, basestring): - self.writePermission = wp - else: - self.writePermission = 'Modify portal content' - if isinstance(self, Ref) and not self.isBack: - # We must initialise the corresponding back reference - self.back.klass = klass - self.back.init(self.back.attribute, self.klass, appName) - if 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) - - def reload(self, klass, obj): - '''In debug mode, we want to reload layouts without restarting Zope. - So this method will prepare a "new", reloaded version of p_self, - that corresponds to p_self after a "reload" of its containing Python - module has been performed.''' - res = getattr(klass, self.name, None) - if not res: return self - if isinstance(self, Ref) and self.isBack: return self - res.init(self.name, klass, obj.getProductConfig().PROJECTNAME) - return res - - def isMultiValued(self): - '''Does this type definition allow to define multiple values?''' - res = False - maxOccurs = self.multiplicity[1] - if (maxOccurs == None) or (maxOccurs > 1): - res = True - return res - - def isSortable(self, usage): - '''Can fields of this type be used for sorting purposes (when sorting - search results (p_usage="search") or when sorting reference fields - (p_usage="ref")?''' - if usage == 'search': - return self.indexed and not self.isMultiValued() and not \ - ((self.type == 'String') and self.isSelection()) - elif usage == 'ref': - return self.type in ('Integer', 'Float', 'Boolean', 'Date') or \ - ((self.type == 'String') and (self.format == 0)) - - def isShowable(self, obj, layoutType): - '''When displaying p_obj on a given p_layoutType, must we show this - field?''' - # Check if the user has the permission to view or edit the field - if layoutType == 'edit': perm = self.writePermission - else: perm = self.readPermission - if not obj.allows(perm): return False - # Evaluate self.show - if callable(self.show): - res = self.callMethod(obj, self.show) - else: - res = self.show - # Take into account possible values 'view', 'edit', 'result'... - if type(res) in sutils.sequenceTypes: - for r in res: - if r == layoutType: return True - return False - elif res in ('view', 'edit', 'result'): - return res == layoutType - return bool(res) - - def isClientVisible(self, obj): - '''This method returns True if this field is visible according to - master/slave relationships.''' - masterData = self.getMasterData() - if not masterData: return True - else: - master, masterValue = masterData - reqValue = master.getRequestValue(obj.REQUEST) - # reqValue can be a list or not - if type(reqValue) not in sutils.sequenceTypes: - return reqValue in masterValue - else: - for m in masterValue: - for r in reqValue: - if m == r: return True - - def formatSync(self, sync): - '''Creates a dictionary indicating, for every layout type, if the field - value must be retrieved synchronously or not.''' - if isinstance(sync, bool): - sync = {'edit': sync, 'view': sync, 'cell': sync} - for layoutType in ('edit', 'view', 'cell'): - if layoutType not in sync: - sync[layoutType] = False - return sync - - def formatMapping(self, mapping): - '''Creates a dict of mappings, one entry by label type (label, descr, - help).''' - if isinstance(mapping, dict): - # Is it a dict like {'label':..., 'descr':...}, or is it directly a - # dict with a mapping? - for k, v in mapping.iteritems(): - if (k not in labelTypes) or isinstance(v, basestring): - # It is already a mapping - return {'label':mapping, 'descr':mapping, 'help':mapping} - # If we are here, we have {'label':..., 'descr':...}. Complete - # it if necessary. - for labelType in labelTypes: - if labelType not in mapping: - mapping[labelType] = None # No mapping for this value. - return mapping - else: - # Mapping is a method that must be applied to any i18n message. - return {'label':mapping, 'descr':mapping, 'help':mapping} - - def formatLayouts(self, layouts): - '''Standardizes the given p_layouts. .''' - # First, get the layouts as a dictionary, if p_layouts is None or - # expressed as a simple string. - areDefault = False - if not layouts: - # Get the default layouts as defined by the subclass - areDefault = True - layouts = self.computeDefaultLayouts() - else: - if isinstance(layouts, basestring): - # The user specified a single layoutString (the "edit" one) - layouts = {'edit': layouts} - elif isinstance(layouts, Table): - # Idem, but with a Table instance - layouts = {'edit': Table(other=layouts)} - else: - # Here, we make a copy of the layouts, because every layout can - # be different, even if the user decides to reuse one from one - # field to another. This is because we modify every layout for - # adding master/slave-related info, focus-related info, etc, - # which can be different from one field to the other. - layouts = copy.deepcopy(layouts) - if 'edit' not in layouts: - defEditLayout = self.computeDefaultLayouts() - if type(defEditLayout) == dict: - defEditLayout = defEditLayout['edit'] - layouts['edit'] = defEditLayout - # We have now a dict of layouts in p_layouts. Ensure now that a Table - # instance is created for every layout (=value from the dict). Indeed, - # a layout could have been expressed as a simple layout string. - for layoutType in layouts.iterkeys(): - if isinstance(layouts[layoutType], basestring): - layouts[layoutType] = Table(layouts[layoutType]) - # Derive "view" and "cell" layouts from the "edit" layout when relevant - if 'view' not in layouts: - layouts['view'] = Table(other=layouts['edit'], derivedType='view') - # Create the "cell" layout from the 'view' layout if not specified. - if 'cell' not in layouts: - layouts['cell'] = Table(other=layouts['view'], derivedType='cell') - # Put the required CSS classes in the layouts - layouts['cell'].addCssClasses('noStyle') - if self.focus: - # We need to make it flashy - layouts['view'].addCssClasses('focus') - layouts['edit'].addCssClasses('focus') - # If layouts are the default ones, set width=None instead of width=100% - # for the field if it is not in a group (excepted for rich texts). - if areDefault and not self.group and \ - not ((self.type == 'String') and (self.format == String.XHTML)): - for layoutType in layouts.iterkeys(): - layouts[layoutType].width = '' - # Remove letters "r" from the layouts if the field is not required. - if not self.required: - for layoutType in layouts.iterkeys(): - layouts[layoutType].removeElement('r') - # Derive some boolean values from the layouts. - self.hasLabel = self.hasLayoutElement('l', layouts) - self.hasDescr = self.hasLayoutElement('d', layouts) - self.hasHelp = self.hasLayoutElement('h', layouts) - # Store Table instance's dicts instead of instances: this way, they can - # be manipulated in ZPTs. - for layoutType in layouts.iterkeys(): - layouts[layoutType] = layouts[layoutType].get() - return layouts - - def hasLayoutElement(self, element, layouts): - '''This method returns True if the given layout p_element can be found - at least once among the various p_layouts defined for this field.''' - for layout in layouts.itervalues(): - if element in layout.layoutString: return True - return False - - def getDefaultLayouts(self): - '''Any subclass can define this for getting a specific set of - default layouts. If None is returned, a global set of default layouts - will be used.''' - - def getInputLayouts(self): - '''Gets, as a string, the layouts as could have been specified as input - value for the Type constructor.''' - res = '{' - for k, v in self.layouts.iteritems(): - res += '"%s":"%s",' % (k, v['layoutString']) - res += '}' - return res - - def computeDefaultLayouts(self): - '''This method gets the default layouts from an Appy type, or a copy - from the global default field layouts when they are not available.''' - res = self.getDefaultLayouts() - if not res: - # Get the global default layouts - res = copy.deepcopy(defaultFieldLayouts) - return res - - def getCss(self, layoutType, res): - '''This method completes the list p_res with the names of CSS files - that are required for displaying widgets of self's type on a given - p_layoutType. p_res is not a set because order of inclusion of CSS - files may be important and may be loosed by using sets.''' - if layoutType in self.cssFiles: - for fileName in self.cssFiles[layoutType]: - if fileName not in res: - res.append(fileName) - - def getJs(self, layoutType, res): - '''This method completes the list p_res with the names of Javascript - files that are required for displaying widgets of self's type on a - given p_layoutType. p_res is not a set because order of inclusion of - CSS files may be important and may be loosed by using sets.''' - if layoutType in self.jsFiles: - for fileName in self.jsFiles[layoutType]: - if fileName not in res: - res.append(fileName) - - def getValue(self, obj): - '''Gets, on_obj, the value conforming to self's type definition.''' - value = getattr(obj.aq_base, self.name, None) - if self.isEmptyValue(value): - # If there is no value, get the default value if any: return - # self.default, of self.default() if it is a method. - if callable(self.default): - try: - return self.callMethod(obj, self.default) - except Exception, e: - # Already logged. Here I do not raise the exception, - # because it can be raised as the result of reindexing - # the object in situations that are not foreseen by - # method in self.default. - return None - else: - return self.default - return value - - def getFormattedValue(self, obj, value, showChanges=False): - '''p_value is a real p_obj(ect) value from a field from this type. This - method returns a pretty, string-formatted version, for displaying - purposes. Needs to be overridden by some child classes. If - p_showChanges is True, the result must also include the changes that - occurred on p_value across the ages.''' - if self.isEmptyValue(value): return '' - return value - - def getIndexType(self): - '''Returns the name of the technical, Zope-level index type for this - field.''' - # Normally, self.indexed contains a Boolean. If a string value is given, - # we consider it to be an index type. It allows to bypass the standard - # way to decide what index type must be used. - if isinstance(self.indexed, str): return self.indexed - if self.name == 'title': return 'TextIndex' - return 'FieldIndex' - - def getIndexValue(self, obj, forSearch=False): - '''This method returns a version for this field value on p_obj that is - ready for indexing purposes. Needs to be overridden by some child - classes. - - If p_forSearch is True, it will return a "string" version of the - index value suitable for a global search.''' - value = self.getValue(obj) - if forSearch and (value != None): - if isinstance(value, unicode): - res = value.encode('utf-8') - elif type(value) in sutils.sequenceTypes: - res = [] - for v in value: - if isinstance(v, unicode): res.append(v.encode('utf-8')) - else: res.append(str(v)) - res = ' '.join(res) - else: - res = str(value) - return res - return value - - def getRequestValue(self, request, requestName=None): - '''Gets a value for this field as carried in the request object. In the - simplest cases, the request value is a single value whose name in the - request is the name of the field. - - Sometimes (ie: a Date: see the overriden method in the Date class), - several request values must be combined. - - Sometimes (ie, a field which is a sub-field in a List), the name of - the request value(s) representing the field value do not correspond - to the field name (ie: the request name includes information about - the container field). In this case, p_requestName must be used for - searching into the request, instead of the field name (self.name).''' - name = requestName or self.name - return request.get(name, None) - - def getStorableValue(self, value): - '''p_value is a valid value initially computed through calling - m_getRequestValue. So, it is a valid string (or list of strings) - representation of the field value coming from the request. - This method computes the real (potentially converted or manipulated - in some other way) value as can be stored in the database.''' - if self.isEmptyValue(value): return None - return value - - def getMasterData(self): - '''Gets the master of this field (and masterValue) or, recursively, of - containing groups when relevant.''' - if self.master: return (self.master, self.masterValue) - if self.group: return self.group.getMasterData() - - def isEmptyValue(self, value, obj=None): - '''Returns True if the p_value must be considered as an empty value.''' - return value in nullValues - - def validateValue(self, obj, value): - '''This method may be overridden by child classes and will be called at - the right moment by m_validate defined below for triggering - type-specific validation. p_value is never empty.''' - return None - - def securityCheck(self, obj, value): - '''This method performs some security checks on the p_value that - represents user input.''' - if not isinstance(value, basestring): return - # Search Javascript code in the value (prevent XSS attacks). - if '/onProcess.''' - return obj.goto(obj.absolute_url()) - -class Integer(Type): - def __init__(self, validator=None, multiplicity=(0,1), default=None, - show=True, page='main', group=None, layouts=None, move=0, - indexed=False, searchable=False, specificReadPermission=False, - specificWritePermission=False, width=5, height=None, - maxChars=13, colspan=1, master=None, masterValue=None, - focus=False, historized=False, mapping=None, label=None, - sdefault=('',''), scolspan=1, swidth=None, sheight=None): - Type.__init__(self, validator, multiplicity, default, show, page, group, - layouts, move, indexed, searchable,specificReadPermission, - specificWritePermission, width, height, maxChars, colspan, - master, masterValue, focus, historized, True, mapping, - label, sdefault, scolspan, swidth, sheight) - self.pythonType = long - - def validateValue(self, obj, value): - try: - value = self.pythonType(value) - except ValueError: - return obj.translate('bad_%s' % self.pythonType.__name__) - - def getStorableValue(self, value): - if not self.isEmptyValue(value): return self.pythonType(value) - - def getFormattedValue(self, obj, value, showChanges=False): - if self.isEmptyValue(value): return '' - return str(value) - -class Float(Type): - allowedDecimalSeps = (',', '.') - allowedThousandsSeps = (' ', '') - def __init__(self, validator=None, multiplicity=(0,1), default=None, - show=True, page='main', group=None, layouts=None, move=0, - indexed=False, searchable=False, specificReadPermission=False, - specificWritePermission=False, width=5, height=None, - maxChars=13, colspan=1, master=None, masterValue=None, - focus=False, historized=False, mapping=None, label=None, - sdefault=('',''), scolspan=1, swidth=None, sheight=None, - precision=None, sep=(',', '.'), tsep=' '): - # The precision is the number of decimal digits. This number is used - # for rendering the float, but the internal float representation is not - # rounded. - self.precision = precision - # The decimal separator can be a tuple if several are allowed, ie - # ('.', ',') - if type(sep) not in sutils.sequenceTypes: - self.sep = (sep,) - else: - self.sep = sep - # Check that the separator(s) are among allowed decimal separators - for sep in self.sep: - if sep not in Float.allowedDecimalSeps: - raise Exception('Char "%s" is not allowed as decimal ' \ - 'separator.' % sep) - self.tsep = tsep - 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, - label, sdefault, scolspan, swidth, sheight) - self.pythonType = float - - def getFormattedValue(self, obj, value, showChanges=False): - return sutils.formatNumber(value, sep=self.sep[0], - precision=self.precision, tsep=self.tsep) - - def validateValue(self, obj, value): - # Replace used separator with the Python separator '.' - for sep in self.sep: value = value.replace(sep, '.') - value = value.replace(self.tsep, '') - try: - value = self.pythonType(value) - except ValueError: - return obj.translate('bad_%s' % self.pythonType.__name__) - - def getStorableValue(self, value): - if not self.isEmptyValue(value): - for sep in self.sep: value = value.replace(sep, '.') - value = value.replace(self.tsep, '') - return self.pythonType(value) - -class String(Type): - # Javascript files sometimes required by this type - jsFiles = {'edit': ('ckeditor/ckeditor.js',), - 'view': ('ckeditor/ckeditor.js',)} - - # Some predefined regular expressions that may be used as validators - c = re.compile - EMAIL = c('[a-zA-Z][\w\.-]*[a-zA-Z0-9]@[a-zA-Z0-9][\w\.-]*[a-zA-Z0-9]\.' \ - '[a-zA-Z][a-zA-Z\.]*[a-zA-Z]') - ALPHANUMERIC = c('[\w-]+') - URL = c('(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*(\.[a-z]{2,5})?' \ - '(([0-9]{1,5})?\/.*)?') - - # Some predefined functions that may also be used as validators - @staticmethod - def _MODULO_97(obj, value, complement=False): - '''p_value must be a string representing a number, like a bank account. - this function checks that the 2 last digits are the result of - computing the modulo 97 of the previous digits. Any non-digit - character is ignored. If p_complement is True, it does compute the - complement of modulo 97 instead of modulo 97. p_obj is not used; - it will be given by the Appy validation machinery, so it must be - specified as parameter. The function returns True if the check is - successful.''' - if not value: return True - # First, remove any non-digit char - v = '' - for c in value: - if digit.match(c): v += c - # There must be at least 3 digits for performing the check - if len(v) < 3: return False - # Separate the real number from the check digits - number = int(v[:-2]) - checkNumber = int(v[-2:]) - # Perform the check - if complement: - return (97 - (number % 97)) == checkNumber - else: - # The check number can't be 0. In this case, we force it to be 97. - # This is the way Belgian bank account numbers work. I hope this - # behaviour is general enough to be implemented here. - mod97 = (number % 97) - if mod97 == 0: return checkNumber == 97 - else: return checkNumber == mod97 - @staticmethod - def MODULO_97(obj, value): return String._MODULO_97(obj, value) - @staticmethod - def MODULO_97_COMPLEMENT(obj, value): - return String._MODULO_97(obj, value, True) - BELGIAN_ENTERPRISE_NUMBER = MODULO_97_COMPLEMENT - @staticmethod - def IBAN(obj, value): - '''Checks that p_value corresponds to a valid IBAN number. IBAN stands - for International Bank Account Number (ISO 13616). If the number is - valid, the method returns True.''' - if not value: return True - # First, remove any non-digit or non-letter char - v = '' - for c in value: - if alpha.match(c): v += c - # Maximum size is 34 chars - if (len(v) < 8) or (len(v) > 34): return False - # 2 first chars must be a valid country code - if not countries.exists(v[:2].upper()): return False - # 2 next chars are a control code whose value must be between 0 and 96. - try: - code = int(v[2:4]) - if (code < 0) or (code > 96): return False - except ValueError: - return False - # Perform the checksum - vv = v[4:] + v[:4] # Put the 4 first chars at the end. - nv = '' - for c in vv: - # Convert each letter into a number (A=10, B=11, etc) - # Ascii code for a is 65, so A=10 if we perform "minus 55" - if letter.match(c): nv += str(ord(c.upper()) - 55) - else: nv += c - return int(nv) % 97 == 1 - @staticmethod - def BIC(obj, value): - '''Checks that p_value corresponds to a valid BIC number. BIC stands - for Bank Identifier Code (ISO 9362). If the number is valid, the - method returns True.''' - if not value: return True - # BIC number must be 8 or 11 chars - if len(value) not in (8, 11): return False - # 4 first chars, representing bank name, must be letters - for c in value[:4]: - if not letter.match(c): return False - # 2 next chars must be a valid country code - if not countries.exists(value[4:6].upper()): return False - # Last chars represent some location within a country (a city, a - # province...). They can only be letters or figures. - for c in value[6:]: - if not alpha.match(c): return False - return True - - # Possible values for "format" - LINE = 0 - TEXT = 1 - XHTML = 2 - PASSWORD = 3 - CAPTCHA = 4 - def __init__(self, validator=None, multiplicity=(0,1), default=None, - format=LINE, show=True, page='main', group=None, layouts=None, - move=0, indexed=False, searchable=False, - specificReadPermission=False, specificWritePermission=False, - width=None, height=None, maxChars=None, colspan=1, master=None, - masterValue=None, focus=False, historized=False, mapping=None, - label=None, sdefault='', scolspan=1, swidth=None, sheight=None, - transform='none', styles=('p','h1','h2','h3','h4'), - allowImageUpload=True, inlineEdit=False): - # According to format, the widget will be different: input field, - # textarea, inline editor... Note that there can be only one String - # field of format CAPTCHA by page, because the captcha challenge is - # stored in the session at some global key. - self.format = format - self.isUrl = validator == String.URL - # When format is XHTML, the list of styles that the user will be able to - # select in the styles dropdown is defined hereafter. - self.styles = styles - # When format is XHTML, do we allow the user to upload images in it ? - self.allowImageUpload = allowImageUpload - # When format in XHTML, can the field be inline-edited (ckeditor)? - self.inlineEdit = inlineEdit - # The following field has a direct impact on the text entered by the - # user. It applies a transformation on it, exactly as does the CSS - # "text-transform" property. Allowed values are those allowed for the - # CSS property: "none" (default), "uppercase", "capitalize" or - # "lowercase". - self.transform = transform - Type.__init__(self, validator, multiplicity, default, show, page, group, - layouts, move, indexed, searchable,specificReadPermission, - specificWritePermission, width, height, maxChars, colspan, - master, masterValue, focus, historized, True, mapping, - label, sdefault, scolspan, swidth, sheight) - self.isSelect = self.isSelection() - # If self.isSelect, self.sdefault must be a list of value(s). - if self.isSelect and not sdefault: - self.sdefault = [] - # Default width, height and maxChars vary according to String format - if width == None: - if format == String.TEXT: self.width = 60 - # This width corresponds to the standard width of an Appy page. - if format == String.XHTML: self.width = None - else: self.width = 30 - if height == None: - if format == String.TEXT: self.height = 5 - elif self.isSelect: self.height = 4 - else: self.height = 1 - if maxChars == None: - if self.isSelect: pass - elif format == String.LINE: self.maxChars = 256 - elif format == String.TEXT: self.maxChars = 9999 - elif format == String.XHTML: self.maxChars = 99999 - elif format == String.PASSWORD: self.maxChars = 20 - self.filterable = self.indexed and (self.format == String.LINE) and \ - not self.isSelect - self.swidth = self.swidth or self.width - self.sheight = self.sheight or self.height - - def isSelection(self): - '''Does the validator of this type definition define a list of values - into which the user must select one or more values?''' - res = True - if type(self.validator) in (list, tuple): - for elem in self.validator: - if not isinstance(elem, basestring): - res = False - break - else: - if not isinstance(self.validator, Selection): - res = False - return res - - def getDefaultLayouts(self): - '''Returns the default layouts for this type. Default layouts can vary - acccording to format, multiplicity or history.''' - if self.format == String.TEXT: - return {'view': 'l-f', 'edit': 'lrv-d-f'} - elif self.format == String.XHTML: - if self.historized: - # self.historized can be a method or a boolean. If it is a - # method, it means that under some condition, historization will - # be enabled. So we come here also in this case. - view = 'lc-f' - else: - view = 'l-f' - return {'view': Table(view, width='100%'), 'edit': 'lrv-d-f'} - elif self.isMultiValued(): - return {'view': 'l-f', 'edit': 'lrv-f'} - - def getValue(self, obj): - # Cheat if this field represents p_obj's state - if self.name == 'state': return obj.State() - value = Type.getValue(self, obj) - if not value: - if self.isMultiValued(): return emptyTuple - else: return value - if isinstance(value, basestring) and self.isMultiValued(): - value = [value] - elif isinstance(value, tuple): - value = list(value) - return value - - def store(self, obj, value): - '''When the value is XHTML, we perform some cleanup.''' - if (self.format == String.XHTML) and value: - # When image upload is allowed, ckeditor inserts some "style" attrs - # (ie for image size when images are resized). So in this case we - # can't remove style-related information. - try: - value = XhtmlCleaner(keepStyles=False).clean(value) - except XhtmlCleaner.Error, e: - # Errors while parsing p_value can't prevent the user from - # storing it. - obj.log('Unparsable XHTML content in field "%s".' % self.name, - type='warning') - Type.store(self, obj, value) - - def getDiffValue(self, obj, value): - '''Returns a version of p_value that includes the cumulative diffs - between successive versions.''' - res = None - lastEvent = None - for event in obj.workflow_history.values()[0]: - if event['action'] != '_datachange_': continue - if self.name not in event['changes']: continue - if res == None: - # We have found the first version of the field - res = event['changes'][self.name][0] or '' - else: - # We need to produce the difference between current result and - # this version. - iMsg, dMsg = obj.getHistoryTexts(lastEvent) - thisVersion = event['changes'][self.name][0] or '' - comparator = HtmlDiff(res, thisVersion, iMsg, dMsg) - res = comparator.get() - lastEvent = event - # Now we need to compare the result with the current version. - iMsg, dMsg = obj.getHistoryTexts(lastEvent) - comparator = HtmlDiff(res, value or '', iMsg, dMsg) - return comparator.get() - - def getFormattedValue(self, obj, value, showChanges=False): - if self.isEmptyValue(value): return '' - res = value - if self.isSelect: - if isinstance(self.validator, Selection): - # Value(s) come from a dynamic vocabulary - val = self.validator - if self.isMultiValued(): - return [val.getText(obj, v, self) for v in value] - else: - return val.getText(obj, value, self) - else: - # Value(s) come from a fixed vocabulary whose texts are in - # i18n files. - t = obj.translate - if self.isMultiValued(): - res = [t('%s_list_%s' % (self.labelId, v)) for v in value] - else: - res = t('%s_list_%s' % (self.labelId, value)) - elif (self.format == String.XHTML) and showChanges: - # Compute the successive changes that occurred on p_value. - res = self.getDiffValue(obj, res) - # If value starts with a carriage return, add a space; else, it will - # be ignored. - if isinstance(res, basestring) and \ - (res.startswith('\n') or res.startswith('\r\n')): res = ' ' + res - return res - - emptyStringTuple = ('',) - emptyValuesCatalogIgnored = (None, '') - def getIndexValue(self, obj, forSearch=False): - '''For indexing purposes, we return only strings, not unicodes.''' - res = Type.getIndexValue(self, obj, forSearch) - if isinstance(res, unicode): - res = res.encode('utf-8') - if res and forSearch and (self.format == String.XHTML): - # Convert the value to simple text. - extractor = XhtmlTextExtractor(raiseOnError=False) - res = extractor.parse('

%s

' % res) - # Ugly catalog: if I give an empty tuple as index value, it keeps the - # previous value. If I give him a tuple containing an empty string, it - # is ok. - if isinstance(res, tuple) and not res: res = self.emptyStringTuple - # Ugly catalog: if value is an empty string or None, it keeps the - # previous index value. - if res in self.emptyValuesCatalogIgnored: res = ' ' - return res - - def getPossibleValues(self,obj,withTranslations=False,withBlankValue=False): - '''Returns the list of possible values for this field if it is a - selection field. If p_withTranslations is True, - instead of returning a list of string values, the result is a list - of tuples (s_value, s_translation). If p_withBlankValue is True, a - blank value is prepended to the list, excepted if the type is - multivalued.''' - if not self.isSelect: raise 'This field is not a selection.' - if isinstance(self.validator, Selection): - # We need to call self.methodName for getting the (dynamic) values. - # If methodName begins with _appy_, it is a special Appy method: - # we will call it on the Mixin (=p_obj) directly. Else, it is a - # user method: we will call it on the wrapper (p_obj.appy()). Some - # args can be hidden into p_methodName, separated with stars, - # like in this example: method1*arg1*arg2. Only string params are - # supported. - methodName = self.validator.methodName - # Unwrap parameters if any. - if methodName.find('*') != -1: - elems = methodName.split('*') - methodName = elems[0] - args = elems[1:] - else: - args = () - # On what object must we call the method that will produce the - # values? - if methodName.startswith('tool:'): - obj = obj.getTool() - methodName = methodName[5:] - # Do we need to call the method on the object or on the wrapper? - if methodName.startswith('_appy_'): - exec 'res = obj.%s(*args)' % methodName - else: - exec 'res = obj.appy().%s(*args)' % methodName - if not withTranslations: res = [v[0] for v in res] - elif isinstance(res, list): res = res[:] - else: - # The list of (static) values is directly given in self.validator. - res = [] - for value in self.validator: - label = '%s_list_%s' % (self.labelId, value) - if withTranslations: - res.append( (value, obj.translate(label)) ) - else: - res.append(value) - if withBlankValue and not self.isMultiValued(): - # Create the blank value to insert at the beginning of the list - if withTranslations: - blankValue = ('', obj.translate('choose_a_value')) - else: - blankValue = '' - # Insert the blank value in the result - if isinstance(res, tuple): - res = (blankValue,) + res - else: - res.insert(0, blankValue) - return res - - def validateValue(self, obj, value): - if self.format == String.CAPTCHA: - challenge = obj.REQUEST.SESSION.get('captcha', None) - # Compute the challenge minus the char to remove - i = challenge['number']-1 - text = challenge['text'][:i] + challenge['text'][i+1:] - if value != text: - return obj.translate('bad_captcha') - elif self.isSelect: - # Check that the value is among possible values - possibleValues = self.getPossibleValues(obj) - if isinstance(value, basestring): - error = value not in possibleValues - else: - error = False - for v in value: - if v not in possibleValues: - error = True - break - if error: return obj.translate('bad_select_value') - - accents = {'é':'e','è':'e','ê':'e','ë':'e','à':'a','â':'a','ä':'a', - 'ù':'u','û':'u','ü':'u','î':'i','ï':'i','ô':'o','ö':'o', - 'ç':'c', 'Ç':'C', - 'Ù':'U','Û':'U','Ü':'U','Î':'I','Ï':'I','Ô':'O','Ö':'O', - 'É':'E','È':'E','Ê':'E','Ë':'E','À':'A','Â':'A','Ä':'A'} - def applyTransform(self, value): - '''Applies a transform as required by self.transform on single - value p_value.''' - if self.transform in ('uppercase', 'lowercase'): - # For those transforms, I will remove any accent, because - # (1) 'é'.upper() or 'Ê'.lower() has no effect; - # (2) most of the time, if the user wants to apply such effect, it - # is for ease of data manipulation, so I guess without accent. - for c, n in self.accents.iteritems(): - if c in value: value = value.replace(c, n) - # Apply the transform - if self.transform == 'lowercase': return value.lower() - elif self.transform == 'uppercase': return value.upper() - elif self.transform == 'capitalize': return value.capitalize() - return value - - def getStorableValue(self, value): - isString = isinstance(value, basestring) - # Apply transform if required - if isString and not self.isEmptyValue(value) and \ - (self.transform != 'none'): - value = self.applyTransform(value) - # Truncate the result if longer than self.maxChars - if isString and self.maxChars and (len(value) > self.maxChars): - value = value[:self.maxChars] - # Get a multivalued value if required. - if value and self.isMultiValued() and \ - (type(value) not in sutils.sequenceTypes): - value = [value] - return value - - def getIndexType(self): - '''Index type varies depending on String parameters.''' - # If String.isSelect, be it multivalued or not, we define a ListIndex: - # this way we can use AND/OR operator. - if self.isSelect: - return 'ListIndex' - elif self.format == String.TEXT: - return 'TextIndex' - elif self.format == String.XHTML: - return 'XhtmlIndex' - return Type.getIndexType(self) - - def getJs(self, layoutType, res): - if self.format == String.XHTML: Type.getJs(self, layoutType, res) - - def getCaptchaChallenge(self, session): - '''Returns a Captcha challenge in the form of a dict. At key "text", - value is a string that the user will be required to re-type, but - without 1 character whose position is at key "number". The challenge - is stored in the p_session, for the server-side subsequent check.''' - length = random.randint(5, 9) # The length of the challenge to encode - number = random.randint(1, length) # The position of the char to remove - text = '' # The challenge the user needs to type (minus one char) - for i in range(length): - j = random.randint(0, 1) - 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 - - def generatePassword(self): - '''Generates a password (we recycle here the captcha challenge - generator).''' - return self.getCaptchaChallenge({})['text'] - -class Boolean(Type): - '''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, - specificWritePermission=False, width=None, height=None, - maxChars=None, colspan=1, master=None, masterValue=None, - focus=False, historized=False, mapping=None, label=None, - sdefault=False, scolspan=1, swidth=None, sheight=None): - Type.__init__(self, validator, multiplicity, default, show, page, group, - layouts, move, indexed, searchable,specificReadPermission, - specificWritePermission, width, height, None, colspan, - master, masterValue, focus, historized, True, mapping, - label, sdefault, scolspan, swidth, sheight) - self.pythonType = bool - - # Layout including a description - dLayouts = {'view': 'lf', 'edit': Table('flrv;=d', width=None)} - # Centered layout, no description - cLayouts = {'view': 'lf|', 'edit': 'flrv|'} - - def getDefaultLayouts(self): - return {'view': 'lf', 'edit': Table('f;lrv;=', width=None)} - - def getValue(self, obj): - '''Never returns "None". Returns always "True" or "False", even if - "None" is stored in the DB.''' - value = Type.getValue(self, obj) - if value == None: return False - return value - - def getFormattedValue(self, obj, value, showChanges=False): - if value: res = obj.translate('yes') - else: res = obj.translate('no') - return res - - def getStorableValue(self, value): - if not self.isEmptyValue(value): - exec 'res = %s' % value - return res - -class Date(Type): - # Required CSS and Javascript files for this type. - cssFiles = {'edit': ('jscalendar/calendar-blue.css',)} - jsFiles = {'edit': ('jscalendar/calendar.js', - 'jscalendar/lang/calendar-en.js', - 'jscalendar/calendar-setup.js')} - # Possible values for "format" - WITH_HOUR = 0 - WITHOUT_HOUR = 1 - dateParts = ('year', 'month', 'day') - hourParts = ('hour', 'minute') - def __init__(self, validator=None, multiplicity=(0,1), default=None, - format=WITH_HOUR, calendar=True, - startYear=time.localtime()[0]-10, - endYear=time.localtime()[0]+10, reverseYears=False, - show=True, page='main', group=None, layouts=None, move=0, - indexed=False, searchable=False, specificReadPermission=False, - specificWritePermission=False, width=None, height=None, - maxChars=None, colspan=1, master=None, masterValue=None, - focus=False, historized=False, mapping=None, label=None, - sdefault=None, scolspan=1, swidth=None, sheight=None): - self.format = format - self.calendar = calendar - self.startYear = startYear - self.endYear = endYear - # If reverseYears is True, in the selection box, available years, from - # self.startYear to self.endYear will be listed in reverse order. - self.reverseYears = reverseYears - Type.__init__(self, validator, multiplicity, default, show, page, group, - layouts, move, indexed, searchable,specificReadPermission, - specificWritePermission, width, height, None, colspan, - master, masterValue, focus, historized, True, mapping, - label, sdefault, scolspan, swidth, sheight) - - def getCss(self, layoutType, res): - # CSS files are only required if the calendar must be shown. - if self.calendar: Type.getCss(self, layoutType, res) - - def getJs(self, layoutType, res): - # Javascript files are only required if the calendar must be shown. - if self.calendar: Type.getJs(self, layoutType, res) - - def getSelectableYears(self): - '''Gets the list of years one may select for this field.''' - res = range(self.startYear, self.endYear + 1) - if self.reverseYears: res.reverse() - return res - - def validateValue(self, obj, value): - DateTime = obj.getProductConfig().DateTime - try: - value = DateTime(value) - except DateTime.DateError, ValueError: - return obj.translate('bad_date') - - def getFormattedValue(self, obj, value, showChanges=False): - if self.isEmptyValue(value): return '' - tool = obj.getTool().appy() - # A problem may occur with some extreme year values. Replace the "year" - # part "by hand". - dateFormat = tool.dateFormat - if '%Y' in dateFormat: - dateFormat = dateFormat.replace('%Y', str(value.year())) - res = value.strftime(dateFormat) - if self.format == Date.WITH_HOUR: - res += ' %s' % value.strftime(tool.hourFormat) - return res - - def getRequestValue(self, request, requestName=None): - name = requestName or self.name - # Manage the "date" part - value = '' - for part in self.dateParts: - valuePart = request.get('%s_%s' % (name, part), None) - if not valuePart: return None - value += valuePart + '/' - value = value[:-1] - # Manage the "hour" part - if self.format == self.WITH_HOUR: - value += ' ' - for part in self.hourParts: - valuePart = request.get('%s_%s' % (name, part), None) - if not valuePart: return None - value += valuePart + ':' - value = value[:-1] - return value - - def getStorableValue(self, value): - if not self.isEmptyValue(value): - import DateTime - return DateTime.DateTime(value) - - def getIndexType(self): return 'DateIndex' - -class File(Type): - def __init__(self, validator=None, multiplicity=(0,1), default=None, - show=True, page='main', group=None, layouts=None, move=0, - indexed=False, searchable=False, specificReadPermission=False, - specificWritePermission=False, width=None, height=None, - maxChars=None, colspan=1, master=None, masterValue=None, - focus=False, historized=False, mapping=None, label=None, - isImage=False, sdefault='', scolspan=1, swidth=None, - sheight=None): - self.isImage = isImage - 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, - label, sdefault, scolspan, swidth, sheight) - - @staticmethod - def getFileObject(filePath, fileName=None, zope=False): - '''Returns a File instance as can be stored in the database or - manipulated in code, filled with content from a file on disk, - located at p_filePath. If you want to give it a name that is more - sexy than the actual basename of filePath, specify it in - p_fileName. - - If p_zope is True, it will be the raw Zope object = an instance of - OFS.Image.File. Else, it will be a FileWrapper instance from Appy.''' - f = file(filePath, 'rb') - if not fileName: - fileName = os.path.basename(filePath) - fileId = 'file.%f' % time.time() - import OFS.Image - res = OFS.Image.File(fileId, fileName, f) - res.filename = fileName - res.content_type = mimetypes.guess_type(fileName)[0] - f.close() - if not zope: res = sutils.FileWrapper(res) - return res - - def getValue(self, obj): - value = Type.getValue(self, obj) - if value: value = sutils.FileWrapper(value) - return value - - def getFormattedValue(self, obj, value, showChanges=False): - if not value: return value - return value._zopeFile - - def getRequestValue(self, request, requestName=None): - name = requestName or self.name - return request.get('%s_file' % name) - - def getDefaultLayouts(self): return {'view':'l-f','edit':'lrv-f'} - - def isEmptyValue(self, value, obj=None): - '''Must p_value be considered as empty?''' - if not obj: return Type.isEmptyValue(self, value) - if value: return False - # If "nochange", the value must not be considered as empty - return obj.REQUEST.get('%s_delete' % self.name) != 'nochange' - - imageExts = ('.jpg', '.jpeg', '.png', '.gif') - def validateValue(self, obj, value): - form = obj.REQUEST.form - action = '%s_delete' % self.name - if (not value or not value.filename) and form.has_key(action) and \ - not form[action]: - # If this key is present but empty, it means that the user selected - # "replace the file with a new one". So in this case he must provide - # a new file to upload. - return obj.translate('file_required') - # Check that, if self.isImage, the uploaded file is really an image - if value and value.filename and self.isImage: - ext = os.path.splitext(value.filename)[1].lower() - if ext not in File.imageExts: - return obj.translate('image_required') - - defaultMimeType = 'application/octet-stream' - def store(self, obj, value): - '''Stores the p_value that represents some file. p_value can be: - * an instance of Zope class ZPublisher.HTTPRequest.FileUpload. In - this case, it is file content coming from a HTTP POST; - * an instance of Zope class OFS.Image.File; - * an instance of appy.shared.utils.FileWrapper, which wraps an - instance of OFS.Image.File and adds useful methods for manipulating - it; - * a string. In this case, the string represents the path of a file - on disk; - * a 2-tuple (fileName, fileContent) where: - - fileName is the name of the file (ie "myFile.odt") - - fileContent is the binary or textual content of the file or an - open file handler. - * a 3-tuple (fileName, fileContent, mimeType) where - - fileName and fileContent have the same meaning than above; - - mimeType is the MIME type of the file. - ''' - if value: - ZFileUpload = obj.o.getProductConfig().FileUpload - OFSImageFile = obj.o.getProductConfig().File - if isinstance(value, ZFileUpload): - # The file content comes from a HTTP POST. - # Retrieve the existing value, or create one if None - existingValue = getattr(obj.aq_base, self.name, None) - if not existingValue: - existingValue = OFSImageFile(self.name, '', '') - # Set mimetype - if value.headers.has_key('content-type'): - mimeType = value.headers['content-type'] - else: - mimeType = File.defaultMimeType - existingValue.content_type = mimeType - # Set filename - fileName = value.filename - filename= fileName[max(fileName.rfind('/'),fileName.rfind('\\'), - fileName.rfind(':'))+1:] - existingValue.filename = fileName - # Set content - existingValue.manage_upload(value) - setattr(obj, self.name, existingValue) - elif isinstance(value, OFSImageFile): - setattr(obj, self.name, value) - elif isinstance(value, sutils.FileWrapper): - setattr(obj, self.name, value._zopeFile) - elif isinstance(value, basestring): - setattr(obj, self.name, File.getFileObject(value, zope=True)) - elif type(value) in sutils.sequenceTypes: - # It should be a 2-tuple or 3-tuple - fileName = None - mimeType = None - if len(value) == 2: - fileName, fileContent = value - elif len(value) == 3: - fileName, fileContent, mimeType = value - else: - raise WRONG_FILE_TUPLE - if fileName: - fileId = 'file.%f' % time.time() - zopeFile = OFSImageFile(fileId, fileName, fileContent) - zopeFile.filename = fileName - if not mimeType: - mimeType = mimetypes.guess_type(fileName)[0] - zopeFile.content_type = mimeType - setattr(obj, self.name, zopeFile) - else: - # I store value "None", excepted if I find in the request the desire - # to keep the file unchanged. - action = None - rq = getattr(obj, 'REQUEST', None) - if rq: action = rq.get('%s_delete' % self.name, None) - if action == 'nochange': pass - else: setattr(obj, self.name, None) - -class Ref(Type): - # 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%')} - - def __init__(self, klass=None, attribute=None, validator=None, - multiplicity=(0,1), default=None, add=False, addConfirm=False, - delete=None, noForm=False, link=True, unlink=None, back=None, - show=True, page='main', group=None, layouts=None, - showHeaders=False, shownInfo=(), select=None, maxPerPage=30, - move=0, indexed=False, searchable=False, - specificReadPermission=False, specificWritePermission=False, - width=None, height=5, maxChars=None, colspan=1, master=None, - masterValue=None, focus=False, historized=False, mapping=None, - label=None, queryable=False, queryFields=None, queryNbCols=1, - navigable=False, searchSelect=None, changeOrder=True, - sdefault='', scolspan=1, swidth=None, sheight=None): - self.klass = klass - self.attribute = attribute - # May the user add new objects through this ref ? - self.add = add - # When the user adds a new object, must a confirmation popup be shown? - self.addConfirm = addConfirm - # May the user delete objects via this Ref? - self.delete = delete - if delete == None: - # By default, one may delete objects via a Ref for which one can - # add objects. - self.delete = bool(self.add) - # If noForm is True, when clicking to create an object through this ref, - # the object will be created automatically, and no creation form will - # be presented to the user. - self.noForm = noForm - # May the user link existing objects through this ref? - self.link = link - # May the user unlink existing objects? - self.unlink = unlink - if unlink == None: - # By default, one may unlink objects via a Ref for which one can - # link objects. - self.unlink = bool(self.link) - self.back = None - if back: - # It is a forward reference - self.isBack = False - # Initialise the backward reference - self.back = back - self.backd = back.__dict__ - back.isBack = True - back.back = self - back.backd = self.__dict__ - # klass may be None in the case we are defining an auto-Ref to the - # same class as the class where this field is defined. In this case, - # when defining the field within the class, write - # myField = Ref(None, ...) - # and, at the end of the class definition (name it K), write: - # K.myField.klass = K - # setattr(K, K.myField.back.attribute, K.myField.back) - if klass: setattr(klass, back.attribute, back) - # When displaying a tabular list of referenced objects, must we show - # the table headers? - self.showHeaders = showHeaders - # When displaying referenced object(s), we will display its title + all - # other fields whose names are listed in the following attribute. - self.shownInfo = list(shownInfo) - if not self.shownInfo: self.shownInfo.append('title') - # If a method is defined in this field "select", it will be used to - # filter the list of available tied objects. - self.select = select - # Maximum number of referenced objects shown at once. - self.maxPerPage = maxPerPage - # Specifies sync - sync = {'view': False, 'edit':True} - # If param p_queryable is True, the user will be able to perform queries - # from the UI within referenced objects. - self.queryable = queryable - # Here is the list of fields that will appear on the search screen. - # If None is specified, by default we take every indexed field - # defined on referenced objects' class. - self.queryFields = queryFields - # The search screen will have this number of columns - self.queryNbCols = queryNbCols - # Within the portlet, will referred elements appear ? - self.navigable = navigable - # The search select method is used if self.indexed is True. In this - # case, we need to know among which values we can search on this field, - # in the search screen. Those values are returned by self.searchSelect, - # which must be a static method accepting the tool as single arg. - self.searchSelect = searchSelect - # If changeOrder is False, it even if the user has the right to modify - # the field, it will not be possible to move objects or sort them. - self.changeOrder = changeOrder - 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, - label, sdefault, scolspan, swidth, sheight) - self.validable = self.link - - def getDefaultLayouts(self): - return {'view': Table('l-f', width='100%'), 'edit': 'lrv-f'} - - def isShowable(self, obj, layoutType): - res = Type.isShowable(self, obj, layoutType) - if not res: return res - # We add here specific Ref rules for preventing to show the field under - # some inappropriate circumstances. - if (layoutType == 'edit') and \ - (self.mayAdd(obj) or not self.link): return False - if self.isBack: - if layoutType == 'edit': return False - else: return getattr(obj.aq_base, self.name, None) - return res - - def getValue(self, obj, type='objects', noListIfSingleObj=False, - startNumber=None, someObjects=False): - '''Returns the objects linked to p_obj through this Ref field. - - If p_type is "objects", it returns the Appy wrappers; - - If p_type is "zobjects", it returns the Zope objects; - - If p_type is "uids", it returns UIDs of objects (= strings). - - * If p_startNumber is None, it returns all referred objects. - * If p_startNumber is a number, it returns self.maxPerPage objects, - starting at p_startNumber. - - If p_noListIfSingleObj is True, it returns the single reference as - an object and not as a list. - - If p_someObjects is True, it returns an instance of SomeObjects - instead of returning a list of references.''' - uids = getattr(obj.aq_base, self.name, []) - if not uids: - # Maybe is there a default value? - defValue = 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 sutils.sequenceTypes: - uids = [o.o.UID() for o in defValue] - else: - uids = [defValue.o.UID()] - # Prepare the result: an instance of SomeObjects, that will be unwrapped - # if not required. - res = gutils.SomeObjects() - res.totalNumber = res.batchSize = len(uids) - batchNeeded = startNumber != None - if batchNeeded: - res.batchSize = self.maxPerPage - if startNumber != None: - res.startNumber = startNumber - # Get the objects given their uids - i = res.startNumber - while i < (res.startNumber + res.batchSize): - if i >= res.totalNumber: break - # Retrieve every reference in the correct format according to p_type - if type == 'uids': - ref = uids[i] - else: - ref = obj.getTool().getObject(uids[i]) - if type == 'objects': - ref = ref.appy() - res.objects.append(ref) - i += 1 - # Manage parameter p_noListIfSingleObj - if res.objects and noListIfSingleObj: - if self.multiplicity[1] == 1: - res.objects = res.objects[0] - if someObjects: return res - return res.objects - - def getFormattedValue(self, obj, value, showChanges=False): - return value - - def getIndexType(self): return 'ListIndex' - - def getIndexValue(self, obj, forSearch=False): - '''Value for indexing is the list of UIDs of linked objects. If - p_forSearch is True, it will return a list of the linked objects' - titles instead.''' - if not forSearch: - res = getattr(obj.aq_base, self.name, []) - if res: - # The index does not like persistent lists. - res = list(res) - else: - # Ugly catalog: if I return an empty list, the previous value - # is kept. - res.append('') - return res - else: - # For the global search: return linked objects' titles. - res = [o.title for o in self.getValue(type='objects')] - if not res: res.append('') - return res - - def validateValue(self, obj, value): - if not self.link: return None - # We only check "link" Refs because in edit views, "add" Refs are - # not visible. So if we check "add" Refs, on an "edit" view we will - # believe that that there is no referred object even if there is. - # If the field is a reference, we must ensure itself that multiplicities - # are enforced. - if not value: - nbOfRefs = 0 - elif isinstance(value, basestring): - nbOfRefs = 1 - else: - nbOfRefs = len(value) - minRef = self.multiplicity[0] - maxRef = self.multiplicity[1] - if maxRef == None: - maxRef = sys.maxint - if nbOfRefs < minRef: - return obj.translate('min_ref_violated') - elif nbOfRefs > maxRef: - return obj.translate('max_ref_violated') - - def linkObject(self, obj, value, back=False): - '''This method links p_value (which can be a list of objects) to p_obj - through this Ref field.''' - # p_value can be a list of objects - if type(value) in sutils.sequenceTypes: - for v in value: self.linkObject(obj, v, back=back) - return - # Gets the list of referred objects (=list of uids), or create it. - obj = obj.o - refs = getattr(obj.aq_base, self.name, None) - if refs == None: - refs = obj.getProductConfig().PersistentList() - setattr(obj, self.name, refs) - # Insert p_value into it. - uid = value.o.UID() - if uid not in refs: - # Where must we insert the object? At the start? At the end? - if callable(self.add): - add = self.callMethod(obj, self.add) - else: - add = self.add - if add == 'start': - refs.insert(0, uid) - else: - refs.append(uid) - # Update the back reference - if not back: self.back.linkObject(value, obj, back=True) - - def unlinkObject(self, obj, value, back=False): - '''This method unlinks p_value (which can be a list of objects) from - p_obj through this Ref field.''' - # p_value can be a list of objects - if type(value) in sutils.sequenceTypes: - for v in value: self.unlinkObject(obj, v, back=back) - return - obj = obj.o - refs = getattr(obj.aq_base, self.name, None) - if not refs: return - # Unlink p_value - uid = value.o.UID() - if uid in refs: - refs.remove(uid) - # Update the back reference - if not back: self.back.unlinkObject(value, obj, back=True) - - def store(self, obj, value): - '''Stores on p_obj, the p_value, which can be: - * None; - * an object UID (=string); - * a list of object UIDs (=list of strings). Generally, UIDs or lists - of UIDs come from Ref fields with link:True edited through the web; - * a Zope object; - * a Appy object; - * a list of Appy or Zope objects.''' - # Standardize p_value into a list of Zope objects - objects = value - if not objects: objects = [] - if type(objects) not in sutils.sequenceTypes: objects = [objects] - tool = obj.getTool() - for i in range(len(objects)): - if isinstance(objects[i], basestring): - # We have a UID here - objects[i] = tool.getObject(objects[i]) - else: - # Be sure to have a Zope object - objects[i] = objects[i].o - uids = [o.UID() for o in objects] - # Unlink objects that are not referred anymore - refs = getattr(obj.aq_base, self.name, None) - if refs: - i = len(refs)-1 - while i >= 0: - if refs[i] not in uids: - # Object having this UID must unlink p_obj - self.back.unlinkObject(tool.getObject(refs[i]), obj) - i -= 1 - # Link new objects - if objects: - self.linkObject(obj, objects) - - def mayAdd(self, obj): - '''May the user create a new referred object from p_obj via this Ref?''' - # We can't (yet) do that on back references. - if self.isBack: return No('is_back') - # Check if this Ref is addable - if callable(self.add): - add = self.callMethod(obj, self.add) - else: - add = self.add - if not add: return No('no_add') - # Have we reached the maximum number of referred elements? - if self.multiplicity[1] != None: - refCount = len(getattr(obj, self.name, ())) - if refCount >= self.multiplicity[1]: return No('max_reached') - # May the user edit this Ref field? - if not obj.allows(self.writePermission): return No('no_write_perm') - # Have the user the correct add permission? - tool = obj.getTool() - addPermission = '%s: Add %s' % (tool.getAppName(), - tool.getPortalType(self.klass)) - folder = obj.getCreateFolder() - if not obj.getUser().has_permission(addPermission, folder): - return No('no_add_perm') - return True - - def checkAdd(self, obj): - '''Compute m_mayAdd above, and raise an Unauthorized exception if - m_mayAdd returns False.''' - may = self.mayAdd(obj) - if not may: - from AccessControl import Unauthorized - raise Unauthorized("User can't write Ref field '%s' (%s)." % \ - (self.name, may.msg)) - - def changeOrderEnabled(self, obj): - '''Is changeOrder enabled?''' - if isinstance(self.changeOrder, bool): - return self.changeOrder - else: - return self.callMethod(obj, self.changeOrder) - -def autoref(klass, field): - '''klass.field is a Ref to p_klass. This kind of auto-reference can't be - declared in the "normal" way, like this: - - class A: - attr1 = Ref(A) - - because at the time Python encounters the static declaration - "attr1 = Ref(A)", class A is not completely defined yet. - - This method allows to overcome this problem. You can write such - auto-reference like this: - - class A: - attr1 = Ref(None) - autoref(A, A.attr1) - ''' - field.klass = klass - setattr(klass, field.back.attribute, field.back) - -class Computed(Type): - def __init__(self, validator=None, multiplicity=(0,1), default=None, - show='view', page='main', group=None, layouts=None, move=0, - indexed=False, searchable=False, specificReadPermission=False, - specificWritePermission=False, width=None, height=None, - maxChars=None, colspan=1, method=None, plainText=True, - master=None, masterValue=None, focus=False, historized=False, - sync=True, mapping=None, label=None, sdefault='', scolspan=1, - swidth=None, sheight=None, context={}): - # The Python method used for computing the field value - self.method = method - # Does field computation produce plain text or XHTML? - self.plainText = plainText - if isinstance(method, basestring): - # When field computation is done with a macro, we know the result - # will be HTML. - self.plainText = False - # The context is a dict (or method returning a dict) that will be given - # to the macro specified in self.method. If the dict contains key - # "someKey", it will be available to the macro as "options/someKey". - self.context = context - Type.__init__(self, None, multiplicity, default, show, page, group, - layouts, move, indexed, searchable,specificReadPermission, - specificWritePermission, width, height, None, colspan, - master, masterValue, focus, historized, sync, mapping, - label, sdefault, scolspan, swidth, sheight) - self.validable = False - - def callMacro(self, obj, macroPath): - '''Returns the macro corresponding to p_macroPath. The base folder - where we search is "ui".''' - # Get the special page in Appy that allows to call a macro - macroPage = obj.ui.callMacro - # Get, from p_macroPath, the page where the macro lies, and the macro - # name. - names = self.method.split('/') - # Get the page where the macro lies - page = obj.ui - for name in names[:-1]: - page = getattr(page, name) - macroName = names[-1] - # Compute the macro context. - ctx = {'contextObj':obj, 'page':page, 'macroName':macroName} - if callable(self.context): - ctx.update(self.context(obj.appy())) - else: - ctx.update(self.context) - return macroPage(obj, **ctx) - - def getValue(self, obj): - '''Computes the value instead of getting it in the database.''' - if not self.method: return - if isinstance(self.method, basestring): - # self.method is a path to a macro that will produce the field value - return self.callMacro(obj, self.method) - else: - # self.method is a method that will return the field value - return self.callMethod(obj, self.method, cache=False) - - def getFormattedValue(self, obj, value, showChanges=False): - if not isinstance(value, basestring): return str(value) - return value - -class Action(Type): - '''An action is a workflow-independent Python method that can be triggered - by the user on a given gen-class. For example, the custom installation - procedure of a gen-application is implemented by an action on the custom - tool class. An action is rendered as a button.''' - def __init__(self, validator=None, multiplicity=(1,1), default=None, - show=True, page='main', group=None, layouts=None, move=0, - indexed=False, searchable=False, specificReadPermission=False, - specificWritePermission=False, width=None, height=None, - maxChars=None, colspan=1, action=None, result='computation', - confirm=False, master=None, masterValue=None, focus=False, - historized=False, mapping=None, label=None): - # Can be a single method or a list/tuple of methods - self.action = action - # For the 'result' param: - # * value 'computation' means that the action will simply compute - # things and redirect the user to the same page, with some status - # message about execution of the action; - # * 'file' means that the result is the binary content of a file that - # the user will download. - # * 'filetmp' is similar to file, but the file is a temp file and Appy - # will delete it as soon as it will be served to the browser. - # * 'redirect' means that the action will lead to the user being - # redirected to some other page. - self.result = result - # If following field "confirm" is True, a popup will ask the user if - # she is really sure about triggering this action. - self.confirm = confirm - 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, - label, None, None, None, None) - self.validable = False - - def getDefaultLayouts(self): return {'view': 'l-f', 'edit': 'lrv-f'} - def __call__(self, obj): - '''Calls the action on p_obj.''' - if type(self.action) in sutils.sequenceTypes: - # There are multiple Python methods - res = [True, ''] - for act in self.action: - actRes = act(obj) - if type(actRes) in sutils.sequenceTypes: - res[0] = res[0] and actRes[0] - if self.result.startswith('file'): - res[1] = res[1] + actRes[1] - else: - res[1] = res[1] + '\n' + actRes[1] - else: - res[0] = res[0] and actRes - else: - # There is only one Python method - actRes = self.action(obj) - if type(actRes) in sutils.sequenceTypes: - res = list(actRes) - else: - res = [actRes, ''] - # If res is None (ie the user-defined action did not return anything), - # we consider the action as successfull. - if res[0] == None: res[0] = True - return res - - def isShowable(self, obj, layoutType): - if layoutType == 'edit': return False - else: return Type.isShowable(self, obj, layoutType) - -class Info(Type): - '''An info is a field whose purpose is to present information - (text, html...) to the user.''' - def __init__(self, validator=None, multiplicity=(1,1), default=None, - show='view', page='main', group=None, layouts=None, move=0, - indexed=False, searchable=False, specificReadPermission=False, - specificWritePermission=False, width=None, height=None, - maxChars=None, colspan=1, master=None, masterValue=None, - focus=False, historized=False, mapping=None, label=None): - 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, - label, None, None, None, None) - self.validable = False - -class Pod(Type): - '''A pod is a field allowing to produce a (PDF, ODT, Word, RTF...) document - from data contained in Appy class and linked objects or anything you - want to put in it. It uses appy.pod.''' - # Layout for rendering a POD field for exporting query results. - rLayouts = {'view': Table('fl', width=None)} - POD_ERROR = 'An error occurred while generating the document. Please ' \ - 'contact the system administrator.' - DELETE_TEMP_DOC_ERROR = 'A temporary document could not be removed. %s.' - def __init__(self, validator=None, default=None, show=('view', 'result'), - page='main', group=None, layouts=None, move=0, indexed=False, - searchable=False, specificReadPermission=False, - specificWritePermission=False, width=None, height=None, - maxChars=None, colspan=1, master=None, masterValue=None, - focus=False, historized=False, mapping=None, label=None, - template=None, context=None, action=None, askAction=False, - stylesMapping={}, freezeFormat='pdf'): - # The following param stores the path to a POD template - self.template = template - # The context is a dict containing a specific pod context, or a method - # that returns such a dict. - self.context = context - # Next one is a method that will be triggered after the document has - # been generated. - self.action = action - # If askAction is True, the action will be triggered only if the user - # checks a checkbox, which, by default, will be unchecked. - self.askAction = askAction - # A global styles mapping that would apply to the whole template - self.stylesMapping = stylesMapping - # Freeze format is by PDF by default - self.freezeFormat = freezeFormat - Type.__init__(self, None, (0,1), default, show, page, group, layouts, - move, indexed, searchable, specificReadPermission, - specificWritePermission, width, height, None, colspan, - master, masterValue, focus, historized, False, mapping, - label, None, None, None, None) - self.validable = False - - def isFrozen(self, obj): - '''Is there a frozen document for p_self on p_obj?''' - value = getattr(obj.o.aq_base, self.name, None) - return isinstance(value, obj.o.getProductConfig().File) - - def getToolInfo(self, obj): - '''Gets information related to this field (p_self) that is available in - the tool: the POD template and the available output formats. If this - field is frozen, available output formats are not available anymore: - only the format of the frozen doc is returned.''' - tool = obj.tool - appyClass = tool.o.getAppyClass(obj.o.meta_type) - # Get the output format(s) - if self.isFrozen(obj): - # The only available format is the one from the frozen document - fileName = getattr(obj.o.aq_base, self.name).filename - formats = (os.path.splitext(fileName)[1][1:],) - else: - # Available formats are those which are selected in the tool. - name = tool.getAttributeName('formats', appyClass, self.name) - formats = getattr(tool, name) - # Get the POD template - name = tool.getAttributeName('podTemplate', appyClass, self.name) - template = getattr(tool, name) - return (template, formats) - - def getValue(self, obj): - '''Gets, on_obj, the value conforming to self's type definition. For a - Pod field, if a file is stored in the field, it means that the - field has been frozen. Else, it means that the value must be - retrieved by calling pod to compute the result.''' - rq = getattr(obj, 'REQUEST', None) - res = getattr(obj.aq_base, self.name, None) - if res and res.size: - # Return the frozen file. - return sutils.FileWrapper(res) - # If we are here, it means that we must call pod to compute the file. - # A Pod field differs from other field types because there can be - # several ways to produce the field value (ie: output file format can be - # odt, pdf,...; self.action can be executed or not...). We get those - # precisions about the way to produce the file from the request object - # and from the tool. If we don't find the request object (or if it does - # not exist, ie, when Zope runs in test mode), we use default values. - obj = obj.appy() - tool = obj.tool - # Get POD template and available formats from the tool. - template, availFormats = self.getToolInfo(obj) - # Get the output format - defaultFormat = 'pdf' - if defaultFormat not in availFormats: defaultFormat = availFormats[0] - outputFormat = getattr(rq, 'podFormat', defaultFormat) - # Get or compute the specific POD context - specificContext = None - if callable(self.context): - specificContext = self.callMethod(obj, self.context) - else: - specificContext = self.context - # Temporary file where to generate the result - tempFileName = '%s/%s_%f.%s' % ( - sutils.getOsTempFolder(), obj.uid, time.time(), outputFormat) - # Define parameters to give to the appy.pod renderer - podContext = {'tool': tool, 'user': obj.user, 'self': obj, 'field':self, - 'now': obj.o.getProductConfig().DateTime(), - '_': obj.translate, 'projectFolder': tool.getDiskFolder()} - # If the POD document is related to a query, get it from the request, - # execute it and put the result in the context. - isQueryRelated = rq.get('queryData', None) - if isQueryRelated: - # Retrieve query params from the request - cmd = ', '.join(tool.o.queryParamNames) - cmd += " = rq['queryData'].split(';')" - exec cmd - # (re-)execute the query, but without any limit on the number of - # results; return Appy objects. - objs = tool.o.executeQuery(obj.o.portal_type, searchName=search, - sortBy=sortKey, sortOrder=sortOrder, filterKey=filterKey, - filterValue=filterValue, maxResults='NO_LIMIT') - podContext['objects'] = [o.appy() for o in objs['objects']] - # Add the field-specific context if present. - if specificContext: - podContext.update(specificContext) - # If a custom param comes from the request, add it to the context. A - # custom param must have format "name:value". Custom params override any - # other value in the request, including values from the field-specific - # context. - customParams = rq.get('customParams', None) - if customParams: - paramsDict = eval(customParams) - podContext.update(paramsDict) - # Define a potential global styles mapping - if callable(self.stylesMapping): - stylesMapping = self.callMethod(obj, self.stylesMapping) - else: - stylesMapping = self.stylesMapping - rendererParams = {'template': StringIO.StringIO(template.content), - 'context': podContext, 'result': tempFileName, - 'stylesMapping': stylesMapping, - 'imageResolver': tool.o.getApp()} - if tool.unoEnabledPython: - rendererParams['pythonWithUnoPath'] = tool.unoEnabledPython - if tool.openOfficePort: - rendererParams['ooPort'] = tool.openOfficePort - # Launch the renderer - try: - renderer = Renderer(**rendererParams) - renderer.run() - except 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, sutils.FileWrapper): - value = value._zopeFile - setattr(obj, self.name, value) - -class List(Type): - '''A list.''' - def __init__(self, fields, validator=None, multiplicity=(0,1), default=None, - show=True, page='main', group=None, layouts=None, move=0, - indexed=False, searchable=False, specificReadPermission=False, - specificWritePermission=False, width=None, height=None, - maxChars=None, colspan=1, master=None, masterValue=None, - focus=False, historized=False, mapping=None, label=None, - subLayouts=Table('fv', width=None)): - 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, - label, None, None, None, None) - 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 - - def getRequestValue(self, request, requestName=None): - '''Concatenates the list from distinct form elements in the request.''' - name = requestName or self.name # A List may be into another List (?) - prefix = name + '*' + self.fields[0][0] + '*' - res = {} - for key in request.keys(): - if not key.startswith(prefix): continue - # I have found a row. Gets its index - row = Object() - if '_' in key: key = key[:key.index('_')] - rowIndex = int(key.split('*')[-1]) - if rowIndex == -1: continue # Ignore the template row. - for subName, subField in self.fields: - keyName = '%s*%s*%s' % (name, subName, rowIndex) - v = subField.getRequestValue(request, requestName=keyName) - setattr(row, subName, v) - res[rowIndex] = row - # Produce a sorted list. - keys = res.keys() - keys.sort() - res = [res[key] for key in keys] - # I store in the request this computed value. This way, when individual - # subFields will need to get their value, they will take it from here, - # instead of taking it from the specific request key. Indeed, specific - # request keys contain row indexes that may be wrong after row deletions - # by the user. - request.set(name, res) - return res - - def getStorableValue(self, value): - '''Gets p_value in a form that can be stored in the database.''' - res = [] - for v in value: - sv = Object() - for name, field in self.fields: - setattr(sv, name, field.getStorableValue(getattr(v, name))) - res.append(sv) - return res - - def getInnerValue(self, outerValue, name, i): - '''Returns the value of inner field named p_name in row number p_i - within the whole list of values p_outerValue.''' - if i == -1: return '' - if not outerValue: return '' - if i >= len(outerValue): return '' - return getattr(outerValue[i], name, '') - - def getCss(self, layoutType, res): - '''Gets the CSS required by sub-fields if any.''' - for name, field in self.fields: - field.getCss(layoutType, res) - - def getJs(self, layoutType, res): - '''Gets the JS required by sub-fields if any.''' - for name, field in self.fields: - field.getJs(layoutType, res) - # Workflow-specific types and default workflows -------------------------------- appyToZopePermissions = { 'read': ('View', 'Access contents information'), @@ -2940,17 +525,6 @@ class Permission: class ReadPermission(Permission): pass class WritePermission(Permission): pass -class No: - '''When you write a workflow condition method and you want to return False - but you want to give to the user some explanations about why a transition - can't be triggered, do not return False, return an instance of No - instead. When creating such an instance, you can specify an error - message.''' - def __init__(self, msg): - self.msg = msg - def __nonzero__(self): - return False - class WorkflowAnonymous: '''One-state workflow allowing anyone to consult and Manager to edit.''' mgr = 'Manager' @@ -2973,30 +547,6 @@ class WorkflowOwner: o = 'Owner' active = State({r:(mgr, o), w:(mgr, o), d:mgr}, initial=True) -# ------------------------------------------------------------------------------ -class Selection: - '''Instances of this class may be given as validator of a String, in order - to tell Appy that the validator is a selection that will be computed - dynamically.''' - def __init__(self, methodName): - # The p_methodName parameter must be the name of a method that will be - # called every time Appy will need to get the list of possible values - # for the related field. It must correspond to an instance method of - # the class defining the related field. This method accepts no argument - # and must return a list (or tuple) of pairs (lists or tuples): - # (id, text), where "id" is one of the possible values for the - # field, and "text" is the value as will be shown on the screen. - # You can use self.translate within this method to produce an - # internationalized version of "text" if needed. - self.methodName = methodName - - def getText(self, obj, value, appyType): - '''Gets the text that corresponds to p_value.''' - for v, text in appyType.getPossibleValues(obj, withTranslations=True): - if v == value: - return text - return value - # ------------------------------------------------------------------------------ class Model: pass class Tool(Model): diff --git a/gen/descriptors.py b/gen/descriptors.py index 6ab2c0b..dadc509 100644 --- a/gen/descriptors.py +++ b/gen/descriptors.py @@ -55,7 +55,7 @@ class ClassDescriptor(Descriptor): except AttributeError: attrValue = getattr(self.modelClass, attrName) hookClass = self.modelClass - if isinstance(attrValue, gen.Type): + if isinstance(attrValue, gen.Field): if not condition or eval(condition): attrs.append( (attrName, attrValue, hookClass) ) # Then, add attributes from parent classes @@ -124,7 +124,7 @@ class ClassDescriptor(Descriptor): attrValue = getattr(self.klass, attrName) except AttributeError: attrValue = getattr(self.modelClass, attrName) - if not isinstance(attrValue, gen.Type): continue + if not isinstance(attrValue, gen.Field): continue FieldDescriptor(attrName, attrValue, self).generate() def isAbstract(self): diff --git a/gen/generator.py b/gen/generator.py index d6d773d..1da14e6 100644 --- a/gen/generator.py +++ b/gen/generator.py @@ -158,7 +158,7 @@ class Generator: workflow.''' res = 'none' for attrValue in klass.__dict__.itervalues(): - if isinstance(attrValue, gen.Type): + if isinstance(attrValue, gen.Field): res = 'class' elif isinstance(attrValue, gen.State): res = 'workflow' @@ -218,7 +218,7 @@ class Generator: # programmatically moreAttrs = [] for eName, eValue in moduleElem.__dict__.iteritems(): - if isinstance(eValue, gen.Type) and (eName not in attrs): + if isinstance(eValue, gen.Field) and (eName not in attrs): moreAttrs.append(eName) # Sort them in alphabetical order: else, order would be random moreAttrs.sort() diff --git a/gen/installer.py b/gen/installer.py index 3fe50eb..9989f31 100644 --- a/gen/installer.py +++ b/gen/installer.py @@ -385,7 +385,7 @@ class ZopeInstaller: for baseClass in klass.wrapperClass.__bases__: if baseClass.__name__ == 'AbstractWrapper': continue for name, appyType in baseClass.__dict__.iteritems(): - if not isinstance(appyType, gen.Type) or \ + if not isinstance(appyType, gen.Field) or \ (isinstance(appyType, gen.Ref) and appyType.isBack): continue # Back refs are initialised within fw refs appyType.init(name, baseClass, appName) diff --git a/gen/mixins/ToolMixin.py b/gen/mixins/ToolMixin.py index 57fc42f..9a466d6 100644 --- a/gen/mixins/ToolMixin.py +++ b/gen/mixins/ToolMixin.py @@ -2,7 +2,7 @@ import os, os.path, sys, re, time, random, types, base64, urllib from appy import Object import appy.gen -from appy.gen import Type, Search, Selection, String, Page +from appy.gen import Search, String, Page from appy.gen.utils import SomeObjects, getClassName, GroupDescr, SearchDescr from appy.gen.mixins import BaseMixin from appy.gen.wrappers import AbstractWrapper @@ -476,6 +476,11 @@ class ToolMixin(BaseMixin): sub-lists of p_sub elements.''' return splitList(l, sub) + def quote(self, s): + '''Returns the quoted version of p_s.''' + if "'" in s: return '"%s"' % s + return "'%s'" % s + def getLayoutType(self): '''Guess the current layout type, according to actual URL.''' url = self.REQUEST['ACTUAL_URL'] diff --git a/gen/ui/widgets/calendar.pt b/gen/ui/widgets/calendar.pt index 4825d1f..49f2500 100644 --- a/gen/ui/widgets/calendar.pt +++ b/gen/ui/widgets/calendar.pt @@ -1,4 +1,4 @@ -View macro +Month view macro