From 1bd77d68c4909bc18c429625e913716dc09c8ef7 Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Wed, 21 Aug 2013 13:54:56 +0200 Subject: [PATCH] [gen] Refactoring. --- fields/__init__.py | 623 ++------------------------------------------- fields/group.py | 370 +++++++++++++++++++++++++++ fields/page.py | 88 +++++++ fields/phase.py | 166 ++++++++++++ fields/ref.py | 11 +- fields/search.py | 178 +++++++++++++ gen/__init__.py | 163 +----------- gen/utils.py | 24 +- 8 files changed, 853 insertions(+), 770 deletions(-) create mode 100644 fields/group.py create mode 100644 fields/page.py create mode 100644 fields/phase.py create mode 100644 fields/search.py diff --git a/fields/__init__.py b/fields/__init__.py index 45e54ed..1f3f34b 100644 --- a/fields/__init__.py +++ b/fields/__init__.py @@ -16,613 +16,24 @@ # ------------------------------------------------------------------------------ import copy, types, re +from appy import Object from appy.gen.layout import Table, defaultFieldLayouts from appy.gen import utils as gutils -from appy.shared import utils as sutils from appy.px import Px -from appy import Object +from appy.shared import utils as sutils +from group import Group +from page import Page -# ------------------------------------------------------------------------------ -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 an object.''' - res = Object() - for elem in Page.subElements: - setattr(res, 'show%s' % elem.capitalize(), \ - self.isShowable(obj, layoutType, elem=elem)) - return res - -# ------------------------------------------------------------------------------ -# Phase. Pages are grouped into phases. -# ------------------------------------------------------------------------------ -class Phase: - '''A group of pages.''' - - pxView = Px(''' - - - - -
::_(label)
- - - - - - - - - - - - ''') - - def __init__(self, name, obj): - self.name = name - self.obj = obj - # The list of names of pages in this phase - self.pages = [] - # The list of hidden pages in this phase - self.hiddenPages = [] - # The dict below stores info about every page listed in self.pages. - self.pagesInfo = {} - self.totalNbOfPhases = None - # The following attributes allows to browse, from a given page, to the - # last page of the previous phase and to the first page of the following - # phase if allowed by phase state. - self.previousPhase = None - self.nextPhase = None - - def addPageLinks(self, field, obj): - '''If p_field is a navigable Ref, we must add, within self.pagesInfo, - objects linked to p_obj through this ReF as links.''' - if field.page.name in self.hiddenPages: return - infos = [] - for ztied in field.getValue(obj, type='zobjects'): - infos.append(Object(title=ztied.title, url=ztied.absolute_url())) - self.pagesInfo[field.page.name].links = infos - - def addPage(self, field, obj, layoutType): - '''Adds page-related information in the phase.''' - # If the page is already there, we have nothing more to do. - if (field.page.name in self.pages) or \ - (field.page.name in self.hiddenPages): return - # Add the page only if it must be shown. - isShowableOnView = field.page.isShowable(obj, 'view') - isShowableOnEdit = field.page.isShowable(obj, 'edit') - if isShowableOnView or isShowableOnEdit: - # The page must be added. - self.pages.append(field.page.name) - # Create the dict about page information and add it in self.pageInfo - pageInfo = Object(page=field.page, showOnView=isShowableOnView, - showOnEdit=isShowableOnEdit, links=None) - pageInfo.update(field.page.getInfo(obj, layoutType)) - self.pagesInfo[field.page.name] = pageInfo - else: - self.hiddenPages.append(field.page.name) - - def computeNextPrevious(self, allPhases): - '''This method also fills fields "previousPhase" and "nextPhase" - if relevant, based on list of p_allPhases.''' - # Identify previous and next phases - for phase in allPhases: - if phase.name == self.name: - i = allPhases.index(phase) - if i > 0: - self.previousPhase = allPhases[i-1] - if i < (len(allPhases)-1): - self.nextPhase = allPhases[i+1] - - def getPreviousPage(self, page): - '''Returns the page that precedes p_page in this phase.''' - try: - pageIndex = self.pages.index(page) - except ValueError: - # The current page is probably not visible anymore. Return the - # first available page in current phase. - res = self.pages[0] - return res, self.pagesInfo[res] - if pageIndex > 0: - # We stay on the same phase, previous page - res = self.pages[pageIndex-1] - return res, self.pagesInfo[res] - else: - if self.previousPhase: - # We go to the last page of previous phase - previousPhase = self.previousPhase - res = previousPhase.pages[-1] - return res, previousPhase.pagesInfo[res] - else: - return None, None - - def getNextPage(self, page): - '''Returns the page that follows p_page in this phase.''' - try: - pageIndex = self.pages.index(page) - except ValueError: - # The current page is probably not visible anymore. Return the - # first available page in current phase. - res = self.pages[0] - return res, self.pagesInfo[res] - if pageIndex < (len(self.pages)-1): - # We stay on the same phase, next page - res = self.pages[pageIndex+1] - return res, self.pagesInfo[res] - else: - if self.nextPhase: - # We go to the first page of next phase - nextPhase = self.nextPhase - res = nextPhase.pages[0] - return res, nextPhase.pagesInfo[res] - else: - return None, None - - -# ------------------------------------------------------------------------------ -# Group. Fields can be grouped. -# ------------------------------------------------------------------------------ -class Group: - '''Used for describing a group of fields 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, fields, uiGroups, page, metaType, forSearch=False): - '''Inserts the UiGroup instance corresponding to this Group instance - into p_fields, the recursive structure used for displaying all - fields in a given p_page (or all searches), and returns this - UiGroup instance.''' - # First, create the corresponding UiGroup if not already in p_uiGroups. - if self.name not in uiGroups: - uiGroup = uiGroups[self.name] = UiGroup(self, page, metaType, - forSearch=forSearch) - # Insert the group at the higher level (ie, directly in p_fields) - # if the group is not itself in a group. - if not self.group: - fields.append(uiGroup) - else: - outerGroup = self.group.insertInto(fields, uiGroups, page, - metaType,forSearch=forSearch) - outerGroup.addField(uiGroup) - else: - uiGroup = uiGroups[self.name] - return uiGroup - -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 UiGroup: - '''On-the-fly-generated data structure that groups all fields sharing the - same appy.fields.Group instance, that some logged user can see.''' - - # PX that renders a help icon for a group. - pxHelp = Px('''''') - - # PX that renders the content of a group. - pxContent = Px(''' - - - - - - - - - - - - - - - - -
- ::_(field.labelId):field.pxHelp -
::_(field.descrId)
::field.hasHeaders and \ - _('%s_col%d' % (field.labelId, (colNb+1))) or ''
- - :field.pxView - :field.pxRender - -
''') - - # PX that renders a group of fields. - pxView = Px(''' - - - -
- - ::_(field.labelId)>:field.pxHelp - -
::_(field.descrId)
- :field.pxContent -
- - - :field.pxContent - - - - - - - - - - - -
- - - - - - - - -
- :_('%s_col%d' % (field.labelId, rowNb)) -
-
- :field.pxView - :field.pxRender -
- -
-
''') - - # PX that renders a group of searches. - pxViewSearches = Px(''' - - -
- - :_(field.labelId) - :field.translated -
- -
- - - - :field.pxViewSearches - - :search.pxView - - -
-
''') - - def __init__(self, group, page, metaType, forSearch=False): - self.type = 'group' - # All p_group attributes become self attributes. - for name, value in group.__dict__.iteritems(): - if not name.startswith('_'): - setattr(self, name, value) - self.columnsWidths = [col.width for col in group.columns] - self.columnsAligns = [col.align for col in group.columns] - # Names of i18n labels - labelName = self.name - prefix = metaType - if group.label: - if isinstance(group.label, basestring): prefix = group.label - else: # It is a tuple (metaType, name) - if group.label[1]: labelName = group.label[1] - if group.label[0]: prefix = group.label[0] - if forSearch: gp = 'searchgroup' - else: gp = 'group' - self.labelId = '%s_%s_%s' % (prefix, gp, labelName) - self.descrId = self.labelId + '_descr' - self.helpId = self.labelId + '_help' - # The name of the page where the group lies - self.page = page.name - # The fields belonging to the group that the current user may see. - # They will be stored by m_addField below as a list of lists because - # they will be rendered as a table. - self.fields = [[]] - # PX to user for rendering this group. - self.px = forSearch and self.pxViewSearches or self.pxView - - def addField(self, field): - '''Adds p_field into self.fields. We try first to add p_field into the - last row. If it is not possible, we create a new row.''' - # Get the last row - lastRow = self.fields[-1] - numberOfColumns = len(self.columnsWidths) - # Compute the number of columns already filled in the last row. - filledColumns = 0 - for rowField in lastRow: filledColumns += rowField.colspan - freeColumns = numberOfColumns - filledColumns - if freeColumns >= field.colspan: - # We can add the widget in the last row. - lastRow.append(field) - else: - if freeColumns: - # Terminate the current row by appending empty cells - for i in range(freeColumns): lastRow.append('') - # Create a new row - self.fields.append([field]) - -# ------------------------------------------------------------------------------ -# Abstract base class for every field. # ------------------------------------------------------------------------------ class Field: '''Basic abstract class for defining any field.''' + + # Some global static variables + nullValues = (None, '', []) + validatorTypes = (types.FunctionType, types.UnboundMethodType, + type(re.compile(''))) + labelTypes = ('label', 'descr', 'help') + # 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. @@ -759,7 +170,7 @@ class 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) + self.masterValue = gutils.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 @@ -937,12 +348,12 @@ class Field: # 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): + if (k not in self.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: + for labelType in self.labelTypes: if labelType not in mapping: mapping[labelType] = None # No mapping for this value. return mapping @@ -1159,7 +570,7 @@ class Field: def isEmptyValue(self, value, obj=None): '''Returns True if the p_value must be considered as an empty value.''' - return value in nullValues + return value in self.nullValues def validateValue(self, obj, value): '''This method may be overridden by child classes and will be called at @@ -1198,9 +609,9 @@ class Field: if message: return message # Evaluate the custom validator if one has been specified value = self.getStorableValue(value) - if self.validator and (type(self.validator) in validatorTypes): + if self.validator and (type(self.validator) in self.validatorTypes): obj = obj.appy() - if type(self.validator) != validatorTypes[-1]: + if type(self.validator) != self.validatorTypes[-1]: # It is a custom function. Execute it. try: validValue = self.validator(obj, value) diff --git a/fields/group.py b/fields/group.py new file mode 100644 index 0000000..979c9f0 --- /dev/null +++ b/fields/group.py @@ -0,0 +1,370 @@ +# ------------------------------------------------------------------------------ +# 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.px import Px +from appy.gen import utils as gutils + +# ------------------------------------------------------------------------------ +class Group: + '''Used for describing a group of fields 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 = gutils.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, fields, uiGroups, page, metaType, forSearch=False): + '''Inserts the UiGroup instance corresponding to this Group instance + into p_fields, the recursive structure used for displaying all + fields in a given p_page (or all searches), and returns this + UiGroup instance.''' + # First, create the corresponding UiGroup if not already in p_uiGroups. + if self.name not in uiGroups: + uiGroup = uiGroups[self.name] = UiGroup(self, page, metaType, + forSearch=forSearch) + # Insert the group at the higher level (ie, directly in p_fields) + # if the group is not itself in a group. + if not self.group: + fields.append(uiGroup) + else: + outerGroup = self.group.insertInto(fields, uiGroups, page, + metaType,forSearch=forSearch) + outerGroup.addField(uiGroup) + else: + uiGroup = uiGroups[self.name] + return uiGroup + +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 UiGroup: + '''On-the-fly-generated data structure that groups all fields sharing the + same appy.fields.Group instance, that some logged user can see.''' + + # PX that renders a help icon for a group. + pxHelp = Px('''''') + + # PX that renders the content of a group. + pxContent = Px(''' + + + + + + + + + + + + + + + + +
+ ::_(field.labelId):field.pxHelp +
::_(field.descrId)
::field.hasHeaders and \ + _('%s_col%d' % (field.labelId, (colNb+1))) or ''
+ + :field.pxView + :field.pxRender + +
''') + + # PX that renders a group of fields. + pxView = Px(''' + + + +
+ + ::_(field.labelId)>:field.pxHelp + +
::_(field.descrId)
+ :field.pxContent +
+ + + :field.pxContent + + + + + + + + + + + +
+ + + + + + + + +
+ :_('%s_col%d' % (field.labelId, rowNb)) +
+
+ :field.pxView + :field.pxRender +
+ +
+
''') + + # PX that renders a group of searches. + pxViewSearches = Px(''' + + +
+ + :_(field.labelId) + :field.translated +
+ +
+ + + + :field.pxViewSearches + + :search.pxView + + +
+
''') + + def __init__(self, group, page, metaType, forSearch=False): + self.type = 'group' + # All p_group attributes become self attributes. + for name, value in group.__dict__.iteritems(): + if not name.startswith('_'): + setattr(self, name, value) + self.columnsWidths = [col.width for col in group.columns] + self.columnsAligns = [col.align for col in group.columns] + # Names of i18n labels + labelName = self.name + prefix = metaType + if group.label: + if isinstance(group.label, basestring): prefix = group.label + else: # It is a tuple (metaType, name) + if group.label[1]: labelName = group.label[1] + if group.label[0]: prefix = group.label[0] + if forSearch: gp = 'searchgroup' + else: gp = 'group' + self.labelId = '%s_%s_%s' % (prefix, gp, labelName) + self.descrId = self.labelId + '_descr' + self.helpId = self.labelId + '_help' + # The name of the page where the group lies + self.page = page.name + # The fields belonging to the group that the current user may see. + # They will be stored by m_addField below as a list of lists because + # they will be rendered as a table. + self.fields = [[]] + # PX to user for rendering this group. + self.px = forSearch and self.pxViewSearches or self.pxView + + def addField(self, field): + '''Adds p_field into self.fields. We try first to add p_field into the + last row. If it is not possible, we create a new row.''' + # Get the last row + lastRow = self.fields[-1] + numberOfColumns = len(self.columnsWidths) + # Compute the number of columns already filled in the last row. + filledColumns = 0 + for rowField in lastRow: filledColumns += rowField.colspan + freeColumns = numberOfColumns - filledColumns + if freeColumns >= field.colspan: + # We can add the widget in the last row. + lastRow.append(field) + else: + if freeColumns: + # Terminate the current row by appending empty cells + for i in range(freeColumns): lastRow.append('') + # Create a new row + self.fields.append([field]) +# ------------------------------------------------------------------------------ diff --git a/fields/page.py b/fields/page.py new file mode 100644 index 0000000..e7b5519 --- /dev/null +++ b/fields/page.py @@ -0,0 +1,88 @@ +# ------------------------------------------------------------------------------ +# 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 + +# ------------------------------------------------------------------------------ +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 an object.''' + res = Object() + for elem in Page.subElements: + setattr(res, 'show%s' % elem.capitalize(), \ + self.isShowable(obj, layoutType, elem=elem)) + return res +# ------------------------------------------------------------------------------ diff --git a/fields/phase.py b/fields/phase.py new file mode 100644 index 0000000..acb1fb9 --- /dev/null +++ b/fields/phase.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.px import Px + +# ------------------------------------------------------------------------------ +class Phase: + '''A group of pages.''' + + pxView = Px(''' + + + + +
::_(label)
+ + + + + + + + + + + + ''') + + def __init__(self, name, obj): + self.name = name + self.obj = obj + # The list of names of pages in this phase + self.pages = [] + # The list of hidden pages in this phase + self.hiddenPages = [] + # The dict below stores info about every page listed in self.pages. + self.pagesInfo = {} + self.totalNbOfPhases = None + # The following attributes allows to browse, from a given page, to the + # last page of the previous phase and to the first page of the following + # phase if allowed by phase state. + self.previousPhase = None + self.nextPhase = None + + def addPageLinks(self, field, obj): + '''If p_field is a navigable Ref, we must add, within self.pagesInfo, + objects linked to p_obj through this ReF as links.''' + if field.page.name in self.hiddenPages: return + infos = [] + for ztied in field.getValue(obj, type='zobjects'): + infos.append(Object(title=ztied.title, url=ztied.absolute_url())) + self.pagesInfo[field.page.name].links = infos + + def addPage(self, field, obj, layoutType): + '''Adds page-related information in the phase.''' + # If the page is already there, we have nothing more to do. + if (field.page.name in self.pages) or \ + (field.page.name in self.hiddenPages): return + # Add the page only if it must be shown. + isShowableOnView = field.page.isShowable(obj, 'view') + isShowableOnEdit = field.page.isShowable(obj, 'edit') + if isShowableOnView or isShowableOnEdit: + # The page must be added. + self.pages.append(field.page.name) + # Create the dict about page information and add it in self.pageInfo + pageInfo = Object(page=field.page, showOnView=isShowableOnView, + showOnEdit=isShowableOnEdit, links=None) + pageInfo.update(field.page.getInfo(obj, layoutType)) + self.pagesInfo[field.page.name] = pageInfo + else: + self.hiddenPages.append(field.page.name) + + def computeNextPrevious(self, allPhases): + '''This method also fills fields "previousPhase" and "nextPhase" + if relevant, based on list of p_allPhases.''' + # Identify previous and next phases + for phase in allPhases: + if phase.name == self.name: + i = allPhases.index(phase) + if i > 0: + self.previousPhase = allPhases[i-1] + if i < (len(allPhases)-1): + self.nextPhase = allPhases[i+1] + + def getPreviousPage(self, page): + '''Returns the page that precedes p_page in this phase.''' + try: + pageIndex = self.pages.index(page) + except ValueError: + # The current page is probably not visible anymore. Return the + # first available page in current phase. + res = self.pages[0] + return res, self.pagesInfo[res] + if pageIndex > 0: + # We stay on the same phase, previous page + res = self.pages[pageIndex-1] + return res, self.pagesInfo[res] + else: + if self.previousPhase: + # We go to the last page of previous phase + previousPhase = self.previousPhase + res = previousPhase.pages[-1] + return res, previousPhase.pagesInfo[res] + else: + return None, None + + def getNextPage(self, page): + '''Returns the page that follows p_page in this phase.''' + try: + pageIndex = self.pages.index(page) + except ValueError: + # The current page is probably not visible anymore. Return the + # first available page in current phase. + res = self.pages[0] + return res, self.pagesInfo[res] + if pageIndex < (len(self.pages)-1): + # We stay on the same phase, next page + res = self.pages[pageIndex+1] + return res, self.pagesInfo[res] + else: + if self.nextPhase: + # We go to the first page of next phase + nextPhase = self.nextPhase + res = nextPhase.pages[0] + return res, nextPhase.pagesInfo[res] + else: + return None, None +# ------------------------------------------------------------------------------ diff --git a/fields/ref.py b/fields/ref.py index 94bda29..2b1af09 100644 --- a/fields/ref.py +++ b/fields/ref.py @@ -16,7 +16,7 @@ # ------------------------------------------------------------------------------ import sys, re -from appy.fields import Field, No +from appy.fields import Field from appy.px import Px from appy.gen.layout import Table from appy.gen import utils as gutils @@ -586,20 +586,21 @@ class Ref(Field): add = self.callMethod(obj, self.add) else: add = self.add - if not add: return No('no_add') + if not add: return gutils.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') + if refCount >= self.multiplicity[1]: return gutils.No('max_reached') # May the user edit this Ref field? - if not obj.allows(self.writePermission): return No('no_write_perm') + if not obj.allows(self.writePermission): + return gutils.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 tool.getUser().has_permission(addPermission, folder): - return No('no_add_perm') + return gutils.No('no_add_perm') return True def checkAdd(self, obj): diff --git a/fields/search.py b/fields/search.py new file mode 100644 index 0000000..47f8361 --- /dev/null +++ b/fields/search.py @@ -0,0 +1,178 @@ +# ------------------------------------------------------------------------------ +# 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.px import Px +from appy.gen import utils as gutils +from appy.gen.indexer import defaultIndexes +from appy.shared import utils as sutils +from group import Group + +# ------------------------------------------------------------------------------ +class Search: + '''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): + self.name = name + # Searches may be visually grouped in the portlet. + self.group = Group.get(group) + self.sortBy = sortBy + self.sortOrder = sortOrder + self.limit = limit + # If this search is the default one, it will be triggered by clicking + # on main link. + self.default = default + self.colspan = colspan + # If a translated name or description is already given here, we will + # use it instead of trying to translate from labels. + self.translated = translated + self.translatedDescr = translatedDescr + # Condition for showing or not this search + self.show = show + # In the dict below, keys are indexed field names or names of standard + # indexes, and values are search values. + self.fields = fields + + @staticmethod + def getIndexName(fieldName, usage='search'): + '''Gets the name of the technical index that corresponds to field named + p_fieldName. Indexes can be used for searching (p_usage="search") or + for sorting (usage="sort"). The method returns None if the field + named p_fieldName can't be used for p_usage.''' + if fieldName == 'title': + if usage == 'search': return 'Title' + else: return 'SortableTitle' + # Indeed, for field 'title', Appy has a specific index + # 'SortableTitle', because index 'Title' is a TextIndex + # (for searchability) and can't be used for sorting. + elif fieldName == 'state': return 'State' + elif fieldName == 'created': return 'Created' + elif fieldName == 'modified': return 'Modified' + elif fieldName in defaultIndexes: return fieldName + else: + return 'get%s%s'% (fieldName[0].upper(),fieldName[1:]) + + @staticmethod + def getSearchValue(fieldName, fieldValue, klass): + '''Returns a transformed p_fieldValue for producing a valid search + value as required for searching in the index corresponding to + p_fieldName.''' + field = getattr(klass, fieldName, None) + if (field and (field.getIndexType() == 'TextIndex')) or \ + (fieldName == 'SearchableText'): + # For TextIndex indexes. We must split p_fieldValue into keywords. + res = gutils.Keywords(fieldValue).get() + elif isinstance(fieldValue, basestring) and fieldValue.endswith('*'): + v = fieldValue[:-1] + # Warning: 'z' is higher than 'Z'! + res = {'query':(v,v+'z'), 'range':'min:max'} + elif type(fieldValue) in sutils.sequenceTypes: + if fieldValue and isinstance(fieldValue[0], basestring): + # We have a list of string values (ie: we need to + # search v1 or v2 or...) + res = fieldValue + else: + # We have a range of (int, float, DateTime...) values + minv, maxv = fieldValue + rangev = 'minmax' + queryv = fieldValue + if minv == None: + rangev = 'max' + queryv = maxv + elif maxv == None: + rangev = 'min' + queryv = minv + res = {'query':queryv, 'range':rangev} + else: + res = fieldValue + return res + + def updateSearchCriteria(self, criteria, klass, advanced=False): + '''This method updates dict p_criteria with all the search criteria + corresponding to this Search instance. If p_advanced is True, + p_criteria correspond to an advanced search, to be stored in the + session: in this case we need to keep the Appy names for parameters + sortBy and sortOrder (and not "resolve" them to Zope's sort_on and + sort_order).''' + # Put search criteria in p_criteria + for fieldName, fieldValue in self.fields.iteritems(): + # Management of searches restricted to objects linked through a + # Ref field: not implemented yet. + if fieldName == '_ref': continue + # Make the correspondence between the name of the field and the + # name of the corresponding index, excepted if advanced is True: in + # that case, the correspondence will be done later. + if not advanced: + attrName = Search.getIndexName(fieldName) + # Express the field value in the way needed by the index + criteria[attrName] = Search.getSearchValue(fieldName, + fieldValue, klass) + else: + criteria[fieldName]= fieldValue + # Add a sort order if specified + if self.sortBy: + if not advanced: + criteria['sort_on'] = Search.getIndexName(self.sortBy, + usage='sort') + if self.sortOrder == 'desc': criteria['sort_order'] = 'reverse' + else: criteria['sort_order'] = None + else: + criteria['sortBy'] = self.sortBy + criteria['sortOrder'] = self.sortOrder + + def isShowable(self, klass, tool): + '''Is this Search instance (defined in p_klass) showable?''' + if self.show.__class__.__name__ == 'staticmethod': + return gutils.callMethod(tool, self.show, klass=klass) + return self.show + +class UiSearch: + '''Instances of this class are generated on-the-fly for manipulating a + Search from the User Interface.''' + # PX for rendering a search. + pxView = Px(''' + ''') + + def __init__(self, search, className, tool): + self.search = search + self.name = search.name + self.type = 'search' + self.colspan = search.colspan + if search.translated: + self.translated = search.translated + self.translatedDescr = search.translatedDescr + else: + # The label may be specific in some special cases. + labelDescr = '' + if search.name == 'allSearch': + label = '%s_plural' % className + elif search.name == 'customSearch': + label = 'search_results' + else: + label = '%s_search_%s' % (className, search.name) + labelDescr = label + '_descr' + self.translated = tool.translate(label) + if labelDescr: + self.translatedDescr = tool.translate(labelDescr) + else: + self.translatedDescr = '' +# ------------------------------------------------------------------------------ diff --git a/gen/__init__.py b/gen/__init__.py index a39e351..6459369 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -1,15 +1,13 @@ # ------------------------------------------------------------------------------ import types, string from appy.gen.mail import sendNotification -from appy.gen.indexer import defaultIndexes from appy.gen import utils as gutils -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, Phase, Group, Field, Column, No +from appy.fields import Field from appy.fields.action import Action from appy.fields.boolean import Boolean from appy.fields.computed import Computed @@ -22,9 +20,14 @@ 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.fields.search import Search, UiSearch +from appy.fields.group import Group, Column +from appy.fields.page import Page +from appy.fields.phase import Phase from appy.gen.layout import Table from appy.px import Px from appy import Object +No = gutils.No # Default Appy permissions ----------------------------------------------------- r, w, d = ('read', 'write', 'delete') @@ -55,160 +58,6 @@ class Import: # and must return a similar, sorted, list. self.sort = sort -class Search: - '''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): - self.name = name - # Searches may be visually grouped in the portlet. - self.group = Group.get(group) - self.sortBy = sortBy - self.sortOrder = sortOrder - self.limit = limit - # If this search is the default one, it will be triggered by clicking - # on main link. - self.default = default - self.colspan = colspan - # If a translated name or description is already given here, we will - # use it instead of trying to translate from labels. - self.translated = translated - self.translatedDescr = translatedDescr - # Condition for showing or not this search - self.show = show - # In the dict below, keys are indexed field names or names of standard - # indexes, and values are search values. - self.fields = fields - - @staticmethod - def getIndexName(fieldName, usage='search'): - '''Gets the name of the technical index that corresponds to field named - p_fieldName. Indexes can be used for searching (p_usage="search") or - for sorting (usage="sort"). The method returns None if the field - named p_fieldName can't be used for p_usage.''' - if fieldName == 'title': - if usage == 'search': return 'Title' - else: return 'SortableTitle' - # Indeed, for field 'title', Appy has a specific index - # 'SortableTitle', because index 'Title' is a TextIndex - # (for searchability) and can't be used for sorting. - elif fieldName == 'state': return 'State' - elif fieldName == 'created': return 'Created' - elif fieldName == 'modified': return 'Modified' - elif fieldName in defaultIndexes: return fieldName - else: - return 'get%s%s'% (fieldName[0].upper(),fieldName[1:]) - - @staticmethod - def getSearchValue(fieldName, fieldValue, klass): - '''Returns a transformed p_fieldValue for producing a valid search - value as required for searching in the index corresponding to - p_fieldName.''' - field = getattr(klass, fieldName, None) - if (field and (field.getIndexType() == 'TextIndex')) or \ - (fieldName == 'SearchableText'): - # For TextIndex indexes. We must split p_fieldValue into keywords. - res = gutils.Keywords(fieldValue).get() - elif isinstance(fieldValue, basestring) and fieldValue.endswith('*'): - v = fieldValue[:-1] - # Warning: 'z' is higher than 'Z'! - res = {'query':(v,v+'z'), 'range':'min:max'} - elif type(fieldValue) in sutils.sequenceTypes: - if fieldValue and isinstance(fieldValue[0], basestring): - # We have a list of string values (ie: we need to - # search v1 or v2 or...) - res = fieldValue - else: - # We have a range of (int, float, DateTime...) values - minv, maxv = fieldValue - rangev = 'minmax' - queryv = fieldValue - if minv == None: - rangev = 'max' - queryv = maxv - elif maxv == None: - rangev = 'min' - queryv = minv - res = {'query':queryv, 'range':rangev} - else: - res = fieldValue - return res - - def updateSearchCriteria(self, criteria, klass, advanced=False): - '''This method updates dict p_criteria with all the search criteria - corresponding to this Search instance. If p_advanced is True, - p_criteria correspond to an advanced search, to be stored in the - session: in this case we need to keep the Appy names for parameters - sortBy and sortOrder (and not "resolve" them to Zope's sort_on and - sort_order).''' - # Put search criteria in p_criteria - for fieldName, fieldValue in self.fields.iteritems(): - # Management of searches restricted to objects linked through a - # Ref field: not implemented yet. - if fieldName == '_ref': continue - # Make the correspondence between the name of the field and the - # name of the corresponding index, excepted if advanced is True: in - # that case, the correspondence will be done later. - if not advanced: - attrName = Search.getIndexName(fieldName) - # Express the field value in the way needed by the index - criteria[attrName] = Search.getSearchValue(fieldName, - fieldValue, klass) - else: - criteria[fieldName]= fieldValue - # Add a sort order if specified - if self.sortBy: - if not advanced: - criteria['sort_on'] = Search.getIndexName(self.sortBy, - usage='sort') - if self.sortOrder == 'desc': criteria['sort_order'] = 'reverse' - else: criteria['sort_order'] = None - else: - criteria['sortBy'] = self.sortBy - criteria['sortOrder'] = self.sortOrder - - def isShowable(self, klass, tool): - '''Is this Search instance (defined in p_klass) showable?''' - if self.show.__class__.__name__ == 'staticmethod': - return gutils.callMethod(tool, self.show, klass=klass) - return self.show - -class UiSearch: - '''Instances of this class are generated on-the-fly for manipulating a - Search from the User Interface.''' - # PX for rendering a search. - pxView = Px(''' - ''') - - def __init__(self, search, className, tool): - self.search = search - self.name = search.name - self.type = 'search' - self.colspan = search.colspan - if search.translated: - self.translated = search.translated - self.translatedDescr = search.translatedDescr - else: - # The label may be specific in some special cases. - labelDescr = '' - if search.name == 'allSearch': - label = '%s_plural' % className - elif search.name == 'customSearch': - label = 'search_results' - else: - label = '%s_search_%s' % (className, search.name) - labelDescr = label + '_descr' - self.translated = tool.translate(label) - if labelDescr: - self.translatedDescr = tool.translate(labelDescr) - else: - self.translatedDescr = '' - # Workflow-specific types and default workflows -------------------------------- appyToZopePermissions = { 'read': ('View', 'Access contents information'), diff --git a/gen/utils.py b/gen/utils.py index ce20dab..e6ca05d 100644 --- a/gen/utils.py +++ b/gen/utils.py @@ -1,6 +1,6 @@ # ------------------------------------------------------------------------------ import re, os, os.path, base64, urllib -from appy.shared.utils import normalizeText +from appy.shared import utils as sutils # Function for creating a Zope object ------------------------------------------ def createObject(folder, id, className, appName, wf=True, noSecurity=False): @@ -96,7 +96,7 @@ class Keywords: toRemove = '?-+*()' def __init__(self, keywords, operator='AND'): # Clean the p_keywords that the user has entered. - words = normalizeText(keywords) + words = sutils.normalizeText(keywords) if words == '*': words = '' for c in self.toRemove: words = words.replace(c, ' ') self.keywords = words.split() @@ -220,4 +220,24 @@ def writeCookie(login, password, request): cookieValue = base64.encodestring('%s:%s' % (login, password)).rstrip() cookieValue = urllib.quote(cookieValue) request.RESPONSE.setCookie('_appy_', cookieValue, path='/') + +# ------------------------------------------------------------------------------ +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 # ------------------------------------------------------------------------------