[gen] More work ZPT->PX.
This commit is contained in:
parent
2e9a832463
commit
34e3a3083e
|
@ -39,4 +39,8 @@ class Object:
|
||||||
def __nonzero__(self):
|
def __nonzero__(self):
|
||||||
return bool(self.__dict__)
|
return bool(self.__dict__)
|
||||||
def get(self, name, default=None): return getattr(self, name, default)
|
def get(self, name, default=None): return getattr(self, name, default)
|
||||||
|
def update(self, other):
|
||||||
|
'''Includes information from p_other into p_self.'''
|
||||||
|
for k, v in other.__dict__.iteritems():
|
||||||
|
setattr(self, k, v)
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
|
@ -275,7 +275,7 @@ class ZodbBackupScript:
|
||||||
optParser.add_option("-p", "--python", dest="python",
|
optParser.add_option("-p", "--python", dest="python",
|
||||||
help="The path to the Python interpreter running "\
|
help="The path to the Python interpreter running "\
|
||||||
"Zope",
|
"Zope",
|
||||||
default='python2.4',metavar="REPOZO",type='string')
|
default='python2.4',metavar="PYTHON",type='string')
|
||||||
optParser.add_option("-r", "--repozo", dest="repozo",
|
optParser.add_option("-r", "--repozo", dest="repozo",
|
||||||
help="The path to repozo.py",
|
help="The path to repozo.py",
|
||||||
default='', metavar="REPOZO", type='string')
|
default='', metavar="REPOZO", type='string')
|
||||||
|
|
|
@ -19,6 +19,8 @@ import copy, types, re
|
||||||
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.shared import utils as sutils
|
||||||
|
from appy.px import Px
|
||||||
|
from appy import Object
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
nullValues = (None, '', [])
|
nullValues = (None, '', [])
|
||||||
|
@ -108,18 +110,167 @@ class Page:
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def getInfo(self, obj, layoutType):
|
def getInfo(self, obj, layoutType):
|
||||||
'''Gets information about this page, for p_obj, as a dict.'''
|
'''Gets information about this page, for p_obj, as an object.'''
|
||||||
res = {}
|
res = Object()
|
||||||
for elem in Page.subElements:
|
for elem in Page.subElements:
|
||||||
res['show%s' % elem.capitalize()] = self.isShowable(obj, layoutType,
|
setattr(res, 'show%s' % elem.capitalize(), \
|
||||||
elem=elem)
|
self.isShowable(obj, layoutType, elem=elem))
|
||||||
return res
|
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.
|
# Group. Fields can be grouped.
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
class Group:
|
class Group:
|
||||||
'''Used for describing a group of widgets within a page.'''
|
'''Used for describing a group of fields within a page.'''
|
||||||
def __init__(self, name, columns=['100%'], wide=True, style='section2',
|
def __init__(self, name, columns=['100%'], wide=True, style='section2',
|
||||||
hasLabel=True, hasDescr=False, hasHelp=False,
|
hasLabel=True, hasDescr=False, hasHelp=False,
|
||||||
hasHeaders=False, group=None, colspan=1, align='center',
|
hasHeaders=False, group=None, colspan=1, align='center',
|
||||||
|
@ -258,27 +409,26 @@ class Group:
|
||||||
self.group.generateLabels(messages, classDescr, walkedGroups,
|
self.group.generateLabels(messages, classDescr, walkedGroups,
|
||||||
forSearch=forSearch)
|
forSearch=forSearch)
|
||||||
|
|
||||||
def insertInto(self, widgets, groupDescrs, page, metaType, forSearch=False):
|
def insertInto(self, fields, uiGroups, page, metaType, forSearch=False):
|
||||||
'''Inserts the GroupDescr instance corresponding to this Group instance
|
'''Inserts the UiGroup instance corresponding to this Group instance
|
||||||
into p_widgets, the recursive structure used for displaying all
|
into p_fields, the recursive structure used for displaying all
|
||||||
widgets in a given p_page (or all searches), and returns this
|
fields in a given p_page (or all searches), and returns this
|
||||||
GroupDescr instance.'''
|
UiGroup instance.'''
|
||||||
# First, create the corresponding GroupDescr if not already in
|
# First, create the corresponding UiGroup if not already in p_uiGroups.
|
||||||
# p_groupDescrs.
|
if self.name not in uiGroups:
|
||||||
if self.name not in groupDescrs:
|
uiGroup = uiGroups[self.name] = UiGroup(self, page, metaType,
|
||||||
groupDescr = groupDescrs[self.name] = gutils.GroupDescr(\
|
forSearch=forSearch)
|
||||||
self, page, metaType, forSearch=forSearch).get()
|
# Insert the group at the higher level (ie, directly in p_fields)
|
||||||
# Insert the group at the higher level (ie, directly in p_widgets)
|
|
||||||
# if the group is not itself in a group.
|
# if the group is not itself in a group.
|
||||||
if not self.group:
|
if not self.group:
|
||||||
widgets.append(groupDescr)
|
fields.append(uiGroup)
|
||||||
else:
|
else:
|
||||||
outerGroupDescr = self.group.insertInto(widgets, groupDescrs,
|
outerGroup = self.group.insertInto(fields, uiGroups, page,
|
||||||
page, metaType, forSearch=forSearch)
|
metaType,forSearch=forSearch)
|
||||||
gutils.GroupDescr.addWidget(outerGroupDescr, groupDescr)
|
outerGroup.addField(uiGroup)
|
||||||
else:
|
else:
|
||||||
groupDescr = groupDescrs[self.name]
|
uiGroup = uiGroups[self.name]
|
||||||
return groupDescr
|
return uiGroup
|
||||||
|
|
||||||
class Column:
|
class Column:
|
||||||
'''Used for describing a column within a Group like defined above.'''
|
'''Used for describing a column within a Group like defined above.'''
|
||||||
|
@ -286,6 +436,188 @@ class Column:
|
||||||
self.width = width
|
self.width = width
|
||||||
self.align = align
|
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.
|
# Abstract base class for every field.
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
@ -298,6 +630,67 @@ class Field:
|
||||||
jsFiles = {}
|
jsFiles = {}
|
||||||
dLayouts = 'lrv-d-f'
|
dLayouts = 'lrv-d-f'
|
||||||
|
|
||||||
|
# Render a field. Optiona vars:
|
||||||
|
# * fieldName can be given as different as field.name for fields included
|
||||||
|
# in List fields: in this case, fieldName includes the row
|
||||||
|
# index.
|
||||||
|
# * showChanges If True, a variant of the field showing successive changes
|
||||||
|
# made to it is shown.
|
||||||
|
pxRender = Px('''
|
||||||
|
<x var="showChanges=showChanges|False;
|
||||||
|
layout=field.layouts[layoutType];
|
||||||
|
name=fieldName|field.name;
|
||||||
|
sync=field.sync[layoutType];
|
||||||
|
outerValue=value|None;
|
||||||
|
rawValue=zobj.getFieldValue(name, onlyIfSync=True, \
|
||||||
|
layoutType=layoutType, \
|
||||||
|
outerValue=outerValue);
|
||||||
|
value=zobj.getFormattedFieldValue(name, rawValue, showChanges);
|
||||||
|
requestValue=zobj.getRequestFieldValue(name);
|
||||||
|
inRequest=req.has_key(name);
|
||||||
|
errors=errors|();
|
||||||
|
inError=name in errors;
|
||||||
|
isMultiple=(field.multiplicity[1] == None) or \
|
||||||
|
(field.multiplicity[1] > 1);
|
||||||
|
masterCss=field.slaves and ('master_%s' % name) or '';
|
||||||
|
slaveCss=field.master and ('slave_%s_%s' % \
|
||||||
|
(field.masterName, '_'.join(field.masterValue))) or '';
|
||||||
|
tagCss=tagCss|'';
|
||||||
|
tagCss=('%s %s' % (slaveCss, tagCss)).strip();
|
||||||
|
tagId='%s_%s' % (zobj.UID(), name);
|
||||||
|
tagName=field.master and 'slave' or '';
|
||||||
|
layoutTarget=field">:tool.pxLayoutedObject</x>''')
|
||||||
|
|
||||||
|
# Displays a field label.
|
||||||
|
pxLabel = Px('''<label if="field.hasLabel and (field.type != 'Action')"
|
||||||
|
lfor="field.name">::zobj.translate('label', field=field)</label>''')
|
||||||
|
|
||||||
|
# Displays a field description.
|
||||||
|
pxDescription = Px('''<span if="field.hasDescr"
|
||||||
|
class="discreet">::zobj.translate('descr', field=field)</span>''')
|
||||||
|
|
||||||
|
# Displays a field help.
|
||||||
|
pxHelp = Px('''<acronym title="zobj.translate('help', field=field)"><img
|
||||||
|
src=":url('help')"/></acronym>''')
|
||||||
|
|
||||||
|
# Displays validation-error-related info about a field.
|
||||||
|
pxValidation = Px('''<x><acronym if="inError" title=":errors[name]"><img
|
||||||
|
src=":url('warning')"/></acronym><img if="not inError"
|
||||||
|
src=":url('warning_no.gif')"/></x>''')
|
||||||
|
|
||||||
|
# Displays the fact that a field is required.
|
||||||
|
pxRequired = Px('''<img src=":url('required.gif')"/>''')
|
||||||
|
|
||||||
|
# Button for showing changes to the field.
|
||||||
|
pxChanges = Px('''<x if=":zobj.hasHistory(name)"><img class="clickable"
|
||||||
|
if="not showChanges" src=":url('changes')" title="_('changes_show')"
|
||||||
|
onclick=":'askField(%s,%s,%s,%s)' % \
|
||||||
|
(q(tagId), q(zobj.absolute_url()), q('view'), q('True'))"/><img
|
||||||
|
class="clickable" if="showChanges" src=":url('changesNo')"
|
||||||
|
onclick=":'askField(%s,%s,%s,%s)' % \
|
||||||
|
(q(tagId), q(zobj.absolute_url(), q('view'), q('True'))"
|
||||||
|
title=":_('changes_hide')"/></x>''')
|
||||||
|
|
||||||
def __init__(self, validator, multiplicity, default, show, page, group,
|
def __init__(self, validator, multiplicity, default, show, page, group,
|
||||||
layouts, move, indexed, searchable, specificReadPermission,
|
layouts, move, indexed, searchable, specificReadPermission,
|
||||||
specificWritePermission, width, height, maxChars, colspan,
|
specificWritePermission, width, height, maxChars, colspan,
|
||||||
|
@ -359,7 +752,7 @@ class Field:
|
||||||
self.maxChars = maxChars or ''
|
self.maxChars = maxChars or ''
|
||||||
# If the widget is in a group with multiple columns, the following
|
# If the widget is in a group with multiple columns, the following
|
||||||
# attribute specifies on how many columns to span the widget.
|
# attribute specifies on how many columns to span the widget.
|
||||||
self.colspan = colspan
|
self.colspan = colspan or 1
|
||||||
# The list of slaves of this field, if it is a master
|
# The list of slaves of this field, if it is a master
|
||||||
self.slaves = []
|
self.slaves = []
|
||||||
# The behaviour of this field may depend on another, "master" field
|
# The behaviour of this field may depend on another, "master" field
|
||||||
|
@ -401,7 +794,7 @@ class Field:
|
||||||
# default value will be present.
|
# default value will be present.
|
||||||
self.sdefault = sdefault
|
self.sdefault = sdefault
|
||||||
# Colspan for rendering the search widget corresponding to this field.
|
# Colspan for rendering the search widget corresponding to this field.
|
||||||
self.scolspan = scolspan
|
self.scolspan = scolspan or 1
|
||||||
# Width and height for the search widget
|
# Width and height for the search widget
|
||||||
self.swidth = swidth or width
|
self.swidth = swidth or width
|
||||||
self.sheight = sheight or height
|
self.sheight = sheight or height
|
||||||
|
|
|
@ -41,14 +41,14 @@ class Boolean(Field):
|
||||||
|
|
||||||
pxSearch = Px('''
|
pxSearch = Px('''
|
||||||
<x var="typedWidget='%s*bool' % widgetName">
|
<x var="typedWidget='%s*bool' % widgetName">
|
||||||
<label lfor=":widgetName">:_(field.labelId)"></label><br/>
|
<label lfor=":widgetName">:_(field.labelId)</label><br/>
|
||||||
<x var="valueId='%s_yes' % name">
|
<x var="valueId='%s_yes' % name">
|
||||||
<input type="radio" value="True" name=":typedWidget" id=":valueId"/>
|
<input type="radio" value="True" name=":typedWidget" id=":valueId"/>
|
||||||
<label lfor=":valueId">:_('yes')</label>
|
<label lfor=":valueId">:_('yes')</label>
|
||||||
</x>
|
</x>
|
||||||
<x var="valueId='%s_no' % name">
|
<x var="valueId='%s_no' % name">
|
||||||
<input type="radio" value="False" name=":typedWidget" id=":valueId"/>
|
<input type="radio" value="False" name=":typedWidget" id=":valueId"/>
|
||||||
<label lfor=":valueId">:_('no')"></label>
|
<label lfor=":valueId">:_('no')</label>
|
||||||
</x>
|
</x>
|
||||||
<x var="valueId='%s_whatever' % name">
|
<x var="valueId='%s_whatever' % name">
|
||||||
<input type="radio" value="" name=":typedWidget" id=":valueId"
|
<input type="radio" value="" name=":typedWidget" id=":valueId"
|
||||||
|
|
|
@ -35,8 +35,8 @@ class Calendar(Field):
|
||||||
otherCalendars=field.getOtherCalendars(zobj, preComputed)"
|
otherCalendars=field.getOtherCalendars(zobj, preComputed)"
|
||||||
id=":ajaxHookId">
|
id=":ajaxHookId">
|
||||||
|
|
||||||
<script type="text/javascript">:'var %s_maxEventLength = %d;' % \
|
<script type="text/javascript">:'var %s_maxEventLength = %d' % \
|
||||||
(field.name, field.maxEventLength)"></script>
|
(field.name, field.maxEventLength)</script>
|
||||||
|
|
||||||
<!-- Month chooser -->
|
<!-- Month chooser -->
|
||||||
<div style="margin-bottom: 5px"
|
<div style="margin-bottom: 5px"
|
||||||
|
@ -100,7 +100,7 @@ class Calendar(Field):
|
||||||
onmouseout="mayEdit and 'this.getElementsByTagName(\
|
onmouseout="mayEdit and 'this.getElementsByTagName(\
|
||||||
%s)[0].style.visibility=%s' % (q('img'), q('hidden')) or ''">
|
%s)[0].style.visibility=%s' % (q('img'), q('hidden')) or ''">
|
||||||
<span>:day</span>
|
<span>:day</span>
|
||||||
<span if="day == 1">:_('month_%s_short' % date.aMonth())"></span>
|
<span if="day == 1">:_('month_%s_short' % date.aMonth())</span>
|
||||||
<!-- Icon for adding an event -->
|
<!-- Icon for adding an event -->
|
||||||
<x if="mayCreate">
|
<x if="mayCreate">
|
||||||
<img class="clickable" style="visibility:hidden"
|
<img class="clickable" style="visibility:hidden"
|
||||||
|
@ -115,11 +115,10 @@ class Calendar(Field):
|
||||||
<img if="mayDelete" class="clickable" style="visibility:hidden"
|
<img if="mayDelete" class="clickable" style="visibility:hidden"
|
||||||
src=":url('delete')"
|
src=":url('delete')"
|
||||||
onclick=":'openEventPopup(%s, %s, %s, %s, null, null)' % \
|
onclick=":'openEventPopup(%s, %s, %s, %s, null, null)' % \
|
||||||
(q('del'), q(field.name), q(dayString), q(str(spansDays)))"/>
|
(q('del'), q(field.name), q(dayString), q(spansDays))"/>
|
||||||
<!-- A single event is allowed for the moment -->
|
<!-- A single event is allowed for the moment -->
|
||||||
<div if="events" var2="eventType=events[0]['eventType']">
|
<div if="events" var2="eventType=events[0]['eventType']">
|
||||||
<span style="color: grey">:field.getEventName(zobj, \
|
<span style="color: grey">:field.getEventName(zobj, eventType)</span>
|
||||||
eventType)"></span>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- Events from other calendars -->
|
<!-- Events from other calendars -->
|
||||||
<x if="otherCalendars"
|
<x if="otherCalendars"
|
||||||
|
@ -149,16 +148,15 @@ class Calendar(Field):
|
||||||
<input type="hidden" name="day"/>
|
<input type="hidden" name="day"/>
|
||||||
|
|
||||||
<!-- Choose an event type -->
|
<!-- Choose an event type -->
|
||||||
<div align="center" style="margin-bottom: 3px">:_('which_event')"></div>
|
<div align="center" style="margin-bottom: 3px">:_('which_event')</div>
|
||||||
<select name="eventType">
|
<select name="eventType">
|
||||||
<option value="">:_('choose_a_value')"></option>
|
<option value="">:_('choose_a_value')</option>
|
||||||
<option for="eventType in allEventTypes"
|
<option for="eventType in allEventTypes"
|
||||||
value=":eventType">:field.getEventName(zobj, eventType)">
|
value=":eventType">:field.getEventName(zobj,eventType)</option>
|
||||||
</option>
|
|
||||||
</select><br/><br/>
|
</select><br/><br/>
|
||||||
<!--Span the event on several days -->
|
<!--Span the event on several days -->
|
||||||
<div align="center" class="discreet" style="margin-bottom: 3px">
|
<div align="center" class="discreet" style="margin-bottom: 3px">
|
||||||
<span>:_('event_span')"></span>
|
<span>:_('event_span')</span>
|
||||||
<input type="text" size="3" name="eventSpan"/>
|
<input type="text" size="3" name="eventSpan"/>
|
||||||
</div>
|
</div>
|
||||||
<input type="button"
|
<input type="button"
|
||||||
|
@ -184,8 +182,7 @@ class Calendar(Field):
|
||||||
<input type="hidden" name="actionType" value="deleteEvent"/>
|
<input type="hidden" name="actionType" value="deleteEvent"/>
|
||||||
<input type="hidden" name="day"/>
|
<input type="hidden" name="day"/>
|
||||||
|
|
||||||
<div align="center" style="margin-bottom: 5px">_('delete_confirm')">
|
<div align="center" style="margin-bottom: 5px">_('delete_confirm')</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Delete successive events ? -->
|
<!-- Delete successive events ? -->
|
||||||
<div class="discreet" style="margin-bottom: 10px"
|
<div class="discreet" style="margin-bottom: 10px"
|
||||||
|
@ -195,7 +192,7 @@ class Calendar(Field):
|
||||||
onClick=":'toggleCheckbox(%s, %s)' % \
|
onClick=":'toggleCheckbox(%s, %s)' % \
|
||||||
(q('%s_cb' % prefix), q('%s_hd' % prefix))"/>
|
(q('%s_cb' % prefix), q('%s_hd' % prefix))"/>
|
||||||
<input type="hidden" id=":prefix + '_hd'" name="deleteNext"/>
|
<input type="hidden" id=":prefix + '_hd'" name="deleteNext"/>
|
||||||
<span>:_('del_next_events')"></span>
|
<span>:_('del_next_events')</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="button" value=":_('yes')"
|
<input type="button" value=":_('yes')"
|
||||||
onClick=":'triggerCalendarEvent(%s, %s, %s, %s)' % \
|
onClick=":'triggerCalendarEvent(%s, %s, %s, %s)' % \
|
||||||
|
|
|
@ -23,17 +23,13 @@ class Computed(Field):
|
||||||
|
|
||||||
# Ajax-called view content of a non sync Computed field.
|
# Ajax-called view content of a non sync Computed field.
|
||||||
pxViewContent = Px('''
|
pxViewContent = Px('''
|
||||||
<x var="name=req['fieldName'];
|
<x var="value=zobj.getFieldValue(name); sync=True">:field.pxView</x>''')
|
||||||
field=zobj.getAppyType(name);
|
|
||||||
value=zobj.getFieldValue(name);
|
|
||||||
sync=True">:field.pxView</x>''')
|
|
||||||
|
|
||||||
pxView = pxCell = pxEdit = Px('''<x>
|
pxView = pxCell = pxEdit = Px('''<x>
|
||||||
<x if="sync">
|
<x if="sync">
|
||||||
<x if="field.plainText">:value</x><x if="not field.plainText">::value></x>
|
<x if="field.plainText">:value</x><x if="not field.plainText">::value</x>
|
||||||
</x>
|
</x>
|
||||||
<div if="not sync">
|
<div if="not sync" var2="ajaxHookId=zobj.UID() + name" id="ajaxHookId">
|
||||||
var2="ajaxHookId=zobj.UID() + name" id="ajaxHookId">
|
|
||||||
<script type="text/javascript">:'askComputedField(%s, %s, %s)' % \
|
<script type="text/javascript">:'askComputedField(%s, %s, %s)' % \
|
||||||
(q(ajaxHookId), q(zobj.absolute_url()), q(name))">
|
(q(ajaxHookId), q(zobj.absolute_url()), q(name))">
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -91,7 +91,7 @@ class Date(Field):
|
||||||
monthFromName='%s_from_month' % name;
|
monthFromName='%s_from_month' % name;
|
||||||
yearFromName='%s*date' % widgetName">
|
yearFromName='%s*date' % widgetName">
|
||||||
<td width="10px"> </td>
|
<td width="10px"> </td>
|
||||||
<td><label>:_('search_from')"></label></td>
|
<td><label>:_('search_from')</label></td>
|
||||||
<td>
|
<td>
|
||||||
<select id=":dayFromName" name=":dayFromName">
|
<select id=":dayFromName" name=":dayFromName">
|
||||||
<option value="">--</option>
|
<option value="">--</option>
|
||||||
|
@ -124,7 +124,7 @@ class Date(Field):
|
||||||
monthToName='%s_to_month' % name;
|
monthToName='%s_to_month' % name;
|
||||||
yearToName='%s_to_year' % name">
|
yearToName='%s_to_year' % name">
|
||||||
<td></td>
|
<td></td>
|
||||||
<td><label>_('search_to')"></label> </td>
|
<td><label>_('search_to')</label> </td>
|
||||||
<td height="20px">
|
<td height="20px">
|
||||||
<select id=":dayToName" name=":dayToName">
|
<select id=":dayToName" name=":dayToName">
|
||||||
<option value="">--</option>
|
<option value="">--</option>
|
||||||
|
|
|
@ -30,7 +30,7 @@ class File(Field):
|
||||||
imgSrc='%s/download?name=%s' % (zobj.absolute_url(), name)">
|
imgSrc='%s/download?name=%s' % (zobj.absolute_url(), name)">
|
||||||
<x if="not empty and not field.isImage">
|
<x if="not empty and not field.isImage">
|
||||||
<a href=":imgSrc">:info.filename</a> -
|
<a href=":imgSrc">:info.filename</a> -
|
||||||
<i class="discreet">'%sKb' % (info.size / 1024)"></i>
|
<i class="discreet">:'%sKb' % (info.size / 1024)</i>
|
||||||
</x>
|
</x>
|
||||||
<x if="not empty and field.isImage"><img src=":imgSrc"/></x>
|
<x if="not empty and field.isImage"><img src=":imgSrc"/></x>
|
||||||
<x if="empty">-</x>
|
<x if="empty">-</x>
|
||||||
|
@ -67,10 +67,8 @@ class File(Field):
|
||||||
<input type="file" name=":'%s_file' % name" id=":'%s_file' % name"
|
<input type="file" name=":'%s_file' % name" id=":'%s_file' % name"
|
||||||
size=":field.width"/>
|
size=":field.width"/>
|
||||||
<script var="isDisabled=empty and 'false' or 'true'"
|
<script var="isDisabled=empty and 'false' or 'true'"
|
||||||
type="text/javascript">:document.getElementById(%s).disabled=%s'%\
|
type="text/javascript">:'document.getElementById(%s).disabled=%s'%\
|
||||||
(q(fName), q(isDisabled))">
|
(q(fName), q(isDisabled))</script></x>''')
|
||||||
</script>
|
|
||||||
</x>''')
|
|
||||||
|
|
||||||
pxSearch = ''
|
pxSearch = ''
|
||||||
|
|
||||||
|
|
|
@ -36,10 +36,10 @@ class Float(Field):
|
||||||
value=":inRequest and requestValue or value" type="text"/>''')
|
value=":inRequest and requestValue or value" type="text"/>''')
|
||||||
|
|
||||||
pxSearch = Px('''<x>
|
pxSearch = Px('''<x>
|
||||||
<label>:_(field.labelId)"></label><br/>
|
<label>:_(field.labelId)</label><br/>
|
||||||
<!-- From -->
|
<!-- From -->
|
||||||
<x var="fromName='%s*float' % widgetName">
|
<x var="fromName='%s*float' % widgetName">
|
||||||
<label lfor=":fromName">:_('search_from')"></label>
|
<label lfor=":fromName">:_('search_from')</label>
|
||||||
<input type="text" name=":fromName" maxlength=":field.maxChars"
|
<input type="text" name=":fromName" maxlength=":field.maxChars"
|
||||||
value=":field.sdefault[0]" size=":field.swidth"/>
|
value=":field.sdefault[0]" size=":field.swidth"/>
|
||||||
</x>
|
</x>
|
||||||
|
|
|
@ -33,7 +33,7 @@ class Integer(Field):
|
||||||
value=":inRequest and requestValue or value" type="text"/>''')
|
value=":inRequest and requestValue or value" type="text"/>''')
|
||||||
|
|
||||||
pxSearch = Px('''<x>
|
pxSearch = Px('''<x>
|
||||||
<label>:_(field.labelId)"></label><br/>
|
<label>:_(field.labelId)</label><br/>
|
||||||
<!-- From -->
|
<!-- From -->
|
||||||
<x var="fromName='%s*int' % widgetName">
|
<x var="fromName='%s*int' % widgetName">
|
||||||
<label lfor=":fromName">:_('search_from')</label>
|
<label lfor=":fromName">:_('search_from')</label>
|
||||||
|
@ -42,7 +42,7 @@ class Integer(Field):
|
||||||
</x>
|
</x>
|
||||||
<!-- To -->
|
<!-- To -->
|
||||||
<x var="toName='%s_to' % name">
|
<x var="toName='%s_to' % name">
|
||||||
<label lfor=":toName">:_('search_to')"></label>
|
<label lfor=":toName">:_('search_to')</label>
|
||||||
<input type="text" name=":toName" maxlength=":field.maxChars"
|
<input type="text" name=":toName" maxlength=":field.maxChars"
|
||||||
value=":field.sdefault[1]" size=":field.swidth"/>
|
value=":field.sdefault[1]" size=":field.swidth"/>
|
||||||
</x><br/>
|
</x><br/>
|
||||||
|
|
|
@ -112,7 +112,7 @@ class Ogone(Field):
|
||||||
res.update(self.callMethod(obj, self.orderMethod))
|
res.update(self.callMethod(obj, self.orderMethod))
|
||||||
# Add user-related information
|
# Add user-related information
|
||||||
res['CN'] = str(tool.getUserName(normalized=True))
|
res['CN'] = str(tool.getUserName(normalized=True))
|
||||||
user = obj.appy().appyUser
|
user = obj.appy().user
|
||||||
res['EMAIL'] = user.email or user.login
|
res['EMAIL'] = user.email or user.login
|
||||||
# Add standard back URLs
|
# Add standard back URLs
|
||||||
siteUrl = tool.getSiteUrl()
|
siteUrl = tool.getSiteUrl()
|
||||||
|
|
103
fields/ref.py
103
fields/ref.py
|
@ -37,8 +37,9 @@ class Ref(Field):
|
||||||
# the URL for allowing to navigate from one object to the next/previous on
|
# the URL for allowing to navigate from one object to the next/previous on
|
||||||
# ui/view.
|
# ui/view.
|
||||||
pxObjectTitle = Px('''
|
pxObjectTitle = Px('''
|
||||||
<x var="navInfo='ref.%s.%s:%s.%d.%d' % (zobj.UID(), field.name, \
|
<x var="includeShownInfo=includeShownInfo|False;
|
||||||
field.pageName, loop.ztied.nb + startNumber, totalNumber);
|
navInfo='ref.%s.%s:%s.%d.%d' % (zobj.UID(), field.name, \
|
||||||
|
field.pageName, loop.ztied.nb + 1 + startNumber, totalNumber);
|
||||||
navInfo=not field.isBack and navInfo or '';
|
navInfo=not field.isBack and navInfo or '';
|
||||||
cssClass=ztied.getCssFor('title')">
|
cssClass=ztied.getCssFor('title')">
|
||||||
<x>::ztied.getSupTitle(navInfo)</x>
|
<x>::ztied.getSupTitle(navInfo)</x>
|
||||||
|
@ -47,7 +48,7 @@ class Ref(Field):
|
||||||
href=":fullUrl" class=":cssClass">:(not includeShownInfo) and \
|
href=":fullUrl" class=":cssClass">:(not includeShownInfo) and \
|
||||||
ztied.Title() or field.getReferenceLabel(ztied.appy())
|
ztied.Title() or field.getReferenceLabel(ztied.appy())
|
||||||
</a><span name="subTitle" style=":showSubTitles and 'display:inline' or \
|
</a><span name="subTitle" style=":showSubTitles and 'display:inline' or \
|
||||||
'display:none'">::ztied.getSubTitle()"</span>
|
'display:none'">::ztied.getSubTitle()</span>
|
||||||
</x>''')
|
</x>''')
|
||||||
|
|
||||||
# This PX displays icons for triggering actions on a given referenced object
|
# This PX displays icons for triggering actions on a given referenced object
|
||||||
|
@ -59,7 +60,7 @@ class Ref(Field):
|
||||||
<td if="not isBack and (len(zobjects)>1) and changeOrder and canWrite"
|
<td if="not isBack and (len(zobjects)>1) and changeOrder and canWrite"
|
||||||
var2="objectIndex=field.getIndexOf(zobj, ztied);
|
var2="objectIndex=field.getIndexOf(zobj, ztied);
|
||||||
ajaxBaseCall=navBaseCall.replace('**v**','%s,%s,{%s:%s,%s:%s}'%\
|
ajaxBaseCall=navBaseCall.replace('**v**','%s,%s,{%s:%s,%s:%s}'%\
|
||||||
(q(startNumber), q('ChangeRefOrder'), q('refObjectUid'),
|
(q(startNumber), q('doChangeOrder'), q('refObjectUid'),
|
||||||
q(ztied.UID()), q('move'), q('**v**')))">
|
q(ztied.UID()), q('move'), q('**v**')))">
|
||||||
<img if="objectIndex > 0" class="clickable" src=":url('arrowUp')"
|
<img if="objectIndex > 0" class="clickable" src=":url('arrowUp')"
|
||||||
title=":_('move_up')"
|
title=":_('move_up')"
|
||||||
|
@ -100,7 +101,7 @@ class Ref(Field):
|
||||||
field.name, field.pageName, 0, totalNumber);
|
field.name, field.pageName, 0, totalNumber);
|
||||||
formCall='goto(%s)' % \
|
formCall='goto(%s)' % \
|
||||||
q('%s/do?action=Create&className=%s&nav=%s' % \
|
q('%s/do?action=Create&className=%s&nav=%s' % \
|
||||||
(folder.absolute_url(), linkedPortalType, navInfo));
|
(folder.absolute_url(), tiedClassName, navInfo));
|
||||||
formCall=not field.addConfirm and formCall or \
|
formCall=not field.addConfirm and formCall or \
|
||||||
'askConfirm(%s,%s,%s)' % (q('script'), q(formCall), \
|
'askConfirm(%s,%s,%s)' % (q('script'), q(formCall), \
|
||||||
q(addConfirmMsg));
|
q(addConfirmMsg));
|
||||||
|
@ -115,22 +116,20 @@ class Ref(Field):
|
||||||
# This PX displays, in a cell header from a ref table, icons for sorting the
|
# This PX displays, in a cell header from a ref table, icons for sorting the
|
||||||
# ref field according to the field that corresponds to this column.
|
# ref field according to the field that corresponds to this column.
|
||||||
pxSortIcons = Px('''
|
pxSortIcons = Px('''
|
||||||
<x if="changeOrder and canWrite and ztool.isSortable(field.name, \
|
<x if="changeOrder and canWrite and ztool.isSortable(refField.name, \
|
||||||
zobjects[0].meta_type, 'ref')"
|
tiedClassName, 'ref')"
|
||||||
var2="ajaxBaseCall=navBaseCall.replace('**v**', '%s,%s,{%s:%s,%s:%s}'% \
|
var2="ajaxBaseCall=navBaseCall.replace('**v**', '%s,%s,{%s:%s,%s:%s}'% \
|
||||||
(q(startNumber), q('SortReference'), q('sortKey'), \
|
(q(startNumber), q('sort'), q('sortKey'), q(refField.name), \
|
||||||
q(field.name), q('reverse'), q('**v**')))">
|
q('reverse'), q('**v**')))">
|
||||||
<img class="clickable" src=":url('sortAsc')"
|
<img class="clickable" src=":url('sortAsc')"
|
||||||
onclick=":ajaxBaseCall.replace('**v**', 'False')"/>
|
onclick=":ajaxBaseCall.replace('**v**', 'False')"/>
|
||||||
<img class="clickable" src=":url('sortDesc')"
|
<img class="clickable" src=":url('sortDesc')"
|
||||||
onclick=":ajaxBaseCall.replace('**v**', 'True')"/>
|
onclick=":ajaxBaseCall.replace('**v**', 'True')"/>
|
||||||
</x>''')
|
</x>''')
|
||||||
|
|
||||||
# This PX is called by a XmlHttpRequest (or directly by pxView) for
|
# PX that displays referred objects through this field.
|
||||||
# displaying the referred objects of a reference field.
|
pxView = pxCell = Px('''
|
||||||
pxViewContent = Px('''
|
<div var="innerRef=req.get('innerRef', False) == 'True';
|
||||||
<div var="field=zobj.getAppyType(req['fieldName']);
|
|
||||||
innerRef=req.get('innerRef', False) == 'True';
|
|
||||||
ajaxHookId=zobj.UID() + field.name;
|
ajaxHookId=zobj.UID() + field.name;
|
||||||
startNumber=int(req.get('%s_startNumber' % ajaxHookId, 0));
|
startNumber=int(req.get('%s_startNumber' % ajaxHookId, 0));
|
||||||
info=field.getLinkedObjects(zobj, startNumber);
|
info=field.getLinkedObjects(zobj, startNumber);
|
||||||
|
@ -139,7 +138,7 @@ class Ref(Field):
|
||||||
batchSize=info.batchSize;
|
batchSize=info.batchSize;
|
||||||
batchNumber=len(zobjects);
|
batchNumber=len(zobjects);
|
||||||
folder=zobj.getCreateFolder();
|
folder=zobj.getCreateFolder();
|
||||||
linkedPortalType=ztool.getPortalType(field.klass);
|
tiedClassName=ztool.getPortalType(field.klass);
|
||||||
canWrite=not field.isBack and zobj.allows(field.writePermission);
|
canWrite=not field.isBack and zobj.allows(field.writePermission);
|
||||||
showPlusIcon=zobj.mayAddReference(field.name);
|
showPlusIcon=zobj.mayAddReference(field.name);
|
||||||
atMostOneRef=(field.multiplicity[1] == 1) and \
|
atMostOneRef=(field.multiplicity[1] == 1) and \
|
||||||
|
@ -182,13 +181,13 @@ class Ref(Field):
|
||||||
<input if="zobjects and field.queryable" type="button" class="button"
|
<input if="zobjects and field.queryable" type="button" class="button"
|
||||||
style=":url('buttonSearch', bg=True)" value=":_('search_title')"
|
style=":url('buttonSearch', bg=True)" value=":_('search_title')"
|
||||||
onclick=":'goto(%s)' % \
|
onclick=":'goto(%s)' % \
|
||||||
q('%s/ui/search?className=%s&ref=%s:%s' % \
|
q('%s/search?className=%s&ref=%s:%s' % \
|
||||||
(ztool.absolute_url(), linkedPortalType, zobj.UID(), \
|
(ztool.absolute_url(), tiedClassName, zobj.UID(), \
|
||||||
field.name))"/>
|
field.name))"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Appy (top) navigation -->
|
<!-- (Top) navigation -->
|
||||||
<x>:obj.pxAppyNavigate</x>
|
<x>:tool.pxNavigate</x>
|
||||||
|
|
||||||
<!-- No object is present -->
|
<!-- No object is present -->
|
||||||
<p class="discreet" if="not zobjects">:_('no_ref')</p>
|
<p class="discreet" if="not zobjects">:_('no_ref')</p>
|
||||||
|
@ -198,35 +197,34 @@ class Ref(Field):
|
||||||
<tr valign="bottom">
|
<tr valign="bottom">
|
||||||
<td>
|
<td>
|
||||||
<!-- Show forward or backward reference(s) -->
|
<!-- Show forward or backward reference(s) -->
|
||||||
<table class="not innerRef and 'list' or ''"
|
<table class=":not innerRef and 'list' or ''"
|
||||||
width=":innerRef and '100%' or field.layouts['view']['width']"
|
width=":innerRef and '100%' or field.layouts['view']['width']"
|
||||||
var="columns=zobjects[0].getColumnsSpecifiers(\
|
var="columns=ztool.getColumnsSpecifiers(tiedClassName, \
|
||||||
field.shownInfo, dir)">
|
field.shownInfo, dir)">
|
||||||
<tr if="field.showHeaders">
|
<tr if="field.showHeaders">
|
||||||
<th for="column in columns" width=":column['width']"
|
<th for="column in columns" width=":column.width"
|
||||||
align="column['align']"
|
align="column.align" var2="refField=column.field">
|
||||||
var2="field=column['field']">
|
<span>:_(refField.labelId)</span>
|
||||||
<span>:_(field.labelId)</span>
|
|
||||||
<x>:field.pxSortIcons</x>
|
<x>:field.pxSortIcons</x>
|
||||||
<x var="className=linkedPortalType">:obj.pxShowDetails</x>
|
<x var="className=tiedClassName">:tool.pxShowDetails</x>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr for="ztied in zobjects" valign="top"
|
<tr for="ztied in zobjects" valign="top"
|
||||||
class=":loop.ztied.odd and 'even' or 'odd'">
|
class=":loop.ztied.odd and 'even' or 'odd'">
|
||||||
<td for="column in columns"
|
<td for="column in columns"
|
||||||
width=":column['width']" align=":column['align']"
|
width=":column.width" align=":column.align"
|
||||||
var2="field=column['field']">
|
var2="refField=column.field">
|
||||||
<!-- The "title" field -->
|
<!-- The "title" field -->
|
||||||
<x if="python: field.name == 'title'">
|
<x if="refField.name == 'title'">
|
||||||
<x>:field.pxObjectTitle</x>
|
<x>:field.pxObjectTitle</x>
|
||||||
<div if="ztied.mayAct()">:field.pxObjectActions</div>
|
<div if="ztied.mayAct()">:field.pxObjectActions</div>
|
||||||
</x>
|
</x>
|
||||||
<!-- Any other field -->
|
<!-- Any other field -->
|
||||||
<x if="field.name != 'title'">
|
<x if="refField.name != 'title'">
|
||||||
<x var="zobj=ztied; obj=ztied.appy(); layoutType='cell';
|
<x var="zobj=ztied; obj=ztied.appy(); layoutType='cell';
|
||||||
innerRef=True"
|
innerRef=True; field=refField"
|
||||||
if="zobj.showField(field.name, \
|
if="zobj.showField(field.name, \
|
||||||
layoutType='result')">:field.pxView</x>
|
layoutType='result')">:field.pxRender</x>
|
||||||
</x>
|
</x>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -235,14 +233,11 @@ class Ref(Field):
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- Appy (bottom) navigation -->
|
<!-- (Bottom) navigation -->
|
||||||
<x>:obj.pxAppyNavigate</x>
|
<x>:tool.pxNavigate</x>
|
||||||
</x>
|
</x>
|
||||||
</div>''')
|
</div>''')
|
||||||
|
|
||||||
pxView = pxCell = Px('''
|
|
||||||
<x var="x=req.set('fieldName', field.name)">:field.pxViewContent</x>''')
|
|
||||||
|
|
||||||
pxEdit = Px('''
|
pxEdit = Px('''
|
||||||
<select if="field.link"
|
<select if="field.link"
|
||||||
var2="requestValue=req.get(name, []);
|
var2="requestValue=req.get(name, []);
|
||||||
|
@ -253,7 +248,7 @@ class Ref(Field):
|
||||||
isBeingCreated=zobj.isTemporary()"
|
isBeingCreated=zobj.isTemporary()"
|
||||||
name=":name" size="isMultiple and field.height or ''"
|
name=":name" size="isMultiple and field.height or ''"
|
||||||
multiple="isMultiple and 'multiple' or ''">
|
multiple="isMultiple and 'multiple' or ''">
|
||||||
<option value="" if="not isMultiple">:_('choose_a_value')"></option>
|
<option value="" if="not isMultiple">:_('choose_a_value')</option>
|
||||||
<option for="ztied in zobjects" var2="uid=ztied.o.UID()"
|
<option for="ztied in zobjects" var2="uid=ztied.o.UID()"
|
||||||
selected=":inRequest and (uid in requestValue) or \
|
selected=":inRequest and (uid in requestValue) or \
|
||||||
(uid in uids)"
|
(uid in uids)"
|
||||||
|
@ -261,7 +256,7 @@ class Ref(Field):
|
||||||
</select>''')
|
</select>''')
|
||||||
|
|
||||||
pxSearch = Px('''<x>
|
pxSearch = Px('''<x>
|
||||||
<label lfor=":widgetName">:_(field.labelId)"></label><br/>
|
<label lfor=":widgetName">:_(field.labelId)</label><br/>
|
||||||
<!-- The "and" / "or" radio buttons -->
|
<!-- The "and" / "or" radio buttons -->
|
||||||
<x if="field.multiplicity[1] != 1"
|
<x if="field.multiplicity[1] != 1"
|
||||||
var2="operName='o_%s' % name;
|
var2="operName='o_%s' % name;
|
||||||
|
@ -269,15 +264,15 @@ class Ref(Field):
|
||||||
andName='%s_and' % operName">
|
andName='%s_and' % operName">
|
||||||
<input type="radio" name=":operName" id=":orName" checked="checked"
|
<input type="radio" name=":operName" id=":orName" checked="checked"
|
||||||
value="or"/>
|
value="or"/>
|
||||||
<label lfor=":orName">:_('search_or')"></label>
|
<label lfor=":orName">:_('search_or')</label>
|
||||||
<input type="radio" name=":operName" id=":andName" value="and"/>
|
<input type="radio" name=":operName" id=":andName" value="and"/>
|
||||||
<label lfor=":andName">:_('search_and')"></label><br/>
|
<label lfor=":andName">:_('search_and')</label><br/>
|
||||||
</x>
|
</x>
|
||||||
<!-- The list of values -->
|
<!-- The list of values -->
|
||||||
<select name=":widgetName" size=":field.sheight" multiple="multiple">
|
<select name=":widgetName" size=":field.sheight" multiple="multiple">
|
||||||
<option for="v in ztool.getSearchValues(name, className)"
|
<option for="v in ztool.getSearchValues(name, className)"
|
||||||
var2="uid=v[0]; title=field.getReferenceLabel(v[1])" value=":uid"
|
var2="uid=v[0]; title=field.getReferenceLabel(v[1])" value=":uid"
|
||||||
title=":title">:ztool.truncateValue(title,field.swidth)"></option>
|
title=":title">:ztool.truncateValue(title,field.swidth)</option>
|
||||||
</select>
|
</select>
|
||||||
</x>''')
|
</x>''')
|
||||||
|
|
||||||
|
@ -603,7 +598,7 @@ class Ref(Field):
|
||||||
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 obj.getUser().has_permission(addPermission, folder):
|
if not tool.getUser().has_permission(addPermission, folder):
|
||||||
return No('no_add_perm')
|
return No('no_add_perm')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -623,6 +618,19 @@ class Ref(Field):
|
||||||
else:
|
else:
|
||||||
return self.callMethod(obj, self.changeOrder)
|
return self.callMethod(obj, self.changeOrder)
|
||||||
|
|
||||||
|
def doChangeOrder(self, obj):
|
||||||
|
'''Moves a referred object up or down.'''
|
||||||
|
rq = obj.REQUEST
|
||||||
|
# Move the item up (-1), down (+1) ?
|
||||||
|
move = (rq['move'] == 'down') and 1 or -1
|
||||||
|
# The UID of the referred object to move
|
||||||
|
uid = rq['refObjectUid']
|
||||||
|
uids = getattr(obj.aq_base, self.name)
|
||||||
|
oldIndex = uids.index(uid)
|
||||||
|
uids.remove(uid)
|
||||||
|
newIndex = oldIndex + move
|
||||||
|
uids.insert(newIndex, uid)
|
||||||
|
|
||||||
def getSelectableObjects(self, obj):
|
def getSelectableObjects(self, obj):
|
||||||
'''This method returns the list of all objects that can be selected to
|
'''This method returns the list of all objects that can be selected to
|
||||||
be linked as references to p_obj via p_self.'''
|
be linked as references to p_obj via p_self.'''
|
||||||
|
@ -647,7 +655,7 @@ class Ref(Field):
|
||||||
if refType.type == 'String':
|
if refType.type == 'String':
|
||||||
if refType.format == 2:
|
if refType.format == 2:
|
||||||
value = self.xhtmlToText.sub(' ', value)
|
value = self.xhtmlToText.sub(' ', value)
|
||||||
elif type(value) in sequenceTypes:
|
elif type(value) in sutils.sequenceTypes:
|
||||||
value = ', '.join(value)
|
value = ', '.join(value)
|
||||||
prefix = ''
|
prefix = ''
|
||||||
if res:
|
if res:
|
||||||
|
@ -664,6 +672,13 @@ class Ref(Field):
|
||||||
if not uids: raise IndexError()
|
if not uids: raise IndexError()
|
||||||
return uids.index(refObj.UID())
|
return uids.index(refObj.UID())
|
||||||
|
|
||||||
|
def sort(self, obj):
|
||||||
|
'''Called when the user wants to sort the content of this field.'''
|
||||||
|
rq = obj.REQUEST
|
||||||
|
sortKey = rq.get('sortKey')
|
||||||
|
reverse = rq.get('reverse') == 'True'
|
||||||
|
obj.appy().sort(self.name, sortKey=sortKey, reverse=reverse)
|
||||||
|
|
||||||
def autoref(klass, field):
|
def autoref(klass, field):
|
||||||
'''klass.field is a Ref to p_klass. This kind of auto-reference can't be
|
'''klass.field is a Ref to p_klass. This kind of auto-reference can't be
|
||||||
declared in the "normal" way, like this:
|
declared in the "normal" way, like this:
|
||||||
|
|
|
@ -96,7 +96,7 @@ class String(Field):
|
||||||
<div if="not mayAjaxEdit" class="xhtml">::value</div>
|
<div if="not mayAjaxEdit" class="xhtml">::value</div>
|
||||||
<div if="mayAjaxEdit" class="xhtml" contenteditable="true"
|
<div if="mayAjaxEdit" class="xhtml" contenteditable="true"
|
||||||
id=":'%s_%s_ck' % (zobj.UID(), name)">::value</div>
|
id=":'%s_%s_ck' % (zobj.UID(), name)">::value</div>
|
||||||
<script if="mayAjaxEdit">:field.getJsInlineInit(zobj)"></script>
|
<script if="mayAjaxEdit">::field.getJsInlineInit(zobj)"></script>
|
||||||
</x>
|
</x>
|
||||||
<input type="hidden" if="masterCss" class=":masterCss" value=":rawValue"
|
<input type="hidden" if="masterCss" class=":masterCss" value=":rawValue"
|
||||||
name=":name" id=":name"/>
|
name=":name" id=":name"/>
|
||||||
|
@ -116,8 +116,7 @@ class String(Field):
|
||||||
size=":isMultiple and field.height or 1">
|
size=":isMultiple and field.height or 1">
|
||||||
<option for="val in possibleValues" value=":val[0]"
|
<option for="val in possibleValues" value=":val[0]"
|
||||||
selected=":field.isSelected(zobj, val[0], rawValue)"
|
selected=":field.isSelected(zobj, val[0], rawValue)"
|
||||||
title=":val[1]">:ztool.truncateValue(val[1], field.width)">
|
title=":val[1]">:ztool.truncateValue(val[1],field.width)</option>
|
||||||
</option>
|
|
||||||
</select>
|
</select>
|
||||||
<x if="isOneLine and not isSelect">
|
<x if="isOneLine and not isSelect">
|
||||||
<input id=":name" name=":name" size=":field.width"
|
<input id=":name" name=":name" size=":field.width"
|
||||||
|
@ -137,18 +136,18 @@ class String(Field):
|
||||||
rows=":field.height">:inRequest and requestValue or value
|
rows=":field.height">:inRequest and requestValue or value
|
||||||
</textarea>
|
</textarea>
|
||||||
<script if="fmt == 2"
|
<script if="fmt == 2"
|
||||||
type="text/javascript">:field.getJsInit(zobj)</script>
|
type="text/javascript">::field.getJsInit(zobj)</script>
|
||||||
</x>
|
</x>
|
||||||
</x>''')
|
</x>''')
|
||||||
|
|
||||||
pxCell = Px('''
|
pxCell = Px('''
|
||||||
<x var="multipleValues=value and isMultiple">
|
<x var="multipleValues=value and isMultiple">
|
||||||
<x if="multipleValues">:', '.join(value)"></x>
|
<x if="multipleValues">:', '.join(value)</x>
|
||||||
<x if="not multipleValues">:field.pxView</x>
|
<x if="not multipleValues">:field.pxView</x>
|
||||||
</x>''')
|
</x>''')
|
||||||
|
|
||||||
pxSearch = Px('''<x>
|
pxSearch = Px('''<x>
|
||||||
<label lfor="widgetName">:_(field.labelId)"></label><br/>
|
<label lfor="widgetName">:_(field.labelId)</label><br/>
|
||||||
<!-- Show a simple search field for most String fields -->
|
<!-- Show a simple search field for most String fields -->
|
||||||
<input if="not field.isSelect" type="text" maxlength=":field.maxChars"
|
<input if="not field.isSelect" type="text" maxlength=":field.maxChars"
|
||||||
size=":field.swidth" value=":field.sdefault"
|
size=":field.swidth" value=":field.sdefault"
|
||||||
|
@ -166,7 +165,7 @@ class String(Field):
|
||||||
value="or"/>
|
value="or"/>
|
||||||
<label lfor=":orName">:_('search_or')</label>
|
<label lfor=":orName">:_('search_or')</label>
|
||||||
<input type="radio" name=":operName" id=":andName" value="and"/>
|
<input type="radio" name=":operName" id=":andName" value="and"/>
|
||||||
<label lfor=":andName">:_('search_and')"></label><br/>
|
<label lfor=":andName">:_('search_and')</label><br/>
|
||||||
</x>
|
</x>
|
||||||
<!-- The list of values -->
|
<!-- The list of values -->
|
||||||
<select var="preSelected=field.sdefault"
|
<select var="preSelected=field.sdefault"
|
||||||
|
@ -656,7 +655,7 @@ class String(Field):
|
||||||
if isinstance(v, int): sv = str(v)
|
if isinstance(v, int): sv = str(v)
|
||||||
else: sv = '"%s"' % v
|
else: sv = '"%s"' % v
|
||||||
ck.append('%s: %s' % (k, sv))
|
ck.append('%s: %s' % (k, sv))
|
||||||
return 'CKEDITOR.replace("%s", {%s})' % (name, ', '.join(ck))
|
return 'CKEDITOR.replace("%s", {%s})' % (self.name, ', '.join(ck))
|
||||||
|
|
||||||
def getJsInlineInit(self, obj):
|
def getJsInlineInit(self, obj):
|
||||||
'''Gets the Javascript init code for enabling inline edition of this
|
'''Gets the Javascript init code for enabling inline edition of this
|
||||||
|
@ -665,8 +664,8 @@ class String(Field):
|
||||||
return "CKEDITOR.disableAutoInline = true;\n" \
|
return "CKEDITOR.disableAutoInline = true;\n" \
|
||||||
"CKEDITOR.inline('%s_%s_ck', {on: {blur: " \
|
"CKEDITOR.inline('%s_%s_ck', {on: {blur: " \
|
||||||
"function( event ) { var data = event.editor.getData(); " \
|
"function( event ) { var data = event.editor.getData(); " \
|
||||||
"askAjaxChunk('%s_%s','POST','%s','page','saveField', "\
|
"askAjaxChunk('%s_%s','POST','%s','%s:pxSave', " \
|
||||||
"{'fieldName':'%s', 'fieldContent': encodeURIComponent(data)}, "\
|
"{'fieldContent': encodeURIComponent(data)}, " \
|
||||||
"null, evalInnerScripts);}}});"% \
|
"null, evalInnerScripts);}}});"% \
|
||||||
(uid, self.name, uid, self.name, obj.absolute_url(), self.name)
|
(uid, self.name, uid, self.name, obj.absolute_url(), self.name)
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ 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, Group, Field, Column, No
|
from appy.fields import Page, Phase, Group, Field, Column, No
|
||||||
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
|
||||||
|
@ -23,6 +23,7 @@ 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.gen.layout import Table
|
from appy.gen.layout import Table
|
||||||
|
from appy.px import Px
|
||||||
from appy import Object
|
from appy import Object
|
||||||
|
|
||||||
# Default Appy permissions -----------------------------------------------------
|
# Default Appy permissions -----------------------------------------------------
|
||||||
|
@ -172,6 +173,42 @@ class Search:
|
||||||
return gutils.callMethod(tool, self.show, klass=klass)
|
return gutils.callMethod(tool, self.show, klass=klass)
|
||||||
return self.show
|
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'),
|
||||||
|
@ -381,7 +418,7 @@ class Transition:
|
||||||
break
|
break
|
||||||
if not startFound: return False
|
if not startFound: return False
|
||||||
# Check that the condition is met
|
# Check that the condition is met
|
||||||
user = obj.getUser()
|
user = obj.getTool().getUser()
|
||||||
if isinstance(self.condition, Role):
|
if isinstance(self.condition, Role):
|
||||||
# Condition is a role. Transition may be triggered if the user has
|
# Condition is a role. Transition may be triggered if the user has
|
||||||
# this role.
|
# this role.
|
||||||
|
|
|
@ -7,31 +7,14 @@ import appy
|
||||||
import appy.version
|
import appy.version
|
||||||
import appy.gen as gen
|
import appy.gen as gen
|
||||||
from appy.gen.po import PoParser
|
from appy.gen.po import PoParser
|
||||||
from appy.gen.utils import updateRolesForPermission, createObject
|
|
||||||
from appy.gen.indexer import defaultIndexes, updateIndexes
|
from appy.gen.indexer import defaultIndexes, updateIndexes
|
||||||
from appy.gen.migrator import Migrator
|
from appy.gen.migrator import Migrator
|
||||||
|
from appy.gen import utils as gutils
|
||||||
from appy.shared.data import languages
|
from appy.shared.data import languages
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
homePage = '''
|
homePage = '<tal:h define="dummy python: request.RESPONSE.redirect(' \
|
||||||
<tal:hp define="tool python: context.config;
|
'context.config.getHomePage())"/>'
|
||||||
dummy python: request.RESPONSE.redirect(tool.getHomePage())">
|
|
||||||
</tal:hp>
|
|
||||||
'''
|
|
||||||
errorPage = '''
|
|
||||||
<tal:main define="tool python: context.config"
|
|
||||||
on-error="string: ServerError">
|
|
||||||
<html metal:use-macro="context/ui/template/macros/main">
|
|
||||||
<div metal:fill-slot="content" tal:define="o python:options">
|
|
||||||
<p tal:condition="o/error_message"
|
|
||||||
tal:content="structure o/error_message"></p>
|
|
||||||
<p>Error type: <b><span tal:replace="o/error_type"/></b></p>
|
|
||||||
<p>Error value: <b><span tal:replace="o/error_value"/></b></p>
|
|
||||||
<p tal:content="structure o/error_tb"></p>
|
|
||||||
</div>
|
|
||||||
</html>
|
|
||||||
</tal:main>
|
|
||||||
'''
|
|
||||||
|
|
||||||
# Stuff for tracking user activity ---------------------------------------------
|
# Stuff for tracking user activity ---------------------------------------------
|
||||||
loggedUsers = {}
|
loggedUsers = {}
|
||||||
|
@ -45,7 +28,7 @@ def traverseWrapper(self, path, response=None, validated_hook=None):
|
||||||
t = time.time()
|
t = time.time()
|
||||||
if os.path.splitext(path)[-1].lower() not in doNotTrack:
|
if os.path.splitext(path)[-1].lower() not in doNotTrack:
|
||||||
# Do nothing when the user gets non-pages
|
# Do nothing when the user gets non-pages
|
||||||
userId = self['AUTHENTICATED_USER'].getId()
|
userId, dummy = gutils.readCookie(self)
|
||||||
if userId:
|
if userId:
|
||||||
loggedUsers[userId] = t
|
loggedUsers[userId] = t
|
||||||
# "Touch" the SESSION object. Else, expiration won't occur.
|
# "Touch" the SESSION object. Else, expiration won't occur.
|
||||||
|
@ -55,11 +38,11 @@ def traverseWrapper(self, path, response=None, validated_hook=None):
|
||||||
def onDelSession(sessionObject, container):
|
def onDelSession(sessionObject, container):
|
||||||
'''This function is called when a session expires.'''
|
'''This function is called when a session expires.'''
|
||||||
rq = container.REQUEST
|
rq = container.REQUEST
|
||||||
if rq.cookies.has_key('__ac') and rq.cookies.has_key('_ZopeId') and \
|
if rq.cookies.has_key('_appy_') and rq.cookies.has_key('_ZopeId') and \
|
||||||
(rq['_ZopeId'] == sessionObject.token):
|
(rq['_ZopeId'] == sessionObject.token):
|
||||||
# The request comes from a guy whose session has expired.
|
# The request comes from a guy whose session has expired.
|
||||||
resp = rq.RESPONSE
|
resp = rq.RESPONSE
|
||||||
resp.expireCookie('__ac', path='/')
|
resp.expireCookie('_appy_', path='/')
|
||||||
resp.setHeader('Content-Type', 'text/html')
|
resp.setHeader('Content-Type', 'text/html')
|
||||||
resp.write('<center>For security reasons, your session has ' \
|
resp.write('<center>For security reasons, your session has ' \
|
||||||
'expired.</center>')
|
'expired.</center>')
|
||||||
|
@ -68,6 +51,9 @@ def onDelSession(sessionObject, container):
|
||||||
class ZopeInstaller:
|
class ZopeInstaller:
|
||||||
'''This Zope installer runs every time Zope starts and encounters this
|
'''This Zope installer runs every time Zope starts and encounters this
|
||||||
generated Zope product.'''
|
generated Zope product.'''
|
||||||
|
# Info about the default users that are always present.
|
||||||
|
defaultUsers = {'admin': ('Manager',), 'system': ('Manager',), 'anon': ()}
|
||||||
|
|
||||||
def __init__(self, zopeContext, config, classes):
|
def __init__(self, zopeContext, config, classes):
|
||||||
self.zopeContext = zopeContext
|
self.zopeContext = zopeContext
|
||||||
self.app = zopeContext._ProductContext__app # The root of the Zope tree
|
self.app = zopeContext._ProductContext__app # The root of the Zope tree
|
||||||
|
@ -142,7 +128,6 @@ class ZopeInstaller:
|
||||||
# Update the error page
|
# Update the error page
|
||||||
if 'standard_error_message' in zopeContent:
|
if 'standard_error_message' in zopeContent:
|
||||||
self.app.manage_delObjects(['standard_error_message'])
|
self.app.manage_delObjects(['standard_error_message'])
|
||||||
manage_addPageTemplate(self.app, 'standard_error_message', '',errorPage)
|
|
||||||
|
|
||||||
def installCatalog(self):
|
def installCatalog(self):
|
||||||
'''Create the catalog at the root of Zope if id does not exist.'''
|
'''Create the catalog at the root of Zope if id does not exist.'''
|
||||||
|
@ -196,7 +181,7 @@ class ZopeInstaller:
|
||||||
|
|
||||||
if 'config' not in zopeContent:
|
if 'config' not in zopeContent:
|
||||||
toolName = '%sTool' % self.productName
|
toolName = '%sTool' % self.productName
|
||||||
createObject(self.app, 'config', toolName, self.productName,
|
gutils.createObject(self.app, 'config', toolName, self.productName,
|
||||||
wf=False, noSecurity=True)
|
wf=False, noSecurity=True)
|
||||||
|
|
||||||
if 'data' not in zopeContent:
|
if 'data' not in zopeContent:
|
||||||
|
@ -220,7 +205,7 @@ class ZopeInstaller:
|
||||||
wrapperClass = tool.getAppyClass(className, wrapper=True)
|
wrapperClass = tool.getAppyClass(className, wrapper=True)
|
||||||
creators = wrapperClass.getCreators(self.config)
|
creators = wrapperClass.getCreators(self.config)
|
||||||
permission = self.getAddPermission(className)
|
permission = self.getAddPermission(className)
|
||||||
updateRolesForPermission(permission, tuple(creators), data)
|
gutils.updateRolesForPermission(permission,tuple(creators),data)
|
||||||
|
|
||||||
# Remove some default objects created by Zope but not useful to Appy
|
# Remove some default objects created by Zope but not useful to Appy
|
||||||
for name in ('standard_html_footer', 'standard_html_header',\
|
for name in ('standard_html_footer', 'standard_html_header',\
|
||||||
|
@ -236,12 +221,13 @@ class ZopeInstaller:
|
||||||
appyTool = tool.appy()
|
appyTool = tool.appy()
|
||||||
appyTool.log('Appy version is "%s".' % appy.version.short)
|
appyTool.log('Appy version is "%s".' % appy.version.short)
|
||||||
|
|
||||||
# Create the admin user if it does not exist.
|
# Create the default users if they do not exist.
|
||||||
if not appyTool.count('User', noSecurity=True, login='admin'):
|
for login, roles in self.defaultUsers.iteritems():
|
||||||
appyTool.create('users', noSecurity=True, login='admin',
|
if not appyTool.count('User', noSecurity=True, login=login):
|
||||||
password1='admin', password2='admin',
|
appyTool.create('users', noSecurity=True, login=login,
|
||||||
email='admin@appyframework.org', roles=['Manager'])
|
password1=login, password2=login,
|
||||||
appyTool.log('Admin user "admin" created.')
|
email='%s@appyframework.org'%login, roles=roles)
|
||||||
|
appyTool.log('User "%s" created.' % login)
|
||||||
|
|
||||||
# Create group "admins" if it does not exist
|
# Create group "admins" if it does not exist
|
||||||
if not appyTool.count('Group', noSecurity=True, login='admins'):
|
if not appyTool.count('Group', noSecurity=True, login='admins'):
|
||||||
|
|
|
@ -42,15 +42,12 @@ rowDelms = ''.join(rowDelimiters.keys())
|
||||||
cellDelimiters = {'|': 'center', ';': 'left', '!': 'right'}
|
cellDelimiters = {'|': 'center', ';': 'left', '!': 'right'}
|
||||||
cellDelms = ''.join(cellDelimiters.keys())
|
cellDelms = ''.join(cellDelimiters.keys())
|
||||||
|
|
||||||
macroDict = {
|
pxDict = {
|
||||||
# Page-related elements
|
# Page-related elements
|
||||||
's': ('page', 'header'), 'w': ('page', 'widgets'),
|
's': 'pxHeader', 'w': 'pxFields', 'n': 'pxNavigateSiblings', 'b': 'pxButtons',
|
||||||
'n': ('navigate', 'objectNavigate'), 'b': ('page', 'buttons'),
|
|
||||||
# Field-related elements
|
# Field-related elements
|
||||||
'l': ('show', 'label'), 'd': ('show', 'description'),
|
'l': 'pxLabel', 'd': 'pxDescription', 'h': 'pxHelp', 'v': 'pxValidation',
|
||||||
'h': ('show', 'help'), 'v': ('show', 'validation'),
|
'r': 'pxRequired', 'c': 'pxChanges'}
|
||||||
'r': ('show', 'required'), 'c': ('show', 'changes'),
|
|
||||||
}
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
class LayoutElement:
|
class LayoutElement:
|
||||||
|
@ -77,8 +74,8 @@ class Cell(LayoutElement):
|
||||||
digits += char
|
digits += char
|
||||||
else:
|
else:
|
||||||
# It is a letter corresponding to a macro
|
# It is a letter corresponding to a macro
|
||||||
if char in macroDict:
|
if char in pxDict:
|
||||||
self.content.append(macroDict[char])
|
self.content.append(pxDict[char])
|
||||||
elif char == 'f':
|
elif char == 'f':
|
||||||
# The exact macro to call will be known at render-time
|
# The exact macro to call will be known at render-time
|
||||||
self.content.append('?')
|
self.content.append('?')
|
||||||
|
@ -209,7 +206,7 @@ class Table(LayoutElement):
|
||||||
|
|
||||||
def removeElement(self, elem):
|
def removeElement(self, elem):
|
||||||
'''Removes given p_elem from myself.'''
|
'''Removes given p_elem from myself.'''
|
||||||
macroToRemove = macroDict[elem]
|
macroToRemove = pxDict[elem]
|
||||||
for row in self.rows:
|
for row in self.rows:
|
||||||
for cell in row['cells']:
|
for cell in row['cells']:
|
||||||
if macroToRemove in cell['content']:
|
if macroToRemove in cell['content']:
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
import os, os.path, sys, re, time, random, types, base64, urllib
|
import os, os.path, sys, re, time, random, types
|
||||||
from appy import Object
|
from appy import Object
|
||||||
import appy.gen
|
import appy.gen
|
||||||
from appy.gen import Search, String, Page, ldap
|
from appy.gen import Search, UiSearch, String, Page, ldap
|
||||||
from appy.gen.utils import SomeObjects, getClassName, GroupDescr, SearchDescr
|
from appy.gen.layout import ColumnLayout
|
||||||
|
from appy.gen import utils as gutils
|
||||||
from appy.gen.mixins import BaseMixin
|
from appy.gen.mixins import BaseMixin
|
||||||
from appy.gen.wrappers import AbstractWrapper
|
from appy.gen.wrappers import AbstractWrapper
|
||||||
from appy.gen.descriptors import ClassDescriptor
|
from appy.gen.descriptors import ClassDescriptor
|
||||||
from appy.gen.mail import sendMail
|
from appy.gen.mail import sendMail
|
||||||
from appy.shared import mimeTypes
|
from appy.shared import mimeTypes
|
||||||
from appy.shared.utils import getOsTempFolder, sequenceTypes, normalizeString, \
|
from appy.shared import utils as sutils
|
||||||
splitList
|
|
||||||
from appy.shared.data import languages
|
from appy.shared.data import languages
|
||||||
try:
|
try:
|
||||||
from AccessControl.ZopeSecurityPolicy import _noroles
|
from AccessControl.ZopeSecurityPolicy import _noroles
|
||||||
|
@ -32,7 +32,7 @@ class ToolMixin(BaseMixin):
|
||||||
appName = self.getProductConfig().PROJECTNAME
|
appName = self.getProductConfig().PROJECTNAME
|
||||||
res = metaTypeOrAppyClass
|
res = metaTypeOrAppyClass
|
||||||
if not isinstance(metaTypeOrAppyClass, basestring):
|
if not isinstance(metaTypeOrAppyClass, basestring):
|
||||||
res = getClassName(metaTypeOrAppyClass, appName)
|
res = gutils.getClassName(metaTypeOrAppyClass, appName)
|
||||||
if res.find('_wrappers') != -1:
|
if res.find('_wrappers') != -1:
|
||||||
elems = res.split('_')
|
elems = res.split('_')
|
||||||
res = '%s%s' % (elems[1], elems[4])
|
res = '%s%s' % (elems[1], elems[4])
|
||||||
|
@ -42,31 +42,31 @@ class ToolMixin(BaseMixin):
|
||||||
def home(self):
|
def home(self):
|
||||||
'''Returns the content of px ToolWrapper.pxHome.'''
|
'''Returns the content of px ToolWrapper.pxHome.'''
|
||||||
tool = self.appy()
|
tool = self.appy()
|
||||||
return tool.pxHome({'self': tool})
|
return tool.pxHome({'obj': None, 'tool': tool})
|
||||||
|
|
||||||
def query(self):
|
def query(self):
|
||||||
'''Returns the content of px ToolWrapper.pxQuery.'''
|
'''Returns the content of px ToolWrapper.pxQuery.'''
|
||||||
tool = self.appy()
|
tool = self.appy()
|
||||||
return tool.pxQuery({'self': tool})
|
return tool.pxQuery({'obj': None, 'tool': tool})
|
||||||
|
|
||||||
def search(self):
|
def search(self):
|
||||||
'''Returns the content of px ToolWrapper.pxSearch.'''
|
'''Returns the content of px ToolWrapper.pxSearch.'''
|
||||||
tool = self.appy()
|
tool = self.appy()
|
||||||
return tool.pxSearch({'self': tool})
|
return tool.pxSearch({'obj': None, 'tool': tool})
|
||||||
|
|
||||||
def getHomePage(self):
|
def getHomePage(self):
|
||||||
'''Return the home page when a user hits the app.'''
|
'''Return the home page when a user hits the app.'''
|
||||||
# If the app defines a method "getHomePage", call it.
|
# If the app defines a method "getHomePage", call it.
|
||||||
appyTool = self.appy()
|
tool = self.appy()
|
||||||
try:
|
try:
|
||||||
url = appyTool.getHomePage()
|
url = tool.getHomePage()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# Bring Managers to the config, lead others to home.pt.
|
# Bring Managers to the config, lead others to home.pt.
|
||||||
user = self.getUser()
|
user = self.getUser()
|
||||||
if user.has_role('Manager'):
|
if user.has_role('Manager'):
|
||||||
url = self.goto(self.absolute_url())
|
url = self.goto(self.absolute_url())
|
||||||
else:
|
else:
|
||||||
url = self.goto(self.getApp().ui.home.absolute_url())
|
url = self.goto('%s/home' % self.getApp().config.absolute_url())
|
||||||
return url
|
return url
|
||||||
|
|
||||||
def getHomeObject(self):
|
def getHomeObject(self):
|
||||||
|
@ -77,15 +77,12 @@ class ToolMixin(BaseMixin):
|
||||||
portlet menu will nevertheless appear: the user will not have the
|
portlet menu will nevertheless appear: the user will not have the
|
||||||
feeling of being lost.'''
|
feeling of being lost.'''
|
||||||
# If the app defines a method "getHomeObject", call it.
|
# If the app defines a method "getHomeObject", call it.
|
||||||
appyTool = self.appy()
|
|
||||||
try:
|
try:
|
||||||
obj = appyTool.getHomeObject()
|
return self.appy().getHomeObject()
|
||||||
if obj: return obj.o
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# For managers, the home object is the config. For others, there is
|
# For managers, the home object is the config. For others, there is
|
||||||
# no default home object.
|
# no default home object.
|
||||||
user = self.getUser()
|
if self.getUser().has_role('Manager'): return self.appy()
|
||||||
if user.has_role('Manager'): return self
|
|
||||||
|
|
||||||
def getCatalog(self):
|
def getCatalog(self):
|
||||||
'''Returns the catalog object.'''
|
'''Returns the catalog object.'''
|
||||||
|
@ -220,31 +217,29 @@ class ToolMixin(BaseMixin):
|
||||||
'''Returns the list of root classes for this application.'''
|
'''Returns the list of root classes for this application.'''
|
||||||
return self.getProductConfig().rootClasses
|
return self.getProductConfig().rootClasses
|
||||||
|
|
||||||
def _appy_getAllFields(self, contentType):
|
def _appy_getAllFields(self, className):
|
||||||
'''Returns the (translated) names of fields of p_contentType.'''
|
'''Returns the (translated) names of fields of p_className.'''
|
||||||
res = []
|
res = []
|
||||||
for appyType in self.getAllAppyTypes(className=contentType):
|
for field in self.getAllAppyTypes(className=className):
|
||||||
res.append((appyType.name, self.translate(appyType.labelId)))
|
res.append((className.name, self.translate(className.labelId)))
|
||||||
# Add object state
|
# Add object state
|
||||||
res.append(('state', self.translate('workflow_state')))
|
res.append(('state', self.translate('workflow_state')))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def _appy_getSearchableFields(self, contentType):
|
def _appy_getSearchableFields(self, className):
|
||||||
'''Returns the (translated) names of fields that may be searched on
|
'''Returns the (translated) names of fields that may be searched on
|
||||||
objects of type p_contentType (=indexed fields).'''
|
objects of type p_className (=indexed fields).'''
|
||||||
res = []
|
res = []
|
||||||
for appyType in self.getAllAppyTypes(className=contentType):
|
for field in self.getAllAppyTypes(className=className):
|
||||||
if appyType.indexed:
|
if field.indexed:
|
||||||
res.append((appyType.name, self.translate(appyType.labelId)))
|
res.append((field.name, self.translate(field.labelId)))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def getSearchInfo(self, contentType, refInfo=None):
|
def getSearchInfo(self, className, refInfo=None):
|
||||||
'''Returns, as a dict:
|
'''Returns, as an object:
|
||||||
- the list of searchable fields (= some fields among all indexed
|
- the list of searchable fields (some among all indexed fields);
|
||||||
fields);
|
|
||||||
- the number of columns for layouting those fields.'''
|
- the number of columns for layouting those fields.'''
|
||||||
fields = []
|
fields = []
|
||||||
fieldDicts = []
|
|
||||||
if refInfo:
|
if refInfo:
|
||||||
# The search is triggered from a Ref field.
|
# The search is triggered from a Ref field.
|
||||||
refObject, fieldName = self.getRefInfo(refInfo)
|
refObject, fieldName = self.getRefInfo(refInfo)
|
||||||
|
@ -253,16 +248,13 @@ class ToolMixin(BaseMixin):
|
||||||
nbOfColumns = refField.queryNbCols
|
nbOfColumns = refField.queryNbCols
|
||||||
else:
|
else:
|
||||||
# The search is triggered from an app-wide search.
|
# The search is triggered from an app-wide search.
|
||||||
at = self.appy()
|
tool = self.appy()
|
||||||
fieldNames = getattr(at, 'searchFieldsFor%s' % contentType,())
|
fieldNames = getattr(tool, 'searchFieldsFor%s' % className,())
|
||||||
nbOfColumns = getattr(at, 'numberOfSearchColumnsFor%s' %contentType)
|
nbOfColumns = getattr(tool, 'numberOfSearchColumnsFor%s' %className)
|
||||||
for name in fieldNames:
|
for name in fieldNames:
|
||||||
appyType = self.getAppyType(name,asDict=False,className=contentType)
|
field = self.getAppyType(name, className=className)
|
||||||
appyDict = self.getAppyType(name, asDict=True,className=contentType)
|
fields.append(field)
|
||||||
fields.append(appyType)
|
return Object(fields=fields, nbOfColumns=nbOfColumns)
|
||||||
fieldDicts.append(appyDict)
|
|
||||||
return {'fields': fields, 'nbOfColumns': nbOfColumns,
|
|
||||||
'fieldDicts': fieldDicts}
|
|
||||||
|
|
||||||
queryParamNames = ('className', 'search', 'sortKey', 'sortOrder',
|
queryParamNames = ('className', 'search', 'sortKey', 'sortOrder',
|
||||||
'filterKey', 'filterValue')
|
'filterKey', 'filterValue')
|
||||||
|
@ -284,16 +276,16 @@ class ToolMixin(BaseMixin):
|
||||||
if hasattr(klass, 'resultMode'): return klass.resultMode
|
if hasattr(klass, 'resultMode'): return klass.resultMode
|
||||||
return 'list' # The default mode
|
return 'list' # The default mode
|
||||||
|
|
||||||
def getImportElements(self, contentType):
|
def getImportElements(self, className):
|
||||||
'''Returns the list of elements that can be imported from p_path for
|
'''Returns the list of elements that can be imported from p_path for
|
||||||
p_contentType.'''
|
p_className.'''
|
||||||
appyClass = self.getAppyClass(contentType)
|
appyClass = self.getAppyClass(className)
|
||||||
importParams = self.getCreateMeans(appyClass)['import']
|
importParams = self.getCreateMeans(appyClass)['import']
|
||||||
onElement = importParams['onElement'].__get__('')
|
onElement = importParams['onElement'].__get__('')
|
||||||
sortMethod = importParams['sort']
|
sortMethod = importParams['sort']
|
||||||
if sortMethod: sortMethod = sortMethod.__get__('')
|
if sortMethod: sortMethod = sortMethod.__get__('')
|
||||||
elems = []
|
elems = []
|
||||||
importType = self.getAppyType('importPathFor%s' % contentType)
|
importType = self.getAppyType('importPathFor%s' % className)
|
||||||
importPath = importType.getValue(self)
|
importPath = importType.getValue(self)
|
||||||
for elem in os.listdir(importPath):
|
for elem in os.listdir(importPath):
|
||||||
elemFullPath = os.path.join(importPath, elem)
|
elemFullPath = os.path.join(importPath, elem)
|
||||||
|
@ -339,17 +331,19 @@ class ToolMixin(BaseMixin):
|
||||||
def getAllowedValue(self):
|
def getAllowedValue(self):
|
||||||
'''Gets, for the currently logged user, the value for index
|
'''Gets, for the currently logged user, the value for index
|
||||||
"Allowed".'''
|
"Allowed".'''
|
||||||
|
tool = self.appy()
|
||||||
user = self.getUser()
|
user = self.getUser()
|
||||||
|
rq = tool.request
|
||||||
# Get the user roles
|
# Get the user roles
|
||||||
res = user.getRoles()
|
res = rq.userRoles
|
||||||
# Add role "Anonymous"
|
# Add role "Anonymous"
|
||||||
if 'Anonymous' not in res: res.append('Anonymous')
|
if 'Anonymous' not in res: res.append('Anonymous')
|
||||||
# Add the user id if not anonymous
|
# Add the user id if not anonymous
|
||||||
userId = user.getId()
|
userId = user.login
|
||||||
if userId: res.append('user:%s' % userId)
|
if userId != 'anon': res.append('user:%s' % userId)
|
||||||
# Add group ids
|
# Add group ids
|
||||||
try:
|
try:
|
||||||
res += ['user:%s' % g for g in user.groups.keys()]
|
res += ['user:%s' % g for g in rq.zopeUser.groups.keys()]
|
||||||
except AttributeError, ae:
|
except AttributeError, ae:
|
||||||
pass # The Zope admin does not have this attribute.
|
pass # The Zope admin does not have this attribute.
|
||||||
return res
|
return res
|
||||||
|
@ -434,7 +428,8 @@ class ToolMixin(BaseMixin):
|
||||||
if refField: maxResults = refField.maxPerPage
|
if refField: maxResults = refField.maxPerPage
|
||||||
else: maxResults = self.appy().numberOfResultsPerPage
|
else: maxResults = self.appy().numberOfResultsPerPage
|
||||||
elif maxResults == 'NO_LIMIT': maxResults = None
|
elif maxResults == 'NO_LIMIT': maxResults = None
|
||||||
res = SomeObjects(brains, maxResults, startNumber,noSecurity=noSecurity)
|
res = gutils.SomeObjects(brains, maxResults, startNumber,
|
||||||
|
noSecurity=noSecurity)
|
||||||
res.brainsToObjects()
|
res.brainsToObjects()
|
||||||
# In some cases (p_remember=True), we need to keep some information
|
# In some cases (p_remember=True), we need to keep some information
|
||||||
# about the query results in the current user's session, allowing him
|
# about the query results in the current user's session, allowing him
|
||||||
|
@ -454,15 +449,14 @@ class ToolMixin(BaseMixin):
|
||||||
self.REQUEST.SESSION['search_%s' % searchName] = uids
|
self.REQUEST.SESSION['search_%s' % searchName] = uids
|
||||||
return res.__dict__
|
return res.__dict__
|
||||||
|
|
||||||
def getResultColumnsLayouts(self, contentType, refInfo):
|
def getResultColumnsLayouts(self, className, refInfo):
|
||||||
'''Returns the column layouts for displaying objects of
|
'''Returns the column layouts for displaying objects of
|
||||||
p_contentType.'''
|
p_className.'''
|
||||||
if refInfo[0]:
|
if refInfo[0]:
|
||||||
res = refInfo[0].getAppyType(refInfo[1]).shownInfo
|
return refInfo[0].getAppyType(refInfo[1]).shownInfo
|
||||||
else:
|
else:
|
||||||
toolFieldName = 'resultColumnsFor%s' % contentType
|
toolFieldName = 'resultColumnsFor%s' % className
|
||||||
res = getattr(self.appy(), toolFieldName)
|
return getattr(self.appy(), toolFieldName)
|
||||||
return res
|
|
||||||
|
|
||||||
def truncateValue(self, value, width=15):
|
def truncateValue(self, value, width=15):
|
||||||
'''Truncates the p_value according to p_width.'''
|
'''Truncates the p_value according to p_width.'''
|
||||||
|
@ -487,10 +481,11 @@ class ToolMixin(BaseMixin):
|
||||||
def splitList(self, l, sub):
|
def splitList(self, l, sub):
|
||||||
'''Returns a list made of the same elements as p_l, but grouped into
|
'''Returns a list made of the same elements as p_l, but grouped into
|
||||||
sub-lists of p_sub elements.'''
|
sub-lists of p_sub elements.'''
|
||||||
return splitList(l, sub)
|
return sutils.splitList(l, sub)
|
||||||
|
|
||||||
def quote(self, s):
|
def quote(self, s):
|
||||||
'''Returns the quoted version of p_s.'''
|
'''Returns the quoted version of p_s.'''
|
||||||
|
if not isinstance(s, basestring): s = str(s)
|
||||||
if "'" in s: return '"%s"' % s
|
if "'" in s: return '"%s"' % s
|
||||||
return "'%s'" % s
|
return "'%s'" % s
|
||||||
|
|
||||||
|
@ -500,21 +495,6 @@ class ToolMixin(BaseMixin):
|
||||||
if url.endswith('/view'): return 'view'
|
if url.endswith('/view'): return 'view'
|
||||||
if url.endswith('/edit') or url.endswith('/do'): return 'edit'
|
if url.endswith('/edit') or url.endswith('/do'): return 'edit'
|
||||||
|
|
||||||
def getPublishedObject(self, layoutType):
|
|
||||||
'''Gets the currently published object, if its meta_class is among
|
|
||||||
application classes.'''
|
|
||||||
# In some situations (ie, we are querying objects), the published object
|
|
||||||
# according to Zope is the tool, but we don't want to consider it that
|
|
||||||
# way.
|
|
||||||
if layoutType not in ('edit', 'view'): return
|
|
||||||
obj = self.REQUEST['PUBLISHED']
|
|
||||||
# If URL is a /do, published object is the "do" method.
|
|
||||||
if type(obj) == types.MethodType: obj = obj.im_self
|
|
||||||
else:
|
|
||||||
parent = obj.getParentNode()
|
|
||||||
if parent.id == 'ui': obj = parent.getParentNode()
|
|
||||||
if obj.meta_type in self.getProductConfig().attributes: return obj
|
|
||||||
|
|
||||||
def getZopeClass(self, name):
|
def getZopeClass(self, name):
|
||||||
'''Returns the Zope class whose name is p_name.'''
|
'''Returns the Zope class whose name is p_name.'''
|
||||||
exec 'from Products.%s.%s import %s as C'% (self.getAppName(),name,name)
|
exec 'from Products.%s.%s import %s as C'% (self.getAppName(),name,name)
|
||||||
|
@ -735,7 +715,7 @@ class ToolMixin(BaseMixin):
|
||||||
rq = self.REQUEST
|
rq = self.REQUEST
|
||||||
self.storeSearchCriteria()
|
self.storeSearchCriteria()
|
||||||
# Go to the screen that displays search results
|
# Go to the screen that displays search results
|
||||||
backUrl = '%s/ui/query?className=%s&&search=customSearch' % \
|
backUrl = '%s/query?className=%s&&search=customSearch' % \
|
||||||
(self.absolute_url(), rq['className'])
|
(self.absolute_url(), rq['className'])
|
||||||
return self.goto(backUrl)
|
return self.goto(backUrl)
|
||||||
|
|
||||||
|
@ -747,15 +727,31 @@ class ToolMixin(BaseMixin):
|
||||||
res += 'var %s = "%s";\n' % (msg, self.translate(msg))
|
res += 'var %s = "%s";\n' % (msg, self.translate(msg))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def getColumnsSpecifiers(self, className, columnLayouts, dir):
|
||||||
|
'''Extracts and returns, from a list of p_columnLayouts, info required
|
||||||
|
for displaying columns of field values for instances of p_className,
|
||||||
|
either in a result screen or for a Ref field.'''
|
||||||
|
res = []
|
||||||
|
for info in columnLayouts:
|
||||||
|
fieldName, width, align = ColumnLayout(info).get()
|
||||||
|
align = self.flipLanguageDirection(align, dir)
|
||||||
|
field = self.getAppyType(fieldName, className)
|
||||||
|
if not field:
|
||||||
|
self.log('Field "%s", used in a column specifier, was not ' \
|
||||||
|
'found.' % fieldName, type='warning')
|
||||||
|
else:
|
||||||
|
res.append(Object(field=field, width=width, align=align))
|
||||||
|
return res
|
||||||
|
|
||||||
def getRefInfo(self, refInfo=None):
|
def getRefInfo(self, refInfo=None):
|
||||||
'''When a search is restricted to objects referenced through a Ref
|
'''When a search is restricted to objects referenced through a Ref
|
||||||
field, this method returns information about this reference: the
|
field, this method returns information about this reference: the
|
||||||
source content type and the Ref field (Appy type). If p_refInfo is
|
source class and the Ref field. If p_refInfo is not given, we search
|
||||||
not given, we search it among search criteria in the session.'''
|
it among search criteria in the session.'''
|
||||||
if not refInfo and (self.REQUEST.get('search', None) == 'customSearch'):
|
if not refInfo and (self.REQUEST.get('search', None) == 'customSearch'):
|
||||||
criteria = self.REQUEST.SESSION.get('searchCriteria', None)
|
criteria = self.REQUEST.SESSION.get('searchCriteria', None)
|
||||||
if criteria and criteria.has_key('_ref'): refInfo = criteria['_ref']
|
if criteria and criteria.has_key('_ref'): refInfo = criteria['_ref']
|
||||||
if not refInfo: return (None, None)
|
if not refInfo: return None, None
|
||||||
objectUid, fieldName = refInfo.split(':')
|
objectUid, fieldName = refInfo.split(':')
|
||||||
obj = self.getObject(objectUid)
|
obj = self.getObject(objectUid)
|
||||||
return obj, fieldName
|
return obj, fieldName
|
||||||
|
@ -771,7 +767,7 @@ class ToolMixin(BaseMixin):
|
||||||
res = []
|
res = []
|
||||||
default = None # Also retrieve the default one here.
|
default = None # Also retrieve the default one here.
|
||||||
groups = {} # The already encountered groups
|
groups = {} # The already encountered groups
|
||||||
page = Page('main') # A dummy page required by class GroupDescr
|
page = Page('main') # A dummy page required by class UiGroup
|
||||||
# Get the searches statically defined on the class
|
# Get the searches statically defined on the class
|
||||||
searches = ClassDescriptor.getSearches(appyClass, tool=self.appy())
|
searches = ClassDescriptor.getSearches(appyClass, tool=self.appy())
|
||||||
# Get the dynamically computed searches
|
# Get the dynamically computed searches
|
||||||
|
@ -779,22 +775,21 @@ class ToolMixin(BaseMixin):
|
||||||
searches += appyClass.getDynamicSearches(self.appy())
|
searches += appyClass.getDynamicSearches(self.appy())
|
||||||
for search in searches:
|
for search in searches:
|
||||||
# Create the search descriptor
|
# Create the search descriptor
|
||||||
sDescr = SearchDescr(search, className, self).get()
|
uiSearch = UiSearch(search, className, self)
|
||||||
if not search.group:
|
if not search.group:
|
||||||
# Insert the search at the highest level, not in any group.
|
# Insert the search at the highest level, not in any group.
|
||||||
res.append(sDescr)
|
res.append(uiSearch)
|
||||||
else:
|
else:
|
||||||
gDescr = search.group.insertInto(res, groups, page, className,
|
uiGroup = search.group.insertInto(res, groups, page, className,
|
||||||
forSearch=True)
|
forSearch=True)
|
||||||
GroupDescr.addWidget(gDescr, sDescr)
|
uiGroup.addField(uiSearch)
|
||||||
# Is this search the default search?
|
# Is this search the default search?
|
||||||
if search.default: default = sDescr
|
if search.default: default = uiSearch
|
||||||
return Object(searches=res, default=default).__dict__
|
return Object(searches=res, default=default)
|
||||||
|
|
||||||
def getSearch(self, className, name, descr=False):
|
def getSearch(self, className, name, ui=False):
|
||||||
'''Gets the Search instance (or a SearchDescr instance if p_descr is
|
'''Gets the Search instance (or a UiSearch instance if p_ui is True)
|
||||||
True) corresponding to the search named p_name, on class
|
corresponding to the search named p_name, on class p_className.'''
|
||||||
p_className.'''
|
|
||||||
if name == 'customSearch':
|
if name == 'customSearch':
|
||||||
# It is a custom search whose parameters are in the session.
|
# It is a custom search whose parameters are in the session.
|
||||||
fields = self.REQUEST.SESSION['searchCriteria']
|
fields = self.REQUEST.SESSION['searchCriteria']
|
||||||
|
@ -812,8 +807,8 @@ class ToolMixin(BaseMixin):
|
||||||
else:
|
else:
|
||||||
# It is the search for every instance of p_className
|
# It is the search for every instance of p_className
|
||||||
res = Search('allSearch')
|
res = Search('allSearch')
|
||||||
# Return a SearchDescr if required.
|
# Return a UiSearch if required.
|
||||||
if descr: res = SearchDescr(res, className, self).get()
|
if ui: res = UiSearch(res, className, self)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def advancedSearchEnabledFor(self, className):
|
def advancedSearchEnabledFor(self, className):
|
||||||
|
@ -829,7 +824,7 @@ class ToolMixin(BaseMixin):
|
||||||
'''This method creates the URL that allows to perform a (non-Ajax)
|
'''This method creates the URL that allows to perform a (non-Ajax)
|
||||||
request for getting queried objects from a search named p_searchName
|
request for getting queried objects from a search named p_searchName
|
||||||
on p_contentType.'''
|
on p_contentType.'''
|
||||||
baseUrl = self.absolute_url() + '/ui'
|
baseUrl = self.absolute_url()
|
||||||
baseParams = 'className=%s' % contentType
|
baseParams = 'className=%s' % contentType
|
||||||
rq = self.REQUEST
|
rq = self.REQUEST
|
||||||
if rq.get('ref'): baseParams += '&ref=%s' % rq.get('ref')
|
if rq.get('ref'): baseParams += '&ref=%s' % rq.get('ref')
|
||||||
|
@ -856,13 +851,14 @@ class ToolMixin(BaseMixin):
|
||||||
return startNumber
|
return startNumber
|
||||||
|
|
||||||
def getNavigationInfo(self):
|
def getNavigationInfo(self):
|
||||||
'''Extracts navigation information from request/nav and returns a dict
|
'''Extracts navigation information from request/nav and returns an
|
||||||
with the info that a page can use for displaying object
|
object with the info that a page can use for displaying object
|
||||||
navigation.'''
|
navigation.'''
|
||||||
res = {}
|
res = Object()
|
||||||
t,d1,d2,currentNumber,totalNumber = self.REQUEST.get('nav').split('.')
|
rq = self.REQUEST
|
||||||
res['currentNumber'] = int(currentNumber)
|
t, d1, d2, currentNumber, totalNumber = rq.get('nav').split('.')
|
||||||
res['totalNumber'] = int(totalNumber)
|
res.currentNumber = int(currentNumber)
|
||||||
|
res.totalNumber = int(totalNumber)
|
||||||
# Compute the label of the search, or ref field
|
# Compute the label of the search, or ref field
|
||||||
if t == 'search':
|
if t == 'search':
|
||||||
searchName = d2
|
searchName = d2
|
||||||
|
@ -875,29 +871,28 @@ class ToolMixin(BaseMixin):
|
||||||
else:
|
else:
|
||||||
# This is a named, predefined search.
|
# This is a named, predefined search.
|
||||||
label = '%s_search_%s' % (d1.split(':')[0], searchName)
|
label = '%s_search_%s' % (d1.split(':')[0], searchName)
|
||||||
res['backText'] = self.translate(label)
|
res.backText = self.translate(label)
|
||||||
# If it is a dynamic search this label does not exist.
|
# If it is a dynamic search this label does not exist.
|
||||||
if ('_' in res['backText']): res['backText'] = ''
|
if ('_' in res.backText): res.backText = ''
|
||||||
else:
|
else:
|
||||||
fieldName, pageName = d2.split(':')
|
fieldName, pageName = d2.split(':')
|
||||||
sourceObj = self.getObject(d1)
|
sourceObj = self.getObject(d1)
|
||||||
label = '%s_%s' % (sourceObj.meta_type, fieldName)
|
label = '%s_%s' % (sourceObj.meta_type, fieldName)
|
||||||
res['backText'] = '%s - %s' % (sourceObj.Title(),
|
res.backText = '%s - %s' % (sourceObj.Title(),self.translate(label))
|
||||||
self.translate(label))
|
|
||||||
newNav = '%s.%s.%s.%%d.%s' % (t, d1, d2, totalNumber)
|
newNav = '%s.%s.%s.%%d.%s' % (t, d1, d2, totalNumber)
|
||||||
# Among, first, previous, next and last, which one do I need?
|
# Among, first, previous, next and last, which one do I need?
|
||||||
previousNeeded = False # Previous ?
|
previousNeeded = False # Previous ?
|
||||||
previousIndex = res['currentNumber'] - 2
|
previousIndex = res.currentNumber - 2
|
||||||
if (previousIndex > -1) and (res['totalNumber'] > previousIndex):
|
if (previousIndex > -1) and (res.totalNumber > previousIndex):
|
||||||
previousNeeded = True
|
previousNeeded = True
|
||||||
nextNeeded = False # Next ?
|
nextNeeded = False # Next ?
|
||||||
nextIndex = res['currentNumber']
|
nextIndex = res.currentNumber
|
||||||
if nextIndex < res['totalNumber']: nextNeeded = True
|
if nextIndex < res.totalNumber: nextNeeded = True
|
||||||
firstNeeded = False # First ?
|
firstNeeded = False # First ?
|
||||||
firstIndex = 0
|
firstIndex = 0
|
||||||
if previousIndex > 0: firstNeeded = True
|
if previousIndex > 0: firstNeeded = True
|
||||||
lastNeeded = False # Last ?
|
lastNeeded = False # Last ?
|
||||||
lastIndex = res['totalNumber'] - 1
|
lastIndex = res.totalNumber - 1
|
||||||
if (nextIndex < lastIndex): lastNeeded = True
|
if (nextIndex < lastIndex): lastNeeded = True
|
||||||
# Get the list of available UIDs surrounding the current object
|
# Get the list of available UIDs surrounding the current object
|
||||||
if t == 'ref': # Manage navigation from a reference
|
if t == 'ref': # Manage navigation from a reference
|
||||||
|
@ -908,16 +903,16 @@ class ToolMixin(BaseMixin):
|
||||||
# Display the reference widget at the page where the current object
|
# Display the reference widget at the page where the current object
|
||||||
# lies.
|
# lies.
|
||||||
startNumberKey = '%s%s_startNumber' % (masterObj.UID(), fieldName)
|
startNumberKey = '%s%s_startNumber' % (masterObj.UID(), fieldName)
|
||||||
startNumber = self.computeStartNumberFrom(res['currentNumber']-1,
|
startNumber = self.computeStartNumberFrom(res.currentNumber-1,
|
||||||
res['totalNumber'], batchSize)
|
res.totalNumber, batchSize)
|
||||||
res['sourceUrl'] = masterObj.getUrl(**{startNumberKey:startNumber,
|
res.sourceUrl = masterObj.getUrl(**{startNumberKey:startNumber,
|
||||||
'page':pageName, 'nav':''})
|
'page':pageName, 'nav':''})
|
||||||
else: # Manage navigation from a search
|
else: # Manage navigation from a search
|
||||||
contentType = d1
|
contentType = d1
|
||||||
searchName = keySuffix = d2
|
searchName = keySuffix = d2
|
||||||
batchSize = self.appy().numberOfResultsPerPage
|
batchSize = self.appy().numberOfResultsPerPage
|
||||||
if not searchName: keySuffix = contentType
|
if not searchName: keySuffix = contentType
|
||||||
s = self.REQUEST.SESSION
|
s = rq.SESSION
|
||||||
searchKey = 'search_%s' % keySuffix
|
searchKey = 'search_%s' % keySuffix
|
||||||
if s.has_key(searchKey): uids = s[searchKey]
|
if s.has_key(searchKey): uids = s[searchKey]
|
||||||
else: uids = {}
|
else: uids = {}
|
||||||
|
@ -928,7 +923,7 @@ class ToolMixin(BaseMixin):
|
||||||
# I do not have this UID in session. I will need to
|
# I do not have this UID in session. I will need to
|
||||||
# retrigger the query by querying all objects surrounding
|
# retrigger the query by querying all objects surrounding
|
||||||
# this one.
|
# this one.
|
||||||
newStartNumber = (res['currentNumber']-1) - (batchSize / 2)
|
newStartNumber = (res.currentNumber-1) - (batchSize / 2)
|
||||||
if newStartNumber < 0: newStartNumber = 0
|
if newStartNumber < 0: newStartNumber = 0
|
||||||
self.executeQuery(contentType, searchName=searchName,
|
self.executeQuery(contentType, searchName=searchName,
|
||||||
startNumber=newStartNumber, remember=True)
|
startNumber=newStartNumber, remember=True)
|
||||||
|
@ -938,15 +933,15 @@ class ToolMixin(BaseMixin):
|
||||||
if not uids.has_key(0): firstNeeded = False
|
if not uids.has_key(0): firstNeeded = False
|
||||||
if not uids.has_key(lastIndex): lastNeeded = False
|
if not uids.has_key(lastIndex): lastNeeded = False
|
||||||
# Compute URL of source object
|
# Compute URL of source object
|
||||||
startNumber = self.computeStartNumberFrom(res['currentNumber']-1,
|
startNumber = self.computeStartNumberFrom(res.currentNumber-1,
|
||||||
res['totalNumber'], batchSize)
|
res.totalNumber, batchSize)
|
||||||
res['sourceUrl'] = self.getQueryUrl(contentType, searchName,
|
res.sourceUrl = self.getQueryUrl(contentType, searchName,
|
||||||
startNumber=startNumber)
|
startNumber=startNumber)
|
||||||
# Compute URLs
|
# Compute URLs
|
||||||
for urlType in ('previous', 'next', 'first', 'last'):
|
for urlType in ('previous', 'next', 'first', 'last'):
|
||||||
exec 'needIt = %sNeeded' % urlType
|
exec 'needIt = %sNeeded' % urlType
|
||||||
urlKey = '%sUrl' % urlType
|
urlKey = '%sUrl' % urlType
|
||||||
res[urlKey] = None
|
setattr(res, urlKey, None)
|
||||||
if needIt:
|
if needIt:
|
||||||
exec 'index = %sIndex' % urlType
|
exec 'index = %sIndex' % urlType
|
||||||
uid = None
|
uid = None
|
||||||
|
@ -959,37 +954,38 @@ class ToolMixin(BaseMixin):
|
||||||
brain = self.getObject(uid, brain=True)
|
brain = self.getObject(uid, brain=True)
|
||||||
if brain:
|
if brain:
|
||||||
sibling = brain.getObject()
|
sibling = brain.getObject()
|
||||||
res[urlKey] = sibling.getUrl(nav=newNav % (index + 1),
|
setattr(res, urlKey, sibling.getUrl(\
|
||||||
page=self.REQUEST.get('page', 'main'))
|
nav=newNav % (index + 1),
|
||||||
|
page=rq.get('page', 'main')))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def getGroupedSearchFields(self, searchInfo):
|
def getGroupedSearchFields(self, searchInfo):
|
||||||
'''This method transforms p_searchInfo['fieldDicts'], which is a "flat"
|
'''This method transforms p_searchInfo.fields, which is a "flat"
|
||||||
list of fields, into a list of lists, where every sub-list having
|
list of fields, into a list of lists, where every sub-list having
|
||||||
length p_searchInfo['nbOfColumns']. For every field, scolspan
|
length p_searchInfo.nbOfColumns. For every field, scolspan
|
||||||
(=colspan "for search") is taken into account.'''
|
(=colspan "for search") is taken into account.'''
|
||||||
res = []
|
res = []
|
||||||
row = []
|
row = []
|
||||||
rowLength = 0
|
rowLength = 0
|
||||||
for field in searchInfo['fieldDicts']:
|
for field in searchInfo.fields:
|
||||||
# Can I insert this field in the current row?
|
# Can I insert this field in the current row?
|
||||||
remaining = searchInfo['nbOfColumns'] - rowLength
|
remaining = searchInfo.nbOfColumns - rowLength
|
||||||
if field['scolspan'] <= remaining:
|
if field.scolspan <= remaining:
|
||||||
# Yes.
|
# Yes.
|
||||||
row.append(field)
|
row.append(field)
|
||||||
rowLength += field['scolspan']
|
rowLength += field.scolspan
|
||||||
else:
|
else:
|
||||||
# We must put the field on a new line. Complete the current one
|
# We must put the field on a new line. Complete the current one
|
||||||
# if not complete.
|
# if not complete.
|
||||||
while rowLength < searchInfo['nbOfColumns']:
|
while rowLength < searchInfo.nbOfColumns:
|
||||||
row.append(None)
|
row.append(None)
|
||||||
rowLength += 1
|
rowLength += 1
|
||||||
res.append(row)
|
res.append(row)
|
||||||
row = [field]
|
row = [field]
|
||||||
rowLength = field['scolspan']
|
rowLength = field.scolspan
|
||||||
# Complete the last unfinished line if required.
|
# Complete the last unfinished line if required.
|
||||||
if row:
|
if row:
|
||||||
while rowLength < searchInfo['nbOfColumns']:
|
while rowLength < searchInfo.nbOfColumns:
|
||||||
row.append(None)
|
row.append(None)
|
||||||
rowLength += 1
|
rowLength += 1
|
||||||
res.append(row)
|
res.append(row)
|
||||||
|
@ -998,11 +994,6 @@ class ToolMixin(BaseMixin):
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# Authentication-related methods
|
# Authentication-related methods
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
def _updateCookie(self, login, password):
|
|
||||||
cookieValue = base64.encodestring('%s:%s' % (login, password)).rstrip()
|
|
||||||
cookieValue = urllib.quote(cookieValue)
|
|
||||||
self.REQUEST.RESPONSE.setCookie('__ac', cookieValue, path='/')
|
|
||||||
|
|
||||||
def _encryptPassword(self, password):
|
def _encryptPassword(self, password):
|
||||||
'''Returns the encrypted version of clear p_password.'''
|
'''Returns the encrypted version of clear p_password.'''
|
||||||
return self.acl_users._encryptPassword(password)
|
return self.acl_users._encryptPassword(password)
|
||||||
|
@ -1011,7 +1002,7 @@ class ToolMixin(BaseMixin):
|
||||||
'''Performs the Zope-level authentication. Returns True if
|
'''Performs the Zope-level authentication. Returns True if
|
||||||
authentication succeeds.'''
|
authentication succeeds.'''
|
||||||
user = self.acl_users.validate(request)
|
user = self.acl_users.validate(request)
|
||||||
return not self.userIsAnon()
|
return user.getUserName() != 'Anonymous User'
|
||||||
|
|
||||||
def _ldapAuthenticate(self, login, password):
|
def _ldapAuthenticate(self, login, password):
|
||||||
'''Performs a LDAP-based authentication. Returns True if authentication
|
'''Performs a LDAP-based authentication. Returns True if authentication
|
||||||
|
@ -1032,16 +1023,16 @@ class ToolMixin(BaseMixin):
|
||||||
if jsEnabled and not cookiesEnabled:
|
if jsEnabled and not cookiesEnabled:
|
||||||
msg = self.translate('enable_cookies')
|
msg = self.translate('enable_cookies')
|
||||||
return self.goto(urlBack, msg)
|
return self.goto(urlBack, msg)
|
||||||
# Extract the login and password
|
# Extract the login and password, and create an authentication cookie
|
||||||
login = rq.get('__ac_name', '')
|
login = rq.get('__ac_name', '')
|
||||||
password = rq.get('__ac_password', '')
|
password = rq.get('__ac_password', '')
|
||||||
|
gutils.writeCookie(login, password, rq)
|
||||||
# Perform the Zope-level authentication
|
# Perform the Zope-level authentication
|
||||||
self._updateCookie(login, password)
|
|
||||||
if self._zopeAuthenticate(rq) or self._ldapAuthenticate(login,password):
|
if self._zopeAuthenticate(rq) or self._ldapAuthenticate(login,password):
|
||||||
msg = self.translate('login_ok')
|
msg = self.translate('login_ok')
|
||||||
logMsg = 'User "%s" logged in.' % login
|
logMsg = 'User "%s" logged in.' % login
|
||||||
else:
|
else:
|
||||||
rq.RESPONSE.expireCookie('__ac', path='/')
|
rq.RESPONSE.expireCookie('_appy_', path='/')
|
||||||
msg = self.translate('login_ko')
|
msg = self.translate('login_ko')
|
||||||
logMsg = 'Authentication failed with login "%s".' % login
|
logMsg = 'Authentication failed with login "%s".' % login
|
||||||
self.log(logMsg)
|
self.log(logMsg)
|
||||||
|
@ -1050,9 +1041,9 @@ class ToolMixin(BaseMixin):
|
||||||
def performLogout(self):
|
def performLogout(self):
|
||||||
'''Logs out the current user when he clicks on "disconnect".'''
|
'''Logs out the current user when he clicks on "disconnect".'''
|
||||||
rq = self.REQUEST
|
rq = self.REQUEST
|
||||||
userId = self.getUser().getId()
|
userId = self.getUser().login
|
||||||
# Perform the logout in acl_users
|
# Perform the logout in acl_users
|
||||||
rq.RESPONSE.expireCookie('__ac', path='/')
|
rq.RESPONSE.expireCookie('_appy_', path='/')
|
||||||
# Invalidate session.
|
# Invalidate session.
|
||||||
try:
|
try:
|
||||||
sdm = self.session_data_manager
|
sdm = self.session_data_manager
|
||||||
|
@ -1083,18 +1074,12 @@ class ToolMixin(BaseMixin):
|
||||||
login, password = self.identify(auth)
|
login, password = self.identify(auth)
|
||||||
if not login:
|
if not login:
|
||||||
# Try to get them from a cookie
|
# Try to get them from a cookie
|
||||||
cookie = request.get('__ac', None)
|
login, password = gutils.readCookie(request)
|
||||||
|
if not login:
|
||||||
|
# Maybe the user just entered his credentials. The cookie could
|
||||||
|
# have been set in the response, but is not in the request.
|
||||||
login = request.get('__ac_name', None)
|
login = request.get('__ac_name', None)
|
||||||
if login and request.form.has_key('__ac_password'):
|
password = request.get('__ac_password', None)
|
||||||
# The user just entered his credentials. The cookie has not been
|
|
||||||
# set yet (it will come in the upcoming HTTP response when the
|
|
||||||
# current request will be served).
|
|
||||||
login = request.get('__ac_name', '')
|
|
||||||
password = request.get('__ac_password', '')
|
|
||||||
elif cookie and (cookie != 'deleted'):
|
|
||||||
cookieValue = base64.decodestring(urllib.unquote(cookie))
|
|
||||||
if ':' in cookieValue:
|
|
||||||
login, password = cookieValue.split(':')
|
|
||||||
# Try to authenticate this user
|
# Try to authenticate this user
|
||||||
user = self.authenticate(login, password, request)
|
user = self.authenticate(login, password, request)
|
||||||
emergency = self._emergency_user
|
emergency = self._emergency_user
|
||||||
|
@ -1123,11 +1108,82 @@ class ToolMixin(BaseMixin):
|
||||||
from AccessControl.User import BasicUserFolder
|
from AccessControl.User import BasicUserFolder
|
||||||
BasicUserFolder.validate = validate
|
BasicUserFolder.validate = validate
|
||||||
|
|
||||||
|
def getUser(self):
|
||||||
|
'''Gets the User instance (Appy wrapper) corresponding to the current
|
||||||
|
user.'''
|
||||||
|
tool = self.appy()
|
||||||
|
rq = tool.request
|
||||||
|
# Try first to return the user that can be cached on the request.
|
||||||
|
if hasattr(rq, 'user'): return rq.user
|
||||||
|
# Get the user login from the authentication cookie.
|
||||||
|
login, password = gutils.readCookie(rq)
|
||||||
|
if not login: # It is the anonymous user or the system.
|
||||||
|
# If we have a real request object, it is the anonymous user.
|
||||||
|
login = (rq.__class__.__name__ == 'Object') and 'system' or 'anon'
|
||||||
|
# Get the User object from a query in the catalog.
|
||||||
|
user = tool.search1('User', noSecurity=True, login=login)
|
||||||
|
rq.user = user
|
||||||
|
# Precompute some values or this usser for performance reasons
|
||||||
|
rq.userRoles = user.getRoles()
|
||||||
|
rq.zopeUser = user.getZopeUser()
|
||||||
|
return user
|
||||||
|
#from AccessControl import getSecurityManager
|
||||||
|
#user = getSecurityManager().getUser()
|
||||||
|
#if not user:
|
||||||
|
# from AccessControl.User import nobody
|
||||||
|
# return nobody
|
||||||
|
#return user
|
||||||
|
|
||||||
|
def getUserLine(self):
|
||||||
|
'''Returns a info about the currently logged user as a 2-tuple: first
|
||||||
|
elem is the one-line user info as shown on every page; second line is
|
||||||
|
the URL to edit user info.'''
|
||||||
|
user = self.getUser()
|
||||||
|
userRoles = self.appy().request.userRoles
|
||||||
|
info = [user.title]
|
||||||
|
rolesToShow = [r for r in userRoles if r != 'Authenticated']
|
||||||
|
if rolesToShow:
|
||||||
|
info.append(', '.join([self.translate('role_%s' % r) \
|
||||||
|
for r in rolesToShow]))
|
||||||
|
# Edit URL for the user.
|
||||||
|
url = None
|
||||||
|
if user.o.mayEdit():
|
||||||
|
url = user.o.getUrl(mode='edit', page='main', nav='')
|
||||||
|
return (' | '.join(info), url)
|
||||||
|
|
||||||
|
def getUserName(self, login=None, normalized=False):
|
||||||
|
'''Gets the user name corresponding to p_login (or the currently logged
|
||||||
|
login if None), or the p_login itself if the user does not exist
|
||||||
|
anymore. If p_normalized is True, special chars in the first and last
|
||||||
|
names are normalized.'''
|
||||||
|
tool = self.appy()
|
||||||
|
if not login: login = tool.user.login
|
||||||
|
# Manage the special case of an anonymous user.
|
||||||
|
if login == 'Anonymous User':
|
||||||
|
name = self.translate('anonymous')
|
||||||
|
if normalized: name = sutils.normalizeString(name)
|
||||||
|
return name
|
||||||
|
# Manage the case of a "real" user.
|
||||||
|
user = tool.search1('User', noSecurity=True, login=login)
|
||||||
|
if not user: return login
|
||||||
|
firstName = user.firstName
|
||||||
|
name = user.name
|
||||||
|
res = ''
|
||||||
|
if firstName:
|
||||||
|
if normalized: firstName = sutils.normalizeString(firstName)
|
||||||
|
res += firstName
|
||||||
|
if name:
|
||||||
|
if normalized: name = sutils.normalizeString(name)
|
||||||
|
if res: res += ' ' + name
|
||||||
|
else: res = name
|
||||||
|
if not res: res = login
|
||||||
|
return res
|
||||||
|
|
||||||
def tempFile(self):
|
def tempFile(self):
|
||||||
'''A temp file has been created in a temp folder. This method returns
|
'''A temp file has been created in a temp folder. This method returns
|
||||||
this file to the browser.'''
|
this file to the browser.'''
|
||||||
rq = self.REQUEST
|
rq = self.REQUEST
|
||||||
baseFolder = os.path.join(getOsTempFolder(), self.getAppName())
|
baseFolder = os.path.join(sutils.getOsTempFolder(), self.getAppName())
|
||||||
baseFolder = os.path.join(baseFolder, rq.SESSION.id)
|
baseFolder = os.path.join(baseFolder, rq.SESSION.id)
|
||||||
fileName = os.path.join(baseFolder, rq.get('name', ''))
|
fileName = os.path.join(baseFolder, rq.get('name', ''))
|
||||||
if os.path.exists(fileName):
|
if os.path.exists(fileName):
|
||||||
|
@ -1146,52 +1202,6 @@ class ToolMixin(BaseMixin):
|
||||||
if ',' in contentType: return ()
|
if ',' in contentType: return ()
|
||||||
return [f.__dict__ for f in self.getAllAppyTypes(contentType) \
|
return [f.__dict__ for f in self.getAllAppyTypes(contentType) \
|
||||||
if (f.type == 'Pod') and (f.show == 'result')]
|
if (f.type == 'Pod') and (f.show == 'result')]
|
||||||
|
|
||||||
def getUserLine(self):
|
|
||||||
'''Returns a info about the currently logged user as a 2-tuple: first
|
|
||||||
elem is the one-line user info as shown on every page; second line is
|
|
||||||
the URL to edit user info.'''
|
|
||||||
appyUser = self.appy().appyUser
|
|
||||||
info = [appyUser.title]
|
|
||||||
rolesToShow = [r for r in appyUser.roles \
|
|
||||||
if r not in ('Authenticated', 'Member')]
|
|
||||||
if rolesToShow:
|
|
||||||
info.append(', '.join([self.translate('role_%s'%r) \
|
|
||||||
for r in rolesToShow]))
|
|
||||||
# Edit URL for the appy user.
|
|
||||||
url = None
|
|
||||||
if appyUser.o.mayEdit():
|
|
||||||
url = appyUser.o.getUrl(mode='edit', page='main', nav='')
|
|
||||||
return (' | '.join(info), url)
|
|
||||||
|
|
||||||
def getUserName(self, login=None, normalized=False):
|
|
||||||
'''Gets the user name corresponding to p_login (or the currently logged
|
|
||||||
login if None), or the p_login itself if the user does not exist
|
|
||||||
anymore. If p_normalized is True, special chars in the first and last
|
|
||||||
names are normalized.'''
|
|
||||||
tool = self.appy()
|
|
||||||
if not login: login = tool.user.getId()
|
|
||||||
# Manage the special case of an anonymous user.
|
|
||||||
if login == 'Anonymous User':
|
|
||||||
name = self.translate('anonymous')
|
|
||||||
if normalized: name = normalizeString(name)
|
|
||||||
return name
|
|
||||||
# Manage the case of a "real" user.
|
|
||||||
user = tool.search1('User', noSecurity=True, login=login)
|
|
||||||
if not user: return login
|
|
||||||
firstName = user.firstName
|
|
||||||
name = user.name
|
|
||||||
res = ''
|
|
||||||
if firstName:
|
|
||||||
if normalized: firstName = normalizeString(firstName)
|
|
||||||
res += firstName
|
|
||||||
if name:
|
|
||||||
if normalized: name = normalizeString(name)
|
|
||||||
if res: res += ' ' + name
|
|
||||||
else: res = name
|
|
||||||
if not res: res = login
|
|
||||||
return res
|
|
||||||
|
|
||||||
def formatDate(self, aDate, withHour=True):
|
def formatDate(self, aDate, withHour=True):
|
||||||
'''Returns aDate formatted as specified by tool.dateFormat.
|
'''Returns aDate formatted as specified by tool.dateFormat.
|
||||||
If p_withHour is True, hour is appended, with a format specified
|
If p_withHour is True, hour is appended, with a format specified
|
||||||
|
@ -1216,7 +1226,7 @@ class ToolMixin(BaseMixin):
|
||||||
htmlMessage = '<a href="%s"><img src="%s/ui/home.gif"/></a>' \
|
htmlMessage = '<a href="%s"><img src="%s/ui/home.gif"/></a>' \
|
||||||
'You are not allowed to access this page.' % \
|
'You are not allowed to access this page.' % \
|
||||||
(siteUrl, siteUrl)
|
(siteUrl, siteUrl)
|
||||||
userId = self.appy().user.getId() or 'system|anon'
|
userId = self.appy().user.login
|
||||||
textMessage = 'Unauthorized for %s @%s.' % \
|
textMessage = 'Unauthorized for %s @%s.' % \
|
||||||
(userId, self.REQUEST.get('PATH_INFO'))
|
(userId, self.REQUEST.get('PATH_INFO'))
|
||||||
else:
|
else:
|
||||||
|
@ -1256,7 +1266,7 @@ class ToolMixin(BaseMixin):
|
||||||
return self.goto(backUrl, msg)
|
return self.goto(backUrl, msg)
|
||||||
# Create a temporary file whose name is the user login and whose
|
# Create a temporary file whose name is the user login and whose
|
||||||
# content is a generated token.
|
# content is a generated token.
|
||||||
f = file(os.path.join(getOsTempFolder(), login), 'w')
|
f = file(os.path.join(sutils.getOsTempFolder(), login), 'w')
|
||||||
token = String().generatePassword()
|
token = String().generatePassword()
|
||||||
f.write(token)
|
f.write(token)
|
||||||
f.close()
|
f.close()
|
||||||
|
@ -1277,7 +1287,7 @@ class ToolMixin(BaseMixin):
|
||||||
# Check if such token exists in temp folder
|
# Check if such token exists in temp folder
|
||||||
res = None
|
res = None
|
||||||
siteUrl = self.getSiteUrl()
|
siteUrl = self.getSiteUrl()
|
||||||
tokenFile = os.path.join(getOsTempFolder(), login)
|
tokenFile = os.path.join(sutils.getOsTempFolder(), login)
|
||||||
if os.path.exists(tokenFile):
|
if os.path.exists(tokenFile):
|
||||||
f = file(tokenFile)
|
f = file(tokenFile)
|
||||||
storedToken = f.read()
|
storedToken = f.read()
|
||||||
|
|
|
@ -7,7 +7,7 @@ import os, os.path, sys, types, urllib, cgi
|
||||||
from appy import Object
|
from appy import Object
|
||||||
import appy.gen as gen
|
import appy.gen as gen
|
||||||
from appy.gen.utils import *
|
from appy.gen.utils import *
|
||||||
from appy.gen.layout import Table, defaultPageLayouts, ColumnLayout
|
from appy.gen.layout import Table, defaultPageLayouts
|
||||||
from appy.gen.descriptors import WorkflowDescriptor, ClassDescriptor
|
from appy.gen.descriptors import WorkflowDescriptor, ClassDescriptor
|
||||||
from appy.shared.utils import sequenceTypes,normalizeText,Traceback,getMimeType
|
from appy.shared.utils import sequenceTypes,normalizeText,Traceback,getMimeType
|
||||||
from appy.shared.data import rtlLanguages
|
from appy.shared.data import rtlLanguages
|
||||||
|
@ -169,7 +169,7 @@ class BaseMixin:
|
||||||
self.workflow_history[key] = tuple(history)
|
self.workflow_history[key] = tuple(history)
|
||||||
appy = self.appy()
|
appy = self.appy()
|
||||||
self.log('Data change event deleted by %s for %s (UID=%s).' % \
|
self.log('Data change event deleted by %s for %s (UID=%s).' % \
|
||||||
(appy.user.getId(), appy.klass.__name__, appy.uid))
|
(appy.user.login, appy.klass.__name__, appy.uid))
|
||||||
self.goto(self.getUrl(rq['HTTP_REFERER']))
|
self.goto(self.getUrl(rq['HTTP_REFERER']))
|
||||||
|
|
||||||
def onUnlink(self):
|
def onUnlink(self):
|
||||||
|
@ -213,13 +213,19 @@ class BaseMixin:
|
||||||
|
|
||||||
def view(self):
|
def view(self):
|
||||||
'''Returns the view PX.'''
|
'''Returns the view PX.'''
|
||||||
appySelf = self.appy()
|
obj = self.appy()
|
||||||
return appySelf.pxView({'self': appySelf})
|
return obj.pxView({'obj': obj, 'tool': obj.tool})
|
||||||
|
|
||||||
def edit(self):
|
def edit(self):
|
||||||
'''Returns the edit PX.'''
|
'''Returns the edit PX.'''
|
||||||
appySelf = self.appy()
|
obj = self.appy()
|
||||||
return appySelf.pxEdit({'self': appySelf})
|
return obj.pxEdit({'obj': obj, 'tool': obj.tool})
|
||||||
|
|
||||||
|
def ajax(self):
|
||||||
|
'''Called via an Ajax request to render some PX whose name is in the
|
||||||
|
request.'''
|
||||||
|
obj = self.appy()
|
||||||
|
return obj.pxAjax({'obj': obj, 'tool': obj.tool})
|
||||||
|
|
||||||
def setLock(self, user, page):
|
def setLock(self, user, page):
|
||||||
'''A p_user edits a given p_page on this object: we will set a lock, to
|
'''A p_user edits a given p_page on this object: we will set a lock, to
|
||||||
|
@ -232,7 +238,7 @@ class BaseMixin:
|
||||||
# Raise an error is the page is already locked by someone else. If the
|
# Raise an error is the page is already locked by someone else. If the
|
||||||
# page is already locked by the same user, we don't mind: he could have
|
# page is already locked by the same user, we don't mind: he could have
|
||||||
# used back/forward buttons of its browser...
|
# used back/forward buttons of its browser...
|
||||||
userId = user.getId()
|
userId = user.login
|
||||||
if (page in self.locks) and (userId != self.locks[page][0]):
|
if (page in self.locks) and (userId != self.locks[page][0]):
|
||||||
from AccessControl import Unauthorized
|
from AccessControl import Unauthorized
|
||||||
raise Unauthorized('This page is locked.')
|
raise Unauthorized('This page is locked.')
|
||||||
|
@ -245,7 +251,7 @@ class BaseMixin:
|
||||||
mind and consider the page as unlocked. If the page is locked, this
|
mind and consider the page as unlocked. If the page is locked, this
|
||||||
method returns the tuple (userId, lockDate).'''
|
method returns the tuple (userId, lockDate).'''
|
||||||
if hasattr(self.aq_base, 'locks') and (page in self.locks):
|
if hasattr(self.aq_base, 'locks') and (page in self.locks):
|
||||||
if (user.getId() != self.locks[page][0]): return self.locks[page]
|
if (user.login != self.locks[page][0]): return self.locks[page]
|
||||||
|
|
||||||
def removeLock(self, page, force=False):
|
def removeLock(self, page, force=False):
|
||||||
'''Removes the lock on the current page. This happens:
|
'''Removes the lock on the current page. This happens:
|
||||||
|
@ -257,7 +263,7 @@ class BaseMixin:
|
||||||
# Raise an error if the user that saves changes is not the one that
|
# Raise an error if the user that saves changes is not the one that
|
||||||
# has locked the page (excepted if p_force is True)
|
# has locked the page (excepted if p_force is True)
|
||||||
if not force:
|
if not force:
|
||||||
userId = self.getUser().getId()
|
userId = self.getTool().getUser().login
|
||||||
if self.locks[page][0] != userId:
|
if self.locks[page][0] != userId:
|
||||||
from AccessControl import Unauthorized
|
from AccessControl import Unauthorized
|
||||||
raise Unauthorized('This page was locked by someone else.')
|
raise Unauthorized('This page was locked by someone else.')
|
||||||
|
@ -270,7 +276,7 @@ class BaseMixin:
|
||||||
view.pt for this page. In this case, we consider that the user has
|
view.pt for this page. In this case, we consider that the user has
|
||||||
left the edit page in an unexpected way and we remove the lock.'''
|
left the edit page in an unexpected way and we remove the lock.'''
|
||||||
if hasattr(self.aq_base, 'locks') and (page in self.locks) and \
|
if hasattr(self.aq_base, 'locks') and (page in self.locks) and \
|
||||||
(user.getId() == self.locks[page][0]):
|
(user.login == self.locks[page][0]):
|
||||||
del self.locks[page]
|
del self.locks[page]
|
||||||
|
|
||||||
def onUnlock(self):
|
def onUnlock(self):
|
||||||
|
@ -420,11 +426,11 @@ class BaseMixin:
|
||||||
# previous pages may have changed). Moreover, previous and next
|
# previous pages may have changed). Moreover, previous and next
|
||||||
# pages may not be available in "edit" mode, so we return the edit
|
# pages may not be available in "edit" mode, so we return the edit
|
||||||
# or view pages depending on page.show.
|
# or view pages depending on page.show.
|
||||||
phaseInfo = self.getAppyPhases(currentOnly=True, layoutType='edit')
|
phaseObj = self.getAppyPhases(currentOnly=True, layoutType='edit')
|
||||||
pageName, pageInfo = self.getPreviousPage(phaseInfo, rq['page'])
|
pageName, pageInfo = phaseObj.getPreviousPage(rq['page'])
|
||||||
if pageName:
|
if pageName:
|
||||||
# Return to the edit or view page?
|
# Return to the edit or view page?
|
||||||
if pageInfo['showOnEdit']:
|
if pageInfo.showOnEdit:
|
||||||
rq.set('page', pageName)
|
rq.set('page', pageName)
|
||||||
# I do not use gotoEdit here because I really need to
|
# I do not use gotoEdit here because I really need to
|
||||||
# redirect the user to the edit page. Indeed, the object
|
# redirect the user to the edit page. Indeed, the object
|
||||||
|
@ -440,11 +446,11 @@ class BaseMixin:
|
||||||
# We remember page name, because the next method may set a new
|
# We remember page name, because the next method may set a new
|
||||||
# current page if the current one is not visible anymore.
|
# current page if the current one is not visible anymore.
|
||||||
pageName = rq['page']
|
pageName = rq['page']
|
||||||
phaseInfo = self.getAppyPhases(currentOnly=True, layoutType='edit')
|
phaseObj = self.getAppyPhases(currentOnly=True, layoutType='edit')
|
||||||
pageName, pageInfo = self.getNextPage(phaseInfo, pageName)
|
pageName, pageInfo = phaseObj.getNextPage(pageName)
|
||||||
if pageName:
|
if pageName:
|
||||||
# Return to the edit or view page?
|
# Return to the edit or view page?
|
||||||
if pageInfo['showOnEdit']:
|
if pageInfo.showOnEdit:
|
||||||
# Same remark as above (click on "previous").
|
# Same remark as above (click on "previous").
|
||||||
return self.goto(obj.getUrl(mode='edit', page=pageName))
|
return self.goto(obj.getUrl(mode='edit', page=pageName))
|
||||||
else:
|
else:
|
||||||
|
@ -543,7 +549,8 @@ class BaseMixin:
|
||||||
|
|
||||||
def addHistoryEvent(self, action, **kw):
|
def addHistoryEvent(self, action, **kw):
|
||||||
'''Adds an event in the object history.'''
|
'''Adds an event in the object history.'''
|
||||||
userId = self.getUser().getId()
|
user = self.getTool().getUser()
|
||||||
|
userId = user and user.login or 'system'
|
||||||
from DateTime import DateTime
|
from DateTime import DateTime
|
||||||
event = {'action': action, 'actor': userId, 'time': DateTime(),
|
event = {'action': action, 'actor': userId, 'time': DateTime(),
|
||||||
'comments': ''}
|
'comments': ''}
|
||||||
|
@ -603,13 +610,13 @@ class BaseMixin:
|
||||||
def gotoEdit(self):
|
def gotoEdit(self):
|
||||||
'''Brings the user to the edit page for this object. This method takes
|
'''Brings the user to the edit page for this object. This method takes
|
||||||
care of not carrying any password value. Unlike m_goto above, there
|
care of not carrying any password value. Unlike m_goto above, there
|
||||||
is no HTTP redirect here: we execute directly macro "edit" and we
|
is no HTTP redirect here: we execute directly PX "edit" and we
|
||||||
return the result.'''
|
return the result.'''
|
||||||
page = self.REQUEST.get('page', 'main')
|
page = self.REQUEST.get('page', 'main')
|
||||||
for field in self.getAppyTypes('edit', page):
|
for field in self.getAppyTypes('edit', page):
|
||||||
if (field.type == 'String') and (field.format in (3,4)):
|
if (field.type == 'String') and (field.format in (3,4)):
|
||||||
self.REQUEST.set(field.name, '')
|
self.REQUEST.set(field.name, '')
|
||||||
return self.ui.edit(self)
|
return self.edit()
|
||||||
|
|
||||||
def showField(self, name, layoutType='view'):
|
def showField(self, name, layoutType='view'):
|
||||||
'''Must I show field named p_name on this p_layoutType ?'''
|
'''Must I show field named p_name on this p_layoutType ?'''
|
||||||
|
@ -755,7 +762,7 @@ class BaseMixin:
|
||||||
res.update(parent)
|
res.update(parent)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def getAppyType(self, name, asDict=False, className=None):
|
def getAppyType(self, name, className=None):
|
||||||
'''Returns the Appy type named p_name. If no p_className is defined, the
|
'''Returns the Appy type named p_name. If no p_className is defined, the
|
||||||
field is supposed to belong to self's class.'''
|
field is supposed to belong to self's class.'''
|
||||||
isInnerType = '*' in name # An inner type lies within a List type.
|
isInnerType = '*' in name # An inner type lies within a List type.
|
||||||
|
@ -770,7 +777,6 @@ class BaseMixin:
|
||||||
klass = self.getTool().getAppyClass(className, wrapper=True)
|
klass = self.getTool().getAppyClass(className, wrapper=True)
|
||||||
res = getattr(klass, name, None)
|
res = getattr(klass, name, None)
|
||||||
if res and isInnerType: res = res.getField(subName)
|
if res and isInnerType: res = res.getField(subName)
|
||||||
if res and asDict: return res.__dict__
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def getAllAppyTypes(self, className=None):
|
def getAllAppyTypes(self, className=None):
|
||||||
|
@ -782,40 +788,39 @@ class BaseMixin:
|
||||||
klass = self.getTool().getAppyClass(className, wrapper=True)
|
klass = self.getTool().getAppyClass(className, wrapper=True)
|
||||||
return klass.__fields__
|
return klass.__fields__
|
||||||
|
|
||||||
def getGroupedAppyTypes(self, layoutType, pageName, cssJs=None):
|
def getGroupedFields(self, layoutType, pageName, cssJs=None):
|
||||||
'''Returns the fields sorted by group. For every field, the appyType
|
'''Returns the fields sorted by group. If a dict is given in p_cssJs,
|
||||||
(dict version) is given. If a dict is given in p_cssJs, we will add
|
we will add it in the css and js files required by the fields.'''
|
||||||
it in the css and js files required by the fields.'''
|
|
||||||
res = []
|
res = []
|
||||||
groups = {} # The already encountered groups
|
groups = {} # The already encountered groups.
|
||||||
# If a dict is given in p_cssJs, we must fill it with the CSS and JS
|
# If a dict is given in p_cssJs, we must fill it with the CSS and JS
|
||||||
# files required for every returned appyType.
|
# files required for every returned field.
|
||||||
collectCssJs = isinstance(cssJs, dict)
|
collectCssJs = isinstance(cssJs, dict)
|
||||||
css = js = None
|
css = js = None
|
||||||
# If param "refresh" is there, we must reload the Python class
|
# If param "refresh" is there, we must reload the Python class
|
||||||
refresh = ('refresh' in self.REQUEST)
|
refresh = ('refresh' in self.REQUEST)
|
||||||
if refresh:
|
if refresh:
|
||||||
klass = self.getClass(reloaded=True)
|
klass = self.getClass(reloaded=True)
|
||||||
for appyType in self.getAllAppyTypes():
|
for field in self.getAllAppyTypes():
|
||||||
if refresh: appyType = appyType.reload(klass, self)
|
if refresh: field = field.reload(klass, self)
|
||||||
if appyType.page.name != pageName: continue
|
if field.page.name != pageName: continue
|
||||||
if not appyType.isShowable(self, layoutType): continue
|
if not field.isShowable(self, layoutType): continue
|
||||||
if collectCssJs:
|
if collectCssJs:
|
||||||
if css == None: css = []
|
if css == None: css = []
|
||||||
appyType.getCss(layoutType, css)
|
field.getCss(layoutType, css)
|
||||||
if js == None: js = []
|
if js == None: js = []
|
||||||
appyType.getJs(layoutType, js)
|
field.getJs(layoutType, js)
|
||||||
if not appyType.group:
|
if not field.group:
|
||||||
res.append(appyType.__dict__)
|
res.append(field)
|
||||||
else:
|
else:
|
||||||
# Insert the GroupDescr instance corresponding to
|
# Insert the UiGroup instance corresponding to field.group at
|
||||||
# appyType.group at the right place
|
# the right place.
|
||||||
groupDescr = appyType.group.insertInto(res, groups,
|
uiGroup = field.group.insertInto(res, groups, field.page,
|
||||||
appyType.page, self.meta_type)
|
self.meta_type)
|
||||||
GroupDescr.addWidget(groupDescr, appyType.__dict__)
|
uiGroup.addField(field)
|
||||||
if collectCssJs:
|
if collectCssJs:
|
||||||
cssJs['css'] = css
|
cssJs['css'] = css or ()
|
||||||
cssJs['js'] = js
|
cssJs['js'] = js or ()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def getAppyTypes(self, layoutType, pageName):
|
def getAppyTypes(self, layoutType, pageName):
|
||||||
|
@ -852,23 +857,6 @@ class BaseMixin:
|
||||||
return klass.styles[elem]
|
return klass.styles[elem]
|
||||||
return elem
|
return elem
|
||||||
|
|
||||||
def getColumnsSpecifiers(self, columnLayouts, dir):
|
|
||||||
'''Extracts and returns, from a list of p_columnLayouts, the information
|
|
||||||
that is necessary for displaying a column in a result screen or for
|
|
||||||
a Ref field.'''
|
|
||||||
res = []
|
|
||||||
tool = self.getTool()
|
|
||||||
for info in columnLayouts:
|
|
||||||
fieldName, width, align = ColumnLayout(info).get()
|
|
||||||
align = tool.flipLanguageDirection(align, dir)
|
|
||||||
field = self.getAppyType(fieldName, asDict=True)
|
|
||||||
if not field:
|
|
||||||
self.log('Field "%s", used in a column specifier, was not ' \
|
|
||||||
'found.' % fieldName, type='warning')
|
|
||||||
else:
|
|
||||||
res.append({'field':field, 'width':width, 'align': align})
|
|
||||||
return res
|
|
||||||
|
|
||||||
def getAppyTransitions(self, includeFake=True, includeNotShowable=False):
|
def getAppyTransitions(self, includeFake=True, includeNotShowable=False):
|
||||||
'''This method returns info about transitions that one can trigger from
|
'''This method returns info about transitions that one can trigger from
|
||||||
the user interface.
|
the user interface.
|
||||||
|
@ -921,21 +909,21 @@ class BaseMixin:
|
||||||
# Get the list of phases
|
# Get the list of phases
|
||||||
res = [] # Ordered list of phases
|
res = [] # Ordered list of phases
|
||||||
phases = {} # Dict of phases
|
phases = {} # Dict of phases
|
||||||
for appyType in self.getAllAppyTypes():
|
for field in self.getAllAppyTypes():
|
||||||
typePhase = appyType.page.phase
|
fieldPhase = field.page.phase
|
||||||
if typePhase not in phases:
|
if fieldPhase not in phases:
|
||||||
phase = PhaseDescr(typePhase, self)
|
phase = gen.Phase(fieldPhase, self)
|
||||||
res.append(phase.__dict__)
|
res.append(phase)
|
||||||
phases[typePhase] = phase
|
phases[fieldPhase] = phase
|
||||||
else:
|
else:
|
||||||
phase = phases[typePhase]
|
phase = phases[fieldPhase]
|
||||||
phase.addPage(appyType, self, layoutType)
|
phase.addPage(field, self, layoutType)
|
||||||
if (appyType.type == 'Ref') and appyType.navigable:
|
if (field.type == 'Ref') and field.navigable:
|
||||||
phase.addPageLinks(appyType, self)
|
phase.addPageLinks(field, self)
|
||||||
# Remove phases that have no visible page
|
# Remove phases that have no visible page
|
||||||
for i in range(len(res)-1, -1, -1):
|
for i in range(len(res)-1, -1, -1):
|
||||||
if not res[i]['pages']:
|
if not res[i].pages:
|
||||||
del phases[res[i]['name']]
|
del phases[res[i].name]
|
||||||
del res[i]
|
del res[i]
|
||||||
# Compute next/previous phases of every phase
|
# Compute next/previous phases of every phase
|
||||||
for ph in phases.itervalues():
|
for ph in phases.itervalues():
|
||||||
|
@ -948,16 +936,16 @@ class BaseMixin:
|
||||||
if not page:
|
if not page:
|
||||||
if layoutType == 'edit': page = self.getDefaultEditPage()
|
if layoutType == 'edit': page = self.getDefaultEditPage()
|
||||||
else: page = self.getDefaultViewPage()
|
else: page = self.getDefaultViewPage()
|
||||||
for phaseInfo in res:
|
for phase in res:
|
||||||
if page in phaseInfo['pages']:
|
if page in phase.pages:
|
||||||
return phaseInfo
|
return phase
|
||||||
# If I am here, it means that the page as defined in the request,
|
# If I am here, it means that the page as defined in the request,
|
||||||
# or the default page, is not existing nor visible in any phase.
|
# or the default page, is not existing nor visible in any phase.
|
||||||
# In this case I find the first visible page among all phases.
|
# In this case I find the first visible page among all phases.
|
||||||
viewAttr = 'showOn%s' % layoutType.capitalize()
|
viewAttr = 'showOn%s' % layoutType.capitalize()
|
||||||
for phase in res:
|
for phase in res:
|
||||||
for page in phase['pages']:
|
for page in phase.pages:
|
||||||
if phase['pagesInfo'][page][viewAttr]:
|
if getattr(phase.pagesInfo[page], viewAttr):
|
||||||
rq.set('page', page)
|
rq.set('page', page)
|
||||||
pageFound = True
|
pageFound = True
|
||||||
break
|
break
|
||||||
|
@ -965,9 +953,9 @@ class BaseMixin:
|
||||||
else:
|
else:
|
||||||
# Return an empty list if we have a single, link-free page within
|
# Return an empty list if we have a single, link-free page within
|
||||||
# a single phase.
|
# a single phase.
|
||||||
if (len(res) == 1) and (len(res[0]['pages']) == 1) and \
|
if (len(res) == 1) and (len(res[0].pages) == 1) and \
|
||||||
not res[0]['pagesInfo'][res[0]['pages'][0]].get('links'):
|
not res[0].pagesInfo[res[0].pages[0]].links:
|
||||||
return None
|
return
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def getSupTitle(self, navInfo=''):
|
def getSupTitle(self, navInfo=''):
|
||||||
|
@ -983,87 +971,6 @@ class BaseMixin:
|
||||||
if hasattr(appyObj, 'getSubTitle'): return appyObj.getSubTitle()
|
if hasattr(appyObj, 'getSubTitle'): return appyObj.getSubTitle()
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
def getPreviousPage(self, phase, page):
|
|
||||||
'''Returns the page that precedes p_page which is in p_phase.'''
|
|
||||||
try:
|
|
||||||
pageIndex = phase['pages'].index(page)
|
|
||||||
except ValueError:
|
|
||||||
# The current page is probably not visible anymore. Return the
|
|
||||||
# first available page in current phase.
|
|
||||||
res = phase['pages'][0]
|
|
||||||
return res, phase['pagesInfo'][res]
|
|
||||||
if pageIndex > 0:
|
|
||||||
# We stay on the same phase, previous page
|
|
||||||
res = phase['pages'][pageIndex-1]
|
|
||||||
resInfo = phase['pagesInfo'][res]
|
|
||||||
return res, resInfo
|
|
||||||
else:
|
|
||||||
if phase['previousPhase']:
|
|
||||||
# We go to the last page of previous phase
|
|
||||||
previousPhase = phase['previousPhase']
|
|
||||||
res = previousPhase['pages'][-1]
|
|
||||||
resInfo = previousPhase['pagesInfo'][res]
|
|
||||||
return res, resInfo
|
|
||||||
else:
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
def getNextPage(self, phase, page):
|
|
||||||
'''Returns the page that follows p_page which is in p_phase.'''
|
|
||||||
try:
|
|
||||||
pageIndex = phase['pages'].index(page)
|
|
||||||
except ValueError:
|
|
||||||
# The current page is probably not visible anymore. Return the
|
|
||||||
# first available page in current phase.
|
|
||||||
res = phase['pages'][0]
|
|
||||||
return res, phase['pagesInfo'][res]
|
|
||||||
if pageIndex < len(phase['pages'])-1:
|
|
||||||
# We stay on the same phase, next page
|
|
||||||
res = phase['pages'][pageIndex+1]
|
|
||||||
resInfo = phase['pagesInfo'][res]
|
|
||||||
return res, resInfo
|
|
||||||
else:
|
|
||||||
if phase['nextPhase']:
|
|
||||||
# We go to the first page of next phase
|
|
||||||
nextPhase = phase['nextPhase']
|
|
||||||
res = nextPhase['pages'][0]
|
|
||||||
resInfo = nextPhase['pagesInfo'][res]
|
|
||||||
return res, resInfo
|
|
||||||
else:
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
def changeRefOrder(self, fieldName, objectUid, newIndex, isDelta):
|
|
||||||
'''This method changes the position of object with uid p_objectUid in
|
|
||||||
reference field p_fieldName to p_newIndex i p_isDelta is False, or
|
|
||||||
to actualIndex+p_newIndex if p_isDelta is True.'''
|
|
||||||
refs = getattr(self.aq_base, fieldName, None)
|
|
||||||
oldIndex = refs.index(objectUid)
|
|
||||||
refs.remove(objectUid)
|
|
||||||
if isDelta:
|
|
||||||
newIndex = oldIndex + newIndex
|
|
||||||
else:
|
|
||||||
pass # To implement later on
|
|
||||||
refs.insert(newIndex, objectUid)
|
|
||||||
|
|
||||||
def onChangeRefOrder(self):
|
|
||||||
'''This method is called when the user wants to change order of an
|
|
||||||
item in a reference field.'''
|
|
||||||
rq = self.REQUEST
|
|
||||||
# Move the item up (-1), down (+1) ?
|
|
||||||
move = -1 # Move up
|
|
||||||
if rq['move'] == 'down':
|
|
||||||
move = 1 # Down
|
|
||||||
isDelta = True
|
|
||||||
self.changeRefOrder(rq['fieldName'], rq['refObjectUid'], move, isDelta)
|
|
||||||
|
|
||||||
def onSortReference(self):
|
|
||||||
'''This method is called when the user wants to sort the content of a
|
|
||||||
reference field.'''
|
|
||||||
rq = self.REQUEST
|
|
||||||
fieldName = rq.get('fieldName')
|
|
||||||
sortKey = rq.get('sortKey')
|
|
||||||
reverse = rq.get('reverse') == 'True'
|
|
||||||
self.appy().sort(fieldName, sortKey=sortKey, reverse=reverse)
|
|
||||||
|
|
||||||
def notifyWorkflowCreated(self):
|
def notifyWorkflowCreated(self):
|
||||||
'''This method is called every time an object is created, be it temp or
|
'''This method is called every time an object is created, be it temp or
|
||||||
not. The objective here is to initialise workflow-related data on
|
not. The objective here is to initialise workflow-related data on
|
||||||
|
@ -1589,9 +1496,9 @@ class BaseMixin:
|
||||||
|
|
||||||
getUrlDefaults = {'page':True, 'nav':True}
|
getUrlDefaults = {'page':True, 'nav':True}
|
||||||
def getUrl(self, base=None, mode='view', **kwargs):
|
def getUrl(self, base=None, mode='view', **kwargs):
|
||||||
'''Returns a Appy URL.
|
'''Returns an URL for this object.
|
||||||
* If p_base is None, it will be the base URL for this object
|
* If p_base is None, it will be the base URL for this object
|
||||||
(ie, self.absolute_url()).
|
(ie, Zope self.absolute_url()).
|
||||||
* p_mode can be "edit", "view" or "raw" (a non-param, base URL)
|
* p_mode can be "edit", "view" or "raw" (a non-param, base URL)
|
||||||
* p_kwargs can store additional parameters to add to the URL.
|
* p_kwargs can store additional parameters to add to the URL.
|
||||||
In this dict, every value that is a string will be added to the
|
In this dict, every value that is a string will be added to the
|
||||||
|
@ -1600,7 +1507,7 @@ class BaseMixin:
|
||||||
param will not be included in the URL at all).'''
|
param will not be included in the URL at all).'''
|
||||||
# Define the URL suffix
|
# Define the URL suffix
|
||||||
suffix = ''
|
suffix = ''
|
||||||
if mode != 'raw': suffix = '/ui/%s' % mode
|
if mode != 'raw': suffix = '/%s' % mode
|
||||||
# Define base URL if omitted
|
# Define base URL if omitted
|
||||||
if not base:
|
if not base:
|
||||||
base = self.absolute_url() + suffix
|
base = self.absolute_url() + suffix
|
||||||
|
@ -1609,9 +1516,8 @@ class BaseMixin:
|
||||||
if '?' in base: base = base[:base.index('?')]
|
if '?' in base: base = base[:base.index('?')]
|
||||||
base = base.strip('/')
|
base = base.strip('/')
|
||||||
for mode in ('view', 'edit'):
|
for mode in ('view', 'edit'):
|
||||||
suffix = 'ui/%s' % mode
|
if base.endswith(mode):
|
||||||
if base.endswith(suffix):
|
base = base[:-len(mode)].strip('/')
|
||||||
base = base[:-len(suffix)].strip('/')
|
|
||||||
break
|
break
|
||||||
return base
|
return base
|
||||||
# Manage default args
|
# Manage default args
|
||||||
|
@ -1633,15 +1539,6 @@ class BaseMixin:
|
||||||
params = ''
|
params = ''
|
||||||
return '%s%s' % (base, params)
|
return '%s%s' % (base, params)
|
||||||
|
|
||||||
def getUser(self):
|
|
||||||
'''Gets the Zope object representing the authenticated user.'''
|
|
||||||
from AccessControl import getSecurityManager
|
|
||||||
user = getSecurityManager().getUser()
|
|
||||||
if not user:
|
|
||||||
from AccessControl.User import nobody
|
|
||||||
return nobody
|
|
||||||
return user
|
|
||||||
|
|
||||||
def getTool(self):
|
def getTool(self):
|
||||||
'''Returns the application tool.'''
|
'''Returns the application tool.'''
|
||||||
return self.getPhysicalRoot().config
|
return self.getPhysicalRoot().config
|
||||||
|
@ -1659,7 +1556,8 @@ class BaseMixin:
|
||||||
data folder) it returns None.'''
|
data folder) it returns None.'''
|
||||||
parent = self.getParentNode()
|
parent = self.getParentNode()
|
||||||
# Not-Managers can't navigate back to the tool
|
# Not-Managers can't navigate back to the tool
|
||||||
if (parent.id == 'config') and not self.getUser().has_role('Manager'):
|
if (parent.id == 'config') and \
|
||||||
|
not self.getTool().getUser().has_role('Manager'):
|
||||||
return False
|
return False
|
||||||
if parent.meta_type not in ('Folder', 'Temporary Folder'): return parent
|
if parent.meta_type not in ('Folder', 'Temporary Folder'): return parent
|
||||||
|
|
||||||
|
@ -1678,7 +1576,7 @@ class BaseMixin:
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def index_html(self):
|
def index_html(self):
|
||||||
'''Redirects to /ui.'''
|
'''Redirects to /view.'''
|
||||||
rq = self.REQUEST
|
rq = self.REQUEST
|
||||||
if rq.has_key('do'):
|
if rq.has_key('do'):
|
||||||
# The user wants to call a method on this object and get its result
|
# The user wants to call a method on this object and get its result
|
||||||
|
@ -1688,10 +1586,6 @@ class BaseMixin:
|
||||||
# The user wants to consult the view page for this object
|
# The user wants to consult the view page for this object
|
||||||
return rq.RESPONSE.redirect(self.getUrl())
|
return rq.RESPONSE.redirect(self.getUrl())
|
||||||
|
|
||||||
def userIsAnon(self):
|
|
||||||
'''Is the currently logged user anonymous ?'''
|
|
||||||
return self.getUser().getUserName() == 'Anonymous User'
|
|
||||||
|
|
||||||
def getUserLanguage(self):
|
def getUserLanguage(self):
|
||||||
'''Gets the language (code) of the current user.'''
|
'''Gets the language (code) of the current user.'''
|
||||||
if not hasattr(self, 'REQUEST'): return 'en'
|
if not hasattr(self, 'REQUEST'): return 'en'
|
||||||
|
@ -1746,15 +1640,13 @@ class BaseMixin:
|
||||||
if not domain: domain = cfg.PROJECTNAME
|
if not domain: domain = cfg.PROJECTNAME
|
||||||
# Get the label name, and the field-specific mapping if any.
|
# Get the label name, and the field-specific mapping if any.
|
||||||
if field:
|
if field:
|
||||||
# p_field is the dict version of a appy type or group
|
if field.type != 'group':
|
||||||
if field['type'] != 'group':
|
fieldMapping = field.mapping[label]
|
||||||
fieldMapping = field['mapping'][label]
|
|
||||||
if fieldMapping:
|
if fieldMapping:
|
||||||
if callable(fieldMapping):
|
if callable(fieldMapping):
|
||||||
appyField = self.getAppyType(field['name'])
|
fieldMapping = field.callMethod(self, fieldMapping)
|
||||||
fieldMapping=appyField.callMethod(self,fieldMapping)
|
|
||||||
mapping.update(fieldMapping)
|
mapping.update(fieldMapping)
|
||||||
label = field['%sId' % label]
|
label = getattr(field, '%sId' % label)
|
||||||
# We will get the translation from a Translation object.
|
# We will get the translation from a Translation object.
|
||||||
# In what language must we get the translation?
|
# In what language must we get the translation?
|
||||||
if not language: language = self.getUserLanguage()
|
if not language: language = self.getUserLanguage()
|
||||||
|
@ -1855,11 +1747,11 @@ class BaseMixin:
|
||||||
|
|
||||||
def allows(self, permission, raiseError=False):
|
def allows(self, permission, raiseError=False):
|
||||||
'''Has the logged user p_permission on p_self ?'''
|
'''Has the logged user p_permission on p_self ?'''
|
||||||
hasPermission = self.getUser().has_permission(permission, self)
|
res = self.getTool().getUser().has_permission(permission, self)
|
||||||
if not hasPermission and raiseError:
|
if not res and raiseError:
|
||||||
from AccessControl import Unauthorized
|
from AccessControl import Unauthorized
|
||||||
raise Unauthorized
|
raise Unauthorized
|
||||||
return hasPermission
|
return res
|
||||||
|
|
||||||
def getEditorInit(self, name):
|
def getEditorInit(self, name):
|
||||||
'''Gets the Javascript init code for displaying a rich editor for
|
'''Gets the Javascript init code for displaying a rich editor for
|
||||||
|
|
|
@ -149,7 +149,7 @@ class User(ModelClass):
|
||||||
name = gen.String(show=showName, **gm)
|
name = gen.String(show=showName, **gm)
|
||||||
firstName = gen.String(show=showName, **gm)
|
firstName = gen.String(show=showName, **gm)
|
||||||
def showEmail(self): pass
|
def showEmail(self): pass
|
||||||
email = gen.String(show=showEmail)
|
email = gen.String(show=showEmail, **gm)
|
||||||
gm['multiplicity'] = (1,1)
|
gm['multiplicity'] = (1,1)
|
||||||
def showLogin(self): pass
|
def showLogin(self): pass
|
||||||
def validateLogin(self): pass
|
def validateLogin(self): pass
|
||||||
|
|
|
@ -115,20 +115,22 @@ function getAjaxChunk(pos) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function askAjaxChunk(hook,mode,url,page,macro,params,beforeSend,onGet) {
|
function askAjaxChunk(hook,mode,url,px,params,beforeSend,onGet) {
|
||||||
/* This function will ask to get a chunk of HTML on the server through a
|
/* This function will ask to get a chunk of XHTML on the server through a
|
||||||
XMLHttpRequest. p_mode can be 'GET' or 'POST'. p_url is the URL of a
|
XMLHttpRequest. p_mode can be 'GET' or 'POST'. p_url is the URL of a
|
||||||
given server object. On this URL we will call the page "ajax.pt" that
|
given server object. On this object we will call method "ajax" that will
|
||||||
will call a specific p_macro in a given p_page with some additional
|
call a specific p_px with some additional p_params (must be an associative
|
||||||
p_params (must be an associative array) if required.
|
array) if required. If p_px is of the form <field name>:<px name>, the PX
|
||||||
|
will be found on the field named <field name> instead of being found
|
||||||
|
directly on the object at p_url.
|
||||||
|
|
||||||
p_hook is the ID of the HTML element that will be filled with the HTML
|
p_hook is the ID of the XHTML element that will be filled with the XHTML
|
||||||
result from the server.
|
result from the server.
|
||||||
|
|
||||||
p_beforeSend is a Javascript function to call before sending the request.
|
p_beforeSend is a Javascript function to call before sending the request.
|
||||||
This function will get 2 args: the XMLHttpRequest object and the
|
This function will get 2 args: the XMLHttpRequest object and the p_params.
|
||||||
p_params. This method can return, in a string, additional parameters to
|
This method can return, in a string, additional parameters to send, ie:
|
||||||
send, ie: "¶m1=blabla¶m2=blabla".
|
"¶m1=blabla¶m2=blabla".
|
||||||
|
|
||||||
p_onGet is a Javascript function to call when we will receive the answer.
|
p_onGet is a Javascript function to call when we will receive the answer.
|
||||||
This function will get 2 args, too: the XMLHttpRequest object and the
|
This function will get 2 args, too: the XMLHttpRequest object and the
|
||||||
|
@ -149,7 +151,7 @@ function askAjaxChunk(hook,mode,url,page,macro,params,beforeSend,onGet) {
|
||||||
var rq = xhrObjects[pos];
|
var rq = xhrObjects[pos];
|
||||||
rq.freed = 0;
|
rq.freed = 0;
|
||||||
// Construct parameters
|
// Construct parameters
|
||||||
var paramsFull = 'page=' + page + '¯o=' + macro;
|
var paramsFull = 'px=' + px;
|
||||||
if (params) {
|
if (params) {
|
||||||
for (var paramName in params)
|
for (var paramName in params)
|
||||||
paramsFull = paramsFull + '&' + paramName + '=' + params[paramName];
|
paramsFull = paramsFull + '&' + paramName + '=' + params[paramName];
|
||||||
|
@ -160,7 +162,7 @@ function askAjaxChunk(hook,mode,url,page,macro,params,beforeSend,onGet) {
|
||||||
if (res) paramsFull = paramsFull + res;
|
if (res) paramsFull = paramsFull + res;
|
||||||
}
|
}
|
||||||
// Construct the URL to call
|
// Construct the URL to call
|
||||||
var urlFull = url + '/ui/ajax';
|
var urlFull = url + '/ajax';
|
||||||
if (mode == 'GET') {
|
if (mode == 'GET') {
|
||||||
urlFull = urlFull + '?' + paramsFull;
|
urlFull = urlFull + '?' + paramsFull;
|
||||||
}
|
}
|
||||||
|
@ -199,41 +201,39 @@ function askQueryResult(hookId, objectUrl, className, searchName,
|
||||||
params['filterValue'] = filterWidget.value;
|
params['filterValue'] = filterWidget.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
askAjaxChunk(hookId,'GET',objectUrl, 'result', 'queryResult', params);
|
askAjaxChunk(hookId, 'GET', objectUrl, 'pxQueryResult', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
function askObjectHistory(hookId, objectUrl, maxPerPage, startNumber) {
|
function askObjectHistory(hookId, objectUrl, maxPerPage, startNumber) {
|
||||||
// Sends an Ajax request for getting the history of an object
|
// Sends an Ajax request for getting the history of an object
|
||||||
var params = {'maxPerPage': maxPerPage, 'startNumber': startNumber};
|
var params = {'maxPerPage': maxPerPage, 'startNumber': startNumber};
|
||||||
askAjaxChunk(hookId, 'GET', objectUrl, 'page', 'objectHistory', params);
|
askAjaxChunk(hookId, 'GET', objectUrl, 'pxHistory', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
function askRefField(hookId, objectUrl, fieldName, innerRef, startNumber,
|
function askRefField(hookId, objectUrl, fieldName, innerRef, startNumber,
|
||||||
action, actionParams){
|
action, actionParams){
|
||||||
// Sends an Ajax request for getting the content of a reference field.
|
// Sends an Ajax request for getting the content of a reference field.
|
||||||
var startKey = hookId + '_startNumber';
|
var startKey = hookId + '_startNumber';
|
||||||
var params = {'fieldName': fieldName, 'innerRef': innerRef };
|
var params = {'innerRef': innerRef };
|
||||||
params[startKey] = startNumber;
|
params[startKey] = startNumber;
|
||||||
if (action) params['action'] = action;
|
if (action) params['action'] = action;
|
||||||
if (actionParams) {
|
if (actionParams) {
|
||||||
for (key in actionParams) { params[key] = actionParams[key]; };
|
for (key in actionParams) { params[key] = actionParams[key]; };
|
||||||
}
|
}
|
||||||
askAjaxChunk(hookId, 'GET', objectUrl, 'widgets/ref', 'viewContent', params);
|
askAjaxChunk(hookId, 'GET', objectUrl, fieldName+':pxView', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
function askComputedField(hookId, objectUrl, fieldName) {
|
function askComputedField(hookId, objectUrl, fieldName) {
|
||||||
// Sends an Ajax request for getting the content of a computed field
|
// Sends an Ajax request for getting the content of a computed field
|
||||||
var params = {'fieldName': fieldName};
|
askAjaxChunk(hookId, 'GET', objectUrl, fieldName+':pxViewContent');
|
||||||
askAjaxChunk(hookId, 'GET', objectUrl, 'widgets/computed', 'viewContent', params);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function askField(hookId, objectUrl, layoutType, showChanges){
|
function askField(hookId, objectUrl, layoutType, showChanges){
|
||||||
// Sends an Ajax request for getting the content of any field.
|
// Sends an Ajax request for getting the content of any field.
|
||||||
var fieldName = hookId.split('_')[1];
|
var fieldName = hookId.split('_')[1];
|
||||||
var params = {'fieldName': fieldName, 'layoutType': layoutType,
|
var params = {'layoutType': layoutType, 'showChanges': showChanges};
|
||||||
'showChanges': showChanges};
|
askAjaxChunk(hookId, 'GET', objectUrl, fieldName+':pxRender', params, null,
|
||||||
askAjaxChunk(hookId, 'GET', objectUrl, 'widgets/show', 'fieldAjax', params,
|
evalInnerScripts);
|
||||||
null, evalInnerScripts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function used by checkbox widgets for having radio-button-like behaviour
|
// Function used by checkbox widgets for having radio-button-like behaviour
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"-//W3C//DTD XHTML 1.0 Strict//EN"
|
"-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">" />
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">" />
|
||||||
<html tal:define="user tool/getUser;
|
<html tal:define="user tool/getUser;
|
||||||
isAnon tool/userIsAnon;
|
isAnon python: user.login == 'anon';
|
||||||
app tool/getApp;
|
app tool/getApp;
|
||||||
appUrl app/absolute_url;
|
appUrl app/absolute_url;
|
||||||
appFolder app/data;
|
appFolder app/data;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
function askMonthView(hookId, objectUrl, fieldName, month) {
|
function askMonthView(hookId, objectUrl, fieldName, month) {
|
||||||
// Sends an Ajax request for getting the view month of a calendar field
|
// Sends an Ajax request for getting the view month of a calendar field
|
||||||
var params = {'fieldName': fieldName, 'month': month};
|
var params = {'month': month};
|
||||||
askAjaxChunk(hookId,'GET',objectUrl,'widgets/calendar','viewMonth', params);
|
askAjaxChunk(hookId, 'GET', objectUrl, fieldName+':pxMonthView', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEventPopup(action, fieldName, day, spansDays,
|
function openEventPopup(action, fieldName, day, spansDays,
|
||||||
|
@ -54,7 +54,8 @@ function openEventPopup(action, fieldName, day, spansDays,
|
||||||
openPopup(prefix + 'Popup');
|
openPopup(prefix + 'Popup');
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerCalendarEvent(action, hookId, fieldName, objectUrl, maxEventLength) {
|
function triggerCalendarEvent(action, hookId, fieldName, objectUrl,
|
||||||
|
maxEventLength) {
|
||||||
/* Sends an Ajax request for triggering a calendar event (create or delete an
|
/* Sends an Ajax request for triggering a calendar event (create or delete an
|
||||||
event) and refreshing the view month. */
|
event) and refreshing the view month. */
|
||||||
var prefix = fieldName + '_' + action + 'Event';
|
var prefix = fieldName + '_' + action + 'Event';
|
||||||
|
@ -82,5 +83,5 @@ function triggerCalendarEvent(action, hookId, fieldName, objectUrl, maxEventLeng
|
||||||
params[elems[i].name] = elems[i].value;
|
params[elems[i].name] = elems[i].value;
|
||||||
}
|
}
|
||||||
closePopup(prefix + 'Popup');
|
closePopup(prefix + 'Popup');
|
||||||
askAjaxChunk(hookId,'POST',objectUrl,'widgets/calendar','viewMonth',params);
|
askAjaxChunk(hookId, 'POST', objectUrl, fieldName+':pxViewMonth', params);
|
||||||
}
|
}
|
||||||
|
|
292
gen/utils.py
292
gen/utils.py
|
@ -1,7 +1,6 @@
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
import re, os, os.path
|
import re, os, os.path, base64, urllib
|
||||||
from appy.shared.utils import normalizeText
|
from appy.shared.utils import normalizeText
|
||||||
from appy.px import Px
|
|
||||||
|
|
||||||
# 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):
|
||||||
|
@ -10,15 +9,14 @@ def createObject(folder, id, className, appName, wf=True, noSecurity=False):
|
||||||
creation of the config object), computing workflow-related info is not
|
creation of the config object), computing workflow-related info is not
|
||||||
possible at this time. This is why this function can be called with
|
possible at this time. This is why this function can be called with
|
||||||
p_wf=False.'''
|
p_wf=False.'''
|
||||||
exec 'from Products.%s.%s import %s as ZopeClass' % (appName, className,
|
exec 'from Products.%s.%s import %s as ZopeClass' % \
|
||||||
className)
|
(appName, className, className)
|
||||||
if not noSecurity:
|
# Get the tool. Depends on whether p_folder is a Zope (temp) folder or not.
|
||||||
# Check that the user can create objects of className
|
isFolder = folder.meta_type.endswith('Folder')
|
||||||
if folder.meta_type.endswith('Folder'): # Folder or temp folder.
|
tool = isFolder and folder.config or folder.getTool()
|
||||||
tool = folder.config
|
|
||||||
else:
|
|
||||||
tool = folder.getTool()
|
|
||||||
user = tool.getUser()
|
user = tool.getUser()
|
||||||
|
if not noSecurity:
|
||||||
|
# Check that the user can create objects of className.
|
||||||
userRoles = user.getRoles()
|
userRoles = user.getRoles()
|
||||||
allowedRoles=ZopeClass.wrapperClass.getCreators(tool.getProductConfig())
|
allowedRoles=ZopeClass.wrapperClass.getCreators(tool.getProductConfig())
|
||||||
allowed = False
|
allowed = False
|
||||||
|
@ -31,267 +29,23 @@ def createObject(folder, id, className, appName, wf=True, noSecurity=False):
|
||||||
raise Unauthorized("User can't create instances of %s" % \
|
raise Unauthorized("User can't create instances of %s" % \
|
||||||
ZopeClass.__name__)
|
ZopeClass.__name__)
|
||||||
obj = ZopeClass(id)
|
obj = ZopeClass(id)
|
||||||
folder._objects = folder._objects + \
|
folder._objects = folder._objects + ({'id':id, 'meta_type':className},)
|
||||||
({'id':id, 'meta_type':className},)
|
|
||||||
folder._setOb(id, obj)
|
folder._setOb(id, obj)
|
||||||
obj = folder._getOb(id) # Important. Else, obj is not really in the folder.
|
obj = folder._getOb(id) # Important. Else, obj is not really in the folder.
|
||||||
obj.portal_type = className
|
obj.portal_type = className
|
||||||
obj.id = id
|
obj.id = id
|
||||||
obj._at_uid = id
|
obj._at_uid = id
|
||||||
user = obj.getUser()
|
# If no user object is there, we are at startup, before default User
|
||||||
if not user.getId():
|
# instances are created.
|
||||||
if user.name == 'System Processes':
|
userId = user and user.login or 'system'
|
||||||
userId = 'admin' # This is what happens when Zope is starting.
|
obj.creator = userId
|
||||||
else:
|
|
||||||
userId = None # Anonymous.
|
|
||||||
else:
|
|
||||||
userId = user.getId()
|
|
||||||
obj.creator = userId or 'Anonymous User'
|
|
||||||
from DateTime import DateTime
|
from DateTime import DateTime
|
||||||
obj.created = DateTime()
|
obj.created = DateTime()
|
||||||
obj.modified = obj.created
|
obj.modified = obj.created
|
||||||
obj.__ac_local_roles__ = { userId: ['Owner'] } # userId can be None (anon).
|
obj.__ac_local_roles__ = { userId: ['Owner'] }
|
||||||
if wf: obj.notifyWorkflowCreated()
|
if wf: obj.notifyWorkflowCreated()
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
# Classes used by edit/view PXs for accessing information ----------------------
|
|
||||||
class Descr:
|
|
||||||
'''Abstract class for description classes.'''
|
|
||||||
def get(self): return self.__dict__
|
|
||||||
|
|
||||||
class GroupDescr(Descr):
|
|
||||||
'''Intermediary, on-the-fly-generated data structure that groups all fields
|
|
||||||
sharing the same appy.gen.Group instance, that some logged user can
|
|
||||||
see.'''
|
|
||||||
# PX that renders a group of fields
|
|
||||||
pxView = Px('''<p>pxGroupedFields</p>''')
|
|
||||||
|
|
||||||
# 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 widgets belonging to the group that the current user may see.
|
|
||||||
# They will be stored by m_addWidget below as a list of lists because
|
|
||||||
# they will be rendered as a table.
|
|
||||||
self.widgets = [[]]
|
|
||||||
# PX to user for rendering this group.
|
|
||||||
self.px = forSearch and self.pxViewSearches or self.pxView
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def addWidget(groupDict, newWidget):
|
|
||||||
'''Adds p_newWidget into p_groupDict['widgets']. We try first to add
|
|
||||||
p_newWidget into the last widget row. If it is not possible, we
|
|
||||||
create a new row.
|
|
||||||
|
|
||||||
This method is a static method taking p_groupDict as first param
|
|
||||||
instead of being an instance method because at this time the object
|
|
||||||
has already been converted to a dict (for being maniputated within
|
|
||||||
ZPTs).'''
|
|
||||||
# Get the last row
|
|
||||||
widgetRow = groupDict['widgets'][-1]
|
|
||||||
numberOfColumns = len(groupDict['columnsWidths'])
|
|
||||||
# Computes the number of columns already filled by widgetRow
|
|
||||||
rowColumns = 0
|
|
||||||
for widget in widgetRow: rowColumns += widget['colspan']
|
|
||||||
freeColumns = numberOfColumns - rowColumns
|
|
||||||
if freeColumns >= newWidget['colspan']:
|
|
||||||
# We can add the widget in the last row.
|
|
||||||
widgetRow.append(newWidget)
|
|
||||||
else:
|
|
||||||
if freeColumns:
|
|
||||||
# Terminate the current row by appending empty cells
|
|
||||||
for i in range(freeColumns): widgetRow.append('')
|
|
||||||
# Create a new row
|
|
||||||
newRow = [newWidget]
|
|
||||||
groupDict['widgets'].append(newRow)
|
|
||||||
|
|
||||||
class PhaseDescr(Descr):
|
|
||||||
'''Describes a phase.'''
|
|
||||||
|
|
||||||
pxPhase = 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].get('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 infor 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
|
|
||||||
self.px = self.pxPhase
|
|
||||||
|
|
||||||
def addPageLinks(self, appyType, obj):
|
|
||||||
'''If p_appyType is a navigable Ref, we must add, within self.pagesInfo,
|
|
||||||
those links.'''
|
|
||||||
if appyType.page.name in self.hiddenPages: return
|
|
||||||
infos = []
|
|
||||||
for obj in appyType.getValue(obj, type="zobjects"):
|
|
||||||
infos.append({'title': obj.title, 'url':obj.absolute_url()})
|
|
||||||
self.pagesInfo[appyType.page.name]['links'] = infos
|
|
||||||
|
|
||||||
def addPage(self, appyType, obj, layoutType):
|
|
||||||
'''Adds page-related information in the phase.'''
|
|
||||||
# If the page is already there, we have nothing more to do.
|
|
||||||
if (appyType.page.name in self.pages) or \
|
|
||||||
(appyType.page.name in self.hiddenPages): return
|
|
||||||
# Add the page only if it must be shown.
|
|
||||||
isShowableOnView = appyType.page.isShowable(obj, 'view')
|
|
||||||
isShowableOnEdit = appyType.page.isShowable(obj, 'edit')
|
|
||||||
if isShowableOnView or isShowableOnEdit:
|
|
||||||
# The page must be added.
|
|
||||||
self.pages.append(appyType.page.name)
|
|
||||||
# Create the dict about page information and add it in self.pageInfo
|
|
||||||
pageInfo = {'page': appyType.page,
|
|
||||||
'showOnView': isShowableOnView,
|
|
||||||
'showOnEdit': isShowableOnEdit}
|
|
||||||
pageInfo.update(appyType.page.getInfo(obj, layoutType))
|
|
||||||
self.pagesInfo[appyType.page.name] = pageInfo
|
|
||||||
else:
|
|
||||||
self.hiddenPages.append(appyType.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 phaseInfo in allPhases:
|
|
||||||
if phaseInfo['name'] == self.name:
|
|
||||||
i = allPhases.index(phaseInfo)
|
|
||||||
if i > 0:
|
|
||||||
self.previousPhase = allPhases[i-1]
|
|
||||||
if i < (len(allPhases)-1):
|
|
||||||
self.nextPhase = allPhases[i+1]
|
|
||||||
|
|
||||||
class SearchDescr(Descr):
|
|
||||||
'''Describes a Search.'''
|
|
||||||
# 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 = ''
|
|
||||||
self.px = self.pxView
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
upperLetter = re.compile('[A-Z]')
|
upperLetter = re.compile('[A-Z]')
|
||||||
def produceNiceMessage(msg):
|
def produceNiceMessage(msg):
|
||||||
|
@ -448,4 +202,22 @@ def callMethod(obj, method, klass=None, cache=True):
|
||||||
res = method(obj)
|
res = method(obj)
|
||||||
rq.methodCache[key] = res
|
rq.methodCache[key] = res
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
# Functions for manipulating the authentication cookie -------------------------
|
||||||
|
def readCookie(request):
|
||||||
|
'''Returns the tuple (login, password) read from the authentication
|
||||||
|
cookie received in p_request. If no user is logged, its returns
|
||||||
|
(None, None).'''
|
||||||
|
cookie = request.get('_appy_', None)
|
||||||
|
if not cookie: return None, None
|
||||||
|
cookieValue = base64.decodestring(urllib.unquote(cookie))
|
||||||
|
if ':' in cookieValue: return cookieValue.split(':')
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def writeCookie(login, password, request):
|
||||||
|
'''Encode p_login and p_password into the cookie set in the p_request.'''
|
||||||
|
cookieValue = base64.encodestring('%s:%s' % (login, password)).rstrip()
|
||||||
|
cookieValue = urllib.quote(cookieValue)
|
||||||
|
request.RESPONSE.setCookie('_appy_', cookieValue, path='/')
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
|
@ -21,6 +21,301 @@ NOT_UNO_ENABLED_PYTHON = '"%s" is not a UNO-enabled Python interpreter. ' \
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
class ToolWrapper(AbstractWrapper):
|
class ToolWrapper(AbstractWrapper):
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Navigation-related PXs
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Icon for hiding/showing details below the title of an object shown in a
|
||||||
|
# list of objects.
|
||||||
|
pxShowDetails = Px('''
|
||||||
|
<img if="ztool.subTitleIsUsed(className) and (field.name == 'title')"
|
||||||
|
class="clickable" src=":url('toggleDetails')"
|
||||||
|
onclick="toggleSubTitles()"/>''')
|
||||||
|
|
||||||
|
# Displays up/down arrows in a table header column for sorting a given
|
||||||
|
# column. Requires variables "sortable", 'filterable' and 'field'.
|
||||||
|
pxSortAndFilter = Px('''<x>
|
||||||
|
<x if="sortable">
|
||||||
|
<img if="(sortKey != field.name) or (sortOrder == 'desc')"
|
||||||
|
onclick=":navBaseCall.replace('**v**', '0,%s,%s,%s' % \
|
||||||
|
(q(field.name), q('asc'), q(filterKey)))"
|
||||||
|
src=":url('sortDown.gif')" class="clickable"/>
|
||||||
|
<img if="(sortKey != field.name) or (sortOrder == 'asc')"
|
||||||
|
onclick=":navBaseCall.replace('**v**', '0,%s,%s,%s' % \
|
||||||
|
(q(field.name), q('desc'), q(filterKey)))"
|
||||||
|
src=":url('sortUp.gif')" class="clickable"/>
|
||||||
|
</x>
|
||||||
|
<x if="filterable">
|
||||||
|
<input type="text" size="7" id=":'%s_%s' % (ajaxHookId, field.name)"
|
||||||
|
value=":filterKey == field.name and filterValue or ''"/>
|
||||||
|
<img onclick=":navBaseCall.replace('**v**', '0, %s,%s,%s' % \
|
||||||
|
(q(sortKey), q(sortOrder), q(field.name)))"
|
||||||
|
src=":url('funnel')" class="clickable"/>
|
||||||
|
</x></x>''')
|
||||||
|
|
||||||
|
# Buttons for navigating among a list of objects (from a Ref field or a
|
||||||
|
# query): next,back,first,last...
|
||||||
|
pxNavigate = Px('''
|
||||||
|
<div if="totalNumber > batchSize" align=":dright">
|
||||||
|
<table class="listNavigate"
|
||||||
|
var="mustSortAndFilter=ajaxHookId == 'queryResult';
|
||||||
|
sortAndFilter=mustSortAndFilter and \
|
||||||
|
',%s,%s,%s' % (q(sortKey),q(sortOrder),q(filterKey)) or ''">
|
||||||
|
<tr valign="middle">
|
||||||
|
<!-- Go to the first page -->
|
||||||
|
<td if="(startNumber != 0) and (startNumber != batchSize)"><img
|
||||||
|
class="clickable" src=":url('arrowLeftDouble')"
|
||||||
|
title=":_('goto_first')"
|
||||||
|
onClick=":navBaseCall.replace('**v**', '0'+sortAndFilter)"/></td>
|
||||||
|
|
||||||
|
<!-- Go to the previous page -->
|
||||||
|
<td var="sNumber=startNumber - batchSize" if="startNumber != 0"><img
|
||||||
|
class="clickable" src=":url('arrowLeftSimple')"
|
||||||
|
title=":_('goto_previous')"
|
||||||
|
onClick=":navBaseCall.replace('**v**', \
|
||||||
|
str(sNumber)+sortAndFilter)"/></td>
|
||||||
|
|
||||||
|
<!-- Explain which elements are currently shown -->
|
||||||
|
<td class="discreet">
|
||||||
|
<x>:startNumber + 1</x><img src=":url('to')"/>
|
||||||
|
<x>:startNumber + batchNumber</x> <b>//</b>
|
||||||
|
<x>:totalNumber</x> </td>
|
||||||
|
|
||||||
|
<!-- Go to the next page -->
|
||||||
|
<td var="sNumber=startNumber + batchSize"
|
||||||
|
if="sNumber < totalNumber"><img class="clickable"
|
||||||
|
src=":url('arrowRightSimple')" title=":_('goto_next')"
|
||||||
|
onClick=":navBaseCall.replace('**v**', \
|
||||||
|
str(sNumber)+sortAndFilter)"/></td>
|
||||||
|
|
||||||
|
<!-- Go to the last page -->
|
||||||
|
<td var="lastPageIsIncomplete=totalNumber % batchSize;
|
||||||
|
nbOfCompletePages=totalNumber/batchSize;
|
||||||
|
nbOfCountedPages=lastPageIsIncomplete and \
|
||||||
|
nbOfCompletePages or nbOfCompletePages-1;
|
||||||
|
sNumber= nbOfCountedPages * batchSize"
|
||||||
|
if="(startNumber != sNumber) and \
|
||||||
|
(startNumber != sNumber-batchSize)"><img class="clickable"
|
||||||
|
src=":url('arrowRightDouble')" title=":_('goto_last')"
|
||||||
|
onClick=":navBaseCall.replace('**v**', \
|
||||||
|
str(sNumber)+sortAndFilter)"/></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>''')
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# PXs for graphical elements shown on every page
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Global elements included in every page.
|
||||||
|
pxPagePrologue = Px('''<x>
|
||||||
|
<!-- Include type-specific CSS and JS. -->
|
||||||
|
<x if="cssJs">
|
||||||
|
<link for="cssFile in cssJs['css']" rel="stylesheet" type="text/css"
|
||||||
|
href=":url(cssFile)"/>
|
||||||
|
<script for="jsFile in cssJs['js']" type="text/javascript"
|
||||||
|
src=":url(jsFile)"></script></x>
|
||||||
|
|
||||||
|
<!-- Javascript messages -->
|
||||||
|
<script type="text/javascript">::ztool.getJavascriptMessages()</script>
|
||||||
|
|
||||||
|
<!-- Global form for deleting an object -->
|
||||||
|
<form id="deleteForm" method="post" action="do">
|
||||||
|
<input type="hidden" name="action" value="Delete"/>
|
||||||
|
<input type="hidden" name="objectUid"/>
|
||||||
|
</form>
|
||||||
|
<!-- Global form for deleting an event from an object's history -->
|
||||||
|
<form id="deleteEventForm" method="post" action="do">
|
||||||
|
<input type="hidden" name="action" value="DeleteEvent"/>
|
||||||
|
<input type="hidden" name="objectUid"/>
|
||||||
|
<input type="hidden" name="eventTime"/>
|
||||||
|
</form>
|
||||||
|
<!-- Global form for unlinking an object -->
|
||||||
|
<form id="unlinkForm" method="post" action="do">
|
||||||
|
<input type="hidden" name="action" value="Unlink"/>
|
||||||
|
<input type="hidden" name="sourceUid"/>
|
||||||
|
<input type="hidden" name="fieldName"/>
|
||||||
|
<input type="hidden" name="targetUid"/>
|
||||||
|
</form>
|
||||||
|
<!-- Global form for unlocking a page -->
|
||||||
|
<form id="unlockForm" method="post" action="do">
|
||||||
|
<input type="hidden" name="action" value="Unlock"/>
|
||||||
|
<input type="hidden" name="objectUid"/>
|
||||||
|
<input type="hidden" name="pageName"/>
|
||||||
|
</form>
|
||||||
|
<!-- Global form for generating a document from a pod template -->
|
||||||
|
<form id="podTemplateForm" name="podTemplateForm" method="post"
|
||||||
|
action=":ztool.absolute_url() + '/generateDocument'">
|
||||||
|
<input type="hidden" name="objectUid"/>
|
||||||
|
<input type="hidden" name="fieldName"/>
|
||||||
|
<input type="hidden" name="podFormat"/>
|
||||||
|
<input type="hidden" name="askAction"/>
|
||||||
|
<input type="hidden" name="queryData"/>
|
||||||
|
<input type="hidden" name="customParams"/>
|
||||||
|
</form>
|
||||||
|
</x>''')
|
||||||
|
|
||||||
|
pxPageBottom = Px('''
|
||||||
|
<script type="text/javascript">initSlaves();</script>''')
|
||||||
|
|
||||||
|
pxPortlet = Px('''
|
||||||
|
<x var="toolUrl=tool.url;
|
||||||
|
queryUrl='%s/query' % toolUrl;
|
||||||
|
currentSearch=req.get('search', None);
|
||||||
|
currentClass=req.get('className', None);
|
||||||
|
currentPage=req['PATH_INFO'].rsplit('/',1)[-1];
|
||||||
|
rootClasses=ztool.getRootClasses();
|
||||||
|
phases=zobj and zobj.getAppyPhases() or None">
|
||||||
|
|
||||||
|
<table class="portletContent"
|
||||||
|
if="zobj and phases and zobj.mayNavigate()"
|
||||||
|
var2="singlePhase=phases and (len(phases) == 1);
|
||||||
|
page=req.get('page', '');
|
||||||
|
mayEdit=zobj.mayEdit()">
|
||||||
|
<x for="phase in phases">:phase.pxView</x>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- One section for every searchable root class -->
|
||||||
|
<x for="rootClass in [rc for rc in rootClasses \
|
||||||
|
if ztool.userMaySearch(rc)]">
|
||||||
|
|
||||||
|
<!-- A separator if required -->
|
||||||
|
<div class="portletSep" var="nb=loop.rootClass.nb"
|
||||||
|
if="(nb != 0) or ((nb == 0) and phases)"></div>
|
||||||
|
|
||||||
|
<!-- Section title (link triggers the default search) -->
|
||||||
|
<div class="portletContent"
|
||||||
|
var="searchInfo=ztool.getGroupedSearches(rootClass)">
|
||||||
|
<div class="portletTitle">
|
||||||
|
<a var="queryParam=searchInfo['default'] and \
|
||||||
|
searchInfo['default']['name'] or ''"
|
||||||
|
href=":'%s?className=%s&search=%s' % \
|
||||||
|
(queryUrl,rootClass,queryParam)"
|
||||||
|
class=":(not currentSearch and (currentClass==rootClass) and \
|
||||||
|
(currentPage=='query')) and \
|
||||||
|
'portletCurrent' or ''">::_(rootClass + '_plural')</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<x var="addPermission='%s: Add %s' % (appName, rootClass);
|
||||||
|
userMayAdd=user.has_permission(addPermission, appFolder);
|
||||||
|
createMeans=ztool.getCreateMeans(rootClass)">
|
||||||
|
|
||||||
|
<!-- Create a new object from a web form -->
|
||||||
|
<input type="button" class="button"
|
||||||
|
if="userMayAdd and ('form' in createMeans)"
|
||||||
|
style=":url('buttonAdd', bg=True)" value=":_('query_create')"
|
||||||
|
onclick=":'goto(%s)' % \
|
||||||
|
q('%s/do?action=Create&className=%s' % \
|
||||||
|
(toolUrl, rootClass))"/>
|
||||||
|
|
||||||
|
<!-- Create object(s) by importing data -->
|
||||||
|
<input type="button" class="button"
|
||||||
|
if="userMayAdd and ('import' in createMeans)"
|
||||||
|
style=":url('buttonImport', bg=True)" value=":_('query_import')"
|
||||||
|
onclick=":'goto(%s)' % \
|
||||||
|
q('%s/import?className=%s' % (toolUrl, rootClass))"/>
|
||||||
|
</x>
|
||||||
|
|
||||||
|
<!-- Searches -->
|
||||||
|
<x if="ztool.advancedSearchEnabledFor(rootClass)">
|
||||||
|
|
||||||
|
<!-- Live search -->
|
||||||
|
<form action=":'%s/do' % toolUrl">
|
||||||
|
<input type="hidden" name="action" value="SearchObjects"/>
|
||||||
|
<input type="hidden" name="className" value=":rootClass"/>
|
||||||
|
<table cellpadding="0" cellspacing="0">
|
||||||
|
<tr valign="bottom">
|
||||||
|
<td><input type="text" size="14" name="w_SearchableText"
|
||||||
|
class="inputSearch"/></td>
|
||||||
|
<td>
|
||||||
|
<input type="image" class="clickable" src=":url('search.gif')"
|
||||||
|
title=":_('search_button')"/></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Advanced search -->
|
||||||
|
<div var="highlighted=(currentClass == rootClass) and \
|
||||||
|
(currentPage == 'search')"
|
||||||
|
class=":highlighted and 'portletSearch portletCurrent' or \
|
||||||
|
'portletSearch'"
|
||||||
|
align=":dright">
|
||||||
|
<a var="text=_('search_title')" style="font-size: 88%"
|
||||||
|
href=":'%s/search?className=%s' % (toolUrl, rootClass)"
|
||||||
|
title=":text"><x>:text</x>...</a>
|
||||||
|
</div>
|
||||||
|
</x>
|
||||||
|
|
||||||
|
<!-- Predefined searches -->
|
||||||
|
<x for="widget in searchInfo['searches']">
|
||||||
|
<x if="widget['type']=='group'">:widget['px']</x>
|
||||||
|
<x if="widget['type']!='group'" var2="search=widget">:search['px']</x>
|
||||||
|
</x>
|
||||||
|
</div>
|
||||||
|
</x>
|
||||||
|
</x>''')
|
||||||
|
|
||||||
|
# The message that is shown when a user triggers an action.
|
||||||
|
pxMessage = Px('''
|
||||||
|
<div var="messages=ztool.consumeMessages()" if="messages" class="message">
|
||||||
|
<!-- The icon for closing the message -->
|
||||||
|
<img src=":url('close')" align=":dright" class="clickable"
|
||||||
|
onclick="this.parentNode.style.display='none'"/>
|
||||||
|
<!-- The message content -->
|
||||||
|
<x>::messages</x>
|
||||||
|
</div>''')
|
||||||
|
|
||||||
|
# The page footer.
|
||||||
|
pxFooter = Px('''
|
||||||
|
<table cellpadding="0" cellspacing="0" width="100%" class="footer">
|
||||||
|
<tr>
|
||||||
|
<td align=":dright">Made with
|
||||||
|
<a href="http://appyframework.org" target="_blank">Appy</a></td></tr>
|
||||||
|
</table>''')
|
||||||
|
|
||||||
|
# Hook for defining a PX that proposes additional links, after the links
|
||||||
|
# corresponding to top-level pages.
|
||||||
|
pxLinks = ''
|
||||||
|
|
||||||
|
# Hook for defining a PX that proposes additional icons after standard
|
||||||
|
# icons in the user strip.
|
||||||
|
pxIcons = ''
|
||||||
|
|
||||||
|
# Displays the content of a layouted object (a page or a field). If the
|
||||||
|
# layouted object is a page, the "layout target" (where to look for PXs)
|
||||||
|
# will be the object whose page is shown; if the layouted object is a field,
|
||||||
|
# the layout target will be this field.
|
||||||
|
pxLayoutedObject = Px('''
|
||||||
|
<table var="layoutCss=layout['css_class'];
|
||||||
|
isCell=layoutType == 'cell'"
|
||||||
|
cellpadding=":layout['cellpadding']"
|
||||||
|
cellspacing=":layout['cellspacing']"
|
||||||
|
width=":not isCell and layout['width'] or ''"
|
||||||
|
align=":not isCell and \
|
||||||
|
ztool.flipLanguageDirection(layout['align'], dir) or ''"
|
||||||
|
class=":tagCss and ('%s %s' % (tagCss, layoutCss)).strip() or \
|
||||||
|
layoutCss"
|
||||||
|
style=":layout['style']" id=":tagId" name=":tagName">
|
||||||
|
|
||||||
|
<!-- The table header row -->
|
||||||
|
<tr if="layout['headerRow']" valign=":layout['headerRow']['valign']">
|
||||||
|
<th for="cell in layout['headerRow']['cells']" width=":cell['width']"
|
||||||
|
align=":ztool.flipLanguageDirection(cell['align'], dir)">
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<!-- The table content -->
|
||||||
|
<tr for="row in layout['rows']" valign=":row['valign']">
|
||||||
|
<td for="cell in row['cells']" colspan=":cell['colspan']"
|
||||||
|
align=":ztool.flipLanguageDirection(cell['align'], dir)"
|
||||||
|
class=":not loop.cell.last and 'cellGap' or ''">
|
||||||
|
<x for="pxName in cell['content']">
|
||||||
|
<x var="px=(pxName == '?') and 'px%s' % layoutType.capitalize() \
|
||||||
|
or pxName">:getattr(layoutTarget, px)</x>
|
||||||
|
<img if="not loop.pxName.last" src=":url('space.gif')"/>
|
||||||
|
</x>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>''')
|
||||||
|
|
||||||
pxHome = Px('''
|
pxHome = Px('''
|
||||||
<table width="300px" height="240px" align="center">
|
<table width="300px" height="240px" align="center">
|
||||||
<tr valign="middle">
|
<tr valign="middle">
|
||||||
|
@ -58,22 +353,22 @@ class ToolWrapper(AbstractWrapper):
|
||||||
<!-- Any other field -->
|
<!-- Any other field -->
|
||||||
<x if="field.name != 'title'">
|
<x if="field.name != 'title'">
|
||||||
<x var="layoutType='cell'; innerRef=True"
|
<x var="layoutType='cell'; innerRef=True"
|
||||||
if="zobj.showField(field.name, 'result')">field.pxView</x>
|
if="zobj.showField(field.name, 'result')">:field.pxRender</x>
|
||||||
</x>
|
</x>
|
||||||
</x>''')
|
</x>''')
|
||||||
|
|
||||||
# Show query results as a list.
|
# Show query results as a list.
|
||||||
pxQueryResultList = Px('''
|
pxQueryResultList = Px('''
|
||||||
<table class="list" width="100%">
|
<table class="list" width="100%" var="showHeaders=showHeaders|True">
|
||||||
<!-- Headers, with filters and sort arrows -->
|
<!-- Headers, with filters and sort arrows -->
|
||||||
<tr if="showHeaders">
|
<tr if="showHeaders">
|
||||||
<th for="column in columns"
|
<th for="column in columns"
|
||||||
var2="widget=column['field'];
|
var2="field=column.field;
|
||||||
sortable=ztool.isSortable(field.name, className, 'search');
|
sortable=ztool.isSortable(field.name, className, 'search');
|
||||||
filterable=widget.filterable"
|
filterable=field.filterable"
|
||||||
width=":column['width']" align=":column['align']">
|
width=":column.width" align=":column.align">
|
||||||
<x>::ztool.truncateText(_(field.labelId))</x>
|
<x>::ztool.truncateText(_(field.labelId))</x>
|
||||||
<x>:self.pxSortAndFilter</x><x>:self.pxShowDetails</x>
|
<x>:tool.pxSortAndFilter</x><x>:tool.pxShowDetails</x>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
@ -83,9 +378,9 @@ class ToolWrapper(AbstractWrapper):
|
||||||
obj=zobj.appy()"
|
obj=zobj.appy()"
|
||||||
class=":loop.zobj.odd and 'even' or 'odd'">
|
class=":loop.zobj.odd and 'even' or 'odd'">
|
||||||
<td for="column in columns"
|
<td for="column in columns"
|
||||||
var2="widget=column['field']" id=":'field_%s' % field.name"
|
var2="field=column.field" id=":'field_%s' % field.name"
|
||||||
width=":column['width']"
|
width=":column.width"
|
||||||
align=":column['align']">:self.pxQueryField</td>
|
align=":column.align">:tool.pxQueryField</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>''')
|
</table>''')
|
||||||
|
|
||||||
|
@ -100,7 +395,7 @@ class ToolWrapper(AbstractWrapper):
|
||||||
style="padding-top: 25px" var2="obj=zobj.appy()">
|
style="padding-top: 25px" var2="obj=zobj.appy()">
|
||||||
<x var="currentNumber=currentNumber + 1"
|
<x var="currentNumber=currentNumber + 1"
|
||||||
for="column in columns"
|
for="column in columns"
|
||||||
var2="widget = column['field']">:self.pxQueryField</x>
|
var2="field=column.field">:tool.pxQueryField</x>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>''')
|
</table>''')
|
||||||
|
@ -118,13 +413,13 @@ class ToolWrapper(AbstractWrapper):
|
||||||
startNumber=req.get('startNumber', '0');
|
startNumber=req.get('startNumber', '0');
|
||||||
startNumber=int(startNumber);
|
startNumber=int(startNumber);
|
||||||
searchName=req.get('search', '');
|
searchName=req.get('search', '');
|
||||||
searchDescr=ztool.getSearch(className, searchName, descr=True);
|
uiSearch=ztool.getSearch(className, searchName, ui=True);
|
||||||
sortKey=req.get('sortKey', '');
|
sortKey=req.get('sortKey', '');
|
||||||
sortOrder=req.get('sortOrder', 'asc');
|
sortOrder=req.get('sortOrder', 'asc');
|
||||||
filterKey=req.get('filterKey', '');
|
filterKey=req.get('filterKey', '');
|
||||||
filterValue=req.get('filterValue', '');
|
filterValue=req.get('filterValue', '');
|
||||||
queryResult=ztool.executeQuery(className, \
|
queryResult=ztool.executeQuery(className, \
|
||||||
search=searchDescr['search'], startNumber=startNumber, \
|
search=uiSearch.search, startNumber=startNumber, \
|
||||||
remember=True, sortBy=sortKey, sortOrder=sortOrder, \
|
remember=True, sortBy=sortKey, sortOrder=sortOrder, \
|
||||||
filterKey=filterKey, filterValue=filterValue, \
|
filterKey=filterKey, filterValue=filterValue, \
|
||||||
refObject=refObject, refField=refField);
|
refObject=refObject, refField=refField);
|
||||||
|
@ -136,25 +431,27 @@ class ToolWrapper(AbstractWrapper):
|
||||||
navBaseCall='askQueryResult(%s,%s,%s,%s,**v**)' % \
|
navBaseCall='askQueryResult(%s,%s,%s,%s,**v**)' % \
|
||||||
(q(ajaxHookId), q(ztool.absolute_url()), q(className), \
|
(q(ajaxHookId), q(ztool.absolute_url()), q(className), \
|
||||||
q(searchName));
|
q(searchName));
|
||||||
newSearchUrl='%s/ui/search?className=%s%s' % \
|
showNewSearch=showNewSearch|True;
|
||||||
|
enableLinks=enableLinks|True;
|
||||||
|
newSearchUrl='%s/search?className=%s%s' % \
|
||||||
(ztool.absolute_url(), className, refUrlPart);
|
(ztool.absolute_url(), className, refUrlPart);
|
||||||
showSubTitles=req.get('showSubTitles', 'true') == 'true';
|
showSubTitles=req.get('showSubTitles', 'true') == 'true';
|
||||||
resultMode=ztool.getResultMode(className)">
|
resultMode=ztool.getResultMode(className)">
|
||||||
|
|
||||||
<x if="zobjects">
|
<x if="zobjects">
|
||||||
<!-- Display here POD templates if required. -->
|
<!-- Display here POD templates if required. -->
|
||||||
<table var="widgets=ztool.getResultPodFields(className);
|
<table var="fields=ztool.getResultPodFields(className);
|
||||||
layoutType='view'"
|
layoutType='view'"
|
||||||
if="zobjects and widgets" align=":dright">
|
if="zobjects and fields" align=":dright">
|
||||||
<tr>
|
<tr>
|
||||||
<td var="zobj=zobjects[0]; obj=zobj.appy()"
|
<td var="zobj=zobjects[0]; obj=zobj.appy()"
|
||||||
for="field in widgets">:field.pxView</td>
|
for="field in fields">:field.pxView</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- The title of the search -->
|
<!-- The title of the search -->
|
||||||
<p>
|
<p>
|
||||||
<x>:searchDescr['translated']</x> (<x>:totalNumber</x>)
|
<x>:uiSearch.translated</x> (<x>:totalNumber</x>)
|
||||||
<x if="showNewSearch and (searchName == 'customSearch')"> —
|
<x if="showNewSearch and (searchName == 'customSearch')"> —
|
||||||
<i><a href=":newSearchUrl">:_('search_new')</a></i>
|
<i><a href=":newSearchUrl">:_('search_new')</a></i>
|
||||||
</x>
|
</x>
|
||||||
|
@ -162,41 +459,37 @@ class ToolWrapper(AbstractWrapper):
|
||||||
<table width="100%">
|
<table width="100%">
|
||||||
<tr>
|
<tr>
|
||||||
<!-- Search description -->
|
<!-- Search description -->
|
||||||
<td if="searchDescr['translatedDescr']">
|
<td if="uiSearch.translatedDescr">
|
||||||
<span class="discreet">:searchDescr['translatedDescr']</span><br/>
|
<span class="discreet">:uiSearch.translatedDescr</span><br/>
|
||||||
</td>
|
</td>
|
||||||
<!-- Appy (top) navigation -->
|
<!-- (Top) navigation -->
|
||||||
<td align=":dright" width="25%"><x>:self.pxAppyNavigate</x></td>
|
<td align=":dright" width="25%"><x>:tool.pxNavigate</x></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- Results, as a list or grid -->
|
<!-- Results, as a list or grid -->
|
||||||
<x var="columnLayouts=ztool.getResultColumnsLayouts(className, refInfo);
|
<x var="columnLayouts=ztool.getResultColumnsLayouts(className, refInfo);
|
||||||
columns=zobjects[0].getColumnsSpecifiers(columnLayouts, dir);
|
columns=ztool.getColumnsSpecifiers(className,columnLayouts, dir);
|
||||||
currentNumber=0">
|
currentNumber=0">
|
||||||
<x if="resultMode == 'list'">:self.pxQueryResultList</x>
|
<x if="resultMode == 'list'">:tool.pxQueryResultList</x>
|
||||||
<x if="resultMode != 'list'">:self.pxQueryResultGrid</x>
|
<x if="resultMode != 'list'">:tool.pxQueryResultGrid</x>
|
||||||
</x>
|
</x>
|
||||||
|
|
||||||
<!-- Appy (bottom) navigation -->
|
<!-- (Bottom) navigation -->
|
||||||
<x>:self.pxAppyNavigate</x>
|
<x>:tool.pxNavigate</x>
|
||||||
</x>
|
</x>
|
||||||
|
|
||||||
<x if="not zobjects">
|
<x if="not zobjects">
|
||||||
<x>:_('query_no_result')></x>
|
<x>:_('query_no_result')</x>
|
||||||
<x if="showNewSearch and (searchName == 'customSearch')"><br/>
|
<x if="showNewSearch and (searchName == 'customSearch')"><br/>
|
||||||
<i class="discreet"><a href=":newSearchUrl">:_('search_new')</a></i></x>
|
<i class="discreet"><a href=":newSearchUrl">:_('search_new')</a></i></x>
|
||||||
</x>
|
</x>
|
||||||
</div>''')
|
</div>''')
|
||||||
|
|
||||||
pxQuery = Px('''
|
pxQuery = Px('''
|
||||||
<x var="className=req['className'];
|
<x var="className=req['className']; searchName=req.get('search', '');
|
||||||
searchName=req.get('search', '');
|
cssJs=None">
|
||||||
cssJs=None;
|
<x>:tool.pxPagePrologue</x><x>:tool.pxQueryResult</x>
|
||||||
showNewSearch=True;
|
|
||||||
showHeaders=True;
|
|
||||||
enableLinks=True">
|
|
||||||
<x>:self.pxPagePrologue</x><x>:self.pxQueryResult</x>
|
|
||||||
</x>''', template=AbstractWrapper.pxTemplate, hook='content')
|
</x>''', template=AbstractWrapper.pxTemplate, hook='content')
|
||||||
|
|
||||||
pxSearch = Px('''
|
pxSearch = Px('''
|
||||||
|
@ -204,7 +497,7 @@ class ToolWrapper(AbstractWrapper):
|
||||||
refInfo=req.get('ref', None);
|
refInfo=req.get('ref', None);
|
||||||
searchInfo=ztool.getSearchInfo(className, refInfo);
|
searchInfo=ztool.getSearchInfo(className, refInfo);
|
||||||
cssJs={};
|
cssJs={};
|
||||||
x=ztool.getCssJs(searchInfo['fields'], 'edit', cssJs)">
|
x=ztool.getCssJs(searchInfo.fields, 'edit', cssJs)">
|
||||||
|
|
||||||
<!-- Include type-specific CSS and JS. -->
|
<!-- Include type-specific CSS and JS. -->
|
||||||
<link for="cssFile in cssJs['css']" rel="stylesheet" type="text/css"
|
<link for="cssFile in cssJs['css']" rel="stylesheet" type="text/css"
|
||||||
|
@ -228,10 +521,10 @@ class ToolWrapper(AbstractWrapper):
|
||||||
<td for="field in searchRow"
|
<td for="field in searchRow"
|
||||||
var2="scolspan=field and field.scolspan or 1"
|
var2="scolspan=field and field.scolspan or 1"
|
||||||
colspan=":scolspan"
|
colspan=":scolspan"
|
||||||
width=":'%d%%' % ((100/searchInfo['nbOfColumns'])*scolspan)">
|
width=":'%d%%' % ((100/searchInfo.nbOfColumns)*scolspan)">
|
||||||
<x if="field"
|
<x if="field"
|
||||||
var2="name=field.name;
|
var2="name=field.name;
|
||||||
widgetName='w_%s' % name">field.pxSearch</x>
|
widgetName='w_%s' % name">:field.pxSearch</x>
|
||||||
<br class="discreet"/>
|
<br class="discreet"/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -249,7 +542,7 @@ class ToolWrapper(AbstractWrapper):
|
||||||
<x var="className=req['className'];
|
<x var="className=req['className'];
|
||||||
importElems=ztool.getImportElements(className);
|
importElems=ztool.getImportElements(className);
|
||||||
allAreImported=True">
|
allAreImported=True">
|
||||||
<x>:self.pxPagePrologue</x>
|
<x>:tool.pxPagePrologue</x>
|
||||||
<script type="text/javascript"><![CDATA[
|
<script type="text/javascript"><![CDATA[
|
||||||
var importedElemsShown = false;
|
var importedElemsShown = false;
|
||||||
function toggleViewableElements() {
|
function toggleViewableElements() {
|
||||||
|
@ -303,7 +596,7 @@ class ToolWrapper(AbstractWrapper):
|
||||||
<input type="hidden" name="importPath" value=""/>
|
<input type="hidden" name="importPath" value=""/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<h1>:_('import_title')"></h1><br/>
|
<h1>:_('import_title')</h1><br/>
|
||||||
<table class="list" width="100%">
|
<table class="list" width="100%">
|
||||||
<tr>
|
<tr>
|
||||||
<th for="columnHeader in importElems[0]">
|
<th for="columnHeader in importElems[0]">
|
||||||
|
|
|
@ -29,11 +29,11 @@ class TranslationWrapper(AbstractWrapper):
|
||||||
# This way, the translator sees the HTML tags and can reproduce them
|
# This way, the translator sees the HTML tags and can reproduce them
|
||||||
# in the translation.
|
# in the translation.
|
||||||
url = self.request['URL']
|
url = self.request['URL']
|
||||||
if url.endswith('/ui/edit') or url.endswith('/do'):
|
if url.endswith('/edit') or url.endswith('/do'):
|
||||||
sourceMsg = sourceMsg.replace('<','<').replace('>','>')
|
sourceMsg = sourceMsg.replace('<','<').replace('>','>')
|
||||||
sourceMsg = sourceMsg.replace('\n', '<br/>')
|
sourceMsg = sourceMsg.replace('\n', '<br/>')
|
||||||
return '<div class="translationLabel"><acronym title="%s" ' \
|
return '<div class="translationLabel"><acronym title="%s" ' \
|
||||||
'style="margin-right: 5px"><img src="ui/help.png"/></acronym>' \
|
'style="margin-right: 5px"><img src="help.png"/></acronym>' \
|
||||||
'%s</div>' % (fieldName, sourceMsg)
|
'%s</div>' % (fieldName, sourceMsg)
|
||||||
|
|
||||||
def show(self, field):
|
def show(self, field):
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
from appy.gen import WorkflowOwner
|
from appy.gen import WorkflowOwner
|
||||||
from appy.gen.wrappers import AbstractWrapper
|
from appy.gen.wrappers import AbstractWrapper
|
||||||
|
from appy.gen import utils as gutils
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
class UserWrapper(AbstractWrapper):
|
class UserWrapper(AbstractWrapper):
|
||||||
|
@ -55,7 +56,7 @@ class UserWrapper(AbstractWrapper):
|
||||||
if self.o.isTemporary(): return 'edit'
|
if self.o.isTemporary(): return 'edit'
|
||||||
# When the user itself (we don't check role Owner because a Manager can
|
# When the user itself (we don't check role Owner because a Manager can
|
||||||
# also own a User instance) wants to edit information about himself.
|
# also own a User instance) wants to edit information about himself.
|
||||||
if self.user.getId() == self.login: return 'edit'
|
if self.user.login == self.login: return 'edit'
|
||||||
|
|
||||||
def setPassword(self, newPassword=None):
|
def setPassword(self, newPassword=None):
|
||||||
'''Sets a p_newPassword for self. If p_newPassword is not given, we
|
'''Sets a p_newPassword for self. If p_newPassword is not given, we
|
||||||
|
@ -67,16 +68,15 @@ class UserWrapper(AbstractWrapper):
|
||||||
newPassword = self.getField('password1').generatePassword()
|
newPassword = self.getField('password1').generatePassword()
|
||||||
msgPart = 'generated'
|
msgPart = 'generated'
|
||||||
login = self.login
|
login = self.login
|
||||||
zopeUser = self.o.acl_users.getUserById(login)
|
zopeUser = self.getZopeUser()
|
||||||
tool = self.tool.o
|
tool = self.tool.o
|
||||||
zopeUser.__ = tool._encryptPassword(newPassword)
|
zopeUser.__ = tool._encryptPassword(newPassword)
|
||||||
if self.user.getId() == login:
|
if self.user.login == login:
|
||||||
# The user for which we change the password is the currently logged
|
# The user for which we change the password is the currently logged
|
||||||
# user. So update the authentication cookie, too.
|
# user. So update the authentication cookie, too.
|
||||||
tool._updateCookie(login, newPassword)
|
gutils.writeCookie(login, newPassword, self.request)
|
||||||
loggedUser = self.user.getId() or 'system|anon'
|
|
||||||
self.log('Password %s by "%s" for "%s".' % \
|
self.log('Password %s by "%s" for "%s".' % \
|
||||||
(msgPart, loggedUser, login))
|
(msgPart, self.user.login, login))
|
||||||
return newPassword
|
return newPassword
|
||||||
|
|
||||||
def checkPassword(self, clearPassword):
|
def checkPassword(self, clearPassword):
|
||||||
|
@ -91,7 +91,7 @@ class UserWrapper(AbstractWrapper):
|
||||||
self.login = newLogin
|
self.login = newLogin
|
||||||
# Update the corresponding Zope-level user
|
# Update the corresponding Zope-level user
|
||||||
aclUsers = self.o.acl_users
|
aclUsers = self.o.acl_users
|
||||||
zopeUser = aclUsers.getUserById(oldLogin)
|
zopeUser = aclUsers.getUser(oldLogin)
|
||||||
zopeUser.name = newLogin
|
zopeUser.name = newLogin
|
||||||
del aclUsers.data[oldLogin]
|
del aclUsers.data[oldLogin]
|
||||||
aclUsers.data[newLogin] = zopeUser
|
aclUsers.data[newLogin] = zopeUser
|
||||||
|
@ -99,8 +99,8 @@ class UserWrapper(AbstractWrapper):
|
||||||
email = self.email
|
email = self.email
|
||||||
if email == oldLogin:
|
if email == oldLogin:
|
||||||
self.email = newLogin
|
self.email = newLogin
|
||||||
# Update the title, which is the login
|
# Update the title
|
||||||
self.title = newLogin
|
self.updateTitle()
|
||||||
# Browse all objects of the database and update potential local roles
|
# Browse all objects of the database and update potential local roles
|
||||||
# that referred to the old login.
|
# that referred to the old login.
|
||||||
context = {'nb': 0, 'old': oldLogin, 'new': newLogin}
|
context = {'nb': 0, 'old': oldLogin, 'new': newLogin}
|
||||||
|
@ -109,7 +109,7 @@ class UserWrapper(AbstractWrapper):
|
||||||
expression="ctx['nb'] += obj.o.applyUserIdChange(" \
|
expression="ctx['nb'] += obj.o.applyUserIdChange(" \
|
||||||
"ctx['old'], ctx['new'])")
|
"ctx['old'], ctx['new'])")
|
||||||
self.log("Login '%s' renamed to '%s' by '%s'." % \
|
self.log("Login '%s' renamed to '%s' by '%s'." % \
|
||||||
(oldLogin, newLogin, self.user.getId()))
|
(oldLogin, newLogin, self.user.login))
|
||||||
self.log('Login change: local roles updated in %d object(s).' % \
|
self.log('Login change: local roles updated in %d object(s).' % \
|
||||||
context['nb'])
|
context['nb'])
|
||||||
|
|
||||||
|
@ -133,12 +133,15 @@ class UserWrapper(AbstractWrapper):
|
||||||
if self.login: self.o._oldLogin = self.login
|
if self.login: self.o._oldLogin = self.login
|
||||||
return self._callCustom('validate', new, errors)
|
return self._callCustom('validate', new, errors)
|
||||||
|
|
||||||
def onEdit(self, created):
|
def updateTitle(self):
|
||||||
# Set a title for this user.
|
'''Sets a title for this user.'''
|
||||||
if self.firstName and self.name:
|
if self.firstName and self.name:
|
||||||
self.title = '%s %s' % (self.name, self.firstName)
|
self.title = '%s %s' % (self.name, self.firstName)
|
||||||
else:
|
else:
|
||||||
self.title = self.login
|
self.title = self.login
|
||||||
|
|
||||||
|
def onEdit(self, created):
|
||||||
|
self.updateTitle()
|
||||||
aclUsers = self.o.acl_users
|
aclUsers = self.o.acl_users
|
||||||
login = self.login
|
login = self.login
|
||||||
if created:
|
if created:
|
||||||
|
@ -158,7 +161,7 @@ class UserWrapper(AbstractWrapper):
|
||||||
self.setLogin(oldLogin, login)
|
self.setLogin(oldLogin, login)
|
||||||
del self.o._oldLogin
|
del self.o._oldLogin
|
||||||
# Update roles at the Zope level.
|
# Update roles at the Zope level.
|
||||||
zopeUser = aclUsers.getUserById(login)
|
zopeUser = self.getZopeUser()
|
||||||
zopeUser.roles = self.roles
|
zopeUser.roles = self.roles
|
||||||
# Update the password if the user has entered new ones.
|
# Update the password if the user has entered new ones.
|
||||||
rq = self.request
|
rq = self.request
|
||||||
|
@ -195,11 +198,19 @@ class UserWrapper(AbstractWrapper):
|
||||||
# Call a custom "onDelete" if any.
|
# Call a custom "onDelete" if any.
|
||||||
return self._callCustom('onDelete')
|
return self._callCustom('onDelete')
|
||||||
|
|
||||||
# Methods that are defined on the Zope user class, wrapped on this class.
|
# Standard Zope user methods -----------------------------------------------
|
||||||
def has_role(self, role, obj=None):
|
def has_role(self, role, obj=None):
|
||||||
user = self.user
|
zopeUser = self.request.zopeUser
|
||||||
if obj: return user.has_role(role, obj)
|
if obj: return zopeUser.has_role(role, obj)
|
||||||
return user.has_role(role)
|
return zopeUser.has_role(role)
|
||||||
|
|
||||||
|
def has_permission(self, permission, obj):
|
||||||
|
return self.request.zopeUser.has_permission(permission, obj)
|
||||||
|
|
||||||
|
def getRoles(self):
|
||||||
|
'''This method collects all the roles for this user, not simply
|
||||||
|
user.roles, but also roles inherited from group membership.'''
|
||||||
|
return self.getZopeUser().getRoles()
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -27,126 +27,37 @@ class AbstractWrapper(object):
|
||||||
'''Any real Appy-managed Zope object has a companion object that is an
|
'''Any real Appy-managed Zope object has a companion object that is an
|
||||||
instance of this class.'''
|
instance of this class.'''
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# Buttons for going to next/previous objects if this one is among bunch of
|
||||||
# Navigation-related PXs
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
# Icon for hiding/showing details below the title.
|
|
||||||
pxShowDetails = Px('''
|
|
||||||
<img if="ztool.subTitleIsUsed(className) and (field.name == 'title')"
|
|
||||||
class="clickable" src=":url('toggleDetails')"
|
|
||||||
onclick="toggleSubTitles()"/>''')
|
|
||||||
|
|
||||||
# Displays up/down arrows in a table header column for sorting a given
|
|
||||||
# column. Requires variables "sortable", 'filterable' and 'field'.
|
|
||||||
pxSortAndFilter = Px('''<x>
|
|
||||||
<x if="sortable">
|
|
||||||
<img if="(sortKey != field.name) or (sortOrder == 'desc')"
|
|
||||||
onclick=":navBaseCall.replace('**v**', '0,%s,%s,%s' % \
|
|
||||||
(q(field.name), q('asc'), q(filterKey)))"
|
|
||||||
src=":url('sortDown.gif')" class="clickable"/>
|
|
||||||
<img if="(sortKey != field.name) or (sortOrder == 'asc')"
|
|
||||||
onclick=":navBaseCall.replace('**v**', '0,%s,%s,%s' % \
|
|
||||||
(q(field.name), q('desc'), q(filterKey)))"
|
|
||||||
src=":url('sortUp.gif')" class="clickable"/>
|
|
||||||
</x>
|
|
||||||
<x if="filterable">
|
|
||||||
<input type="text" size="7" id=":'%s_%s' % (ajaxHookId, field.name)"
|
|
||||||
value=":filterKey == field.name and filterValue or ''"/>
|
|
||||||
<img onclick=":navBaseCall.replace('**v**', '0, %s,%s,%s' % \
|
|
||||||
(q(sortKey), q(sortOrder), q(field.name)))"
|
|
||||||
src=":url('funnel')" class="clickable"/>
|
|
||||||
</x></x>''')
|
|
||||||
|
|
||||||
# Buttons for navigating among a list of elements: next,back,first,last...
|
|
||||||
pxAppyNavigate = Px('''
|
|
||||||
<div if="totalNumber > batchSize" align=":dright">
|
|
||||||
<table class="listNavigate"
|
|
||||||
var="mustSortAndFilter=ajaxHookId == 'queryResult';
|
|
||||||
sortAndFilter=mustSortAndFilter and \
|
|
||||||
',%s,%s,%s' % (q(sortKey),q(sortOrder),q(filterKey)) or ''">
|
|
||||||
<tr valign="middle">
|
|
||||||
<!-- Go to the first page -->
|
|
||||||
<td if="(startNumber != 0) and (startNumber != batchSize)"><img
|
|
||||||
class="clickable" src=":url('arrowLeftDouble')"
|
|
||||||
title=":_('goto_first')"
|
|
||||||
onClick=":navBaseCall.replace('**v**', '0'+sortAndFilter)"/></td>
|
|
||||||
|
|
||||||
<!-- Go to the previous page -->
|
|
||||||
<td var="sNumber=startNumber - batchSize" if="startNumber != 0"><img
|
|
||||||
class="clickable" src=":url('arrowLeftSimple')"
|
|
||||||
title=":_('goto_previous')"
|
|
||||||
onClick="navBaseCall.replace('**v**', \
|
|
||||||
str(sNumber)+sortAndFilter)"/></td>
|
|
||||||
|
|
||||||
<!-- Explain which elements are currently shown -->
|
|
||||||
<td class="discreet">
|
|
||||||
<x>:startNumber + 1</x><img src=":url('to')"/>
|
|
||||||
<x>:startNumber + batchNumber</x> <b>//</b>
|
|
||||||
<x>:totalNumber</x> </td>
|
|
||||||
|
|
||||||
<!-- Go to the next page -->
|
|
||||||
<td var="sNumber=startNumber + batchSize"
|
|
||||||
if="sNumber < totalNumber"><img class="clickable"
|
|
||||||
src=":url('arrowRightSimple')" title=":_('goto_next')"
|
|
||||||
onClick=":navBaseCall.replace('**v**', \
|
|
||||||
str(sNumber)+sortAndFilter)"/></td>
|
|
||||||
|
|
||||||
<!-- Go to the last page -->
|
|
||||||
<td var="lastPageIsIncomplete=totalNumber % batchSize;
|
|
||||||
nbOfCompletePages=totalNumber/batchSize;
|
|
||||||
nbOfCountedPages=lastPageIsIncomplete and \
|
|
||||||
nbOfCompletePages or nbOfCompletePages-1;
|
|
||||||
sNumber= nbOfCountedPages * batchSize"
|
|
||||||
if="(startNumber != sNumber) and \
|
|
||||||
(startNumber != sNumber-batchSize)"><img class="clickable"
|
|
||||||
src=":url('arrowRightDouble')" title=":_('goto_last')"
|
|
||||||
onClick="navBaseCall.replace('**v**', \
|
|
||||||
str(sNumber)+sortAndFilter)"/></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>''')
|
|
||||||
|
|
||||||
# Buttons for going to next/previous elements if this one is among bunch of
|
|
||||||
# referenced or searched objects. currentNumber starts with 1.
|
# referenced or searched objects. currentNumber starts with 1.
|
||||||
pxObjectNavigate = Px('''
|
pxNavigateSiblings = Px('''
|
||||||
<div if="req.get('nav', None)"
|
<div if="req.get('nav', None)" var2="ni=ztool.getNavigationInfo()">
|
||||||
var2="navInfo=ztool.getNavigationInfo();
|
|
||||||
currentNumber=navInfo['currentNumber'];
|
|
||||||
totalNumber=navInfo['totalNumber'];
|
|
||||||
firstUrl=navInfo['firstUrl'];
|
|
||||||
previousUrl=navInfo['previousUrl'];
|
|
||||||
nextUrl=navInfo['nextUrl'];
|
|
||||||
lastUrl=navInfo['lastUrl'];
|
|
||||||
sourceUrl=navInfo['sourceUrl'];
|
|
||||||
backText=navInfo['backText']">
|
|
||||||
|
|
||||||
<!-- Go to the source URL (search or referred object) -->
|
<!-- Go to the source URL (search or referred object) -->
|
||||||
<a if="sourceUrl" href=":sourceUrl"><img
|
<a if="ni.sourceUrl" href=":ni.sourceUrl"><img
|
||||||
var="gotoSource=_('goto_source');
|
var="gotoSource=_('goto_source');
|
||||||
goBack=backText and ('%s - %s' % (backText, gotoSource)) \
|
goBack=ni.backText and ('%s - %s' % (ni.backText, gotoSource)) \
|
||||||
or gotoSource"
|
or gotoSource"
|
||||||
src=":url('gotoSource')" title=":goBack"/></a>
|
src=":url('gotoSource')" title=":goBack"/></a>
|
||||||
|
|
||||||
<!-- Go to the first page -->
|
<!-- Go to the first page -->
|
||||||
<a if="firstUrl" href=":firstUrl"><img title=":_('goto_first')"
|
<a if="ni.firstUrl" href=":ni.firstUrl"><img title=":_('goto_first')"
|
||||||
src=":url('arrowLeftDouble')"/></a>
|
src=":url('arrowLeftDouble')"/></a>
|
||||||
|
|
||||||
<!-- Go to the previous page -->
|
<!-- Go to the previous page -->
|
||||||
<a if="previousUrl" href=":previousUrl"><img title=":_('goto_previous')"
|
<a if="ni.previousUrl" href=":ni.previousUrl"><img
|
||||||
src=":url('arrowLeftSimple')"/></a>
|
title=":_('goto_previous')" src=":url('arrowLeftSimple')"/></a>
|
||||||
|
|
||||||
<!-- Explain which element is currently shown -->
|
<!-- Explain which element is currently shown -->
|
||||||
<span class="discreet">
|
<span class="discreet">
|
||||||
<x>:currentNumber</x> <b>//</b>
|
<x>:ni.currentNumber</x> <b>//</b>
|
||||||
<x>:totalNumber</x>
|
<x>:ni.totalNumber</x>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Go to the next page -->
|
<!-- Go to the next page -->
|
||||||
<a if="nextUrl" href=":nextUrl"><img title=":_('goto_next')"
|
<a if="ni.nextUrl" href=":ni.nextUrl"><img title=":_('goto_next')"
|
||||||
src=":url('arrowRightSimple')"/></a>
|
src=":url('arrowRightSimple')"/></a>
|
||||||
|
|
||||||
<!-- Go to the last page -->
|
<!-- Go to the last page -->
|
||||||
<a if="lastUrl" href=":lastUrl"><img title=":_('goto_last')"
|
<a if="ni.lastUrl" href=":ni.lastUrl"><img title=":_('goto_last')"
|
||||||
src=":url('arrowRightDouble')"/></a>
|
src=":url('arrowRightDouble')"/></a>
|
||||||
</div>''')
|
</div>''')
|
||||||
|
|
||||||
|
@ -164,200 +75,21 @@ class AbstractWrapper(object):
|
||||||
</x>
|
</x>
|
||||||
</td>
|
</td>
|
||||||
<!-- Object navigation -->
|
<!-- Object navigation -->
|
||||||
<td align=":dright">:self.pxObjectNavigate</td>
|
<td align=":dright">:obj.pxNavigateSiblings</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>''')
|
</table>''')
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
# PXs for graphical elements shown on every page
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
# Global elements included in every page.
|
|
||||||
pxPagePrologue = Px('''<x>
|
|
||||||
<!-- Include type-specific CSS and JS. -->
|
|
||||||
<x if="cssJs">
|
|
||||||
<link for="cssFile in cssJs['css']" rel="stylesheet" type="text/css"
|
|
||||||
href=":url(cssFile)"/>
|
|
||||||
<script for="jsFile in cssJs['js']" type="text/javascript"
|
|
||||||
src=":url(jsFile)"></script></x>
|
|
||||||
|
|
||||||
<!-- Javascript messages -->
|
|
||||||
<script type="text/javascript">:ztool.getJavascriptMessages()</script>
|
|
||||||
|
|
||||||
<!-- Global form for deleting an object -->
|
|
||||||
<form id="deleteForm" method="post" action="do">
|
|
||||||
<input type="hidden" name="action" value="Delete"/>
|
|
||||||
<input type="hidden" name="objectUid"/>
|
|
||||||
</form>
|
|
||||||
<!-- Global form for deleting an event from an object's history -->
|
|
||||||
<form id="deleteEventForm" method="post" action="do">
|
|
||||||
<input type="hidden" name="action" value="DeleteEvent"/>
|
|
||||||
<input type="hidden" name="objectUid"/>
|
|
||||||
<input type="hidden" name="eventTime"/>
|
|
||||||
</form>
|
|
||||||
<!-- Global form for unlinking an object -->
|
|
||||||
<form id="unlinkForm" method="post" action="do">
|
|
||||||
<input type="hidden" name="action" value="Unlink"/>
|
|
||||||
<input type="hidden" name="sourceUid"/>
|
|
||||||
<input type="hidden" name="fieldName"/>
|
|
||||||
<input type="hidden" name="targetUid"/>
|
|
||||||
</form>
|
|
||||||
<!-- Global form for unlocking a page -->
|
|
||||||
<form id="unlockForm" method="post" action="do">
|
|
||||||
<input type="hidden" name="action" value="Unlock"/>
|
|
||||||
<input type="hidden" name="objectUid"/>
|
|
||||||
<input type="hidden" name="pageName"/>
|
|
||||||
</form>
|
|
||||||
<!-- Global form for generating a document from a pod template -->
|
|
||||||
<form id="podTemplateForm" name="podTemplateForm" method="post"
|
|
||||||
action=":ztool.absolute_url() + '/generateDocument'">
|
|
||||||
<input type="hidden" name="objectUid"/>
|
|
||||||
<input type="hidden" name="fieldName"/>
|
|
||||||
<input type="hidden" name="podFormat"/>
|
|
||||||
<input type="hidden" name="askAction"/>
|
|
||||||
<input type="hidden" name="queryData"/>
|
|
||||||
<input type="hidden" name="customParams"/>
|
|
||||||
</form>
|
|
||||||
</x>''')
|
|
||||||
|
|
||||||
pxPageBottom = Px('''
|
|
||||||
<script type="text/javascript">initSlaves();</script>''')
|
|
||||||
|
|
||||||
pxPortlet = Px('''
|
|
||||||
<x var="toolUrl=tool.url;
|
|
||||||
queryUrl='%s/ui/query' % toolUrl;
|
|
||||||
currentSearch=req.get('search', None);
|
|
||||||
currentClass=req.get('className', None);
|
|
||||||
currentPage=req['PATH_INFO'].rsplit('/',1)[-1];
|
|
||||||
rootClasses=ztool.getRootClasses();
|
|
||||||
phases=zobj and zobj.getAppyPhases() or None">
|
|
||||||
|
|
||||||
<table class="portletContent"
|
|
||||||
if="zobj and phases and zobj.mayNavigate()"
|
|
||||||
var2="singlePhase=phases and (len(phases) == 1);
|
|
||||||
page=req.get('page', '');
|
|
||||||
mayEdit=zobj.mayEdit()">
|
|
||||||
<x for="phase in phases">:phase['px']</x>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<!-- One section for every searchable root class -->
|
|
||||||
<x for="rootClass in [rc for rc in rootClasses \
|
|
||||||
if ztool.userMaySearch(rc)]">
|
|
||||||
|
|
||||||
<!-- A separator if required -->
|
|
||||||
<div class="portletSep" var="nb=loop.rootClass.nb"
|
|
||||||
if="(nb != 0) or ((nb == 0) and phases)"></div>
|
|
||||||
|
|
||||||
<!-- Section title (link triggers the default search) -->
|
|
||||||
<div class="portletContent"
|
|
||||||
var="searchInfo=ztool.getGroupedSearches(rootClass)">
|
|
||||||
<div class="portletTitle">
|
|
||||||
<a var="queryParam=searchInfo['default'] and \
|
|
||||||
searchInfo['default']['name'] or ''"
|
|
||||||
href=":'%s?className=%s&search=%s' % \
|
|
||||||
(queryUrl,rootClass,queryParam)"
|
|
||||||
class=":(not currentSearch and (currentClass==rootClass) and \
|
|
||||||
(currentPage=='query')) and \
|
|
||||||
'portletCurrent' or ''">::_(rootClass + '_plural')</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Actions -->
|
|
||||||
<x var="addPermission='%s: Add %s' % (appName, rootClass);
|
|
||||||
userMayAdd=user.has_permission(addPermission, appFolder);
|
|
||||||
createMeans=ztool.getCreateMeans(rootClass)">
|
|
||||||
|
|
||||||
<!-- Create a new object from a web form -->
|
|
||||||
<input type="button" class="button"
|
|
||||||
if="userMayAdd and ('form' in createMeans)"
|
|
||||||
style=":url('buttonAdd', bg=True)" value=":_('query_create')"
|
|
||||||
onclick=":'goto(%s)' % \
|
|
||||||
q('%s/do?action=Create&className=%s' % \
|
|
||||||
(toolUrl, rootClass))"/>
|
|
||||||
|
|
||||||
<!-- Create object(s) by importing data -->
|
|
||||||
<input type="button" class="button"
|
|
||||||
if="userMayAdd and ('import' in createMeans)"
|
|
||||||
style=":url('buttonImport', bg=True)" value=":_('query_import')"
|
|
||||||
onclick=":'goto(%s)' % \
|
|
||||||
q('%s/ui/import?className=%s' % (toolUrl, rootClass))"/>
|
|
||||||
</x>
|
|
||||||
|
|
||||||
<!-- Searches -->
|
|
||||||
<x if="ztool.advancedSearchEnabledFor(rootClass)">
|
|
||||||
|
|
||||||
<!-- Live search -->
|
|
||||||
<form action=":'%s/do' % toolUrl">
|
|
||||||
<input type="hidden" name="action" value="SearchObjects"/>
|
|
||||||
<input type="hidden" name="className" value=":rootClass"/>
|
|
||||||
<table cellpadding="0" cellspacing="0">
|
|
||||||
<tr valign="bottom">
|
|
||||||
<td><input type="text" size="14" name="w_SearchableText"
|
|
||||||
class="inputSearch"/></td>
|
|
||||||
<td>
|
|
||||||
<input type="image" class="clickable" src=":url('search.gif')"
|
|
||||||
title=":_('search_button')"/></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- Advanced search -->
|
|
||||||
<div var="highlighted=(currentClass == rootClass) and \
|
|
||||||
(currentPage == 'search')"
|
|
||||||
class=":highlighted and 'portletSearch portletCurrent' or \
|
|
||||||
'portletSearch'"
|
|
||||||
align=":dright">
|
|
||||||
<a var="text=_('search_title')" style="font-size: 88%"
|
|
||||||
href=":'%s/ui/search?className=%s' % (toolUrl, rootClass)"
|
|
||||||
title=":text"><x>:text</x>...</a>
|
|
||||||
</div>
|
|
||||||
</x>
|
|
||||||
|
|
||||||
<!-- Predefined searches -->
|
|
||||||
<x for="widget in searchInfo['searches']">
|
|
||||||
<x if="widget['type']=='group'">:widget['px']</x>
|
|
||||||
<x if="widget['type']!='group'" var2="search=widget">:search['px']</x>
|
|
||||||
</x>
|
|
||||||
</div>
|
|
||||||
</x>
|
|
||||||
</x>''')
|
|
||||||
|
|
||||||
# The message that is shown when a user triggers an action.
|
|
||||||
pxMessage = Px('''
|
|
||||||
<div var="messages=ztool.consumeMessages()" if="messages" class="message">
|
|
||||||
<!-- The icon for closing the message -->
|
|
||||||
<img src=":url('close')" align=":dright" class="clickable"
|
|
||||||
onclick="this.parentNode.style.display='none'"/>
|
|
||||||
<!-- The message content -->
|
|
||||||
<x>::messages</x>
|
|
||||||
</div>''')
|
|
||||||
|
|
||||||
# The page footer.
|
|
||||||
pxFooter = Px('''
|
|
||||||
<table cellpadding="0" cellspacing="0" width="100%" class="footer">
|
|
||||||
<tr>
|
|
||||||
<td align=":dright">Made with
|
|
||||||
<a href="http://appyframework.org" target="_blank">Appy</a></td></tr>
|
|
||||||
</table>''')
|
|
||||||
|
|
||||||
# Hook for defining a PX that proposes additional links, after the links
|
|
||||||
# corresponding to top-level pages.
|
|
||||||
pxLinks = ''
|
|
||||||
|
|
||||||
# Hook for defining a PX that proposes additional icons after standard
|
|
||||||
# icons in the user strip.
|
|
||||||
pxIcons = ''
|
|
||||||
|
|
||||||
# The template PX for all pages.
|
# The template PX for all pages.
|
||||||
pxTemplate = Px('''
|
pxTemplate = Px('''
|
||||||
<html var="tool=self.tool; ztool=tool.o; user=tool.user;
|
<html var="ztool=tool.o; user=tool.user;
|
||||||
isAnon=ztool.userIsAnon(); app=ztool.getApp();
|
obj=obj or ztool.getHomeObject();
|
||||||
|
zobj=obj and obj.o or None;
|
||||||
|
isAnon=user.login=='anon'; app=ztool.getApp();
|
||||||
appFolder=app.data; url = ztool.getIncludeUrl;
|
appFolder=app.data; url = ztool.getIncludeUrl;
|
||||||
appName=ztool.getAppName(); _=ztool.translate;
|
appName=ztool.getAppName(); _=ztool.translate;
|
||||||
req=ztool.REQUEST; resp=req.RESPONSE;
|
req=ztool.REQUEST; resp=req.RESPONSE;
|
||||||
lang=ztool.getUserLanguage(); q=ztool.quote;
|
lang=ztool.getUserLanguage(); q=ztool.quote;
|
||||||
layoutType=ztool.getLayoutType();
|
layoutType=ztool.getLayoutType();
|
||||||
zobj=ztool.getPublishedObject(layoutType) or \
|
|
||||||
ztool.getHomeObject();
|
|
||||||
obj = zobj and zobj.appy() or None;
|
|
||||||
showPortlet=ztool.showPortlet(zobj, layoutType);
|
showPortlet=ztool.showPortlet(zobj, layoutType);
|
||||||
dir=ztool.getLanguageDirection(lang);
|
dir=ztool.getLanguageDirection(lang);
|
||||||
discreetLogin=ztool.getProductConfig(True).discreetLogin;
|
discreetLogin=ztool.getProductConfig(True).discreetLogin;
|
||||||
|
@ -436,7 +168,7 @@ class AbstractWrapper(object):
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- Additional links -->
|
<!-- Additional links -->
|
||||||
<x>:self.pxLinks</x>
|
<x>:tool.pxLinks</x>
|
||||||
|
|
||||||
<!-- Top-level pages -->
|
<!-- Top-level pages -->
|
||||||
<a for="page in tool.pages" class="pageLink"
|
<a for="page in tool.pages" class="pageLink"
|
||||||
|
@ -461,7 +193,7 @@ class AbstractWrapper(object):
|
||||||
|
|
||||||
<!-- The message strip -->
|
<!-- The message strip -->
|
||||||
<tr valign="top">
|
<tr valign="top">
|
||||||
<td><div style="position: relative">:self.pxMessage</div></td>
|
<td><div style="position: relative">:tool.pxMessage</div></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<!-- The user strip -->
|
<!-- The user strip -->
|
||||||
|
@ -509,7 +241,7 @@ class AbstractWrapper(object):
|
||||||
title=":_('%sTool' % appName)">
|
title=":_('%sTool' % appName)">
|
||||||
<img src=":url('appyConfig.gif')"/></a>
|
<img src=":url('appyConfig.gif')"/></a>
|
||||||
<!-- Additional icons -->
|
<!-- Additional icons -->
|
||||||
<x>:self.pxIcons</x>
|
<x>:tool.pxIcons</x>
|
||||||
<!-- Log out -->
|
<!-- Log out -->
|
||||||
<a href=":tool.url + '/performLogout'" title=":_('app_logout')">
|
<a href=":tool.url + '/performLogout'" title=":_('app_logout')">
|
||||||
<img src=":url('logout.gif')"/></a>
|
<img src=":url('logout.gif')"/></a>
|
||||||
|
@ -530,14 +262,14 @@ class AbstractWrapper(object):
|
||||||
|
|
||||||
<!-- The navigation strip -->
|
<!-- The navigation strip -->
|
||||||
<tr if="zobj and showPortlet and (layoutType != 'edit')">
|
<tr if="zobj and showPortlet and (layoutType != 'edit')">
|
||||||
<td>:self.pxNavigationStrip</td>
|
<td>:obj.pxNavigationStrip</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<table width="100%" cellpadding="0" cellspacing="0">
|
<table width="100%" cellpadding="0" cellspacing="0">
|
||||||
<tr valign="top">
|
<tr valign="top">
|
||||||
<!-- The portlet -->
|
<!-- The portlet -->
|
||||||
<td if="showPortlet" class="portlet">:self.pxPortlet</td>
|
<td if="showPortlet" class="portlet">:tool.pxPortlet</td>
|
||||||
<!-- Page content -->
|
<!-- Page content -->
|
||||||
<td class="content">:content</td>
|
<td class="content">:content</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -545,7 +277,7 @@ class AbstractWrapper(object):
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<tr><td>:self.pxFooter</td></tr>
|
<tr><td>:tool.pxFooter</td></tr>
|
||||||
</table>
|
</table>
|
||||||
</body>
|
</body>
|
||||||
</html>''', prologue=Px.xhtmlPrologue)
|
</html>''', prologue=Px.xhtmlPrologue)
|
||||||
|
@ -555,7 +287,7 @@ class AbstractWrapper(object):
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
# This PX displays an object's history.
|
# This PX displays an object's history.
|
||||||
pxObjectHistory = Px('''
|
pxHistory = Px('''
|
||||||
<x var="startNumber=req.get'startNumber', 0);
|
<x var="startNumber=req.get'startNumber', 0);
|
||||||
startNumber=int(startNumber);
|
startNumber=int(startNumber);
|
||||||
batchSize=int(req.get('maxPerPage', 5));
|
batchSize=int(req.get('maxPerPage', 5));
|
||||||
|
@ -568,7 +300,7 @@ class AbstractWrapper(object):
|
||||||
(q(ajaxHookId), q(zobj.absolute_url()), batchSize)">
|
(q(ajaxHookId), q(zobj.absolute_url()), batchSize)">
|
||||||
|
|
||||||
<!-- Navigate between history pages -->
|
<!-- Navigate between history pages -->
|
||||||
<x>:self.pxAppyNavigate</x>
|
<x>:tool.pxNavigate</x>
|
||||||
<!-- History -->
|
<!-- History -->
|
||||||
<table width="100%" class="history">
|
<table width="100%" class="history">
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -610,8 +342,8 @@ class AbstractWrapper(object):
|
||||||
<th align=":dleft" width="70%">:_('previous_value')</th>
|
<th align=":dleft" width="70%">:_('previous_value')</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr for="change in event['changes'].items()" valign="top"
|
<tr for="change in event['changes'].items()" valign="top"
|
||||||
var2="appyType=zobj.getAppyType(change[0], asDict=True)">
|
var2="field=zobj.getAppyType(change[0])">
|
||||||
<td>::_(appyType['labelId'])</td>
|
<td>::_(field.labelId)</td>
|
||||||
<td>::change[1][0]</td>
|
<td>::change[1][0]</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
@ -655,7 +387,7 @@ class AbstractWrapper(object):
|
||||||
|
|
||||||
# Displays header information about an object: title, workflow-related info,
|
# Displays header information about an object: title, workflow-related info,
|
||||||
# history...
|
# history...
|
||||||
pxObjectHeader = Px('''
|
pxHeader = Px('''
|
||||||
<div if="not zobj.isTemporary()"
|
<div if="not zobj.isTemporary()"
|
||||||
var2="hasHistory=zobj.hasHistory();
|
var2="hasHistory=zobj.hasHistory();
|
||||||
historyMaxPerPage=req.get('maxPerPage', 5);
|
historyMaxPerPage=req.get('maxPerPage', 5);
|
||||||
|
@ -710,16 +442,16 @@ class AbstractWrapper(object):
|
||||||
</div>''')
|
</div>''')
|
||||||
|
|
||||||
# Shows the range of buttons (next, previous, save,...) and the workflow
|
# Shows the range of buttons (next, previous, save,...) and the workflow
|
||||||
# transitions.
|
# transitions for a given object.
|
||||||
pxObjectButtons = Px('''
|
pxButtons = Px('''
|
||||||
<table cellpadding="2" cellspacing="0" style="margin-top: 7px"
|
<table cellpadding="2" cellspacing="0" style="margin-top: 7px"
|
||||||
var="previousPage=zobj.getPreviousPage(phaseInfo, page)[0];
|
var="previousPage=phaseObj.getPreviousPage(page)[0];
|
||||||
nextPage=zobj.getNextPage(phaseInfo, page)[0];
|
nextPage=phaseObj.getNextPage(page)[0];
|
||||||
isEdit=layoutType == 'edit';
|
isEdit=layoutType == 'edit';
|
||||||
pageInfo=phaseInfo['pagesInfo'][page]">
|
pageInfo=phaseObj.pagesInfo[page]">
|
||||||
<tr>
|
<tr>
|
||||||
<!-- Previous -->
|
<!-- Previous -->
|
||||||
<td if="previousPage and pageInfo['showPrevious']">
|
<td if="previousPage and pageInfo.showPrevious">
|
||||||
<!-- Button on the edit page -->
|
<!-- Button on the edit page -->
|
||||||
<x if="isEdit">
|
<x if="isEdit">
|
||||||
<input type="button" class="button" value=":_('page_previous')"
|
<input type="button" class="button" value=":_('page_previous')"
|
||||||
|
@ -735,20 +467,20 @@ class AbstractWrapper(object):
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Save -->
|
<!-- Save -->
|
||||||
<td if="isEdit and pageInfo['showSave']">
|
<td if="isEdit and pageInfo.showSave">
|
||||||
<input type="button" class="button" onClick="submitAppyForm('save')"
|
<input type="button" class="button" onClick="submitAppyForm('save')"
|
||||||
style=":url(buttonSave', bg=True)" value=":_('object_save')"/>
|
style=":url('buttonSave', bg=True)" value=":_('object_save')"/>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Cancel -->
|
<!-- Cancel -->
|
||||||
<td if="isEdit and pageInfo['showCancel']">
|
<td if="isEdit and pageInfo.showCancel">
|
||||||
<input type="button" class="button" onClick="submitAppyForm('cancel')"
|
<input type="button" class="button" onClick="submitAppyForm('cancel')"
|
||||||
style=":url('buttonCancel', bg=True)" value=":_('object_cancel')"/>
|
style=":url('buttonCancel', bg=True)" value=":_('object_cancel')"/>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td if="not isEdit"
|
<td if="not isEdit"
|
||||||
var2="locked=zobj.isLocked(user, page);
|
var2="locked=zobj.isLocked(user, page);
|
||||||
editable=pageInfo['showOnEdit'] and zobj.mayEdit()">
|
editable=pageInfo.showOnEdit and zobj.mayEdit()">
|
||||||
|
|
||||||
<!-- Edit -->
|
<!-- Edit -->
|
||||||
<input type="button" class="button" if="editable and not locked"
|
<input type="button" class="button" if="editable and not locked"
|
||||||
|
@ -765,7 +497,7 @@ class AbstractWrapper(object):
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Next -->
|
<!-- Next -->
|
||||||
<td if="nextPage and pageInfo['showNext']">
|
<td if="nextPage and pageInfo.showNext">
|
||||||
<!-- Button on the edit page -->
|
<!-- Button on the edit page -->
|
||||||
<x if="isEdit">
|
<x if="isEdit">
|
||||||
<input type="button" class="button" onClick="submitAppyForm('next')"
|
<input type="button" class="button" onClick="submitAppyForm('next')"
|
||||||
|
@ -780,7 +512,7 @@ class AbstractWrapper(object):
|
||||||
|
|
||||||
<!-- Workflow transitions -->
|
<!-- Workflow transitions -->
|
||||||
<td var="targetObj=zobj"
|
<td var="targetObj=zobj"
|
||||||
if="targetObj.showTransitions(layoutType)">:self.pxTransitions</td>
|
if="targetObj.showTransitions(layoutType)">:obj.pxTransitions</td>
|
||||||
|
|
||||||
<!-- Refresh -->
|
<!-- Refresh -->
|
||||||
<td if="zobj.isDebug()">
|
<td if="zobj.isDebug()">
|
||||||
|
@ -791,21 +523,29 @@ class AbstractWrapper(object):
|
||||||
</tr>
|
</tr>
|
||||||
</table>''')
|
</table>''')
|
||||||
|
|
||||||
pxLayoutedObject = Px('''<p>Layouted object</p>''')
|
# Displays the fields of a given page for a given object.
|
||||||
|
pxFields = Px('''
|
||||||
|
<table width=":layout['width']">
|
||||||
|
<tr for="field in groupedFields">
|
||||||
|
<td if="field.type == 'group'">:field.pxView</td>
|
||||||
|
<td if="field.type != 'group'">:field.pxRender</td>
|
||||||
|
</tr>
|
||||||
|
</table>''')
|
||||||
|
|
||||||
pxView = Px('''
|
pxView = Px('''
|
||||||
<x var="x=zobj.allows('View', raiseError=True);
|
<x var="x=zobj.allows('View', raiseError=True);
|
||||||
errors=req.get('errors', {});
|
errors=req.get('errors', {});
|
||||||
layout=zobj.getPageLayout(layoutType);
|
layout=zobj.getPageLayout(layoutType);
|
||||||
phaseInfo=zobj.getAppyPhases(currentOnly=True, layoutType='view');
|
phaseObj=zobj.getAppyPhases(currentOnly=True, layoutType='view');
|
||||||
phase=phaseInfo['name'];
|
phase=phaseObj.name;
|
||||||
cssJs={};
|
cssJs={};
|
||||||
page=req.get('page',None) or zobj.getDefaultViewPage();
|
page=req.get('page',None) or zobj.getDefaultViewPage();
|
||||||
x=zobj.removeMyLock(user, page);
|
x=zobj.removeMyLock(user, page);
|
||||||
groupedWidgets=zobj.getGroupedAppyTypes(layoutType, page, \
|
groupedFields=zobj.getGroupedFields(layoutType, page,cssJs=cssJs)">
|
||||||
cssJs=cssJs)">
|
<x>:tool.pxPagePrologue</x>
|
||||||
<x>:self.pxPagePrologue</x>
|
<x var="tagId='pageLayout'; tagName=''; tagCss='';
|
||||||
<x var="tagId='pageLayout'">:self.pxLayoutedObject</x>
|
layoutTarget=obj">:tool.pxLayoutedObject</x>
|
||||||
<x>:self.pxPageBottom</x>
|
<x>:tool.pxPageBottom</x>
|
||||||
</x>''', template=pxTemplate, hook='content')
|
</x>''', template=pxTemplate, hook='content')
|
||||||
|
|
||||||
pxEdit = Px('''
|
pxEdit = Px('''
|
||||||
|
@ -813,15 +553,14 @@ class AbstractWrapper(object):
|
||||||
errors=req.get('errors', None) or {};
|
errors=req.get('errors', None) or {};
|
||||||
layout=zobj.getPageLayout(layoutType);
|
layout=zobj.getPageLayout(layoutType);
|
||||||
cssJs={};
|
cssJs={};
|
||||||
phaseInfo=zobj.getAppyPhases(currentOnly=True, \
|
phaseObj=zobj.getAppyPhases(currentOnly=True, \
|
||||||
layoutType=layoutType);
|
layoutType=layoutType);
|
||||||
phase=phaseInfo['name'];
|
phase=phaseObj.name;
|
||||||
page=req.get('page', None) or zobj.getDefaultEditPage();
|
page=req.get('page', None) or zobj.getDefaultEditPage();
|
||||||
x=zobj.setLock(user, page);
|
x=zobj.setLock(user, page);
|
||||||
confirmMsg=req.get('confirmMsg', None);
|
confirmMsg=req.get('confirmMsg', None);
|
||||||
groupedWidgets=zobj.getGroupedAppyTypes(layoutType, page, \
|
groupedFields=zobj.getGroupedFields(layoutType,page, cssJs=cssJs)">
|
||||||
cssJs=cssJs)">
|
<x>:tool.pxPagePrologue</x>
|
||||||
<x>:self.pxPagePrologue</x>
|
|
||||||
<!-- Warn the user that the form should be left via buttons -->
|
<!-- Warn the user that the form should be left via buttons -->
|
||||||
<script type="text/javascript"><![CDATA[
|
<script type="text/javascript"><![CDATA[
|
||||||
window.onbeforeunload = function(e){
|
window.onbeforeunload = function(e){
|
||||||
|
@ -840,14 +579,45 @@ class AbstractWrapper(object):
|
||||||
<input type="hidden" name="page" value=":page"/>
|
<input type="hidden" name="page" value=":page"/>
|
||||||
<input type="hidden" name="nav" value=":req.get('nav', None)"/>
|
<input type="hidden" name="nav" value=":req.get('nav', None)"/>
|
||||||
<input type="hidden" name="confirmed" value="False"/>
|
<input type="hidden" name="confirmed" value="False"/>
|
||||||
<x var="tagId='pageLayout'">:self.pxLayoutedObject</x>
|
<x var="tagId='pageLayout'; tagName=''; tagCss='';
|
||||||
|
layoutTarget=obj">:tool.pxLayoutedObject</x>
|
||||||
</form>
|
</form>
|
||||||
<script type="text/javascript"
|
<script type="text/javascript"
|
||||||
if="confirmMsg">:'askConfirm(%s,%s,%s)' % \
|
if="confirmMsg">:'askConfirm(%s,%s,%s)' % \
|
||||||
(q('script'), q('postConfirmedEditForm()'), q(confirmMsg))</script>
|
(q('script'), q('postConfirmedEditForm()'), q(confirmMsg))</script>
|
||||||
<x>:self.pxPageBottom</x>
|
<x>:tool.pxPageBottom</x>
|
||||||
</x>''', template=pxTemplate, hook='content')
|
</x>''', template=pxTemplate, hook='content')
|
||||||
|
|
||||||
|
# PX called via asynchronous requests from the browser. Keys "Expires" and
|
||||||
|
# "CacheControl" are used to prevent IE to cache returned pages (which is
|
||||||
|
# the default IE behaviour with Ajax requests).
|
||||||
|
pxAjax = Px('''
|
||||||
|
<x var="zobj=obj.o; ztool=tool.o; user=tool.user;
|
||||||
|
isAnon=user.login == 'anon'; app=ztool.getApp();
|
||||||
|
appFolder=app.data; url = ztool.getIncludeUrl;
|
||||||
|
appName=ztool.getAppName(); _=ztool.translate;
|
||||||
|
req=ztool.REQUEST; resp=req.RESPONSE;
|
||||||
|
lang=ztool.getUserLanguage(); q=ztool.quote;
|
||||||
|
action=req.get('action', None);
|
||||||
|
px=req['px'].split(':');
|
||||||
|
field=(len(px) == 2) and zobj.getAppyType(px[0]) or None;
|
||||||
|
dir=ztool.getLanguageDirection(lang);
|
||||||
|
dleft=(dir == 'ltr') and 'left' or 'right';
|
||||||
|
dright=(dir == 'ltr') and 'right' or 'left';
|
||||||
|
x=resp.setHeader('Content-type', ztool.xhtmlEncoding);
|
||||||
|
x=resp.setHeader('Expires', 'Thu, 11 Dec 1975 12:05:00 GMT+2');
|
||||||
|
x=resp.setHeader('Content-Language', lang);
|
||||||
|
x=resp.setHeader('CacheControl', 'no-cache')">
|
||||||
|
|
||||||
|
<!-- If an action is defined, execute it on p_zobj or on p_field. -->
|
||||||
|
<x if="action and not field" var2="x=getattr(zobj, action)()"></x>
|
||||||
|
<x if="action and field" var2="x=getattr(field, action)(zobj)"></x>
|
||||||
|
|
||||||
|
<!-- Then, call the PX on p_obj or on p_field. -->
|
||||||
|
<x if="not field">:getattr(obj, px[0])</x>
|
||||||
|
<x if="field">:getattr(field, px[1])</x>
|
||||||
|
</x>''')
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# Class methods
|
# Class methods
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
@ -936,15 +706,7 @@ class AbstractWrapper(object):
|
||||||
o = self.o
|
o = self.o
|
||||||
key = o.workflow_history.keys()[0]
|
key = o.workflow_history.keys()[0]
|
||||||
return o.workflow_history[key]
|
return o.workflow_history[key]
|
||||||
elif name == 'user':
|
elif name == 'user': return self.o.getTool().getUser()
|
||||||
return self.o.getUser()
|
|
||||||
elif name == 'appyUser':
|
|
||||||
user = self.search1('User', noSecurity=True,
|
|
||||||
login=self.o.getUser().getId())
|
|
||||||
if user: return user
|
|
||||||
if self.o.getUser().getUserName() == 'System Processes':
|
|
||||||
return self.search1('User', noSecurity=True, login='admin')
|
|
||||||
return
|
|
||||||
elif name == 'fields': return self.o.getAllAppyTypes()
|
elif name == 'fields': return self.o.getAllAppyTypes()
|
||||||
elif name == 'siteUrl': return self.o.getTool().getSiteUrl()
|
elif name == 'siteUrl': return self.o.getTool().getSiteUrl()
|
||||||
# Now, let's try to return a real attribute.
|
# Now, let's try to return a real attribute.
|
||||||
|
|
|
@ -77,11 +77,25 @@ class BufferAction:
|
||||||
dumpTb=dumpTb)
|
dumpTb=dumpTb)
|
||||||
self.buffer.evaluate(result, context)
|
self.buffer.evaluate(result, context)
|
||||||
|
|
||||||
|
def _evalExpr(self, expr, context):
|
||||||
|
'''Evaluates p_expr with p_context. p_expr can contain an error expr,
|
||||||
|
in the form "someExpr|errorExpr". If it is the case, if the "normal"
|
||||||
|
expr raises an error, the "error" expr is evaluated instead.'''
|
||||||
|
if '|' not in expr:
|
||||||
|
res = eval(expr, context)
|
||||||
|
else:
|
||||||
|
expr, errorExpr = expr.rsplit('|', 1)
|
||||||
|
try:
|
||||||
|
res = eval(expr, context)
|
||||||
|
except Exception:
|
||||||
|
res = eval(errorExpr, context)
|
||||||
|
return res
|
||||||
|
|
||||||
def evaluateExpression(self, result, context, expr):
|
def evaluateExpression(self, result, context, expr):
|
||||||
'''Evaluates expression p_expr with the current p_context. Returns a
|
'''Evaluates expression p_expr with the current p_context. Returns a
|
||||||
tuple (result, errorOccurred).'''
|
tuple (result, errorOccurred).'''
|
||||||
try:
|
try:
|
||||||
res = eval(expr, context)
|
res = self._evalExpr(expr, context)
|
||||||
error = False
|
error = False
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
res = None
|
res = None
|
||||||
|
@ -169,7 +183,10 @@ class ForAction(BufferAction):
|
||||||
self.iter = iter # Name of the iterator variable used in the each loop
|
self.iter = iter # Name of the iterator variable used in the each loop
|
||||||
|
|
||||||
def initialiseLoop(self, context, elems):
|
def initialiseLoop(self, context, elems):
|
||||||
'''Initialises information about the loop, before entering into it.'''
|
'''Initialises information about the loop, before entering into it. It
|
||||||
|
is possible that this loop overrides an outer loop whose iterator
|
||||||
|
has the same name. This method returns a tuple
|
||||||
|
(loop, outerOverriddenLoop).'''
|
||||||
# The "loop" object, made available in the POD context, contains info
|
# The "loop" object, made available in the POD context, contains info
|
||||||
# about all currently walked loops. For every walked loop, a specific
|
# about all currently walked loops. For every walked loop, a specific
|
||||||
# object, le'ts name it curLoop, accessible at getattr(loop, self.iter),
|
# object, le'ts name it curLoop, accessible at getattr(loop, self.iter),
|
||||||
|
@ -195,11 +212,17 @@ class ForAction(BufferAction):
|
||||||
context['loop'] = Object()
|
context['loop'] = Object()
|
||||||
try:
|
try:
|
||||||
total = len(elems)
|
total = len(elems)
|
||||||
except:
|
except Exception:
|
||||||
total = 0
|
total = 0
|
||||||
curLoop = Object(length=total)
|
curLoop = Object(length=total)
|
||||||
|
# Does this loop overrides an outer loop whose iterator has the same
|
||||||
|
# name ?
|
||||||
|
outerLoop = None
|
||||||
|
if hasattr(context['loop'], self.iter):
|
||||||
|
outerLoop = getattr(context['loop'], self.iter)
|
||||||
|
# Put this loop in the global object "loop".
|
||||||
setattr(context['loop'], self.iter, curLoop)
|
setattr(context['loop'], self.iter, curLoop)
|
||||||
return curLoop
|
return curLoop, outerLoop
|
||||||
|
|
||||||
def do(self, result, context, elems):
|
def do(self, result, context, elems):
|
||||||
'''Performs the "for" action. p_elems is the list of elements to
|
'''Performs the "for" action. p_elems is the list of elements to
|
||||||
|
@ -229,7 +252,7 @@ class ForAction(BufferAction):
|
||||||
if not elems:
|
if not elems:
|
||||||
result.dumpElement(Cell.OD.elem)
|
result.dumpElement(Cell.OD.elem)
|
||||||
# Enter the "for" loop.
|
# Enter the "for" loop.
|
||||||
loop = self.initialiseLoop(context, elems)
|
loop, outerLoop = self.initialiseLoop(context, elems)
|
||||||
i = -1
|
i = -1
|
||||||
for item in elems:
|
for item in elems:
|
||||||
i += 1
|
i += 1
|
||||||
|
@ -277,11 +300,13 @@ class ForAction(BufferAction):
|
||||||
context[self.iter] = ''
|
context[self.iter] = ''
|
||||||
for i in range(nbOfMissingCellsLastLine):
|
for i in range(nbOfMissingCellsLastLine):
|
||||||
self.buffer.evaluate(result, context, subElements=False)
|
self.buffer.evaluate(result, context, subElements=False)
|
||||||
# Delete the object representing info about the current loop.
|
# Delete the current loop object and restore the overridden one if any.
|
||||||
try:
|
try:
|
||||||
delattr(context['loop'], self.iter)
|
delattr(context['loop'], self.iter)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
if outerLoop:
|
||||||
|
setattr(context['loop'], self.iter, outerLoop)
|
||||||
# Restore hidden variable if any
|
# Restore hidden variable if any
|
||||||
if hasHiddenVariable:
|
if hasHiddenVariable:
|
||||||
context[self.iter] = hiddenVariable
|
context[self.iter] = hiddenVariable
|
||||||
|
|
|
@ -19,6 +19,7 @@ import re
|
||||||
from xml.sax.saxutils import quoteattr
|
from xml.sax.saxutils import quoteattr
|
||||||
from appy.shared.xml_parser import xmlPrologue, escapeXml
|
from appy.shared.xml_parser import xmlPrologue, escapeXml
|
||||||
from appy.pod import PodError
|
from appy.pod import PodError
|
||||||
|
from appy.shared.utils import Traceback
|
||||||
from appy.pod.elements import *
|
from appy.pod.elements import *
|
||||||
from appy.pod.actions import IfAction, ElseAction, ForAction, VariablesAction, \
|
from appy.pod.actions import IfAction, ElseAction, ForAction, VariablesAction, \
|
||||||
NullAction
|
NullAction
|
||||||
|
|
|
@ -74,17 +74,32 @@ class Table(PodElement):
|
||||||
class Expression(PodElement):
|
class Expression(PodElement):
|
||||||
'''Represents a Python expression that is found in a pod or px.'''
|
'''Represents a Python expression that is found in a pod or px.'''
|
||||||
OD = None
|
OD = None
|
||||||
def __init__(self, pyExpr, pod):
|
def extractInfo(self, py):
|
||||||
# The Python expression
|
'''Within p_py, several elements can be included:
|
||||||
self.expr = pyExpr.strip()
|
- the fact that XML chars must be escaped or not (leading ":")
|
||||||
self.pod = pod # True if I work for pod, False if I work for px.
|
- the "normal" Python expression,
|
||||||
# Must we, when evaluating the expression, escape XML special chars
|
- an optional "error" expression, that is evaluated when the normal
|
||||||
# or not?
|
expression raises an exception.
|
||||||
if self.expr.startswith(':'):
|
This method return a tuple (escapeXml, normaExpr, errorExpr).'''
|
||||||
self.expr = self.expr[1:]
|
# Determine if we must escape XML chars or not.
|
||||||
self.escapeXml = False
|
escapeXml = True
|
||||||
|
if py.startswith(':'):
|
||||||
|
py = py[1:]
|
||||||
|
escapeXml = False
|
||||||
|
# Extract normal and error expression
|
||||||
|
if '|' not in py:
|
||||||
|
expr = py
|
||||||
|
errorExpr = None
|
||||||
else:
|
else:
|
||||||
self.escapeXml = True
|
expr, errorExpr = py.rsplit('|', 1)
|
||||||
|
expr = expr.strip()
|
||||||
|
errorExpr = errorExpr.strip()
|
||||||
|
return escapeXml, expr, errorExpr
|
||||||
|
|
||||||
|
def __init__(self, py, pod):
|
||||||
|
# Extract parts from expression p_py.
|
||||||
|
self.escapeXml, self.expr, self.errorExpr = self.extractInfo(py.strip())
|
||||||
|
self.pod = pod # True if I work for pod, False if I work for px.
|
||||||
if self.pod:
|
if self.pod:
|
||||||
# pod-only: store here the expression's true result (before being
|
# pod-only: store here the expression's true result (before being
|
||||||
# converted to a string).
|
# converted to a string).
|
||||||
|
@ -98,6 +113,18 @@ class Expression(PodElement):
|
||||||
# self.result and self.evaluated are not used by PX, because they
|
# self.result and self.evaluated are not used by PX, because they
|
||||||
# are not thread-safe.
|
# are not thread-safe.
|
||||||
|
|
||||||
|
def _eval(self, context):
|
||||||
|
'''Evaluates self.expr with p_context. If self.errorExpr is defined,
|
||||||
|
evaluate it if self.expr raises an error.'''
|
||||||
|
if self.errorExpr:
|
||||||
|
try:
|
||||||
|
res = eval(self.expr, context)
|
||||||
|
except Exception:
|
||||||
|
res = eval(self.errorExpr, context)
|
||||||
|
else:
|
||||||
|
res = eval(self.expr, context)
|
||||||
|
return res
|
||||||
|
|
||||||
def evaluate(self, context):
|
def evaluate(self, context):
|
||||||
'''Evaluates the Python expression (self.expr) with a given
|
'''Evaluates the Python expression (self.expr) with a given
|
||||||
p_context, and returns the result. More precisely, it returns a
|
p_context, and returns the result. More precisely, it returns a
|
||||||
|
@ -114,8 +141,7 @@ class Expression(PodElement):
|
||||||
# with another context.
|
# with another context.
|
||||||
self.evaluated = False
|
self.evaluated = False
|
||||||
else:
|
else:
|
||||||
# Evaluates the Python expression
|
res = self._eval(context)
|
||||||
res = eval(self.expr, context)
|
|
||||||
# pod-only: cache the expression result.
|
# pod-only: cache the expression result.
|
||||||
if self.pod: self.result = res
|
if self.pod: self.result = res
|
||||||
# Converts the expr result to a string that can be inserted in the
|
# Converts the expr result to a string that can be inserted in the
|
||||||
|
|
3647
pod/test/Tests.rtf
3647
pod/test/Tests.rtf
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue