[gen] Refactoring.
This commit is contained in:
parent
34e3a3083e
commit
1bd77d68c4
|
@ -16,613 +16,24 @@
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
import copy, types, re
|
import copy, types, re
|
||||||
|
from appy import Object
|
||||||
from appy.gen.layout import Table, defaultFieldLayouts
|
from appy.gen.layout import Table, defaultFieldLayouts
|
||||||
from appy.gen import utils as gutils
|
from appy.gen import utils as gutils
|
||||||
from appy.shared import utils as sutils
|
|
||||||
from appy.px import Px
|
from appy.px import Px
|
||||||
from appy import Object
|
from appy.shared import utils as sutils
|
||||||
|
from group import Group
|
||||||
|
from page import Page
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
class Field:
|
||||||
|
'''Basic abstract class for defining any field.'''
|
||||||
|
|
||||||
|
# Some global static variables
|
||||||
nullValues = (None, '', [])
|
nullValues = (None, '', [])
|
||||||
validatorTypes = (types.FunctionType, types.UnboundMethodType,
|
validatorTypes = (types.FunctionType, types.UnboundMethodType,
|
||||||
type(re.compile('')))
|
type(re.compile('')))
|
||||||
labelTypes = ('label', 'descr', 'help')
|
labelTypes = ('label', 'descr', 'help')
|
||||||
|
|
||||||
def initMasterValue(v):
|
|
||||||
'''Standardizes p_v as a list of strings.'''
|
|
||||||
if not isinstance(v, bool) and not v: res = []
|
|
||||||
elif type(v) not in sutils.sequenceTypes: res = [v]
|
|
||||||
else: res = v
|
|
||||||
return [str(v) for v in res]
|
|
||||||
|
|
||||||
class No:
|
|
||||||
'''When you write a workflow condition method and you want to return False
|
|
||||||
but you want to give to the user some explanations about why a transition
|
|
||||||
can't be triggered, do not return False, return an instance of No
|
|
||||||
instead. When creating such an instance, you can specify an error
|
|
||||||
message.'''
|
|
||||||
def __init__(self, msg):
|
|
||||||
self.msg = msg
|
|
||||||
def __nonzero__(self):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Page. Every field lives into a Page.
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
class Page:
|
|
||||||
'''Used for describing a page, its related phase, show condition, etc.'''
|
|
||||||
subElements = ('save', 'cancel', 'previous', 'next', 'edit')
|
|
||||||
def __init__(self, name, phase='main', show=True, showSave=True,
|
|
||||||
showCancel=True, showPrevious=True, showNext=True,
|
|
||||||
showEdit=True):
|
|
||||||
self.name = name
|
|
||||||
self.phase = phase
|
|
||||||
self.show = show
|
|
||||||
# When editing the page, must I show the "save" button?
|
|
||||||
self.showSave = showSave
|
|
||||||
# When editing the page, must I show the "cancel" button?
|
|
||||||
self.showCancel = showCancel
|
|
||||||
# When editing the page, and when a previous page exists, must I show
|
|
||||||
# the "previous" button?
|
|
||||||
self.showPrevious = showPrevious
|
|
||||||
# When editing the page, and when a next page exists, must I show the
|
|
||||||
# "next" button?
|
|
||||||
self.showNext = showNext
|
|
||||||
# When viewing the page, must I show the "edit" button?
|
|
||||||
self.showEdit = showEdit
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get(pageData):
|
|
||||||
'''Produces a Page instance from p_pageData. User-defined p_pageData
|
|
||||||
can be:
|
|
||||||
(a) a string containing the name of the page;
|
|
||||||
(b) a string containing <pageName>_<phaseName>;
|
|
||||||
(c) a Page instance.
|
|
||||||
This method returns always a Page instance.'''
|
|
||||||
res = pageData
|
|
||||||
if res and isinstance(res, basestring):
|
|
||||||
# Page data is given as a string.
|
|
||||||
pageElems = pageData.rsplit('_', 1)
|
|
||||||
if len(pageElems) == 1: # We have case (a)
|
|
||||||
res = Page(pageData)
|
|
||||||
else: # We have case (b)
|
|
||||||
res = Page(pageData[0], phase=pageData[1])
|
|
||||||
return res
|
|
||||||
|
|
||||||
def isShowable(self, obj, layoutType, elem='page'):
|
|
||||||
'''Must this page be shown for p_obj? "Show value" can be True, False
|
|
||||||
or 'view' (page is available only in "view" mode).
|
|
||||||
|
|
||||||
If p_elem is not "page", this method returns the fact that a
|
|
||||||
sub-element is viewable or not (buttons "save", "cancel", etc).'''
|
|
||||||
# Define what attribute to test for "showability".
|
|
||||||
showAttr = 'show'
|
|
||||||
if elem != 'page':
|
|
||||||
showAttr = 'show%s' % elem.capitalize()
|
|
||||||
# Get the value of the show attribute as identified above.
|
|
||||||
show = getattr(self, showAttr)
|
|
||||||
if callable(show):
|
|
||||||
show = show(obj.appy())
|
|
||||||
# Show value can be 'view', for example. Thanks to p_layoutType,
|
|
||||||
# convert show value to a real final boolean value.
|
|
||||||
res = show
|
|
||||||
if res == 'view': res = layoutType == 'view'
|
|
||||||
return res
|
|
||||||
|
|
||||||
def getInfo(self, obj, layoutType):
|
|
||||||
'''Gets information about this page, for p_obj, as 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('''
|
|
||||||
<tr var="singlePage=len(phase.pages) == 1">
|
|
||||||
<td var="label='%s_phase_%s' % (zobj.meta_type, phase.name)">
|
|
||||||
|
|
||||||
<!-- The title of the phase -->
|
|
||||||
<div class="portletGroup"
|
|
||||||
if="not singlePhase and not singlePage">::_(label)</div>
|
|
||||||
|
|
||||||
<!-- The page(s) within the phase -->
|
|
||||||
<x for="aPage in phase.pages">
|
|
||||||
<!-- First line: page name and icons -->
|
|
||||||
<div if="not (singlePhase and singlePage)"
|
|
||||||
class=":aPage==page and 'portletCurrent portletPage' or \
|
|
||||||
'portletPage'">
|
|
||||||
<a href=":zobj.getUrl(page=aPage)">::_('%s_page_%s' % \
|
|
||||||
(zobj.meta_type, aPage))</a>
|
|
||||||
<x var="locked=zobj.isLocked(user, aPage);
|
|
||||||
editable=mayEdit and phase.pagesInfo[aPage].showOnEdit">
|
|
||||||
<a if="editable and not locked"
|
|
||||||
href=":zobj.getUrl(mode='edit', page=aPage)">
|
|
||||||
<img src=":url('edit')" title=":_('object_edit')"/></a>
|
|
||||||
<a if="editable and locked">
|
|
||||||
<img style="cursor: help"
|
|
||||||
var="lockDate=tool.formatDate(locked[1]);
|
|
||||||
lockMap={'user':ztool.getUserName(locked[0]), \
|
|
||||||
'date':lockDate};
|
|
||||||
lockMsg=_('page_locked', mapping=lockMap)"
|
|
||||||
src=":url('locked')" title=":lockMsg"/></a>
|
|
||||||
<a if="editable and locked and user.has_role('Manager')">
|
|
||||||
<img class="clickable" title=":_('page_unlock')" src=":url('unlock')"
|
|
||||||
onclick=":'onUnlockPage(%s,%s)' % \
|
|
||||||
(q(zobj.UID()), q(aPage))"/></a>
|
|
||||||
</x>
|
|
||||||
</div>
|
|
||||||
<!-- Next lines: links -->
|
|
||||||
<x var="links=phase.pagesInfo[aPage].links" if="links">
|
|
||||||
<div for="link in links"><a href=":link.url">:link.title</a></div>
|
|
||||||
</x>
|
|
||||||
</x>
|
|
||||||
</td>
|
|
||||||
</tr>''')
|
|
||||||
|
|
||||||
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 <groupName>_<numberOfColumns>.
|
|
||||||
groupElems = groupData.rsplit('_', 1)
|
|
||||||
if len(groupElems) == 1:
|
|
||||||
res = Group(groupElems[0])
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
nbOfColumns = int(groupElems[1])
|
|
||||||
except ValueError:
|
|
||||||
nbOfColumns = 1
|
|
||||||
width = 100.0 / nbOfColumns
|
|
||||||
res = Group(groupElems[0], ['%.2f%%' % width] * nbOfColumns)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def getMasterData(self):
|
|
||||||
'''Gets the master of this group (and masterValue) or, recursively, of
|
|
||||||
containing groups when relevant.'''
|
|
||||||
if self.master: return (self.master, self.masterValue)
|
|
||||||
if self.group: return self.group.getMasterData()
|
|
||||||
|
|
||||||
def generateLabels(self, messages, classDescr, walkedGroups,
|
|
||||||
forSearch=False):
|
|
||||||
'''This method allows to generate all the needed i18n labels related to
|
|
||||||
this group. p_messages is the list of i18n p_messages (a PoMessages
|
|
||||||
instance) that we are currently building; p_classDescr is the
|
|
||||||
descriptor of the class where this group is defined. If p_forSearch
|
|
||||||
is True, this group is used for grouping searches, and not fields.'''
|
|
||||||
# A part of the group label depends on p_forSearch.
|
|
||||||
if forSearch: gp = 'searchgroup'
|
|
||||||
else: gp = 'group'
|
|
||||||
if self.hasLabel:
|
|
||||||
msgId = '%s_%s_%s' % (classDescr.name, gp, self.name)
|
|
||||||
messages.append(msgId, self.name)
|
|
||||||
if self.hasDescr:
|
|
||||||
msgId = '%s_%s_%s_descr' % (classDescr.name, gp, self.name)
|
|
||||||
messages.append(msgId, ' ', nice=False)
|
|
||||||
if self.hasHelp:
|
|
||||||
msgId = '%s_%s_%s_help' % (classDescr.name, gp, self.name)
|
|
||||||
messages.append(msgId, ' ', nice=False)
|
|
||||||
if self.hasHeaders:
|
|
||||||
for i in range(self.nbOfHeaders):
|
|
||||||
msgId = '%s_%s_%s_col%d' % (classDescr.name, gp, self.name, i+1)
|
|
||||||
messages.append(msgId, ' ', nice=False)
|
|
||||||
walkedGroups.add(self)
|
|
||||||
if self.group and (self.group not in walkedGroups) and \
|
|
||||||
not self.group.label:
|
|
||||||
# We remember walked groups for avoiding infinite recursion.
|
|
||||||
self.group.generateLabels(messages, classDescr, walkedGroups,
|
|
||||||
forSearch=forSearch)
|
|
||||||
|
|
||||||
def insertInto(self, 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('''<acronym title="obj.translate('help', field=field)"><img
|
|
||||||
src=":url('help')"/></acronym>''')
|
|
||||||
|
|
||||||
# PX that renders the content of a group.
|
|
||||||
pxContent = Px('''
|
|
||||||
<table var="cellgap=field.cellgap" width=":field.wide"
|
|
||||||
align=":ztool.flipLanguageDirection(field.align, dir)"
|
|
||||||
id=":tagId" name=":tagName" class=":groupCss"
|
|
||||||
cellspacing=":field.cellspacing" cellpadding=":field.cellpadding">
|
|
||||||
<!-- Display the title of the group if not rendered a fieldset. -->
|
|
||||||
<tr if="(field.style != 'fieldset') and field.hasLabel">
|
|
||||||
<td colspan=":len(field.columnsWidths)" class=":field.style"
|
|
||||||
align=":dleft">
|
|
||||||
<x>::_(field.labelId)</x><x if="field.hasHelp">:field.pxHelp</x>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr if="(field.style != 'fieldset') and field.hasDescr">
|
|
||||||
<td colspan=":len(field.columnsWidths)"
|
|
||||||
class="discreet">::_(field.descrId)</td>
|
|
||||||
</tr>
|
|
||||||
<!-- The column headers -->
|
|
||||||
<tr>
|
|
||||||
<th for="colNb in range(len(field.columnsWidths))"
|
|
||||||
align="ztool.flipLanguageDirection(field.columnsAligns[colNb], dir)"
|
|
||||||
width=":field.columnsWidths[colNb]">::field.hasHeaders and \
|
|
||||||
_('%s_col%d' % (field.labelId, (colNb+1))) or ''</th>
|
|
||||||
</tr>
|
|
||||||
<!-- The rows of widgets -->
|
|
||||||
<tr valign=":field.valign" for="row in field.fields">
|
|
||||||
<td for="field in row"
|
|
||||||
colspan="field.colspan"
|
|
||||||
style=":not loop.field.last and ('padding-right:%s'% cellgap) or ''">
|
|
||||||
<x if="field">
|
|
||||||
<x if="field.type == 'group'">:field.pxView</x>
|
|
||||||
<x if="field.type != 'group'">:field.pxRender</x>
|
|
||||||
</x>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>''')
|
|
||||||
|
|
||||||
# PX that renders a group of fields.
|
|
||||||
pxView = Px('''
|
|
||||||
<x var="tagCss=field.master and ('slave_%s_%s' % \
|
|
||||||
(field.masterName, '_'.join(field.masterValue))) or '';
|
|
||||||
widgetCss=field.css_class;
|
|
||||||
groupCss=tagCss and ('%s %s' % (tagCss, widgetCss)) or widgetCss;
|
|
||||||
tagName=field.master and 'slave' or '';
|
|
||||||
tagId='%s_%s' % (zobj.UID(), field.name)">
|
|
||||||
|
|
||||||
<!-- Render the group as a fieldset if required -->
|
|
||||||
<fieldset if="field.style == 'fieldset'">
|
|
||||||
<legend if="field.hasLabel">
|
|
||||||
<i>::_(field.labelId)></i><x if="field.hasHelp">:field.pxHelp</x>
|
|
||||||
</legend>
|
|
||||||
<div if="field.hasDescr" class="discreet">::_(field.descrId)</div>
|
|
||||||
<x>:field.pxContent</x>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<!-- Render the group as a section if required -->
|
|
||||||
<x if="field.style not in ('fieldset', 'tabs')">:field.pxContent</x>
|
|
||||||
|
|
||||||
<!-- Render the group as tabs if required -->
|
|
||||||
<x if="field.style == 'tabs'" var2="lenFields=len(field.fields)">
|
|
||||||
<table width=":field.wide" class=":groupCss" id=":tagId" name=":tagName">
|
|
||||||
<!-- First row: the tabs. -->
|
|
||||||
<tr valign="middle"><td style="border-bottom: 1px solid #ff8040">
|
|
||||||
<table style="position:relative; bottom:-2px"
|
|
||||||
cellpadding="0" cellspacing="0">
|
|
||||||
<tr valign="bottom">
|
|
||||||
<x for="row in field.fields"
|
|
||||||
var2="rowNb=loop.row.nb;
|
|
||||||
tabId='tab_%s_%d_%d' % (field.name, rowNb, lenFields)">
|
|
||||||
<td><img src=":url('tabLeft')" id=":'%s_left' % tabId"/></td>
|
|
||||||
<td style=":url('tabBg', bg=True)" id=":tabId">
|
|
||||||
<a onclick=":'showTab(%s)' % q('%s_%d_%d' % (field.name, rowNb, \
|
|
||||||
lenFields))"
|
|
||||||
class="clickable">:_('%s_col%d' % (field.labelId, rowNb))</a>
|
|
||||||
</td>
|
|
||||||
<td><img id=":'%s_right' % tabId" src=":url('tabRight')"/></td>
|
|
||||||
</x>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td></tr>
|
|
||||||
|
|
||||||
<!-- Other rows: the fields -->
|
|
||||||
<tr for="row in field.fields"
|
|
||||||
id=":'tabcontent_%s_%d_%d' % (field.name, loop.row.nb, lenFields)"
|
|
||||||
style=":loop.row.nb==0 and 'display:table-row' or 'display:none')">
|
|
||||||
<td var="field=row[0]">
|
|
||||||
<x if="field.type == 'group'">:field.pxView</x>
|
|
||||||
<x if="field.type != 'group'">:field.pxRender</x>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<script type="text/javascript">:'initTab(%s,%s)' % \
|
|
||||||
(q('tab_%s' % field.name), q('%s_1_%d' % (field.name, lenFields)))">
|
|
||||||
</script>
|
|
||||||
</x>
|
|
||||||
</x>''')
|
|
||||||
|
|
||||||
# PX that renders a group of searches.
|
|
||||||
pxViewSearches = Px('''
|
|
||||||
<x var="expanded=req.get(field.labelId, 'collapsed') == 'expanded'">
|
|
||||||
<!-- Group name, prefixed by the expand/collapse icon -->
|
|
||||||
<div class="portletGroup">
|
|
||||||
<img class="clickable" style="margin-right: 3px" align=":dleft"
|
|
||||||
id=":'%s_img' % field.labelId"
|
|
||||||
src=":expanded and url('collapse.gif') or url('expand.gif')"
|
|
||||||
onclick=":'toggleCookie(%s)' % q(field.labelId)"/>
|
|
||||||
<x if="not field.translated">:_(field.labelId)</x>
|
|
||||||
<x if="field.translated">:field.translated</x>
|
|
||||||
</div>
|
|
||||||
<!-- Group content -->
|
|
||||||
<div var="display=expanded and 'display:block' or 'display:none'"
|
|
||||||
id=":field.labelId" style=":'padding-left: 10px; %s' % display">
|
|
||||||
<x for="searches in field.widgets">
|
|
||||||
<x for="elem in searches">
|
|
||||||
<!-- An inner group within this group -->
|
|
||||||
<x if="elem.type == 'group'"
|
|
||||||
var2="field=elem">:field.pxViewSearches</x>
|
|
||||||
<!-- A search -->
|
|
||||||
<x if="elem.type != 'group'" var2="search=elem">:search.pxView</x>
|
|
||||||
</x>
|
|
||||||
</x>
|
|
||||||
</div>
|
|
||||||
</x>''')
|
|
||||||
|
|
||||||
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.'''
|
|
||||||
# Those attributes can be overridden by subclasses for defining,
|
# Those attributes can be overridden by subclasses for defining,
|
||||||
# respectively, names of CSS and Javascript files that are required by this
|
# respectively, names of CSS and Javascript files that are required by this
|
||||||
# field, keyed by layoutType.
|
# field, keyed by layoutType.
|
||||||
|
@ -759,7 +170,7 @@ class Field:
|
||||||
self.master = master
|
self.master = master
|
||||||
if master: self.master.slaves.append(self)
|
if master: self.master.slaves.append(self)
|
||||||
# When master has some value(s), there is impact on this field.
|
# 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.
|
# If a field must retain attention in a particular way, set focus=True.
|
||||||
# It will be rendered in a special way.
|
# It will be rendered in a special way.
|
||||||
self.focus = focus
|
self.focus = focus
|
||||||
|
@ -937,12 +348,12 @@ class Field:
|
||||||
# Is it a dict like {'label':..., 'descr':...}, or is it directly a
|
# Is it a dict like {'label':..., 'descr':...}, or is it directly a
|
||||||
# dict with a mapping?
|
# dict with a mapping?
|
||||||
for k, v in mapping.iteritems():
|
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
|
# It is already a mapping
|
||||||
return {'label':mapping, 'descr':mapping, 'help':mapping}
|
return {'label':mapping, 'descr':mapping, 'help':mapping}
|
||||||
# If we are here, we have {'label':..., 'descr':...}. Complete
|
# If we are here, we have {'label':..., 'descr':...}. Complete
|
||||||
# it if necessary.
|
# it if necessary.
|
||||||
for labelType in labelTypes:
|
for labelType in self.labelTypes:
|
||||||
if labelType not in mapping:
|
if labelType not in mapping:
|
||||||
mapping[labelType] = None # No mapping for this value.
|
mapping[labelType] = None # No mapping for this value.
|
||||||
return mapping
|
return mapping
|
||||||
|
@ -1159,7 +570,7 @@ class Field:
|
||||||
|
|
||||||
def isEmptyValue(self, value, obj=None):
|
def isEmptyValue(self, value, obj=None):
|
||||||
'''Returns True if the p_value must be considered as an empty value.'''
|
'''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):
|
def validateValue(self, obj, value):
|
||||||
'''This method may be overridden by child classes and will be called at
|
'''This method may be overridden by child classes and will be called at
|
||||||
|
@ -1198,9 +609,9 @@ class Field:
|
||||||
if message: return message
|
if message: return message
|
||||||
# Evaluate the custom validator if one has been specified
|
# Evaluate the custom validator if one has been specified
|
||||||
value = self.getStorableValue(value)
|
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()
|
obj = obj.appy()
|
||||||
if type(self.validator) != validatorTypes[-1]:
|
if type(self.validator) != self.validatorTypes[-1]:
|
||||||
# It is a custom function. Execute it.
|
# It is a custom function. Execute it.
|
||||||
try:
|
try:
|
||||||
validValue = self.validator(obj, value)
|
validValue = self.validator(obj, value)
|
||||||
|
|
370
fields/group.py
Normal file
370
fields/group.py
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
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 <groupName>_<numberOfColumns>.
|
||||||
|
groupElems = groupData.rsplit('_', 1)
|
||||||
|
if len(groupElems) == 1:
|
||||||
|
res = Group(groupElems[0])
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
nbOfColumns = int(groupElems[1])
|
||||||
|
except ValueError:
|
||||||
|
nbOfColumns = 1
|
||||||
|
width = 100.0 / nbOfColumns
|
||||||
|
res = Group(groupElems[0], ['%.2f%%' % width] * nbOfColumns)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def getMasterData(self):
|
||||||
|
'''Gets the master of this group (and masterValue) or, recursively, of
|
||||||
|
containing groups when relevant.'''
|
||||||
|
if self.master: return (self.master, self.masterValue)
|
||||||
|
if self.group: return self.group.getMasterData()
|
||||||
|
|
||||||
|
def generateLabels(self, messages, classDescr, walkedGroups,
|
||||||
|
forSearch=False):
|
||||||
|
'''This method allows to generate all the needed i18n labels related to
|
||||||
|
this group. p_messages is the list of i18n p_messages (a PoMessages
|
||||||
|
instance) that we are currently building; p_classDescr is the
|
||||||
|
descriptor of the class where this group is defined. If p_forSearch
|
||||||
|
is True, this group is used for grouping searches, and not fields.'''
|
||||||
|
# A part of the group label depends on p_forSearch.
|
||||||
|
if forSearch: gp = 'searchgroup'
|
||||||
|
else: gp = 'group'
|
||||||
|
if self.hasLabel:
|
||||||
|
msgId = '%s_%s_%s' % (classDescr.name, gp, self.name)
|
||||||
|
messages.append(msgId, self.name)
|
||||||
|
if self.hasDescr:
|
||||||
|
msgId = '%s_%s_%s_descr' % (classDescr.name, gp, self.name)
|
||||||
|
messages.append(msgId, ' ', nice=False)
|
||||||
|
if self.hasHelp:
|
||||||
|
msgId = '%s_%s_%s_help' % (classDescr.name, gp, self.name)
|
||||||
|
messages.append(msgId, ' ', nice=False)
|
||||||
|
if self.hasHeaders:
|
||||||
|
for i in range(self.nbOfHeaders):
|
||||||
|
msgId = '%s_%s_%s_col%d' % (classDescr.name, gp, self.name, i+1)
|
||||||
|
messages.append(msgId, ' ', nice=False)
|
||||||
|
walkedGroups.add(self)
|
||||||
|
if self.group and (self.group not in walkedGroups) and \
|
||||||
|
not self.group.label:
|
||||||
|
# We remember walked groups for avoiding infinite recursion.
|
||||||
|
self.group.generateLabels(messages, classDescr, walkedGroups,
|
||||||
|
forSearch=forSearch)
|
||||||
|
|
||||||
|
def insertInto(self, 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('''<acronym title="obj.translate('help', field=field)"><img
|
||||||
|
src=":url('help')"/></acronym>''')
|
||||||
|
|
||||||
|
# PX that renders the content of a group.
|
||||||
|
pxContent = Px('''
|
||||||
|
<table var="cellgap=field.cellgap" width=":field.wide"
|
||||||
|
align=":ztool.flipLanguageDirection(field.align, dir)"
|
||||||
|
id=":tagId" name=":tagName" class=":groupCss"
|
||||||
|
cellspacing=":field.cellspacing" cellpadding=":field.cellpadding">
|
||||||
|
<!-- Display the title of the group if not rendered a fieldset. -->
|
||||||
|
<tr if="(field.style != 'fieldset') and field.hasLabel">
|
||||||
|
<td colspan=":len(field.columnsWidths)" class=":field.style"
|
||||||
|
align=":dleft">
|
||||||
|
<x>::_(field.labelId)</x><x if="field.hasHelp">:field.pxHelp</x>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr if="(field.style != 'fieldset') and field.hasDescr">
|
||||||
|
<td colspan=":len(field.columnsWidths)"
|
||||||
|
class="discreet">::_(field.descrId)</td>
|
||||||
|
</tr>
|
||||||
|
<!-- The column headers -->
|
||||||
|
<tr>
|
||||||
|
<th for="colNb in range(len(field.columnsWidths))"
|
||||||
|
align="ztool.flipLanguageDirection(field.columnsAligns[colNb], dir)"
|
||||||
|
width=":field.columnsWidths[colNb]">::field.hasHeaders and \
|
||||||
|
_('%s_col%d' % (field.labelId, (colNb+1))) or ''</th>
|
||||||
|
</tr>
|
||||||
|
<!-- The rows of widgets -->
|
||||||
|
<tr valign=":field.valign" for="row in field.fields">
|
||||||
|
<td for="field in row"
|
||||||
|
colspan="field.colspan"
|
||||||
|
style=":not loop.field.last and ('padding-right:%s'% cellgap) or ''">
|
||||||
|
<x if="field">
|
||||||
|
<x if="field.type == 'group'">:field.pxView</x>
|
||||||
|
<x if="field.type != 'group'">:field.pxRender</x>
|
||||||
|
</x>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>''')
|
||||||
|
|
||||||
|
# PX that renders a group of fields.
|
||||||
|
pxView = Px('''
|
||||||
|
<x var="tagCss=field.master and ('slave_%s_%s' % \
|
||||||
|
(field.masterName, '_'.join(field.masterValue))) or '';
|
||||||
|
widgetCss=field.css_class;
|
||||||
|
groupCss=tagCss and ('%s %s' % (tagCss, widgetCss)) or widgetCss;
|
||||||
|
tagName=field.master and 'slave' or '';
|
||||||
|
tagId='%s_%s' % (zobj.UID(), field.name)">
|
||||||
|
|
||||||
|
<!-- Render the group as a fieldset if required -->
|
||||||
|
<fieldset if="field.style == 'fieldset'">
|
||||||
|
<legend if="field.hasLabel">
|
||||||
|
<i>::_(field.labelId)></i><x if="field.hasHelp">:field.pxHelp</x>
|
||||||
|
</legend>
|
||||||
|
<div if="field.hasDescr" class="discreet">::_(field.descrId)</div>
|
||||||
|
<x>:field.pxContent</x>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- Render the group as a section if required -->
|
||||||
|
<x if="field.style not in ('fieldset', 'tabs')">:field.pxContent</x>
|
||||||
|
|
||||||
|
<!-- Render the group as tabs if required -->
|
||||||
|
<x if="field.style == 'tabs'" var2="lenFields=len(field.fields)">
|
||||||
|
<table width=":field.wide" class=":groupCss" id=":tagId" name=":tagName">
|
||||||
|
<!-- First row: the tabs. -->
|
||||||
|
<tr valign="middle"><td style="border-bottom: 1px solid #ff8040">
|
||||||
|
<table style="position:relative; bottom:-2px"
|
||||||
|
cellpadding="0" cellspacing="0">
|
||||||
|
<tr valign="bottom">
|
||||||
|
<x for="row in field.fields"
|
||||||
|
var2="rowNb=loop.row.nb;
|
||||||
|
tabId='tab_%s_%d_%d' % (field.name, rowNb, lenFields)">
|
||||||
|
<td><img src=":url('tabLeft')" id=":'%s_left' % tabId"/></td>
|
||||||
|
<td style=":url('tabBg', bg=True)" id=":tabId">
|
||||||
|
<a onclick=":'showTab(%s)' % q('%s_%d_%d' % (field.name, rowNb, \
|
||||||
|
lenFields))"
|
||||||
|
class="clickable">:_('%s_col%d' % (field.labelId, rowNb))</a>
|
||||||
|
</td>
|
||||||
|
<td><img id=":'%s_right' % tabId" src=":url('tabRight')"/></td>
|
||||||
|
</x>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
|
||||||
|
<!-- Other rows: the fields -->
|
||||||
|
<tr for="row in field.fields"
|
||||||
|
id=":'tabcontent_%s_%d_%d' % (field.name, loop.row.nb, lenFields)"
|
||||||
|
style=":loop.row.nb==0 and 'display:table-row' or 'display:none')">
|
||||||
|
<td var="field=row[0]">
|
||||||
|
<x if="field.type == 'group'">:field.pxView</x>
|
||||||
|
<x if="field.type != 'group'">:field.pxRender</x>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<script type="text/javascript">:'initTab(%s,%s)' % \
|
||||||
|
(q('tab_%s' % field.name), q('%s_1_%d' % (field.name, lenFields)))">
|
||||||
|
</script>
|
||||||
|
</x>
|
||||||
|
</x>''')
|
||||||
|
|
||||||
|
# PX that renders a group of searches.
|
||||||
|
pxViewSearches = Px('''
|
||||||
|
<x var="expanded=req.get(field.labelId, 'collapsed') == 'expanded'">
|
||||||
|
<!-- Group name, prefixed by the expand/collapse icon -->
|
||||||
|
<div class="portletGroup">
|
||||||
|
<img class="clickable" style="margin-right: 3px" align=":dleft"
|
||||||
|
id=":'%s_img' % field.labelId"
|
||||||
|
src=":expanded and url('collapse.gif') or url('expand.gif')"
|
||||||
|
onclick=":'toggleCookie(%s)' % q(field.labelId)"/>
|
||||||
|
<x if="not field.translated">:_(field.labelId)</x>
|
||||||
|
<x if="field.translated">:field.translated</x>
|
||||||
|
</div>
|
||||||
|
<!-- Group content -->
|
||||||
|
<div var="display=expanded and 'display:block' or 'display:none'"
|
||||||
|
id=":field.labelId" style=":'padding-left: 10px; %s' % display">
|
||||||
|
<x for="searches in field.widgets">
|
||||||
|
<x for="elem in searches">
|
||||||
|
<!-- An inner group within this group -->
|
||||||
|
<x if="elem.type == 'group'"
|
||||||
|
var2="field=elem">:field.pxViewSearches</x>
|
||||||
|
<!-- A search -->
|
||||||
|
<x if="elem.type != 'group'" var2="search=elem">:search.pxView</x>
|
||||||
|
</x>
|
||||||
|
</x>
|
||||||
|
</div>
|
||||||
|
</x>''')
|
||||||
|
|
||||||
|
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])
|
||||||
|
# ------------------------------------------------------------------------------
|
88
fields/page.py
Normal file
88
fields/page.py
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
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 <pageName>_<phaseName>;
|
||||||
|
(c) a Page instance.
|
||||||
|
This method returns always a Page instance.'''
|
||||||
|
res = pageData
|
||||||
|
if res and isinstance(res, basestring):
|
||||||
|
# Page data is given as a string.
|
||||||
|
pageElems = pageData.rsplit('_', 1)
|
||||||
|
if len(pageElems) == 1: # We have case (a)
|
||||||
|
res = Page(pageData)
|
||||||
|
else: # We have case (b)
|
||||||
|
res = Page(pageData[0], phase=pageData[1])
|
||||||
|
return res
|
||||||
|
|
||||||
|
def isShowable(self, obj, layoutType, elem='page'):
|
||||||
|
'''Must this page be shown for p_obj? "Show value" can be True, False
|
||||||
|
or 'view' (page is available only in "view" mode).
|
||||||
|
|
||||||
|
If p_elem is not "page", this method returns the fact that a
|
||||||
|
sub-element is viewable or not (buttons "save", "cancel", etc).'''
|
||||||
|
# Define what attribute to test for "showability".
|
||||||
|
showAttr = 'show'
|
||||||
|
if elem != 'page':
|
||||||
|
showAttr = 'show%s' % elem.capitalize()
|
||||||
|
# Get the value of the show attribute as identified above.
|
||||||
|
show = getattr(self, showAttr)
|
||||||
|
if callable(show):
|
||||||
|
show = show(obj.appy())
|
||||||
|
# Show value can be 'view', for example. Thanks to p_layoutType,
|
||||||
|
# convert show value to a real final boolean value.
|
||||||
|
res = show
|
||||||
|
if res == 'view': res = layoutType == 'view'
|
||||||
|
return res
|
||||||
|
|
||||||
|
def getInfo(self, obj, layoutType):
|
||||||
|
'''Gets information about this page, for p_obj, as an object.'''
|
||||||
|
res = Object()
|
||||||
|
for elem in Page.subElements:
|
||||||
|
setattr(res, 'show%s' % elem.capitalize(), \
|
||||||
|
self.isShowable(obj, layoutType, elem=elem))
|
||||||
|
return res
|
||||||
|
# ------------------------------------------------------------------------------
|
166
fields/phase.py
Normal file
166
fields/phase.py
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# This file is part of Appy, a framework for building applications in the Python
|
||||||
|
# language. Copyright (C) 2007 Gaetan Delannay
|
||||||
|
|
||||||
|
# Appy is free software; you can redistribute it and/or modify it under the
|
||||||
|
# terms of the GNU General Public License as published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
|
||||||
|
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# Appy. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
from appy import Object
|
||||||
|
from appy.px import Px
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
class Phase:
|
||||||
|
'''A group of pages.'''
|
||||||
|
|
||||||
|
pxView = Px('''
|
||||||
|
<tr var="singlePage=len(phase.pages) == 1">
|
||||||
|
<td var="label='%s_phase_%s' % (zobj.meta_type, phase.name)">
|
||||||
|
|
||||||
|
<!-- The title of the phase -->
|
||||||
|
<div class="portletGroup"
|
||||||
|
if="not singlePhase and not singlePage">::_(label)</div>
|
||||||
|
|
||||||
|
<!-- The page(s) within the phase -->
|
||||||
|
<x for="aPage in phase.pages">
|
||||||
|
<!-- First line: page name and icons -->
|
||||||
|
<div if="not (singlePhase and singlePage)"
|
||||||
|
class=":aPage==page and 'portletCurrent portletPage' or \
|
||||||
|
'portletPage'">
|
||||||
|
<a href=":zobj.getUrl(page=aPage)">::_('%s_page_%s' % \
|
||||||
|
(zobj.meta_type, aPage))</a>
|
||||||
|
<x var="locked=zobj.isLocked(user, aPage);
|
||||||
|
editable=mayEdit and phase.pagesInfo[aPage].showOnEdit">
|
||||||
|
<a if="editable and not locked"
|
||||||
|
href=":zobj.getUrl(mode='edit', page=aPage)">
|
||||||
|
<img src=":url('edit')" title=":_('object_edit')"/></a>
|
||||||
|
<a if="editable and locked">
|
||||||
|
<img style="cursor: help"
|
||||||
|
var="lockDate=tool.formatDate(locked[1]);
|
||||||
|
lockMap={'user':ztool.getUserName(locked[0]), \
|
||||||
|
'date':lockDate};
|
||||||
|
lockMsg=_('page_locked', mapping=lockMap)"
|
||||||
|
src=":url('locked')" title=":lockMsg"/></a>
|
||||||
|
<a if="editable and locked and user.has_role('Manager')">
|
||||||
|
<img class="clickable" title=":_('page_unlock')" src=":url('unlock')"
|
||||||
|
onclick=":'onUnlockPage(%s,%s)' % \
|
||||||
|
(q(zobj.UID()), q(aPage))"/></a>
|
||||||
|
</x>
|
||||||
|
</div>
|
||||||
|
<!-- Next lines: links -->
|
||||||
|
<x var="links=phase.pagesInfo[aPage].links" if="links">
|
||||||
|
<div for="link in links"><a href=":link.url">:link.title</a></div>
|
||||||
|
</x>
|
||||||
|
</x>
|
||||||
|
</td>
|
||||||
|
</tr>''')
|
||||||
|
|
||||||
|
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
|
||||||
|
# ------------------------------------------------------------------------------
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
import sys, re
|
import sys, re
|
||||||
from appy.fields import Field, No
|
from appy.fields import Field
|
||||||
from appy.px import Px
|
from appy.px import Px
|
||||||
from appy.gen.layout import Table
|
from appy.gen.layout import Table
|
||||||
from appy.gen import utils as gutils
|
from appy.gen import utils as gutils
|
||||||
|
@ -586,20 +586,21 @@ class Ref(Field):
|
||||||
add = self.callMethod(obj, self.add)
|
add = self.callMethod(obj, self.add)
|
||||||
else:
|
else:
|
||||||
add = self.add
|
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?
|
# Have we reached the maximum number of referred elements?
|
||||||
if self.multiplicity[1] != None:
|
if self.multiplicity[1] != None:
|
||||||
refCount = len(getattr(obj, self.name, ()))
|
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?
|
# 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?
|
# Have the user the correct add permission?
|
||||||
tool = obj.getTool()
|
tool = obj.getTool()
|
||||||
addPermission = '%s: Add %s' % (tool.getAppName(),
|
addPermission = '%s: Add %s' % (tool.getAppName(),
|
||||||
tool.getPortalType(self.klass))
|
tool.getPortalType(self.klass))
|
||||||
folder = obj.getCreateFolder()
|
folder = obj.getCreateFolder()
|
||||||
if not tool.getUser().has_permission(addPermission, folder):
|
if not tool.getUser().has_permission(addPermission, folder):
|
||||||
return No('no_add_perm')
|
return gutils.No('no_add_perm')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def checkAdd(self, obj):
|
def checkAdd(self, obj):
|
||||||
|
|
178
fields/search.py
Normal file
178
fields/search.py
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
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('''
|
||||||
|
<div class="portletSearch">
|
||||||
|
<a href=":'%s?className=%s&search=%s' % \
|
||||||
|
(queryUrl, rootClass, search['name'])"
|
||||||
|
class=":search['name'] == currentSearch and 'portletCurrent' or ''"
|
||||||
|
title=":search['translatedDescr']">:search['translated']</a>
|
||||||
|
</div>''')
|
||||||
|
|
||||||
|
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 = ''
|
||||||
|
# ------------------------------------------------------------------------------
|
163
gen/__init__.py
163
gen/__init__.py
|
@ -1,15 +1,13 @@
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
import types, string
|
import types, string
|
||||||
from appy.gen.mail import sendNotification
|
from appy.gen.mail import sendNotification
|
||||||
from appy.gen.indexer import defaultIndexes
|
|
||||||
from appy.gen import utils as gutils
|
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).
|
# 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.
|
# 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.action import Action
|
||||||
from appy.fields.boolean import Boolean
|
from appy.fields.boolean import Boolean
|
||||||
from appy.fields.computed import Computed
|
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.pod import Pod
|
||||||
from appy.fields.ref import Ref, autoref
|
from appy.fields.ref import Ref, autoref
|
||||||
from appy.fields.string import String, Selection
|
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.gen.layout import Table
|
||||||
from appy.px import Px
|
from appy.px import Px
|
||||||
from appy import Object
|
from appy import Object
|
||||||
|
No = gutils.No
|
||||||
|
|
||||||
# Default Appy permissions -----------------------------------------------------
|
# Default Appy permissions -----------------------------------------------------
|
||||||
r, w, d = ('read', 'write', 'delete')
|
r, w, d = ('read', 'write', 'delete')
|
||||||
|
@ -55,160 +58,6 @@ class Import:
|
||||||
# and must return a similar, sorted, list.
|
# and must return a similar, sorted, list.
|
||||||
self.sort = sort
|
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('''
|
|
||||||
<div class="portletSearch">
|
|
||||||
<a href=":'%s?className=%s&search=%s' % \
|
|
||||||
(queryUrl, rootClass, search['name'])"
|
|
||||||
class=":search['name'] == currentSearch and 'portletCurrent' or ''"
|
|
||||||
title=":search['translatedDescr']">:search['translated']</a>
|
|
||||||
</div>''')
|
|
||||||
|
|
||||||
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 --------------------------------
|
# Workflow-specific types and default workflows --------------------------------
|
||||||
appyToZopePermissions = {
|
appyToZopePermissions = {
|
||||||
'read': ('View', 'Access contents information'),
|
'read': ('View', 'Access contents information'),
|
||||||
|
|
24
gen/utils.py
24
gen/utils.py
|
@ -1,6 +1,6 @@
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
import re, os, os.path, base64, urllib
|
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 ------------------------------------------
|
# Function for creating a Zope object ------------------------------------------
|
||||||
def createObject(folder, id, className, appName, wf=True, noSecurity=False):
|
def createObject(folder, id, className, appName, wf=True, noSecurity=False):
|
||||||
|
@ -96,7 +96,7 @@ class Keywords:
|
||||||
toRemove = '?-+*()'
|
toRemove = '?-+*()'
|
||||||
def __init__(self, keywords, operator='AND'):
|
def __init__(self, keywords, operator='AND'):
|
||||||
# Clean the p_keywords that the user has entered.
|
# Clean the p_keywords that the user has entered.
|
||||||
words = normalizeText(keywords)
|
words = sutils.normalizeText(keywords)
|
||||||
if words == '*': words = ''
|
if words == '*': words = ''
|
||||||
for c in self.toRemove: words = words.replace(c, ' ')
|
for c in self.toRemove: words = words.replace(c, ' ')
|
||||||
self.keywords = words.split()
|
self.keywords = words.split()
|
||||||
|
@ -220,4 +220,24 @@ def writeCookie(login, password, request):
|
||||||
cookieValue = base64.encodestring('%s:%s' % (login, password)).rstrip()
|
cookieValue = base64.encodestring('%s:%s' % (login, password)).rstrip()
|
||||||
cookieValue = urllib.quote(cookieValue)
|
cookieValue = urllib.quote(cookieValue)
|
||||||
request.RESPONSE.setCookie('_appy_', cookieValue, path='/')
|
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
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
Loading…
Reference in a new issue