[gen] More work ZPT->PX.
This commit is contained in:
parent
2e9a832463
commit
34e3a3083e
|
@ -39,4 +39,8 @@ class Object:
|
|||
def __nonzero__(self):
|
||||
return bool(self.__dict__)
|
||||
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",
|
||||
help="The path to the Python interpreter running "\
|
||||
"Zope",
|
||||
default='python2.4',metavar="REPOZO",type='string')
|
||||
default='python2.4',metavar="PYTHON",type='string')
|
||||
optParser.add_option("-r", "--repozo", dest="repozo",
|
||||
help="The path to repozo.py",
|
||||
default='', metavar="REPOZO", type='string')
|
||||
|
|
|
@ -19,6 +19,8 @@ import copy, types, re
|
|||
from appy.gen.layout import Table, defaultFieldLayouts
|
||||
from appy.gen import utils as gutils
|
||||
from appy.shared import utils as sutils
|
||||
from appy.px import Px
|
||||
from appy import Object
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
nullValues = (None, '', [])
|
||||
|
@ -108,18 +110,167 @@ class Page:
|
|||
return res
|
||||
|
||||
def getInfo(self, obj, layoutType):
|
||||
'''Gets information about this page, for p_obj, as a dict.'''
|
||||
res = {}
|
||||
'''Gets information about this page, for p_obj, as an object.'''
|
||||
res = Object()
|
||||
for elem in Page.subElements:
|
||||
res['show%s' % elem.capitalize()] = self.isShowable(obj, layoutType,
|
||||
elem=elem)
|
||||
setattr(res, 'show%s' % elem.capitalize(), \
|
||||
self.isShowable(obj, layoutType, elem=elem))
|
||||
return res
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Phase. Pages are grouped into phases.
|
||||
# ------------------------------------------------------------------------------
|
||||
class Phase:
|
||||
'''A group of pages.'''
|
||||
|
||||
pxView = Px('''
|
||||
<tr var="singlePage=len(phase.pages) == 1">
|
||||
<td var="label='%s_phase_%s' % (zobj.meta_type, phase.name)">
|
||||
|
||||
<!-- The title of the phase -->
|
||||
<div class="portletGroup"
|
||||
if="not singlePhase and not singlePage">::_(label)</div>
|
||||
|
||||
<!-- The page(s) within the phase -->
|
||||
<x for="aPage in phase.pages">
|
||||
<!-- First line: page name and icons -->
|
||||
<div if="not (singlePhase and singlePage)"
|
||||
class=":aPage==page and 'portletCurrent portletPage' or \
|
||||
'portletPage'">
|
||||
<a href=":zobj.getUrl(page=aPage)">::_('%s_page_%s' % \
|
||||
(zobj.meta_type, aPage))</a>
|
||||
<x var="locked=zobj.isLocked(user, aPage);
|
||||
editable=mayEdit and phase.pagesInfo[aPage].showOnEdit">
|
||||
<a if="editable and not locked"
|
||||
href=":zobj.getUrl(mode='edit', page=aPage)">
|
||||
<img src=":url('edit')" title=":_('object_edit')"/></a>
|
||||
<a if="editable and locked">
|
||||
<img style="cursor: help"
|
||||
var="lockDate=tool.formatDate(locked[1]);
|
||||
lockMap={'user':ztool.getUserName(locked[0]), \
|
||||
'date':lockDate};
|
||||
lockMsg=_('page_locked', mapping=lockMap)"
|
||||
src=":url('locked')" title=":lockMsg"/></a>
|
||||
<a if="editable and locked and user.has_role('Manager')">
|
||||
<img class="clickable" title=":_('page_unlock')" src=":url('unlock')"
|
||||
onclick=":'onUnlockPage(%s,%s)' % \
|
||||
(q(zobj.UID()), q(aPage))"/></a>
|
||||
</x>
|
||||
</div>
|
||||
<!-- Next lines: links -->
|
||||
<x var="links=phase.pagesInfo[aPage].links" if="links">
|
||||
<div for="link in links"><a href=":link.url">:link.title</a></div>
|
||||
</x>
|
||||
</x>
|
||||
</td>
|
||||
</tr>''')
|
||||
|
||||
def __init__(self, name, obj):
|
||||
self.name = name
|
||||
self.obj = obj
|
||||
# The list of names of pages in this phase
|
||||
self.pages = []
|
||||
# The list of hidden pages in this phase
|
||||
self.hiddenPages = []
|
||||
# The dict below stores info about every page listed in self.pages.
|
||||
self.pagesInfo = {}
|
||||
self.totalNbOfPhases = None
|
||||
# The following attributes allows to browse, from a given page, to the
|
||||
# last page of the previous phase and to the first page of the following
|
||||
# phase if allowed by phase state.
|
||||
self.previousPhase = None
|
||||
self.nextPhase = None
|
||||
|
||||
def addPageLinks(self, field, obj):
|
||||
'''If p_field is a navigable Ref, we must add, within self.pagesInfo,
|
||||
objects linked to p_obj through this ReF as links.'''
|
||||
if field.page.name in self.hiddenPages: return
|
||||
infos = []
|
||||
for ztied in field.getValue(obj, type='zobjects'):
|
||||
infos.append(Object(title=ztied.title, url=ztied.absolute_url()))
|
||||
self.pagesInfo[field.page.name].links = infos
|
||||
|
||||
def addPage(self, field, obj, layoutType):
|
||||
'''Adds page-related information in the phase.'''
|
||||
# If the page is already there, we have nothing more to do.
|
||||
if (field.page.name in self.pages) or \
|
||||
(field.page.name in self.hiddenPages): return
|
||||
# Add the page only if it must be shown.
|
||||
isShowableOnView = field.page.isShowable(obj, 'view')
|
||||
isShowableOnEdit = field.page.isShowable(obj, 'edit')
|
||||
if isShowableOnView or isShowableOnEdit:
|
||||
# The page must be added.
|
||||
self.pages.append(field.page.name)
|
||||
# Create the dict about page information and add it in self.pageInfo
|
||||
pageInfo = Object(page=field.page, showOnView=isShowableOnView,
|
||||
showOnEdit=isShowableOnEdit, links=None)
|
||||
pageInfo.update(field.page.getInfo(obj, layoutType))
|
||||
self.pagesInfo[field.page.name] = pageInfo
|
||||
else:
|
||||
self.hiddenPages.append(field.page.name)
|
||||
|
||||
def computeNextPrevious(self, allPhases):
|
||||
'''This method also fills fields "previousPhase" and "nextPhase"
|
||||
if relevant, based on list of p_allPhases.'''
|
||||
# Identify previous and next phases
|
||||
for phase in allPhases:
|
||||
if phase.name == self.name:
|
||||
i = allPhases.index(phase)
|
||||
if i > 0:
|
||||
self.previousPhase = allPhases[i-1]
|
||||
if i < (len(allPhases)-1):
|
||||
self.nextPhase = allPhases[i+1]
|
||||
|
||||
def getPreviousPage(self, page):
|
||||
'''Returns the page that precedes p_page in this phase.'''
|
||||
try:
|
||||
pageIndex = self.pages.index(page)
|
||||
except ValueError:
|
||||
# The current page is probably not visible anymore. Return the
|
||||
# first available page in current phase.
|
||||
res = self.pages[0]
|
||||
return res, self.pagesInfo[res]
|
||||
if pageIndex > 0:
|
||||
# We stay on the same phase, previous page
|
||||
res = self.pages[pageIndex-1]
|
||||
return res, self.pagesInfo[res]
|
||||
else:
|
||||
if self.previousPhase:
|
||||
# We go to the last page of previous phase
|
||||
previousPhase = self.previousPhase
|
||||
res = previousPhase.pages[-1]
|
||||
return res, previousPhase.pagesInfo[res]
|
||||
else:
|
||||
return None, None
|
||||
|
||||
def getNextPage(self, page):
|
||||
'''Returns the page that follows p_page in this phase.'''
|
||||
try:
|
||||
pageIndex = self.pages.index(page)
|
||||
except ValueError:
|
||||
# The current page is probably not visible anymore. Return the
|
||||
# first available page in current phase.
|
||||
res = self.pages[0]
|
||||
return res, self.pagesInfo[res]
|
||||
if pageIndex < (len(self.pages)-1):
|
||||
# We stay on the same phase, next page
|
||||
res = self.pages[pageIndex+1]
|
||||
return res, self.pagesInfo[res]
|
||||
else:
|
||||
if self.nextPhase:
|
||||
# We go to the first page of next phase
|
||||
nextPhase = self.nextPhase
|
||||
res = nextPhase.pages[0]
|
||||
return res, nextPhase.pagesInfo[res]
|
||||
else:
|
||||
return None, None
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Group. Fields can be grouped.
|
||||
# ------------------------------------------------------------------------------
|
||||
class Group:
|
||||
'''Used for describing a group of widgets within a page.'''
|
||||
'''Used for describing a group of fields within a page.'''
|
||||
def __init__(self, name, columns=['100%'], wide=True, style='section2',
|
||||
hasLabel=True, hasDescr=False, hasHelp=False,
|
||||
hasHeaders=False, group=None, colspan=1, align='center',
|
||||
|
@ -258,27 +409,26 @@ class Group:
|
|||
self.group.generateLabels(messages, classDescr, walkedGroups,
|
||||
forSearch=forSearch)
|
||||
|
||||
def insertInto(self, widgets, groupDescrs, page, metaType, forSearch=False):
|
||||
'''Inserts the GroupDescr instance corresponding to this Group instance
|
||||
into p_widgets, the recursive structure used for displaying all
|
||||
widgets in a given p_page (or all searches), and returns this
|
||||
GroupDescr instance.'''
|
||||
# First, create the corresponding GroupDescr if not already in
|
||||
# p_groupDescrs.
|
||||
if self.name not in groupDescrs:
|
||||
groupDescr = groupDescrs[self.name] = gutils.GroupDescr(\
|
||||
self, page, metaType, forSearch=forSearch).get()
|
||||
# Insert the group at the higher level (ie, directly in p_widgets)
|
||||
def insertInto(self, fields, uiGroups, page, metaType, forSearch=False):
|
||||
'''Inserts the UiGroup instance corresponding to this Group instance
|
||||
into p_fields, the recursive structure used for displaying all
|
||||
fields in a given p_page (or all searches), and returns this
|
||||
UiGroup instance.'''
|
||||
# First, create the corresponding UiGroup if not already in p_uiGroups.
|
||||
if self.name not in uiGroups:
|
||||
uiGroup = uiGroups[self.name] = UiGroup(self, page, metaType,
|
||||
forSearch=forSearch)
|
||||
# Insert the group at the higher level (ie, directly in p_fields)
|
||||
# if the group is not itself in a group.
|
||||
if not self.group:
|
||||
widgets.append(groupDescr)
|
||||
fields.append(uiGroup)
|
||||
else:
|
||||
outerGroupDescr = self.group.insertInto(widgets, groupDescrs,
|
||||
page, metaType, forSearch=forSearch)
|
||||
gutils.GroupDescr.addWidget(outerGroupDescr, groupDescr)
|
||||
outerGroup = self.group.insertInto(fields, uiGroups, page,
|
||||
metaType,forSearch=forSearch)
|
||||
outerGroup.addField(uiGroup)
|
||||
else:
|
||||
groupDescr = groupDescrs[self.name]
|
||||
return groupDescr
|
||||
uiGroup = uiGroups[self.name]
|
||||
return uiGroup
|
||||
|
||||
class Column:
|
||||
'''Used for describing a column within a Group like defined above.'''
|
||||
|
@ -286,6 +436,188 @@ class Column:
|
|||
self.width = width
|
||||
self.align = align
|
||||
|
||||
class UiGroup:
|
||||
'''On-the-fly-generated data structure that groups all fields sharing the
|
||||
same appy.fields.Group instance, that some logged user can see.'''
|
||||
|
||||
# PX that renders a help icon for a group.
|
||||
pxHelp = Px('''<acronym title="obj.translate('help', field=field)"><img
|
||||
src=":url('help')"/></acronym>''')
|
||||
|
||||
# PX that renders the content of a group.
|
||||
pxContent = Px('''
|
||||
<table var="cellgap=field.cellgap" width=":field.wide"
|
||||
align=":ztool.flipLanguageDirection(field.align, dir)"
|
||||
id=":tagId" name=":tagName" class=":groupCss"
|
||||
cellspacing=":field.cellspacing" cellpadding=":field.cellpadding">
|
||||
<!-- Display the title of the group if not rendered a fieldset. -->
|
||||
<tr if="(field.style != 'fieldset') and field.hasLabel">
|
||||
<td colspan=":len(field.columnsWidths)" class=":field.style"
|
||||
align=":dleft">
|
||||
<x>::_(field.labelId)</x><x if="field.hasHelp">:field.pxHelp</x>
|
||||
</td>
|
||||
</tr>
|
||||
<tr if="(field.style != 'fieldset') and field.hasDescr">
|
||||
<td colspan=":len(field.columnsWidths)"
|
||||
class="discreet">::_(field.descrId)</td>
|
||||
</tr>
|
||||
<!-- The column headers -->
|
||||
<tr>
|
||||
<th for="colNb in range(len(field.columnsWidths))"
|
||||
align="ztool.flipLanguageDirection(field.columnsAligns[colNb], dir)"
|
||||
width=":field.columnsWidths[colNb]">::field.hasHeaders and \
|
||||
_('%s_col%d' % (field.labelId, (colNb+1))) or ''</th>
|
||||
</tr>
|
||||
<!-- The rows of widgets -->
|
||||
<tr valign=":field.valign" for="row in field.fields">
|
||||
<td for="field in row"
|
||||
colspan="field.colspan"
|
||||
style=":not loop.field.last and ('padding-right:%s'% cellgap) or ''">
|
||||
<x if="field">
|
||||
<x if="field.type == 'group'">:field.pxView</x>
|
||||
<x if="field.type != 'group'">:field.pxRender</x>
|
||||
</x>
|
||||
</td>
|
||||
</tr>
|
||||
</table>''')
|
||||
|
||||
# PX that renders a group of fields.
|
||||
pxView = Px('''
|
||||
<x var="tagCss=field.master and ('slave_%s_%s' % \
|
||||
(field.masterName, '_'.join(field.masterValue))) or '';
|
||||
widgetCss=field.css_class;
|
||||
groupCss=tagCss and ('%s %s' % (tagCss, widgetCss)) or widgetCss;
|
||||
tagName=field.master and 'slave' or '';
|
||||
tagId='%s_%s' % (zobj.UID(), field.name)">
|
||||
|
||||
<!-- Render the group as a fieldset if required -->
|
||||
<fieldset if="field.style == 'fieldset'">
|
||||
<legend if="field.hasLabel">
|
||||
<i>::_(field.labelId)></i><x if="field.hasHelp">:field.pxHelp</x>
|
||||
</legend>
|
||||
<div if="field.hasDescr" class="discreet">::_(field.descrId)</div>
|
||||
<x>:field.pxContent</x>
|
||||
</fieldset>
|
||||
|
||||
<!-- Render the group as a section if required -->
|
||||
<x if="field.style not in ('fieldset', 'tabs')">:field.pxContent</x>
|
||||
|
||||
<!-- Render the group as tabs if required -->
|
||||
<x if="field.style == 'tabs'" var2="lenFields=len(field.fields)">
|
||||
<table width=":field.wide" class=":groupCss" id=":tagId" name=":tagName">
|
||||
<!-- First row: the tabs. -->
|
||||
<tr valign="middle"><td style="border-bottom: 1px solid #ff8040">
|
||||
<table style="position:relative; bottom:-2px"
|
||||
cellpadding="0" cellspacing="0">
|
||||
<tr valign="bottom">
|
||||
<x for="row in field.fields"
|
||||
var2="rowNb=loop.row.nb;
|
||||
tabId='tab_%s_%d_%d' % (field.name, rowNb, lenFields)">
|
||||
<td><img src=":url('tabLeft')" id=":'%s_left' % tabId"/></td>
|
||||
<td style=":url('tabBg', bg=True)" id=":tabId">
|
||||
<a onclick=":'showTab(%s)' % q('%s_%d_%d' % (field.name, rowNb, \
|
||||
lenFields))"
|
||||
class="clickable">:_('%s_col%d' % (field.labelId, rowNb))</a>
|
||||
</td>
|
||||
<td><img id=":'%s_right' % tabId" src=":url('tabRight')"/></td>
|
||||
</x>
|
||||
</tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
|
||||
<!-- Other rows: the fields -->
|
||||
<tr for="row in field.fields"
|
||||
id=":'tabcontent_%s_%d_%d' % (field.name, loop.row.nb, lenFields)"
|
||||
style=":loop.row.nb==0 and 'display:table-row' or 'display:none')">
|
||||
<td var="field=row[0]">
|
||||
<x if="field.type == 'group'">:field.pxView</x>
|
||||
<x if="field.type != 'group'">:field.pxRender</x>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<script type="text/javascript">:'initTab(%s,%s)' % \
|
||||
(q('tab_%s' % field.name), q('%s_1_%d' % (field.name, lenFields)))">
|
||||
</script>
|
||||
</x>
|
||||
</x>''')
|
||||
|
||||
# PX that renders a group of searches.
|
||||
pxViewSearches = Px('''
|
||||
<x var="expanded=req.get(field.labelId, 'collapsed') == 'expanded'">
|
||||
<!-- Group name, prefixed by the expand/collapse icon -->
|
||||
<div class="portletGroup">
|
||||
<img class="clickable" style="margin-right: 3px" align=":dleft"
|
||||
id=":'%s_img' % field.labelId"
|
||||
src=":expanded and url('collapse.gif') or url('expand.gif')"
|
||||
onclick=":'toggleCookie(%s)' % q(field.labelId)"/>
|
||||
<x if="not field.translated">:_(field.labelId)</x>
|
||||
<x if="field.translated">:field.translated</x>
|
||||
</div>
|
||||
<!-- Group content -->
|
||||
<div var="display=expanded and 'display:block' or 'display:none'"
|
||||
id=":field.labelId" style=":'padding-left: 10px; %s' % display">
|
||||
<x for="searches in field.widgets">
|
||||
<x for="elem in searches">
|
||||
<!-- An inner group within this group -->
|
||||
<x if="elem.type == 'group'"
|
||||
var2="field=elem">:field.pxViewSearches</x>
|
||||
<!-- A search -->
|
||||
<x if="elem.type != 'group'" var2="search=elem">:search.pxView</x>
|
||||
</x>
|
||||
</x>
|
||||
</div>
|
||||
</x>''')
|
||||
|
||||
def __init__(self, group, page, metaType, forSearch=False):
|
||||
self.type = 'group'
|
||||
# All p_group attributes become self attributes.
|
||||
for name, value in group.__dict__.iteritems():
|
||||
if not name.startswith('_'):
|
||||
setattr(self, name, value)
|
||||
self.columnsWidths = [col.width for col in group.columns]
|
||||
self.columnsAligns = [col.align for col in group.columns]
|
||||
# Names of i18n labels
|
||||
labelName = self.name
|
||||
prefix = metaType
|
||||
if group.label:
|
||||
if isinstance(group.label, basestring): prefix = group.label
|
||||
else: # It is a tuple (metaType, name)
|
||||
if group.label[1]: labelName = group.label[1]
|
||||
if group.label[0]: prefix = group.label[0]
|
||||
if forSearch: gp = 'searchgroup'
|
||||
else: gp = 'group'
|
||||
self.labelId = '%s_%s_%s' % (prefix, gp, labelName)
|
||||
self.descrId = self.labelId + '_descr'
|
||||
self.helpId = self.labelId + '_help'
|
||||
# The name of the page where the group lies
|
||||
self.page = page.name
|
||||
# The fields belonging to the group that the current user may see.
|
||||
# They will be stored by m_addField below as a list of lists because
|
||||
# they will be rendered as a table.
|
||||
self.fields = [[]]
|
||||
# PX to user for rendering this group.
|
||||
self.px = forSearch and self.pxViewSearches or self.pxView
|
||||
|
||||
def addField(self, field):
|
||||
'''Adds p_field into self.fields. We try first to add p_field into the
|
||||
last row. If it is not possible, we create a new row.'''
|
||||
# Get the last row
|
||||
lastRow = self.fields[-1]
|
||||
numberOfColumns = len(self.columnsWidths)
|
||||
# Compute the number of columns already filled in the last row.
|
||||
filledColumns = 0
|
||||
for rowField in lastRow: filledColumns += rowField.colspan
|
||||
freeColumns = numberOfColumns - filledColumns
|
||||
if freeColumns >= field.colspan:
|
||||
# We can add the widget in the last row.
|
||||
lastRow.append(field)
|
||||
else:
|
||||
if freeColumns:
|
||||
# Terminate the current row by appending empty cells
|
||||
for i in range(freeColumns): lastRow.append('')
|
||||
# Create a new row
|
||||
self.fields.append([field])
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Abstract base class for every field.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
@ -298,6 +630,67 @@ class Field:
|
|||
jsFiles = {}
|
||||
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,
|
||||
layouts, move, indexed, searchable, specificReadPermission,
|
||||
specificWritePermission, width, height, maxChars, colspan,
|
||||
|
@ -359,7 +752,7 @@ class Field:
|
|||
self.maxChars = maxChars or ''
|
||||
# If the widget is in a group with multiple columns, the following
|
||||
# attribute specifies on how many columns to span the widget.
|
||||
self.colspan = colspan
|
||||
self.colspan = colspan or 1
|
||||
# The list of slaves of this field, if it is a master
|
||||
self.slaves = []
|
||||
# The behaviour of this field may depend on another, "master" field
|
||||
|
@ -401,7 +794,7 @@ class Field:
|
|||
# default value will be present.
|
||||
self.sdefault = sdefault
|
||||
# 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
|
||||
self.swidth = swidth or width
|
||||
self.sheight = sheight or height
|
||||
|
|
|
@ -41,14 +41,14 @@ class Boolean(Field):
|
|||
|
||||
pxSearch = Px('''
|
||||
<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">
|
||||
<input type="radio" value="True" name=":typedWidget" id=":valueId"/>
|
||||
<label lfor=":valueId">:_('yes')</label>
|
||||
</x>
|
||||
<x var="valueId='%s_no' % name">
|
||||
<input type="radio" value="False" name=":typedWidget" id=":valueId"/>
|
||||
<label lfor=":valueId">:_('no')"></label>
|
||||
<label lfor=":valueId">:_('no')</label>
|
||||
</x>
|
||||
<x var="valueId='%s_whatever' % name">
|
||||
<input type="radio" value="" name=":typedWidget" id=":valueId"
|
||||
|
|
|
@ -35,8 +35,8 @@ class Calendar(Field):
|
|||
otherCalendars=field.getOtherCalendars(zobj, preComputed)"
|
||||
id=":ajaxHookId">
|
||||
|
||||
<script type="text/javascript">:'var %s_maxEventLength = %d;' % \
|
||||
(field.name, field.maxEventLength)"></script>
|
||||
<script type="text/javascript">:'var %s_maxEventLength = %d' % \
|
||||
(field.name, field.maxEventLength)</script>
|
||||
|
||||
<!-- Month chooser -->
|
||||
<div style="margin-bottom: 5px"
|
||||
|
@ -100,7 +100,7 @@ class Calendar(Field):
|
|||
onmouseout="mayEdit and 'this.getElementsByTagName(\
|
||||
%s)[0].style.visibility=%s' % (q('img'), q('hidden')) or ''">
|
||||
<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 -->
|
||||
<x if="mayCreate">
|
||||
<img class="clickable" style="visibility:hidden"
|
||||
|
@ -115,11 +115,10 @@ class Calendar(Field):
|
|||
<img if="mayDelete" class="clickable" style="visibility:hidden"
|
||||
src=":url('delete')"
|
||||
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 -->
|
||||
<div if="events" var2="eventType=events[0]['eventType']">
|
||||
<span style="color: grey">:field.getEventName(zobj, \
|
||||
eventType)"></span>
|
||||
<span style="color: grey">:field.getEventName(zobj, eventType)</span>
|
||||
</div>
|
||||
<!-- Events from other calendars -->
|
||||
<x if="otherCalendars"
|
||||
|
@ -149,16 +148,15 @@ class Calendar(Field):
|
|||
<input type="hidden" name="day"/>
|
||||
|
||||
<!-- 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">
|
||||
<option value="">:_('choose_a_value')"></option>
|
||||
<option value="">:_('choose_a_value')</option>
|
||||
<option for="eventType in allEventTypes"
|
||||
value=":eventType">:field.getEventName(zobj, eventType)">
|
||||
</option>
|
||||
value=":eventType">:field.getEventName(zobj,eventType)</option>
|
||||
</select><br/><br/>
|
||||
<!--Span the event on several days -->
|
||||
<div align="center" class="discreet" style="margin-bottom: 3px">
|
||||
<span>:_('event_span')"></span>
|
||||
<span>:_('event_span')</span>
|
||||
<input type="text" size="3" name="eventSpan"/>
|
||||
</div>
|
||||
<input type="button"
|
||||
|
@ -184,8 +182,7 @@ class Calendar(Field):
|
|||
<input type="hidden" name="actionType" value="deleteEvent"/>
|
||||
<input type="hidden" name="day"/>
|
||||
|
||||
<div align="center" style="margin-bottom: 5px">_('delete_confirm')">
|
||||
</div>
|
||||
<div align="center" style="margin-bottom: 5px">_('delete_confirm')</div>
|
||||
|
||||
<!-- Delete successive events ? -->
|
||||
<div class="discreet" style="margin-bottom: 10px"
|
||||
|
@ -195,7 +192,7 @@ class Calendar(Field):
|
|||
onClick=":'toggleCheckbox(%s, %s)' % \
|
||||
(q('%s_cb' % prefix), q('%s_hd' % prefix))"/>
|
||||
<input type="hidden" id=":prefix + '_hd'" name="deleteNext"/>
|
||||
<span>:_('del_next_events')"></span>
|
||||
<span>:_('del_next_events')</span>
|
||||
</div>
|
||||
<input type="button" value=":_('yes')"
|
||||
onClick=":'triggerCalendarEvent(%s, %s, %s, %s)' % \
|
||||
|
|
|
@ -23,17 +23,13 @@ class Computed(Field):
|
|||
|
||||
# Ajax-called view content of a non sync Computed field.
|
||||
pxViewContent = Px('''
|
||||
<x var="name=req['fieldName'];
|
||||
field=zobj.getAppyType(name);
|
||||
value=zobj.getFieldValue(name);
|
||||
sync=True">:field.pxView</x>''')
|
||||
<x var="value=zobj.getFieldValue(name); sync=True">:field.pxView</x>''')
|
||||
|
||||
pxView = pxCell = pxEdit = Px('''<x>
|
||||
<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>
|
||||
<div if="not sync">
|
||||
var2="ajaxHookId=zobj.UID() + name" id="ajaxHookId">
|
||||
<div if="not sync" var2="ajaxHookId=zobj.UID() + name" id="ajaxHookId">
|
||||
<script type="text/javascript">:'askComputedField(%s, %s, %s)' % \
|
||||
(q(ajaxHookId), q(zobj.absolute_url()), q(name))">
|
||||
</script>
|
||||
|
|
|
@ -91,7 +91,7 @@ class Date(Field):
|
|||
monthFromName='%s_from_month' % name;
|
||||
yearFromName='%s*date' % widgetName">
|
||||
<td width="10px"> </td>
|
||||
<td><label>:_('search_from')"></label></td>
|
||||
<td><label>:_('search_from')</label></td>
|
||||
<td>
|
||||
<select id=":dayFromName" name=":dayFromName">
|
||||
<option value="">--</option>
|
||||
|
@ -124,7 +124,7 @@ class Date(Field):
|
|||
monthToName='%s_to_month' % name;
|
||||
yearToName='%s_to_year' % name">
|
||||
<td></td>
|
||||
<td><label>_('search_to')"></label> </td>
|
||||
<td><label>_('search_to')</label> </td>
|
||||
<td height="20px">
|
||||
<select id=":dayToName" name=":dayToName">
|
||||
<option value="">--</option>
|
||||
|
|
|
@ -30,7 +30,7 @@ class File(Field):
|
|||
imgSrc='%s/download?name=%s' % (zobj.absolute_url(), name)">
|
||||
<x if="not empty and not field.isImage">
|
||||
<a href=":imgSrc">:info.filename</a> -
|
||||
<i class="discreet">'%sKb' % (info.size / 1024)"></i>
|
||||
<i class="discreet">:'%sKb' % (info.size / 1024)</i>
|
||||
</x>
|
||||
<x if="not empty and field.isImage"><img src=":imgSrc"/></x>
|
||||
<x if="empty">-</x>
|
||||
|
@ -67,10 +67,8 @@ class File(Field):
|
|||
<input type="file" name=":'%s_file' % name" id=":'%s_file' % name"
|
||||
size=":field.width"/>
|
||||
<script var="isDisabled=empty and 'false' or 'true'"
|
||||
type="text/javascript">:document.getElementById(%s).disabled=%s'%\
|
||||
(q(fName), q(isDisabled))">
|
||||
</script>
|
||||
</x>''')
|
||||
type="text/javascript">:'document.getElementById(%s).disabled=%s'%\
|
||||
(q(fName), q(isDisabled))</script></x>''')
|
||||
|
||||
pxSearch = ''
|
||||
|
||||
|
|
|
@ -36,10 +36,10 @@ class Float(Field):
|
|||
value=":inRequest and requestValue or value" type="text"/>''')
|
||||
|
||||
pxSearch = Px('''<x>
|
||||
<label>:_(field.labelId)"></label><br/>
|
||||
<label>:_(field.labelId)</label><br/>
|
||||
<!-- From -->
|
||||
<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"
|
||||
value=":field.sdefault[0]" size=":field.swidth"/>
|
||||
</x>
|
||||
|
|
|
@ -33,7 +33,7 @@ class Integer(Field):
|
|||
value=":inRequest and requestValue or value" type="text"/>''')
|
||||
|
||||
pxSearch = Px('''<x>
|
||||
<label>:_(field.labelId)"></label><br/>
|
||||
<label>:_(field.labelId)</label><br/>
|
||||
<!-- From -->
|
||||
<x var="fromName='%s*int' % widgetName">
|
||||
<label lfor=":fromName">:_('search_from')</label>
|
||||
|
@ -42,7 +42,7 @@ class Integer(Field):
|
|||
</x>
|
||||
<!-- To -->
|
||||
<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"
|
||||
value=":field.sdefault[1]" size=":field.swidth"/>
|
||||
</x><br/>
|
||||
|
|
|
@ -112,7 +112,7 @@ class Ogone(Field):
|
|||
res.update(self.callMethod(obj, self.orderMethod))
|
||||
# Add user-related information
|
||||
res['CN'] = str(tool.getUserName(normalized=True))
|
||||
user = obj.appy().appyUser
|
||||
user = obj.appy().user
|
||||
res['EMAIL'] = user.email or user.login
|
||||
# Add standard back URLs
|
||||
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
|
||||
# ui/view.
|
||||
pxObjectTitle = Px('''
|
||||
<x var="navInfo='ref.%s.%s:%s.%d.%d' % (zobj.UID(), field.name, \
|
||||
field.pageName, loop.ztied.nb + startNumber, totalNumber);
|
||||
<x var="includeShownInfo=includeShownInfo|False;
|
||||
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 '';
|
||||
cssClass=ztied.getCssFor('title')">
|
||||
<x>::ztied.getSupTitle(navInfo)</x>
|
||||
|
@ -47,7 +48,7 @@ class Ref(Field):
|
|||
href=":fullUrl" class=":cssClass">:(not includeShownInfo) and \
|
||||
ztied.Title() or field.getReferenceLabel(ztied.appy())
|
||||
</a><span name="subTitle" style=":showSubTitles and 'display:inline' or \
|
||||
'display:none'">::ztied.getSubTitle()"</span>
|
||||
'display:none'">::ztied.getSubTitle()</span>
|
||||
</x>''')
|
||||
|
||||
# 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"
|
||||
var2="objectIndex=field.getIndexOf(zobj, ztied);
|
||||
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**')))">
|
||||
<img if="objectIndex > 0" class="clickable" src=":url('arrowUp')"
|
||||
title=":_('move_up')"
|
||||
|
@ -100,7 +101,7 @@ class Ref(Field):
|
|||
field.name, field.pageName, 0, totalNumber);
|
||||
formCall='goto(%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 \
|
||||
'askConfirm(%s,%s,%s)' % (q('script'), q(formCall), \
|
||||
q(addConfirmMsg));
|
||||
|
@ -115,22 +116,20 @@ class Ref(Field):
|
|||
# This PX displays, in a cell header from a ref table, icons for sorting the
|
||||
# ref field according to the field that corresponds to this column.
|
||||
pxSortIcons = Px('''
|
||||
<x if="changeOrder and canWrite and ztool.isSortable(field.name, \
|
||||
zobjects[0].meta_type, 'ref')"
|
||||
<x if="changeOrder and canWrite and ztool.isSortable(refField.name, \
|
||||
tiedClassName, 'ref')"
|
||||
var2="ajaxBaseCall=navBaseCall.replace('**v**', '%s,%s,{%s:%s,%s:%s}'% \
|
||||
(q(startNumber), q('SortReference'), q('sortKey'), \
|
||||
q(field.name), q('reverse'), q('**v**')))">
|
||||
(q(startNumber), q('sort'), q('sortKey'), q(refField.name), \
|
||||
q('reverse'), q('**v**')))">
|
||||
<img class="clickable" src=":url('sortAsc')"
|
||||
onclick=":ajaxBaseCall.replace('**v**', 'False')"/>
|
||||
<img class="clickable" src=":url('sortDesc')"
|
||||
onclick=":ajaxBaseCall.replace('**v**', 'True')"/>
|
||||
</x>''')
|
||||
|
||||
# This PX is called by a XmlHttpRequest (or directly by pxView) for
|
||||
# displaying the referred objects of a reference field.
|
||||
pxViewContent = Px('''
|
||||
<div var="field=zobj.getAppyType(req['fieldName']);
|
||||
innerRef=req.get('innerRef', False) == 'True';
|
||||
# PX that displays referred objects through this field.
|
||||
pxView = pxCell = Px('''
|
||||
<div var="innerRef=req.get('innerRef', False) == 'True';
|
||||
ajaxHookId=zobj.UID() + field.name;
|
||||
startNumber=int(req.get('%s_startNumber' % ajaxHookId, 0));
|
||||
info=field.getLinkedObjects(zobj, startNumber);
|
||||
|
@ -139,7 +138,7 @@ class Ref(Field):
|
|||
batchSize=info.batchSize;
|
||||
batchNumber=len(zobjects);
|
||||
folder=zobj.getCreateFolder();
|
||||
linkedPortalType=ztool.getPortalType(field.klass);
|
||||
tiedClassName=ztool.getPortalType(field.klass);
|
||||
canWrite=not field.isBack and zobj.allows(field.writePermission);
|
||||
showPlusIcon=zobj.mayAddReference(field.name);
|
||||
atMostOneRef=(field.multiplicity[1] == 1) and \
|
||||
|
@ -182,13 +181,13 @@ class Ref(Field):
|
|||
<input if="zobjects and field.queryable" type="button" class="button"
|
||||
style=":url('buttonSearch', bg=True)" value=":_('search_title')"
|
||||
onclick=":'goto(%s)' % \
|
||||
q('%s/ui/search?className=%s&ref=%s:%s' % \
|
||||
(ztool.absolute_url(), linkedPortalType, zobj.UID(), \
|
||||
q('%s/search?className=%s&ref=%s:%s' % \
|
||||
(ztool.absolute_url(), tiedClassName, zobj.UID(), \
|
||||
field.name))"/>
|
||||
</div>
|
||||
|
||||
<!-- Appy (top) navigation -->
|
||||
<x>:obj.pxAppyNavigate</x>
|
||||
<!-- (Top) navigation -->
|
||||
<x>:tool.pxNavigate</x>
|
||||
|
||||
<!-- No object is present -->
|
||||
<p class="discreet" if="not zobjects">:_('no_ref')</p>
|
||||
|
@ -198,35 +197,34 @@ class Ref(Field):
|
|||
<tr valign="bottom">
|
||||
<td>
|
||||
<!-- 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']"
|
||||
var="columns=zobjects[0].getColumnsSpecifiers(\
|
||||
var="columns=ztool.getColumnsSpecifiers(tiedClassName, \
|
||||
field.shownInfo, dir)">
|
||||
<tr if="field.showHeaders">
|
||||
<th for="column in columns" width=":column['width']"
|
||||
align="column['align']"
|
||||
var2="field=column['field']">
|
||||
<span>:_(field.labelId)</span>
|
||||
<th for="column in columns" width=":column.width"
|
||||
align="column.align" var2="refField=column.field">
|
||||
<span>:_(refField.labelId)</span>
|
||||
<x>:field.pxSortIcons</x>
|
||||
<x var="className=linkedPortalType">:obj.pxShowDetails</x>
|
||||
<x var="className=tiedClassName">:tool.pxShowDetails</x>
|
||||
</th>
|
||||
</tr>
|
||||
<tr for="ztied in zobjects" valign="top"
|
||||
class=":loop.ztied.odd and 'even' or 'odd'">
|
||||
<td for="column in columns"
|
||||
width=":column['width']" align=":column['align']"
|
||||
var2="field=column['field']">
|
||||
width=":column.width" align=":column.align"
|
||||
var2="refField=column.field">
|
||||
<!-- The "title" field -->
|
||||
<x if="python: field.name == 'title'">
|
||||
<x if="refField.name == 'title'">
|
||||
<x>:field.pxObjectTitle</x>
|
||||
<div if="ztied.mayAct()">:field.pxObjectActions</div>
|
||||
</x>
|
||||
<!-- Any other field -->
|
||||
<x if="field.name != 'title'">
|
||||
<x if="refField.name != 'title'">
|
||||
<x var="zobj=ztied; obj=ztied.appy(); layoutType='cell';
|
||||
innerRef=True"
|
||||
innerRef=True; field=refField"
|
||||
if="zobj.showField(field.name, \
|
||||
layoutType='result')">:field.pxView</x>
|
||||
layoutType='result')">:field.pxRender</x>
|
||||
</x>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -235,14 +233,11 @@ class Ref(Field):
|
|||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Appy (bottom) navigation -->
|
||||
<x>:obj.pxAppyNavigate</x>
|
||||
<!-- (Bottom) navigation -->
|
||||
<x>:tool.pxNavigate</x>
|
||||
</x>
|
||||
</div>''')
|
||||
|
||||
pxView = pxCell = Px('''
|
||||
<x var="x=req.set('fieldName', field.name)">:field.pxViewContent</x>''')
|
||||
|
||||
pxEdit = Px('''
|
||||
<select if="field.link"
|
||||
var2="requestValue=req.get(name, []);
|
||||
|
@ -253,7 +248,7 @@ class Ref(Field):
|
|||
isBeingCreated=zobj.isTemporary()"
|
||||
name=":name" size="isMultiple and field.height 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()"
|
||||
selected=":inRequest and (uid in requestValue) or \
|
||||
(uid in uids)"
|
||||
|
@ -261,7 +256,7 @@ class Ref(Field):
|
|||
</select>''')
|
||||
|
||||
pxSearch = Px('''<x>
|
||||
<label lfor=":widgetName">:_(field.labelId)"></label><br/>
|
||||
<label lfor=":widgetName">:_(field.labelId)</label><br/>
|
||||
<!-- The "and" / "or" radio buttons -->
|
||||
<x if="field.multiplicity[1] != 1"
|
||||
var2="operName='o_%s' % name;
|
||||
|
@ -269,15 +264,15 @@ class Ref(Field):
|
|||
andName='%s_and' % operName">
|
||||
<input type="radio" name=":operName" id=":orName" checked="checked"
|
||||
value="or"/>
|
||||
<label lfor=":orName">:_('search_or')"></label>
|
||||
<label lfor=":orName">:_('search_or')</label>
|
||||
<input type="radio" name=":operName" id=":andName" value="and"/>
|
||||
<label lfor=":andName">:_('search_and')"></label><br/>
|
||||
<label lfor=":andName">:_('search_and')</label><br/>
|
||||
</x>
|
||||
<!-- The list of values -->
|
||||
<select name=":widgetName" size=":field.sheight" multiple="multiple">
|
||||
<option for="v in ztool.getSearchValues(name, className)"
|
||||
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>
|
||||
</x>''')
|
||||
|
||||
|
@ -603,7 +598,7 @@ class Ref(Field):
|
|||
addPermission = '%s: Add %s' % (tool.getAppName(),
|
||||
tool.getPortalType(self.klass))
|
||||
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 True
|
||||
|
||||
|
@ -623,6 +618,19 @@ class Ref(Field):
|
|||
else:
|
||||
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):
|
||||
'''This method returns the list of all objects that can be selected to
|
||||
be linked as references to p_obj via p_self.'''
|
||||
|
@ -647,7 +655,7 @@ class Ref(Field):
|
|||
if refType.type == 'String':
|
||||
if refType.format == 2:
|
||||
value = self.xhtmlToText.sub(' ', value)
|
||||
elif type(value) in sequenceTypes:
|
||||
elif type(value) in sutils.sequenceTypes:
|
||||
value = ', '.join(value)
|
||||
prefix = ''
|
||||
if res:
|
||||
|
@ -664,6 +672,13 @@ class Ref(Field):
|
|||
if not uids: raise IndexError()
|
||||
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):
|
||||
'''klass.field is a Ref to p_klass. This kind of auto-reference can't be
|
||||
declared in the "normal" way, like this:
|
||||
|
|
|
@ -96,7 +96,7 @@ class String(Field):
|
|||
<div if="not mayAjaxEdit" class="xhtml">::value</div>
|
||||
<div if="mayAjaxEdit" class="xhtml" contenteditable="true"
|
||||
id=":'%s_%s_ck' % (zobj.UID(), name)">::value</div>
|
||||
<script if="mayAjaxEdit">:field.getJsInlineInit(zobj)"></script>
|
||||
<script if="mayAjaxEdit">::field.getJsInlineInit(zobj)"></script>
|
||||
</x>
|
||||
<input type="hidden" if="masterCss" class=":masterCss" value=":rawValue"
|
||||
name=":name" id=":name"/>
|
||||
|
@ -116,8 +116,7 @@ class String(Field):
|
|||
size=":isMultiple and field.height or 1">
|
||||
<option for="val in possibleValues" value=":val[0]"
|
||||
selected=":field.isSelected(zobj, val[0], rawValue)"
|
||||
title=":val[1]">:ztool.truncateValue(val[1], field.width)">
|
||||
</option>
|
||||
title=":val[1]">:ztool.truncateValue(val[1],field.width)</option>
|
||||
</select>
|
||||
<x if="isOneLine and not isSelect">
|
||||
<input id=":name" name=":name" size=":field.width"
|
||||
|
@ -137,18 +136,18 @@ class String(Field):
|
|||
rows=":field.height">:inRequest and requestValue or value
|
||||
</textarea>
|
||||
<script if="fmt == 2"
|
||||
type="text/javascript">:field.getJsInit(zobj)</script>
|
||||
type="text/javascript">::field.getJsInit(zobj)</script>
|
||||
</x>
|
||||
</x>''')
|
||||
|
||||
pxCell = Px('''
|
||||
<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>''')
|
||||
|
||||
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 -->
|
||||
<input if="not field.isSelect" type="text" maxlength=":field.maxChars"
|
||||
size=":field.swidth" value=":field.sdefault"
|
||||
|
@ -166,7 +165,7 @@ class String(Field):
|
|||
value="or"/>
|
||||
<label lfor=":orName">:_('search_or')</label>
|
||||
<input type="radio" name=":operName" id=":andName" value="and"/>
|
||||
<label lfor=":andName">:_('search_and')"></label><br/>
|
||||
<label lfor=":andName">:_('search_and')</label><br/>
|
||||
</x>
|
||||
<!-- The list of values -->
|
||||
<select var="preSelected=field.sdefault"
|
||||
|
@ -656,7 +655,7 @@ class String(Field):
|
|||
if isinstance(v, int): sv = str(v)
|
||||
else: sv = '"%s"' % v
|
||||
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):
|
||||
'''Gets the Javascript init code for enabling inline edition of this
|
||||
|
@ -665,8 +664,8 @@ class String(Field):
|
|||
return "CKEDITOR.disableAutoInline = true;\n" \
|
||||
"CKEDITOR.inline('%s_%s_ck', {on: {blur: " \
|
||||
"function( event ) { var data = event.editor.getData(); " \
|
||||
"askAjaxChunk('%s_%s','POST','%s','page','saveField', "\
|
||||
"{'fieldName':'%s', 'fieldContent': encodeURIComponent(data)}, "\
|
||||
"askAjaxChunk('%s_%s','POST','%s','%s:pxSave', " \
|
||||
"{'fieldContent': encodeURIComponent(data)}, " \
|
||||
"null, evalInnerScripts);}}});"% \
|
||||
(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).
|
||||
# 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.boolean import Boolean
|
||||
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.string import String, Selection
|
||||
from appy.gen.layout import Table
|
||||
from appy.px import Px
|
||||
from appy import Object
|
||||
|
||||
# Default Appy permissions -----------------------------------------------------
|
||||
|
@ -172,6 +173,42 @@ class Search:
|
|||
return gutils.callMethod(tool, self.show, klass=klass)
|
||||
return self.show
|
||||
|
||||
class UiSearch:
|
||||
'''Instances of this class are generated on-the-fly for manipulating a
|
||||
Search from the User Interface.'''
|
||||
# PX for rendering a search.
|
||||
pxView = Px('''
|
||||
<div class="portletSearch">
|
||||
<a href=":'%s?className=%s&search=%s' % \
|
||||
(queryUrl, rootClass, search['name'])"
|
||||
class=":search['name'] == currentSearch and 'portletCurrent' or ''"
|
||||
title=":search['translatedDescr']">:search['translated']</a>
|
||||
</div>''')
|
||||
|
||||
def __init__(self, search, className, tool):
|
||||
self.search = search
|
||||
self.name = search.name
|
||||
self.type = 'search'
|
||||
self.colspan = search.colspan
|
||||
if search.translated:
|
||||
self.translated = search.translated
|
||||
self.translatedDescr = search.translatedDescr
|
||||
else:
|
||||
# The label may be specific in some special cases.
|
||||
labelDescr = ''
|
||||
if search.name == 'allSearch':
|
||||
label = '%s_plural' % className
|
||||
elif search.name == 'customSearch':
|
||||
label = 'search_results'
|
||||
else:
|
||||
label = '%s_search_%s' % (className, search.name)
|
||||
labelDescr = label + '_descr'
|
||||
self.translated = tool.translate(label)
|
||||
if labelDescr:
|
||||
self.translatedDescr = tool.translate(labelDescr)
|
||||
else:
|
||||
self.translatedDescr = ''
|
||||
|
||||
# Workflow-specific types and default workflows --------------------------------
|
||||
appyToZopePermissions = {
|
||||
'read': ('View', 'Access contents information'),
|
||||
|
@ -381,7 +418,7 @@ class Transition:
|
|||
break
|
||||
if not startFound: return False
|
||||
# Check that the condition is met
|
||||
user = obj.getUser()
|
||||
user = obj.getTool().getUser()
|
||||
if isinstance(self.condition, Role):
|
||||
# Condition is a role. Transition may be triggered if the user has
|
||||
# this role.
|
||||
|
|
|
@ -7,31 +7,14 @@ import appy
|
|||
import appy.version
|
||||
import appy.gen as gen
|
||||
from appy.gen.po import PoParser
|
||||
from appy.gen.utils import updateRolesForPermission, createObject
|
||||
from appy.gen.indexer import defaultIndexes, updateIndexes
|
||||
from appy.gen.migrator import Migrator
|
||||
from appy.gen import utils as gutils
|
||||
from appy.shared.data import languages
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
homePage = '''
|
||||
<tal:hp define="tool python: context.config;
|
||||
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>
|
||||
'''
|
||||
homePage = '<tal:h define="dummy python: request.RESPONSE.redirect(' \
|
||||
'context.config.getHomePage())"/>'
|
||||
|
||||
# Stuff for tracking user activity ---------------------------------------------
|
||||
loggedUsers = {}
|
||||
|
@ -45,7 +28,7 @@ def traverseWrapper(self, path, response=None, validated_hook=None):
|
|||
t = time.time()
|
||||
if os.path.splitext(path)[-1].lower() not in doNotTrack:
|
||||
# Do nothing when the user gets non-pages
|
||||
userId = self['AUTHENTICATED_USER'].getId()
|
||||
userId, dummy = gutils.readCookie(self)
|
||||
if userId:
|
||||
loggedUsers[userId] = t
|
||||
# "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):
|
||||
'''This function is called when a session expires.'''
|
||||
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):
|
||||
# The request comes from a guy whose session has expired.
|
||||
resp = rq.RESPONSE
|
||||
resp.expireCookie('__ac', path='/')
|
||||
resp.expireCookie('_appy_', path='/')
|
||||
resp.setHeader('Content-Type', 'text/html')
|
||||
resp.write('<center>For security reasons, your session has ' \
|
||||
'expired.</center>')
|
||||
|
@ -68,6 +51,9 @@ def onDelSession(sessionObject, container):
|
|||
class ZopeInstaller:
|
||||
'''This Zope installer runs every time Zope starts and encounters this
|
||||
generated Zope product.'''
|
||||
# Info about the default users that are always present.
|
||||
defaultUsers = {'admin': ('Manager',), 'system': ('Manager',), 'anon': ()}
|
||||
|
||||
def __init__(self, zopeContext, config, classes):
|
||||
self.zopeContext = zopeContext
|
||||
self.app = zopeContext._ProductContext__app # The root of the Zope tree
|
||||
|
@ -142,7 +128,6 @@ class ZopeInstaller:
|
|||
# Update the error page
|
||||
if 'standard_error_message' in zopeContent:
|
||||
self.app.manage_delObjects(['standard_error_message'])
|
||||
manage_addPageTemplate(self.app, 'standard_error_message', '',errorPage)
|
||||
|
||||
def installCatalog(self):
|
||||
'''Create the catalog at the root of Zope if id does not exist.'''
|
||||
|
@ -196,8 +181,8 @@ class ZopeInstaller:
|
|||
|
||||
if 'config' not in zopeContent:
|
||||
toolName = '%sTool' % self.productName
|
||||
createObject(self.app, 'config', toolName, self.productName,
|
||||
wf=False, noSecurity=True)
|
||||
gutils.createObject(self.app, 'config', toolName, self.productName,
|
||||
wf=False, noSecurity=True)
|
||||
|
||||
if 'data' not in zopeContent:
|
||||
manage_addFolder(self.app, 'data')
|
||||
|
@ -220,7 +205,7 @@ class ZopeInstaller:
|
|||
wrapperClass = tool.getAppyClass(className, wrapper=True)
|
||||
creators = wrapperClass.getCreators(self.config)
|
||||
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
|
||||
for name in ('standard_html_footer', 'standard_html_header',\
|
||||
|
@ -236,12 +221,13 @@ class ZopeInstaller:
|
|||
appyTool = tool.appy()
|
||||
appyTool.log('Appy version is "%s".' % appy.version.short)
|
||||
|
||||
# Create the admin user if it does not exist.
|
||||
if not appyTool.count('User', noSecurity=True, login='admin'):
|
||||
appyTool.create('users', noSecurity=True, login='admin',
|
||||
password1='admin', password2='admin',
|
||||
email='admin@appyframework.org', roles=['Manager'])
|
||||
appyTool.log('Admin user "admin" created.')
|
||||
# Create the default users if they do not exist.
|
||||
for login, roles in self.defaultUsers.iteritems():
|
||||
if not appyTool.count('User', noSecurity=True, login=login):
|
||||
appyTool.create('users', noSecurity=True, login=login,
|
||||
password1=login, password2=login,
|
||||
email='%s@appyframework.org'%login, roles=roles)
|
||||
appyTool.log('User "%s" created.' % login)
|
||||
|
||||
# Create group "admins" if it does not exist
|
||||
if not appyTool.count('Group', noSecurity=True, login='admins'):
|
||||
|
|
|
@ -42,15 +42,12 @@ rowDelms = ''.join(rowDelimiters.keys())
|
|||
cellDelimiters = {'|': 'center', ';': 'left', '!': 'right'}
|
||||
cellDelms = ''.join(cellDelimiters.keys())
|
||||
|
||||
macroDict = {
|
||||
pxDict = {
|
||||
# Page-related elements
|
||||
's': ('page', 'header'), 'w': ('page', 'widgets'),
|
||||
'n': ('navigate', 'objectNavigate'), 'b': ('page', 'buttons'),
|
||||
's': 'pxHeader', 'w': 'pxFields', 'n': 'pxNavigateSiblings', 'b': 'pxButtons',
|
||||
# Field-related elements
|
||||
'l': ('show', 'label'), 'd': ('show', 'description'),
|
||||
'h': ('show', 'help'), 'v': ('show', 'validation'),
|
||||
'r': ('show', 'required'), 'c': ('show', 'changes'),
|
||||
}
|
||||
'l': 'pxLabel', 'd': 'pxDescription', 'h': 'pxHelp', 'v': 'pxValidation',
|
||||
'r': 'pxRequired', 'c': 'pxChanges'}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class LayoutElement:
|
||||
|
@ -77,8 +74,8 @@ class Cell(LayoutElement):
|
|||
digits += char
|
||||
else:
|
||||
# It is a letter corresponding to a macro
|
||||
if char in macroDict:
|
||||
self.content.append(macroDict[char])
|
||||
if char in pxDict:
|
||||
self.content.append(pxDict[char])
|
||||
elif char == 'f':
|
||||
# The exact macro to call will be known at render-time
|
||||
self.content.append('?')
|
||||
|
@ -209,7 +206,7 @@ class Table(LayoutElement):
|
|||
|
||||
def removeElement(self, elem):
|
||||
'''Removes given p_elem from myself.'''
|
||||
macroToRemove = macroDict[elem]
|
||||
macroToRemove = pxDict[elem]
|
||||
for row in self.rows:
|
||||
for cell in row['cells']:
|
||||
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
|
||||
import appy.gen
|
||||
from appy.gen import Search, String, Page, ldap
|
||||
from appy.gen.utils import SomeObjects, getClassName, GroupDescr, SearchDescr
|
||||
from appy.gen import Search, UiSearch, String, Page, ldap
|
||||
from appy.gen.layout import ColumnLayout
|
||||
from appy.gen import utils as gutils
|
||||
from appy.gen.mixins import BaseMixin
|
||||
from appy.gen.wrappers import AbstractWrapper
|
||||
from appy.gen.descriptors import ClassDescriptor
|
||||
from appy.gen.mail import sendMail
|
||||
from appy.shared import mimeTypes
|
||||
from appy.shared.utils import getOsTempFolder, sequenceTypes, normalizeString, \
|
||||
splitList
|
||||
from appy.shared import utils as sutils
|
||||
from appy.shared.data import languages
|
||||
try:
|
||||
from AccessControl.ZopeSecurityPolicy import _noroles
|
||||
|
@ -32,7 +32,7 @@ class ToolMixin(BaseMixin):
|
|||
appName = self.getProductConfig().PROJECTNAME
|
||||
res = metaTypeOrAppyClass
|
||||
if not isinstance(metaTypeOrAppyClass, basestring):
|
||||
res = getClassName(metaTypeOrAppyClass, appName)
|
||||
res = gutils.getClassName(metaTypeOrAppyClass, appName)
|
||||
if res.find('_wrappers') != -1:
|
||||
elems = res.split('_')
|
||||
res = '%s%s' % (elems[1], elems[4])
|
||||
|
@ -42,31 +42,31 @@ class ToolMixin(BaseMixin):
|
|||
def home(self):
|
||||
'''Returns the content of px ToolWrapper.pxHome.'''
|
||||
tool = self.appy()
|
||||
return tool.pxHome({'self': tool})
|
||||
return tool.pxHome({'obj': None, 'tool': tool})
|
||||
|
||||
def query(self):
|
||||
'''Returns the content of px ToolWrapper.pxQuery.'''
|
||||
tool = self.appy()
|
||||
return tool.pxQuery({'self': tool})
|
||||
return tool.pxQuery({'obj': None, 'tool': tool})
|
||||
|
||||
def search(self):
|
||||
'''Returns the content of px ToolWrapper.pxSearch.'''
|
||||
tool = self.appy()
|
||||
return tool.pxSearch({'self': tool})
|
||||
return tool.pxSearch({'obj': None, 'tool': tool})
|
||||
|
||||
def getHomePage(self):
|
||||
'''Return the home page when a user hits the app.'''
|
||||
# If the app defines a method "getHomePage", call it.
|
||||
appyTool = self.appy()
|
||||
tool = self.appy()
|
||||
try:
|
||||
url = appyTool.getHomePage()
|
||||
url = tool.getHomePage()
|
||||
except AttributeError:
|
||||
# Bring Managers to the config, lead others to home.pt.
|
||||
user = self.getUser()
|
||||
if user.has_role('Manager'):
|
||||
url = self.goto(self.absolute_url())
|
||||
else:
|
||||
url = self.goto(self.getApp().ui.home.absolute_url())
|
||||
url = self.goto('%s/home' % self.getApp().config.absolute_url())
|
||||
return url
|
||||
|
||||
def getHomeObject(self):
|
||||
|
@ -77,15 +77,12 @@ class ToolMixin(BaseMixin):
|
|||
portlet menu will nevertheless appear: the user will not have the
|
||||
feeling of being lost.'''
|
||||
# If the app defines a method "getHomeObject", call it.
|
||||
appyTool = self.appy()
|
||||
try:
|
||||
obj = appyTool.getHomeObject()
|
||||
if obj: return obj.o
|
||||
return self.appy().getHomeObject()
|
||||
except AttributeError:
|
||||
# For managers, the home object is the config. For others, there is
|
||||
# no default home object.
|
||||
user = self.getUser()
|
||||
if user.has_role('Manager'): return self
|
||||
if self.getUser().has_role('Manager'): return self.appy()
|
||||
|
||||
def getCatalog(self):
|
||||
'''Returns the catalog object.'''
|
||||
|
@ -220,31 +217,29 @@ class ToolMixin(BaseMixin):
|
|||
'''Returns the list of root classes for this application.'''
|
||||
return self.getProductConfig().rootClasses
|
||||
|
||||
def _appy_getAllFields(self, contentType):
|
||||
'''Returns the (translated) names of fields of p_contentType.'''
|
||||
def _appy_getAllFields(self, className):
|
||||
'''Returns the (translated) names of fields of p_className.'''
|
||||
res = []
|
||||
for appyType in self.getAllAppyTypes(className=contentType):
|
||||
res.append((appyType.name, self.translate(appyType.labelId)))
|
||||
for field in self.getAllAppyTypes(className=className):
|
||||
res.append((className.name, self.translate(className.labelId)))
|
||||
# Add object state
|
||||
res.append(('state', self.translate('workflow_state')))
|
||||
return res
|
||||
|
||||
def _appy_getSearchableFields(self, contentType):
|
||||
def _appy_getSearchableFields(self, className):
|
||||
'''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 = []
|
||||
for appyType in self.getAllAppyTypes(className=contentType):
|
||||
if appyType.indexed:
|
||||
res.append((appyType.name, self.translate(appyType.labelId)))
|
||||
for field in self.getAllAppyTypes(className=className):
|
||||
if field.indexed:
|
||||
res.append((field.name, self.translate(field.labelId)))
|
||||
return res
|
||||
|
||||
def getSearchInfo(self, contentType, refInfo=None):
|
||||
'''Returns, as a dict:
|
||||
- the list of searchable fields (= some fields among all indexed
|
||||
fields);
|
||||
def getSearchInfo(self, className, refInfo=None):
|
||||
'''Returns, as an object:
|
||||
- the list of searchable fields (some among all indexed fields);
|
||||
- the number of columns for layouting those fields.'''
|
||||
fields = []
|
||||
fieldDicts = []
|
||||
if refInfo:
|
||||
# The search is triggered from a Ref field.
|
||||
refObject, fieldName = self.getRefInfo(refInfo)
|
||||
|
@ -253,16 +248,13 @@ class ToolMixin(BaseMixin):
|
|||
nbOfColumns = refField.queryNbCols
|
||||
else:
|
||||
# The search is triggered from an app-wide search.
|
||||
at = self.appy()
|
||||
fieldNames = getattr(at, 'searchFieldsFor%s' % contentType,())
|
||||
nbOfColumns = getattr(at, 'numberOfSearchColumnsFor%s' %contentType)
|
||||
tool = self.appy()
|
||||
fieldNames = getattr(tool, 'searchFieldsFor%s' % className,())
|
||||
nbOfColumns = getattr(tool, 'numberOfSearchColumnsFor%s' %className)
|
||||
for name in fieldNames:
|
||||
appyType = self.getAppyType(name,asDict=False,className=contentType)
|
||||
appyDict = self.getAppyType(name, asDict=True,className=contentType)
|
||||
fields.append(appyType)
|
||||
fieldDicts.append(appyDict)
|
||||
return {'fields': fields, 'nbOfColumns': nbOfColumns,
|
||||
'fieldDicts': fieldDicts}
|
||||
field = self.getAppyType(name, className=className)
|
||||
fields.append(field)
|
||||
return Object(fields=fields, nbOfColumns=nbOfColumns)
|
||||
|
||||
queryParamNames = ('className', 'search', 'sortKey', 'sortOrder',
|
||||
'filterKey', 'filterValue')
|
||||
|
@ -284,16 +276,16 @@ class ToolMixin(BaseMixin):
|
|||
if hasattr(klass, 'resultMode'): return klass.resultMode
|
||||
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
|
||||
p_contentType.'''
|
||||
appyClass = self.getAppyClass(contentType)
|
||||
p_className.'''
|
||||
appyClass = self.getAppyClass(className)
|
||||
importParams = self.getCreateMeans(appyClass)['import']
|
||||
onElement = importParams['onElement'].__get__('')
|
||||
sortMethod = importParams['sort']
|
||||
if sortMethod: sortMethod = sortMethod.__get__('')
|
||||
elems = []
|
||||
importType = self.getAppyType('importPathFor%s' % contentType)
|
||||
importType = self.getAppyType('importPathFor%s' % className)
|
||||
importPath = importType.getValue(self)
|
||||
for elem in os.listdir(importPath):
|
||||
elemFullPath = os.path.join(importPath, elem)
|
||||
|
@ -339,17 +331,19 @@ class ToolMixin(BaseMixin):
|
|||
def getAllowedValue(self):
|
||||
'''Gets, for the currently logged user, the value for index
|
||||
"Allowed".'''
|
||||
tool = self.appy()
|
||||
user = self.getUser()
|
||||
rq = tool.request
|
||||
# Get the user roles
|
||||
res = user.getRoles()
|
||||
res = rq.userRoles
|
||||
# Add role "Anonymous"
|
||||
if 'Anonymous' not in res: res.append('Anonymous')
|
||||
# Add the user id if not anonymous
|
||||
userId = user.getId()
|
||||
if userId: res.append('user:%s' % userId)
|
||||
userId = user.login
|
||||
if userId != 'anon': res.append('user:%s' % userId)
|
||||
# Add group ids
|
||||
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:
|
||||
pass # The Zope admin does not have this attribute.
|
||||
return res
|
||||
|
@ -434,7 +428,8 @@ class ToolMixin(BaseMixin):
|
|||
if refField: maxResults = refField.maxPerPage
|
||||
else: maxResults = self.appy().numberOfResultsPerPage
|
||||
elif maxResults == 'NO_LIMIT': maxResults = None
|
||||
res = SomeObjects(brains, maxResults, startNumber,noSecurity=noSecurity)
|
||||
res = gutils.SomeObjects(brains, maxResults, startNumber,
|
||||
noSecurity=noSecurity)
|
||||
res.brainsToObjects()
|
||||
# In some cases (p_remember=True), we need to keep some information
|
||||
# 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
|
||||
return res.__dict__
|
||||
|
||||
def getResultColumnsLayouts(self, contentType, refInfo):
|
||||
def getResultColumnsLayouts(self, className, refInfo):
|
||||
'''Returns the column layouts for displaying objects of
|
||||
p_contentType.'''
|
||||
p_className.'''
|
||||
if refInfo[0]:
|
||||
res = refInfo[0].getAppyType(refInfo[1]).shownInfo
|
||||
return refInfo[0].getAppyType(refInfo[1]).shownInfo
|
||||
else:
|
||||
toolFieldName = 'resultColumnsFor%s' % contentType
|
||||
res = getattr(self.appy(), toolFieldName)
|
||||
return res
|
||||
toolFieldName = 'resultColumnsFor%s' % className
|
||||
return getattr(self.appy(), toolFieldName)
|
||||
|
||||
def truncateValue(self, value, width=15):
|
||||
'''Truncates the p_value according to p_width.'''
|
||||
|
@ -487,10 +481,11 @@ class ToolMixin(BaseMixin):
|
|||
def splitList(self, l, sub):
|
||||
'''Returns a list made of the same elements as p_l, but grouped into
|
||||
sub-lists of p_sub elements.'''
|
||||
return splitList(l, sub)
|
||||
return sutils.splitList(l, sub)
|
||||
|
||||
def quote(self, s):
|
||||
'''Returns the quoted version of p_s.'''
|
||||
if not isinstance(s, basestring): s = str(s)
|
||||
if "'" in s: return '"%s"' % s
|
||||
return "'%s'" % s
|
||||
|
||||
|
@ -500,21 +495,6 @@ class ToolMixin(BaseMixin):
|
|||
if url.endswith('/view'): return 'view'
|
||||
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):
|
||||
'''Returns the Zope class whose name is p_name.'''
|
||||
exec 'from Products.%s.%s import %s as C'% (self.getAppName(),name,name)
|
||||
|
@ -735,7 +715,7 @@ class ToolMixin(BaseMixin):
|
|||
rq = self.REQUEST
|
||||
self.storeSearchCriteria()
|
||||
# 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'])
|
||||
return self.goto(backUrl)
|
||||
|
||||
|
@ -747,15 +727,31 @@ class ToolMixin(BaseMixin):
|
|||
res += 'var %s = "%s";\n' % (msg, self.translate(msg))
|
||||
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):
|
||||
'''When a search is restricted to objects referenced through a Ref
|
||||
field, this method returns information about this reference: the
|
||||
source content type and the Ref field (Appy type). If p_refInfo is
|
||||
not given, we search it among search criteria in the session.'''
|
||||
source class and the Ref field. If p_refInfo is not given, we search
|
||||
it among search criteria in the session.'''
|
||||
if not refInfo and (self.REQUEST.get('search', None) == 'customSearch'):
|
||||
criteria = self.REQUEST.SESSION.get('searchCriteria', None)
|
||||
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(':')
|
||||
obj = self.getObject(objectUid)
|
||||
return obj, fieldName
|
||||
|
@ -771,7 +767,7 @@ class ToolMixin(BaseMixin):
|
|||
res = []
|
||||
default = None # Also retrieve the default one here.
|
||||
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
|
||||
searches = ClassDescriptor.getSearches(appyClass, tool=self.appy())
|
||||
# Get the dynamically computed searches
|
||||
|
@ -779,22 +775,21 @@ class ToolMixin(BaseMixin):
|
|||
searches += appyClass.getDynamicSearches(self.appy())
|
||||
for search in searches:
|
||||
# Create the search descriptor
|
||||
sDescr = SearchDescr(search, className, self).get()
|
||||
uiSearch = UiSearch(search, className, self)
|
||||
if not search.group:
|
||||
# Insert the search at the highest level, not in any group.
|
||||
res.append(sDescr)
|
||||
res.append(uiSearch)
|
||||
else:
|
||||
gDescr = search.group.insertInto(res, groups, page, className,
|
||||
forSearch=True)
|
||||
GroupDescr.addWidget(gDescr, sDescr)
|
||||
uiGroup = search.group.insertInto(res, groups, page, className,
|
||||
forSearch=True)
|
||||
uiGroup.addField(uiSearch)
|
||||
# Is this search the default search?
|
||||
if search.default: default = sDescr
|
||||
return Object(searches=res, default=default).__dict__
|
||||
if search.default: default = uiSearch
|
||||
return Object(searches=res, default=default)
|
||||
|
||||
def getSearch(self, className, name, descr=False):
|
||||
'''Gets the Search instance (or a SearchDescr instance if p_descr is
|
||||
True) corresponding to the search named p_name, on class
|
||||
p_className.'''
|
||||
def getSearch(self, className, name, ui=False):
|
||||
'''Gets the Search instance (or a UiSearch instance if p_ui is True)
|
||||
corresponding to the search named p_name, on class p_className.'''
|
||||
if name == 'customSearch':
|
||||
# It is a custom search whose parameters are in the session.
|
||||
fields = self.REQUEST.SESSION['searchCriteria']
|
||||
|
@ -812,8 +807,8 @@ class ToolMixin(BaseMixin):
|
|||
else:
|
||||
# It is the search for every instance of p_className
|
||||
res = Search('allSearch')
|
||||
# Return a SearchDescr if required.
|
||||
if descr: res = SearchDescr(res, className, self).get()
|
||||
# Return a UiSearch if required.
|
||||
if ui: res = UiSearch(res, className, self)
|
||||
return res
|
||||
|
||||
def advancedSearchEnabledFor(self, className):
|
||||
|
@ -829,7 +824,7 @@ class ToolMixin(BaseMixin):
|
|||
'''This method creates the URL that allows to perform a (non-Ajax)
|
||||
request for getting queried objects from a search named p_searchName
|
||||
on p_contentType.'''
|
||||
baseUrl = self.absolute_url() + '/ui'
|
||||
baseUrl = self.absolute_url()
|
||||
baseParams = 'className=%s' % contentType
|
||||
rq = self.REQUEST
|
||||
if rq.get('ref'): baseParams += '&ref=%s' % rq.get('ref')
|
||||
|
@ -856,13 +851,14 @@ class ToolMixin(BaseMixin):
|
|||
return startNumber
|
||||
|
||||
def getNavigationInfo(self):
|
||||
'''Extracts navigation information from request/nav and returns a dict
|
||||
with the info that a page can use for displaying object
|
||||
'''Extracts navigation information from request/nav and returns an
|
||||
object with the info that a page can use for displaying object
|
||||
navigation.'''
|
||||
res = {}
|
||||
t,d1,d2,currentNumber,totalNumber = self.REQUEST.get('nav').split('.')
|
||||
res['currentNumber'] = int(currentNumber)
|
||||
res['totalNumber'] = int(totalNumber)
|
||||
res = Object()
|
||||
rq = self.REQUEST
|
||||
t, d1, d2, currentNumber, totalNumber = rq.get('nav').split('.')
|
||||
res.currentNumber = int(currentNumber)
|
||||
res.totalNumber = int(totalNumber)
|
||||
# Compute the label of the search, or ref field
|
||||
if t == 'search':
|
||||
searchName = d2
|
||||
|
@ -875,29 +871,28 @@ class ToolMixin(BaseMixin):
|
|||
else:
|
||||
# This is a named, predefined search.
|
||||
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 ('_' in res['backText']): res['backText'] = ''
|
||||
if ('_' in res.backText): res.backText = ''
|
||||
else:
|
||||
fieldName, pageName = d2.split(':')
|
||||
sourceObj = self.getObject(d1)
|
||||
label = '%s_%s' % (sourceObj.meta_type, fieldName)
|
||||
res['backText'] = '%s - %s' % (sourceObj.Title(),
|
||||
self.translate(label))
|
||||
res.backText = '%s - %s' % (sourceObj.Title(),self.translate(label))
|
||||
newNav = '%s.%s.%s.%%d.%s' % (t, d1, d2, totalNumber)
|
||||
# Among, first, previous, next and last, which one do I need?
|
||||
previousNeeded = False # Previous ?
|
||||
previousIndex = res['currentNumber'] - 2
|
||||
if (previousIndex > -1) and (res['totalNumber'] > previousIndex):
|
||||
previousIndex = res.currentNumber - 2
|
||||
if (previousIndex > -1) and (res.totalNumber > previousIndex):
|
||||
previousNeeded = True
|
||||
nextNeeded = False # Next ?
|
||||
nextIndex = res['currentNumber']
|
||||
if nextIndex < res['totalNumber']: nextNeeded = True
|
||||
nextIndex = res.currentNumber
|
||||
if nextIndex < res.totalNumber: nextNeeded = True
|
||||
firstNeeded = False # First ?
|
||||
firstIndex = 0
|
||||
if previousIndex > 0: firstNeeded = True
|
||||
lastNeeded = False # Last ?
|
||||
lastIndex = res['totalNumber'] - 1
|
||||
lastIndex = res.totalNumber - 1
|
||||
if (nextIndex < lastIndex): lastNeeded = True
|
||||
# Get the list of available UIDs surrounding the current object
|
||||
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
|
||||
# lies.
|
||||
startNumberKey = '%s%s_startNumber' % (masterObj.UID(), fieldName)
|
||||
startNumber = self.computeStartNumberFrom(res['currentNumber']-1,
|
||||
res['totalNumber'], batchSize)
|
||||
res['sourceUrl'] = masterObj.getUrl(**{startNumberKey:startNumber,
|
||||
'page':pageName, 'nav':''})
|
||||
startNumber = self.computeStartNumberFrom(res.currentNumber-1,
|
||||
res.totalNumber, batchSize)
|
||||
res.sourceUrl = masterObj.getUrl(**{startNumberKey:startNumber,
|
||||
'page':pageName, 'nav':''})
|
||||
else: # Manage navigation from a search
|
||||
contentType = d1
|
||||
searchName = keySuffix = d2
|
||||
batchSize = self.appy().numberOfResultsPerPage
|
||||
if not searchName: keySuffix = contentType
|
||||
s = self.REQUEST.SESSION
|
||||
s = rq.SESSION
|
||||
searchKey = 'search_%s' % keySuffix
|
||||
if s.has_key(searchKey): uids = s[searchKey]
|
||||
else: uids = {}
|
||||
|
@ -928,7 +923,7 @@ class ToolMixin(BaseMixin):
|
|||
# I do not have this UID in session. I will need to
|
||||
# retrigger the query by querying all objects surrounding
|
||||
# this one.
|
||||
newStartNumber = (res['currentNumber']-1) - (batchSize / 2)
|
||||
newStartNumber = (res.currentNumber-1) - (batchSize / 2)
|
||||
if newStartNumber < 0: newStartNumber = 0
|
||||
self.executeQuery(contentType, searchName=searchName,
|
||||
startNumber=newStartNumber, remember=True)
|
||||
|
@ -938,15 +933,15 @@ class ToolMixin(BaseMixin):
|
|||
if not uids.has_key(0): firstNeeded = False
|
||||
if not uids.has_key(lastIndex): lastNeeded = False
|
||||
# Compute URL of source object
|
||||
startNumber = self.computeStartNumberFrom(res['currentNumber']-1,
|
||||
res['totalNumber'], batchSize)
|
||||
res['sourceUrl'] = self.getQueryUrl(contentType, searchName,
|
||||
startNumber=startNumber)
|
||||
startNumber = self.computeStartNumberFrom(res.currentNumber-1,
|
||||
res.totalNumber, batchSize)
|
||||
res.sourceUrl = self.getQueryUrl(contentType, searchName,
|
||||
startNumber=startNumber)
|
||||
# Compute URLs
|
||||
for urlType in ('previous', 'next', 'first', 'last'):
|
||||
exec 'needIt = %sNeeded' % urlType
|
||||
urlKey = '%sUrl' % urlType
|
||||
res[urlKey] = None
|
||||
setattr(res, urlKey, None)
|
||||
if needIt:
|
||||
exec 'index = %sIndex' % urlType
|
||||
uid = None
|
||||
|
@ -959,37 +954,38 @@ class ToolMixin(BaseMixin):
|
|||
brain = self.getObject(uid, brain=True)
|
||||
if brain:
|
||||
sibling = brain.getObject()
|
||||
res[urlKey] = sibling.getUrl(nav=newNav % (index + 1),
|
||||
page=self.REQUEST.get('page', 'main'))
|
||||
setattr(res, urlKey, sibling.getUrl(\
|
||||
nav=newNav % (index + 1),
|
||||
page=rq.get('page', 'main')))
|
||||
return res
|
||||
|
||||
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
|
||||
length p_searchInfo['nbOfColumns']. For every field, scolspan
|
||||
length p_searchInfo.nbOfColumns. For every field, scolspan
|
||||
(=colspan "for search") is taken into account.'''
|
||||
res = []
|
||||
row = []
|
||||
rowLength = 0
|
||||
for field in searchInfo['fieldDicts']:
|
||||
for field in searchInfo.fields:
|
||||
# Can I insert this field in the current row?
|
||||
remaining = searchInfo['nbOfColumns'] - rowLength
|
||||
if field['scolspan'] <= remaining:
|
||||
remaining = searchInfo.nbOfColumns - rowLength
|
||||
if field.scolspan <= remaining:
|
||||
# Yes.
|
||||
row.append(field)
|
||||
rowLength += field['scolspan']
|
||||
rowLength += field.scolspan
|
||||
else:
|
||||
# We must put the field on a new line. Complete the current one
|
||||
# if not complete.
|
||||
while rowLength < searchInfo['nbOfColumns']:
|
||||
while rowLength < searchInfo.nbOfColumns:
|
||||
row.append(None)
|
||||
rowLength += 1
|
||||
res.append(row)
|
||||
row = [field]
|
||||
rowLength = field['scolspan']
|
||||
rowLength = field.scolspan
|
||||
# Complete the last unfinished line if required.
|
||||
if row:
|
||||
while rowLength < searchInfo['nbOfColumns']:
|
||||
while rowLength < searchInfo.nbOfColumns:
|
||||
row.append(None)
|
||||
rowLength += 1
|
||||
res.append(row)
|
||||
|
@ -998,11 +994,6 @@ class ToolMixin(BaseMixin):
|
|||
# --------------------------------------------------------------------------
|
||||
# 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):
|
||||
'''Returns the encrypted version of clear p_password.'''
|
||||
return self.acl_users._encryptPassword(password)
|
||||
|
@ -1011,7 +1002,7 @@ class ToolMixin(BaseMixin):
|
|||
'''Performs the Zope-level authentication. Returns True if
|
||||
authentication succeeds.'''
|
||||
user = self.acl_users.validate(request)
|
||||
return not self.userIsAnon()
|
||||
return user.getUserName() != 'Anonymous User'
|
||||
|
||||
def _ldapAuthenticate(self, login, password):
|
||||
'''Performs a LDAP-based authentication. Returns True if authentication
|
||||
|
@ -1032,16 +1023,16 @@ class ToolMixin(BaseMixin):
|
|||
if jsEnabled and not cookiesEnabled:
|
||||
msg = self.translate('enable_cookies')
|
||||
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', '')
|
||||
password = rq.get('__ac_password', '')
|
||||
password = rq.get('__ac_password', '')
|
||||
gutils.writeCookie(login, password, rq)
|
||||
# Perform the Zope-level authentication
|
||||
self._updateCookie(login, password)
|
||||
if self._zopeAuthenticate(rq) or self._ldapAuthenticate(login,password):
|
||||
msg = self.translate('login_ok')
|
||||
logMsg = 'User "%s" logged in.' % login
|
||||
else:
|
||||
rq.RESPONSE.expireCookie('__ac', path='/')
|
||||
rq.RESPONSE.expireCookie('_appy_', path='/')
|
||||
msg = self.translate('login_ko')
|
||||
logMsg = 'Authentication failed with login "%s".' % login
|
||||
self.log(logMsg)
|
||||
|
@ -1050,9 +1041,9 @@ class ToolMixin(BaseMixin):
|
|||
def performLogout(self):
|
||||
'''Logs out the current user when he clicks on "disconnect".'''
|
||||
rq = self.REQUEST
|
||||
userId = self.getUser().getId()
|
||||
userId = self.getUser().login
|
||||
# Perform the logout in acl_users
|
||||
rq.RESPONSE.expireCookie('__ac', path='/')
|
||||
rq.RESPONSE.expireCookie('_appy_', path='/')
|
||||
# Invalidate session.
|
||||
try:
|
||||
sdm = self.session_data_manager
|
||||
|
@ -1083,18 +1074,12 @@ class ToolMixin(BaseMixin):
|
|||
login, password = self.identify(auth)
|
||||
if not login:
|
||||
# Try to get them from a cookie
|
||||
cookie = request.get('__ac', None)
|
||||
login = request.get('__ac_name', None)
|
||||
if login and request.form.has_key('__ac_password'):
|
||||
# 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(':')
|
||||
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)
|
||||
password = request.get('__ac_password', None)
|
||||
# Try to authenticate this user
|
||||
user = self.authenticate(login, password, request)
|
||||
emergency = self._emergency_user
|
||||
|
@ -1123,11 +1108,82 @@ class ToolMixin(BaseMixin):
|
|||
from AccessControl.User import BasicUserFolder
|
||||
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):
|
||||
'''A temp file has been created in a temp folder. This method returns
|
||||
this file to the browser.'''
|
||||
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)
|
||||
fileName = os.path.join(baseFolder, rq.get('name', ''))
|
||||
if os.path.exists(fileName):
|
||||
|
@ -1146,52 +1202,6 @@ class ToolMixin(BaseMixin):
|
|||
if ',' in contentType: return ()
|
||||
return [f.__dict__ for f in self.getAllAppyTypes(contentType) \
|
||||
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):
|
||||
'''Returns aDate formatted as specified by tool.dateFormat.
|
||||
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>' \
|
||||
'You are not allowed to access this page.' % \
|
||||
(siteUrl, siteUrl)
|
||||
userId = self.appy().user.getId() or 'system|anon'
|
||||
userId = self.appy().user.login
|
||||
textMessage = 'Unauthorized for %s @%s.' % \
|
||||
(userId, self.REQUEST.get('PATH_INFO'))
|
||||
else:
|
||||
|
@ -1256,7 +1266,7 @@ class ToolMixin(BaseMixin):
|
|||
return self.goto(backUrl, msg)
|
||||
# Create a temporary file whose name is the user login and whose
|
||||
# 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()
|
||||
f.write(token)
|
||||
f.close()
|
||||
|
@ -1277,7 +1287,7 @@ class ToolMixin(BaseMixin):
|
|||
# Check if such token exists in temp folder
|
||||
res = None
|
||||
siteUrl = self.getSiteUrl()
|
||||
tokenFile = os.path.join(getOsTempFolder(), login)
|
||||
tokenFile = os.path.join(sutils.getOsTempFolder(), login)
|
||||
if os.path.exists(tokenFile):
|
||||
f = file(tokenFile)
|
||||
storedToken = f.read()
|
||||
|
|
|
@ -7,7 +7,7 @@ import os, os.path, sys, types, urllib, cgi
|
|||
from appy import Object
|
||||
import appy.gen as gen
|
||||
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.shared.utils import sequenceTypes,normalizeText,Traceback,getMimeType
|
||||
from appy.shared.data import rtlLanguages
|
||||
|
@ -169,7 +169,7 @@ class BaseMixin:
|
|||
self.workflow_history[key] = tuple(history)
|
||||
appy = self.appy()
|
||||
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']))
|
||||
|
||||
def onUnlink(self):
|
||||
|
@ -213,13 +213,19 @@ class BaseMixin:
|
|||
|
||||
def view(self):
|
||||
'''Returns the view PX.'''
|
||||
appySelf = self.appy()
|
||||
return appySelf.pxView({'self': appySelf})
|
||||
obj = self.appy()
|
||||
return obj.pxView({'obj': obj, 'tool': obj.tool})
|
||||
|
||||
def edit(self):
|
||||
'''Returns the edit PX.'''
|
||||
appySelf = self.appy()
|
||||
return appySelf.pxEdit({'self': appySelf})
|
||||
obj = self.appy()
|
||||
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):
|
||||
'''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
|
||||
# page is already locked by the same user, we don't mind: he could have
|
||||
# used back/forward buttons of its browser...
|
||||
userId = user.getId()
|
||||
userId = user.login
|
||||
if (page in self.locks) and (userId != self.locks[page][0]):
|
||||
from AccessControl import Unauthorized
|
||||
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
|
||||
method returns the tuple (userId, lockDate).'''
|
||||
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):
|
||||
'''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
|
||||
# has locked the page (excepted if p_force is True)
|
||||
if not force:
|
||||
userId = self.getUser().getId()
|
||||
userId = self.getTool().getUser().login
|
||||
if self.locks[page][0] != userId:
|
||||
from AccessControl import Unauthorized
|
||||
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
|
||||
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 \
|
||||
(user.getId() == self.locks[page][0]):
|
||||
(user.login == self.locks[page][0]):
|
||||
del self.locks[page]
|
||||
|
||||
def onUnlock(self):
|
||||
|
@ -420,11 +426,11 @@ class BaseMixin:
|
|||
# previous pages may have changed). Moreover, previous and next
|
||||
# pages may not be available in "edit" mode, so we return the edit
|
||||
# or view pages depending on page.show.
|
||||
phaseInfo = self.getAppyPhases(currentOnly=True, layoutType='edit')
|
||||
pageName, pageInfo = self.getPreviousPage(phaseInfo, rq['page'])
|
||||
phaseObj = self.getAppyPhases(currentOnly=True, layoutType='edit')
|
||||
pageName, pageInfo = phaseObj.getPreviousPage(rq['page'])
|
||||
if pageName:
|
||||
# Return to the edit or view page?
|
||||
if pageInfo['showOnEdit']:
|
||||
if pageInfo.showOnEdit:
|
||||
rq.set('page', pageName)
|
||||
# I do not use gotoEdit here because I really need to
|
||||
# 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
|
||||
# current page if the current one is not visible anymore.
|
||||
pageName = rq['page']
|
||||
phaseInfo = self.getAppyPhases(currentOnly=True, layoutType='edit')
|
||||
pageName, pageInfo = self.getNextPage(phaseInfo, pageName)
|
||||
phaseObj = self.getAppyPhases(currentOnly=True, layoutType='edit')
|
||||
pageName, pageInfo = phaseObj.getNextPage(pageName)
|
||||
if pageName:
|
||||
# Return to the edit or view page?
|
||||
if pageInfo['showOnEdit']:
|
||||
if pageInfo.showOnEdit:
|
||||
# Same remark as above (click on "previous").
|
||||
return self.goto(obj.getUrl(mode='edit', page=pageName))
|
||||
else:
|
||||
|
@ -543,7 +549,8 @@ class BaseMixin:
|
|||
|
||||
def addHistoryEvent(self, action, **kw):
|
||||
'''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
|
||||
event = {'action': action, 'actor': userId, 'time': DateTime(),
|
||||
'comments': ''}
|
||||
|
@ -603,13 +610,13 @@ class BaseMixin:
|
|||
def gotoEdit(self):
|
||||
'''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
|
||||
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.'''
|
||||
page = self.REQUEST.get('page', 'main')
|
||||
for field in self.getAppyTypes('edit', page):
|
||||
if (field.type == 'String') and (field.format in (3,4)):
|
||||
self.REQUEST.set(field.name, '')
|
||||
return self.ui.edit(self)
|
||||
return self.edit()
|
||||
|
||||
def showField(self, name, layoutType='view'):
|
||||
'''Must I show field named p_name on this p_layoutType ?'''
|
||||
|
@ -755,7 +762,7 @@ class BaseMixin:
|
|||
res.update(parent)
|
||||
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
|
||||
field is supposed to belong to self's class.'''
|
||||
isInnerType = '*' in name # An inner type lies within a List type.
|
||||
|
@ -770,7 +777,6 @@ class BaseMixin:
|
|||
klass = self.getTool().getAppyClass(className, wrapper=True)
|
||||
res = getattr(klass, name, None)
|
||||
if res and isInnerType: res = res.getField(subName)
|
||||
if res and asDict: return res.__dict__
|
||||
return res
|
||||
|
||||
def getAllAppyTypes(self, className=None):
|
||||
|
@ -782,40 +788,39 @@ class BaseMixin:
|
|||
klass = self.getTool().getAppyClass(className, wrapper=True)
|
||||
return klass.__fields__
|
||||
|
||||
def getGroupedAppyTypes(self, layoutType, pageName, cssJs=None):
|
||||
'''Returns the fields sorted by group. For every field, the appyType
|
||||
(dict version) is given. If a dict is given in p_cssJs, we will add
|
||||
it in the css and js files required by the fields.'''
|
||||
def getGroupedFields(self, layoutType, pageName, cssJs=None):
|
||||
'''Returns the fields sorted by group. If a dict is given in p_cssJs,
|
||||
we will add it in the css and js files required by the fields.'''
|
||||
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
|
||||
# files required for every returned appyType.
|
||||
# files required for every returned field.
|
||||
collectCssJs = isinstance(cssJs, dict)
|
||||
css = js = None
|
||||
# If param "refresh" is there, we must reload the Python class
|
||||
refresh = ('refresh' in self.REQUEST)
|
||||
if refresh:
|
||||
klass = self.getClass(reloaded=True)
|
||||
for appyType in self.getAllAppyTypes():
|
||||
if refresh: appyType = appyType.reload(klass, self)
|
||||
if appyType.page.name != pageName: continue
|
||||
if not appyType.isShowable(self, layoutType): continue
|
||||
for field in self.getAllAppyTypes():
|
||||
if refresh: field = field.reload(klass, self)
|
||||
if field.page.name != pageName: continue
|
||||
if not field.isShowable(self, layoutType): continue
|
||||
if collectCssJs:
|
||||
if css == None: css = []
|
||||
appyType.getCss(layoutType, css)
|
||||
field.getCss(layoutType, css)
|
||||
if js == None: js = []
|
||||
appyType.getJs(layoutType, js)
|
||||
if not appyType.group:
|
||||
res.append(appyType.__dict__)
|
||||
field.getJs(layoutType, js)
|
||||
if not field.group:
|
||||
res.append(field)
|
||||
else:
|
||||
# Insert the GroupDescr instance corresponding to
|
||||
# appyType.group at the right place
|
||||
groupDescr = appyType.group.insertInto(res, groups,
|
||||
appyType.page, self.meta_type)
|
||||
GroupDescr.addWidget(groupDescr, appyType.__dict__)
|
||||
# Insert the UiGroup instance corresponding to field.group at
|
||||
# the right place.
|
||||
uiGroup = field.group.insertInto(res, groups, field.page,
|
||||
self.meta_type)
|
||||
uiGroup.addField(field)
|
||||
if collectCssJs:
|
||||
cssJs['css'] = css
|
||||
cssJs['js'] = js
|
||||
cssJs['css'] = css or ()
|
||||
cssJs['js'] = js or ()
|
||||
return res
|
||||
|
||||
def getAppyTypes(self, layoutType, pageName):
|
||||
|
@ -852,23 +857,6 @@ class BaseMixin:
|
|||
return klass.styles[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):
|
||||
'''This method returns info about transitions that one can trigger from
|
||||
the user interface.
|
||||
|
@ -921,21 +909,21 @@ class BaseMixin:
|
|||
# Get the list of phases
|
||||
res = [] # Ordered list of phases
|
||||
phases = {} # Dict of phases
|
||||
for appyType in self.getAllAppyTypes():
|
||||
typePhase = appyType.page.phase
|
||||
if typePhase not in phases:
|
||||
phase = PhaseDescr(typePhase, self)
|
||||
res.append(phase.__dict__)
|
||||
phases[typePhase] = phase
|
||||
for field in self.getAllAppyTypes():
|
||||
fieldPhase = field.page.phase
|
||||
if fieldPhase not in phases:
|
||||
phase = gen.Phase(fieldPhase, self)
|
||||
res.append(phase)
|
||||
phases[fieldPhase] = phase
|
||||
else:
|
||||
phase = phases[typePhase]
|
||||
phase.addPage(appyType, self, layoutType)
|
||||
if (appyType.type == 'Ref') and appyType.navigable:
|
||||
phase.addPageLinks(appyType, self)
|
||||
phase = phases[fieldPhase]
|
||||
phase.addPage(field, self, layoutType)
|
||||
if (field.type == 'Ref') and field.navigable:
|
||||
phase.addPageLinks(field, self)
|
||||
# Remove phases that have no visible page
|
||||
for i in range(len(res)-1, -1, -1):
|
||||
if not res[i]['pages']:
|
||||
del phases[res[i]['name']]
|
||||
if not res[i].pages:
|
||||
del phases[res[i].name]
|
||||
del res[i]
|
||||
# Compute next/previous phases of every phase
|
||||
for ph in phases.itervalues():
|
||||
|
@ -948,16 +936,16 @@ class BaseMixin:
|
|||
if not page:
|
||||
if layoutType == 'edit': page = self.getDefaultEditPage()
|
||||
else: page = self.getDefaultViewPage()
|
||||
for phaseInfo in res:
|
||||
if page in phaseInfo['pages']:
|
||||
return phaseInfo
|
||||
for phase in res:
|
||||
if page in phase.pages:
|
||||
return phase
|
||||
# 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.
|
||||
# In this case I find the first visible page among all phases.
|
||||
viewAttr = 'showOn%s' % layoutType.capitalize()
|
||||
for phase in res:
|
||||
for page in phase['pages']:
|
||||
if phase['pagesInfo'][page][viewAttr]:
|
||||
for page in phase.pages:
|
||||
if getattr(phase.pagesInfo[page], viewAttr):
|
||||
rq.set('page', page)
|
||||
pageFound = True
|
||||
break
|
||||
|
@ -965,9 +953,9 @@ class BaseMixin:
|
|||
else:
|
||||
# Return an empty list if we have a single, link-free page within
|
||||
# a single phase.
|
||||
if (len(res) == 1) and (len(res[0]['pages']) == 1) and \
|
||||
not res[0]['pagesInfo'][res[0]['pages'][0]].get('links'):
|
||||
return None
|
||||
if (len(res) == 1) and (len(res[0].pages) == 1) and \
|
||||
not res[0].pagesInfo[res[0].pages[0]].links:
|
||||
return
|
||||
return res
|
||||
|
||||
def getSupTitle(self, navInfo=''):
|
||||
|
@ -983,87 +971,6 @@ class BaseMixin:
|
|||
if hasattr(appyObj, 'getSubTitle'): return appyObj.getSubTitle()
|
||||
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):
|
||||
'''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
|
||||
|
@ -1589,9 +1496,9 @@ class BaseMixin:
|
|||
|
||||
getUrlDefaults = {'page':True, 'nav':True}
|
||||
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
|
||||
(ie, self.absolute_url()).
|
||||
(ie, Zope self.absolute_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.
|
||||
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).'''
|
||||
# Define the URL suffix
|
||||
suffix = ''
|
||||
if mode != 'raw': suffix = '/ui/%s' % mode
|
||||
if mode != 'raw': suffix = '/%s' % mode
|
||||
# Define base URL if omitted
|
||||
if not base:
|
||||
base = self.absolute_url() + suffix
|
||||
|
@ -1609,9 +1516,8 @@ class BaseMixin:
|
|||
if '?' in base: base = base[:base.index('?')]
|
||||
base = base.strip('/')
|
||||
for mode in ('view', 'edit'):
|
||||
suffix = 'ui/%s' % mode
|
||||
if base.endswith(suffix):
|
||||
base = base[:-len(suffix)].strip('/')
|
||||
if base.endswith(mode):
|
||||
base = base[:-len(mode)].strip('/')
|
||||
break
|
||||
return base
|
||||
# Manage default args
|
||||
|
@ -1633,15 +1539,6 @@ class BaseMixin:
|
|||
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):
|
||||
'''Returns the application tool.'''
|
||||
return self.getPhysicalRoot().config
|
||||
|
@ -1659,7 +1556,8 @@ class BaseMixin:
|
|||
data folder) it returns None.'''
|
||||
parent = self.getParentNode()
|
||||
# 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
|
||||
if parent.meta_type not in ('Folder', 'Temporary Folder'): return parent
|
||||
|
||||
|
@ -1678,7 +1576,7 @@ class BaseMixin:
|
|||
return res
|
||||
|
||||
def index_html(self):
|
||||
'''Redirects to /ui.'''
|
||||
'''Redirects to /view.'''
|
||||
rq = self.REQUEST
|
||||
if rq.has_key('do'):
|
||||
# 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
|
||||
return rq.RESPONSE.redirect(self.getUrl())
|
||||
|
||||
def userIsAnon(self):
|
||||
'''Is the currently logged user anonymous ?'''
|
||||
return self.getUser().getUserName() == 'Anonymous User'
|
||||
|
||||
def getUserLanguage(self):
|
||||
'''Gets the language (code) of the current user.'''
|
||||
if not hasattr(self, 'REQUEST'): return 'en'
|
||||
|
@ -1746,15 +1640,13 @@ class BaseMixin:
|
|||
if not domain: domain = cfg.PROJECTNAME
|
||||
# Get the label name, and the field-specific mapping if any.
|
||||
if field:
|
||||
# p_field is the dict version of a appy type or group
|
||||
if field['type'] != 'group':
|
||||
fieldMapping = field['mapping'][label]
|
||||
if field.type != 'group':
|
||||
fieldMapping = field.mapping[label]
|
||||
if fieldMapping:
|
||||
if callable(fieldMapping):
|
||||
appyField = self.getAppyType(field['name'])
|
||||
fieldMapping=appyField.callMethod(self,fieldMapping)
|
||||
fieldMapping = field.callMethod(self, fieldMapping)
|
||||
mapping.update(fieldMapping)
|
||||
label = field['%sId' % label]
|
||||
label = getattr(field, '%sId' % label)
|
||||
# We will get the translation from a Translation object.
|
||||
# In what language must we get the translation?
|
||||
if not language: language = self.getUserLanguage()
|
||||
|
@ -1855,11 +1747,11 @@ class BaseMixin:
|
|||
|
||||
def allows(self, permission, raiseError=False):
|
||||
'''Has the logged user p_permission on p_self ?'''
|
||||
hasPermission = self.getUser().has_permission(permission, self)
|
||||
if not hasPermission and raiseError:
|
||||
res = self.getTool().getUser().has_permission(permission, self)
|
||||
if not res and raiseError:
|
||||
from AccessControl import Unauthorized
|
||||
raise Unauthorized
|
||||
return hasPermission
|
||||
return res
|
||||
|
||||
def getEditorInit(self, name):
|
||||
'''Gets the Javascript init code for displaying a rich editor for
|
||||
|
|
|
@ -149,7 +149,7 @@ class User(ModelClass):
|
|||
name = gen.String(show=showName, **gm)
|
||||
firstName = gen.String(show=showName, **gm)
|
||||
def showEmail(self): pass
|
||||
email = gen.String(show=showEmail)
|
||||
email = gen.String(show=showEmail, **gm)
|
||||
gm['multiplicity'] = (1,1)
|
||||
def showLogin(self): pass
|
||||
def validateLogin(self): pass
|
||||
|
|
|
@ -115,20 +115,22 @@ function getAjaxChunk(pos) {
|
|||
}
|
||||
}
|
||||
|
||||
function askAjaxChunk(hook,mode,url,page,macro,params,beforeSend,onGet) {
|
||||
/* This function will ask to get a chunk of HTML on the server through a
|
||||
function askAjaxChunk(hook,mode,url,px,params,beforeSend,onGet) {
|
||||
/* 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
|
||||
given server object. On this URL we will call the page "ajax.pt" that
|
||||
will call a specific p_macro in a given p_page with some additional
|
||||
p_params (must be an associative array) if required.
|
||||
given server object. On this object we will call method "ajax" that will
|
||||
call a specific p_px with some additional p_params (must be an associative
|
||||
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.
|
||||
|
||||
p_beforeSend is a Javascript function to call before sending the request.
|
||||
This function will get 2 args: the XMLHttpRequest object and the
|
||||
p_params. This method can return, in a string, additional parameters to
|
||||
send, ie: "¶m1=blabla¶m2=blabla".
|
||||
This function will get 2 args: the XMLHttpRequest object and the p_params.
|
||||
This method can return, in a string, additional parameters to send, ie:
|
||||
"¶m1=blabla¶m2=blabla".
|
||||
|
||||
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
|
||||
|
@ -149,7 +151,7 @@ function askAjaxChunk(hook,mode,url,page,macro,params,beforeSend,onGet) {
|
|||
var rq = xhrObjects[pos];
|
||||
rq.freed = 0;
|
||||
// Construct parameters
|
||||
var paramsFull = 'page=' + page + '¯o=' + macro;
|
||||
var paramsFull = 'px=' + px;
|
||||
if (params) {
|
||||
for (var paramName in params)
|
||||
paramsFull = paramsFull + '&' + paramName + '=' + params[paramName];
|
||||
|
@ -160,7 +162,7 @@ function askAjaxChunk(hook,mode,url,page,macro,params,beforeSend,onGet) {
|
|||
if (res) paramsFull = paramsFull + res;
|
||||
}
|
||||
// Construct the URL to call
|
||||
var urlFull = url + '/ui/ajax';
|
||||
var urlFull = url + '/ajax';
|
||||
if (mode == 'GET') {
|
||||
urlFull = urlFull + '?' + paramsFull;
|
||||
}
|
||||
|
@ -199,41 +201,39 @@ function askQueryResult(hookId, objectUrl, className, searchName,
|
|||
params['filterValue'] = filterWidget.value;
|
||||
}
|
||||
}
|
||||
askAjaxChunk(hookId,'GET',objectUrl, 'result', 'queryResult', params);
|
||||
askAjaxChunk(hookId, 'GET', objectUrl, 'pxQueryResult', params);
|
||||
}
|
||||
|
||||
function askObjectHistory(hookId, objectUrl, maxPerPage, startNumber) {
|
||||
// Sends an Ajax request for getting the history of an object
|
||||
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,
|
||||
action, actionParams){
|
||||
// Sends an Ajax request for getting the content of a reference field.
|
||||
var startKey = hookId + '_startNumber';
|
||||
var params = {'fieldName': fieldName, 'innerRef': innerRef };
|
||||
var params = {'innerRef': innerRef };
|
||||
params[startKey] = startNumber;
|
||||
if (action) params['action'] = action;
|
||||
if (actionParams) {
|
||||
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) {
|
||||
// Sends an Ajax request for getting the content of a computed field
|
||||
var params = {'fieldName': fieldName};
|
||||
askAjaxChunk(hookId, 'GET', objectUrl, 'widgets/computed', 'viewContent', params);
|
||||
askAjaxChunk(hookId, 'GET', objectUrl, fieldName+':pxViewContent');
|
||||
}
|
||||
|
||||
function askField(hookId, objectUrl, layoutType, showChanges){
|
||||
// Sends an Ajax request for getting the content of any field.
|
||||
var fieldName = hookId.split('_')[1];
|
||||
var params = {'fieldName': fieldName, 'layoutType': layoutType,
|
||||
'showChanges': showChanges};
|
||||
askAjaxChunk(hookId, 'GET', objectUrl, 'widgets/show', 'fieldAjax', params,
|
||||
null, evalInnerScripts);
|
||||
var params = {'layoutType': layoutType, 'showChanges': showChanges};
|
||||
askAjaxChunk(hookId, 'GET', objectUrl, fieldName+':pxRender', params, null,
|
||||
evalInnerScripts);
|
||||
}
|
||||
|
||||
// Function used by checkbox widgets for having radio-button-like behaviour
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">" />
|
||||
<html tal:define="user tool/getUser;
|
||||
isAnon tool/userIsAnon;
|
||||
isAnon python: user.login == 'anon';
|
||||
app tool/getApp;
|
||||
appUrl app/absolute_url;
|
||||
appFolder app/data;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
function askMonthView(hookId, objectUrl, fieldName, month) {
|
||||
// Sends an Ajax request for getting the view month of a calendar field
|
||||
var params = {'fieldName': fieldName, 'month': month};
|
||||
askAjaxChunk(hookId,'GET',objectUrl,'widgets/calendar','viewMonth', params);
|
||||
var params = {'month': month};
|
||||
askAjaxChunk(hookId, 'GET', objectUrl, fieldName+':pxMonthView', params);
|
||||
}
|
||||
|
||||
function openEventPopup(action, fieldName, day, spansDays,
|
||||
|
@ -54,7 +54,8 @@ function openEventPopup(action, fieldName, day, spansDays,
|
|||
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
|
||||
event) and refreshing the view month. */
|
||||
var prefix = fieldName + '_' + action + 'Event';
|
||||
|
@ -82,5 +83,5 @@ function triggerCalendarEvent(action, hookId, fieldName, objectUrl, maxEventLeng
|
|||
params[elems[i].name] = elems[i].value;
|
||||
}
|
||||
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.px import Px
|
||||
|
||||
# Function for creating a Zope object ------------------------------------------
|
||||
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
|
||||
possible at this time. This is why this function can be called with
|
||||
p_wf=False.'''
|
||||
exec 'from Products.%s.%s import %s as ZopeClass' % (appName, className,
|
||||
className)
|
||||
exec 'from Products.%s.%s import %s as ZopeClass' % \
|
||||
(appName, className, className)
|
||||
# Get the tool. Depends on whether p_folder is a Zope (temp) folder or not.
|
||||
isFolder = folder.meta_type.endswith('Folder')
|
||||
tool = isFolder and folder.config or folder.getTool()
|
||||
user = tool.getUser()
|
||||
if not noSecurity:
|
||||
# Check that the user can create objects of className
|
||||
if folder.meta_type.endswith('Folder'): # Folder or temp folder.
|
||||
tool = folder.config
|
||||
else:
|
||||
tool = folder.getTool()
|
||||
user = tool.getUser()
|
||||
# Check that the user can create objects of className.
|
||||
userRoles = user.getRoles()
|
||||
allowedRoles=ZopeClass.wrapperClass.getCreators(tool.getProductConfig())
|
||||
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" % \
|
||||
ZopeClass.__name__)
|
||||
obj = ZopeClass(id)
|
||||
folder._objects = folder._objects + \
|
||||
({'id':id, 'meta_type':className},)
|
||||
folder._objects = folder._objects + ({'id':id, 'meta_type':className},)
|
||||
folder._setOb(id, obj)
|
||||
obj = folder._getOb(id) # Important. Else, obj is not really in the folder.
|
||||
obj.portal_type = className
|
||||
obj.id = id
|
||||
obj._at_uid = id
|
||||
user = obj.getUser()
|
||||
if not user.getId():
|
||||
if user.name == 'System Processes':
|
||||
userId = 'admin' # This is what happens when Zope is starting.
|
||||
else:
|
||||
userId = None # Anonymous.
|
||||
else:
|
||||
userId = user.getId()
|
||||
obj.creator = userId or 'Anonymous User'
|
||||
# If no user object is there, we are at startup, before default User
|
||||
# instances are created.
|
||||
userId = user and user.login or 'system'
|
||||
obj.creator = userId
|
||||
from DateTime import DateTime
|
||||
obj.created = DateTime()
|
||||
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()
|
||||
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]')
|
||||
def produceNiceMessage(msg):
|
||||
|
@ -448,4 +202,22 @@ def callMethod(obj, method, klass=None, cache=True):
|
|||
res = method(obj)
|
||||
rq.methodCache[key] = 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):
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 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('''
|
||||
<table width="300px" height="240px" align="center">
|
||||
<tr valign="middle">
|
||||
|
@ -58,22 +353,22 @@ class ToolWrapper(AbstractWrapper):
|
|||
<!-- Any other field -->
|
||||
<x if="field.name != 'title'">
|
||||
<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>''')
|
||||
|
||||
# Show query results as a list.
|
||||
pxQueryResultList = Px('''
|
||||
<table class="list" width="100%">
|
||||
<table class="list" width="100%" var="showHeaders=showHeaders|True">
|
||||
<!-- Headers, with filters and sort arrows -->
|
||||
<tr if="showHeaders">
|
||||
<th for="column in columns"
|
||||
var2="widget=column['field'];
|
||||
var2="field=column.field;
|
||||
sortable=ztool.isSortable(field.name, className, 'search');
|
||||
filterable=widget.filterable"
|
||||
width=":column['width']" align=":column['align']">
|
||||
filterable=field.filterable"
|
||||
width=":column.width" align=":column.align">
|
||||
<x>::ztool.truncateText(_(field.labelId))</x>
|
||||
<x>:self.pxSortAndFilter</x><x>:self.pxShowDetails</x>
|
||||
<x>:tool.pxSortAndFilter</x><x>:tool.pxShowDetails</x>
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
|
@ -83,9 +378,9 @@ class ToolWrapper(AbstractWrapper):
|
|||
obj=zobj.appy()"
|
||||
class=":loop.zobj.odd and 'even' or 'odd'">
|
||||
<td for="column in columns"
|
||||
var2="widget=column['field']" id=":'field_%s' % field.name"
|
||||
width=":column['width']"
|
||||
align=":column['align']">:self.pxQueryField</td>
|
||||
var2="field=column.field" id=":'field_%s' % field.name"
|
||||
width=":column.width"
|
||||
align=":column.align">:tool.pxQueryField</td>
|
||||
</tr>
|
||||
</table>''')
|
||||
|
||||
|
@ -100,7 +395,7 @@ class ToolWrapper(AbstractWrapper):
|
|||
style="padding-top: 25px" var2="obj=zobj.appy()">
|
||||
<x var="currentNumber=currentNumber + 1"
|
||||
for="column in columns"
|
||||
var2="widget = column['field']">:self.pxQueryField</x>
|
||||
var2="field=column.field">:tool.pxQueryField</x>
|
||||
</td>
|
||||
</tr>
|
||||
</table>''')
|
||||
|
@ -118,13 +413,13 @@ class ToolWrapper(AbstractWrapper):
|
|||
startNumber=req.get('startNumber', '0');
|
||||
startNumber=int(startNumber);
|
||||
searchName=req.get('search', '');
|
||||
searchDescr=ztool.getSearch(className, searchName, descr=True);
|
||||
uiSearch=ztool.getSearch(className, searchName, ui=True);
|
||||
sortKey=req.get('sortKey', '');
|
||||
sortOrder=req.get('sortOrder', 'asc');
|
||||
filterKey=req.get('filterKey', '');
|
||||
filterValue=req.get('filterValue', '');
|
||||
queryResult=ztool.executeQuery(className, \
|
||||
search=searchDescr['search'], startNumber=startNumber, \
|
||||
search=uiSearch.search, startNumber=startNumber, \
|
||||
remember=True, sortBy=sortKey, sortOrder=sortOrder, \
|
||||
filterKey=filterKey, filterValue=filterValue, \
|
||||
refObject=refObject, refField=refField);
|
||||
|
@ -136,25 +431,27 @@ class ToolWrapper(AbstractWrapper):
|
|||
navBaseCall='askQueryResult(%s,%s,%s,%s,**v**)' % \
|
||||
(q(ajaxHookId), q(ztool.absolute_url()), q(className), \
|
||||
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);
|
||||
showSubTitles=req.get('showSubTitles', 'true') == 'true';
|
||||
resultMode=ztool.getResultMode(className)">
|
||||
|
||||
<x if="zobjects">
|
||||
<!-- Display here POD templates if required. -->
|
||||
<table var="widgets=ztool.getResultPodFields(className);
|
||||
<table var="fields=ztool.getResultPodFields(className);
|
||||
layoutType='view'"
|
||||
if="zobjects and widgets" align=":dright">
|
||||
if="zobjects and fields" align=":dright">
|
||||
<tr>
|
||||
<td var="zobj=zobjects[0]; obj=zobj.appy()"
|
||||
for="field in widgets">:field.pxView</td>
|
||||
for="field in fields">:field.pxView</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- The title of the search -->
|
||||
<p>
|
||||
<x>:searchDescr['translated']</x> (<x>:totalNumber</x>)
|
||||
<x>:uiSearch.translated</x> (<x>:totalNumber</x>)
|
||||
<x if="showNewSearch and (searchName == 'customSearch')"> —
|
||||
<i><a href=":newSearchUrl">:_('search_new')</a></i>
|
||||
</x>
|
||||
|
@ -162,41 +459,37 @@ class ToolWrapper(AbstractWrapper):
|
|||
<table width="100%">
|
||||
<tr>
|
||||
<!-- Search description -->
|
||||
<td if="searchDescr['translatedDescr']">
|
||||
<span class="discreet">:searchDescr['translatedDescr']</span><br/>
|
||||
<td if="uiSearch.translatedDescr">
|
||||
<span class="discreet">:uiSearch.translatedDescr</span><br/>
|
||||
</td>
|
||||
<!-- Appy (top) navigation -->
|
||||
<td align=":dright" width="25%"><x>:self.pxAppyNavigate</x></td>
|
||||
<!-- (Top) navigation -->
|
||||
<td align=":dright" width="25%"><x>:tool.pxNavigate</x></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Results, as a list or grid -->
|
||||
<x var="columnLayouts=ztool.getResultColumnsLayouts(className, refInfo);
|
||||
columns=zobjects[0].getColumnsSpecifiers(columnLayouts, dir);
|
||||
columns=ztool.getColumnsSpecifiers(className,columnLayouts, dir);
|
||||
currentNumber=0">
|
||||
<x if="resultMode == 'list'">:self.pxQueryResultList</x>
|
||||
<x if="resultMode != 'list'">:self.pxQueryResultGrid</x>
|
||||
<x if="resultMode == 'list'">:tool.pxQueryResultList</x>
|
||||
<x if="resultMode != 'list'">:tool.pxQueryResultGrid</x>
|
||||
</x>
|
||||
|
||||
<!-- Appy (bottom) navigation -->
|
||||
<x>:self.pxAppyNavigate</x>
|
||||
<!-- (Bottom) navigation -->
|
||||
<x>:tool.pxNavigate</x>
|
||||
</x>
|
||||
|
||||
<x if="not zobjects">
|
||||
<x>:_('query_no_result')></x>
|
||||
<x>:_('query_no_result')</x>
|
||||
<x if="showNewSearch and (searchName == 'customSearch')"><br/>
|
||||
<i class="discreet"><a href=":newSearchUrl">:_('search_new')</a></i></x>
|
||||
</x>
|
||||
</div>''')
|
||||
|
||||
pxQuery = Px('''
|
||||
<x var="className=req['className'];
|
||||
searchName=req.get('search', '');
|
||||
cssJs=None;
|
||||
showNewSearch=True;
|
||||
showHeaders=True;
|
||||
enableLinks=True">
|
||||
<x>:self.pxPagePrologue</x><x>:self.pxQueryResult</x>
|
||||
<x var="className=req['className']; searchName=req.get('search', '');
|
||||
cssJs=None">
|
||||
<x>:tool.pxPagePrologue</x><x>:tool.pxQueryResult</x>
|
||||
</x>''', template=AbstractWrapper.pxTemplate, hook='content')
|
||||
|
||||
pxSearch = Px('''
|
||||
|
@ -204,7 +497,7 @@ class ToolWrapper(AbstractWrapper):
|
|||
refInfo=req.get('ref', None);
|
||||
searchInfo=ztool.getSearchInfo(className, refInfo);
|
||||
cssJs={};
|
||||
x=ztool.getCssJs(searchInfo['fields'], 'edit', cssJs)">
|
||||
x=ztool.getCssJs(searchInfo.fields, 'edit', cssJs)">
|
||||
|
||||
<!-- Include type-specific CSS and JS. -->
|
||||
<link for="cssFile in cssJs['css']" rel="stylesheet" type="text/css"
|
||||
|
@ -228,10 +521,10 @@ class ToolWrapper(AbstractWrapper):
|
|||
<td for="field in searchRow"
|
||||
var2="scolspan=field and field.scolspan or 1"
|
||||
colspan=":scolspan"
|
||||
width=":'%d%%' % ((100/searchInfo['nbOfColumns'])*scolspan)">
|
||||
width=":'%d%%' % ((100/searchInfo.nbOfColumns)*scolspan)">
|
||||
<x if="field"
|
||||
var2="name=field.name;
|
||||
widgetName='w_%s' % name">field.pxSearch</x>
|
||||
widgetName='w_%s' % name">:field.pxSearch</x>
|
||||
<br class="discreet"/>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -249,7 +542,7 @@ class ToolWrapper(AbstractWrapper):
|
|||
<x var="className=req['className'];
|
||||
importElems=ztool.getImportElements(className);
|
||||
allAreImported=True">
|
||||
<x>:self.pxPagePrologue</x>
|
||||
<x>:tool.pxPagePrologue</x>
|
||||
<script type="text/javascript"><![CDATA[
|
||||
var importedElemsShown = false;
|
||||
function toggleViewableElements() {
|
||||
|
@ -303,7 +596,7 @@ class ToolWrapper(AbstractWrapper):
|
|||
<input type="hidden" name="importPath" value=""/>
|
||||
</form>
|
||||
|
||||
<h1>:_('import_title')"></h1><br/>
|
||||
<h1>:_('import_title')</h1><br/>
|
||||
<table class="list" width="100%">
|
||||
<tr>
|
||||
<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
|
||||
# in the translation.
|
||||
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('\n', '<br/>')
|
||||
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)
|
||||
|
||||
def show(self, field):
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
from appy.gen import WorkflowOwner
|
||||
from appy.gen.wrappers import AbstractWrapper
|
||||
from appy.gen import utils as gutils
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class UserWrapper(AbstractWrapper):
|
||||
|
@ -55,7 +56,7 @@ class UserWrapper(AbstractWrapper):
|
|||
if self.o.isTemporary(): return 'edit'
|
||||
# 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.
|
||||
if self.user.getId() == self.login: return 'edit'
|
||||
if self.user.login == self.login: return 'edit'
|
||||
|
||||
def setPassword(self, newPassword=None):
|
||||
'''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()
|
||||
msgPart = 'generated'
|
||||
login = self.login
|
||||
zopeUser = self.o.acl_users.getUserById(login)
|
||||
zopeUser = self.getZopeUser()
|
||||
tool = self.tool.o
|
||||
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
|
||||
# user. So update the authentication cookie, too.
|
||||
tool._updateCookie(login, newPassword)
|
||||
loggedUser = self.user.getId() or 'system|anon'
|
||||
gutils.writeCookie(login, newPassword, self.request)
|
||||
self.log('Password %s by "%s" for "%s".' % \
|
||||
(msgPart, loggedUser, login))
|
||||
(msgPart, self.user.login, login))
|
||||
return newPassword
|
||||
|
||||
def checkPassword(self, clearPassword):
|
||||
|
@ -91,7 +91,7 @@ class UserWrapper(AbstractWrapper):
|
|||
self.login = newLogin
|
||||
# Update the corresponding Zope-level user
|
||||
aclUsers = self.o.acl_users
|
||||
zopeUser = aclUsers.getUserById(oldLogin)
|
||||
zopeUser = aclUsers.getUser(oldLogin)
|
||||
zopeUser.name = newLogin
|
||||
del aclUsers.data[oldLogin]
|
||||
aclUsers.data[newLogin] = zopeUser
|
||||
|
@ -99,8 +99,8 @@ class UserWrapper(AbstractWrapper):
|
|||
email = self.email
|
||||
if email == oldLogin:
|
||||
self.email = newLogin
|
||||
# Update the title, which is the login
|
||||
self.title = newLogin
|
||||
# Update the title
|
||||
self.updateTitle()
|
||||
# Browse all objects of the database and update potential local roles
|
||||
# that referred to the old login.
|
||||
context = {'nb': 0, 'old': oldLogin, 'new': newLogin}
|
||||
|
@ -109,7 +109,7 @@ class UserWrapper(AbstractWrapper):
|
|||
expression="ctx['nb'] += obj.o.applyUserIdChange(" \
|
||||
"ctx['old'], ctx['new'])")
|
||||
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).' % \
|
||||
context['nb'])
|
||||
|
||||
|
@ -133,12 +133,15 @@ class UserWrapper(AbstractWrapper):
|
|||
if self.login: self.o._oldLogin = self.login
|
||||
return self._callCustom('validate', new, errors)
|
||||
|
||||
def onEdit(self, created):
|
||||
# Set a title for this user.
|
||||
def updateTitle(self):
|
||||
'''Sets a title for this user.'''
|
||||
if self.firstName and self.name:
|
||||
self.title = '%s %s' % (self.name, self.firstName)
|
||||
else:
|
||||
self.title = self.login
|
||||
|
||||
def onEdit(self, created):
|
||||
self.updateTitle()
|
||||
aclUsers = self.o.acl_users
|
||||
login = self.login
|
||||
if created:
|
||||
|
@ -158,7 +161,7 @@ class UserWrapper(AbstractWrapper):
|
|||
self.setLogin(oldLogin, login)
|
||||
del self.o._oldLogin
|
||||
# Update roles at the Zope level.
|
||||
zopeUser = aclUsers.getUserById(login)
|
||||
zopeUser = self.getZopeUser()
|
||||
zopeUser.roles = self.roles
|
||||
# Update the password if the user has entered new ones.
|
||||
rq = self.request
|
||||
|
@ -195,11 +198,19 @@ class UserWrapper(AbstractWrapper):
|
|||
# Call a custom "onDelete" if any.
|
||||
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):
|
||||
user = self.user
|
||||
if obj: return user.has_role(role, obj)
|
||||
return user.has_role(role)
|
||||
zopeUser = self.request.zopeUser
|
||||
if obj: return zopeUser.has_role(role, obj)
|
||||
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:
|
||||
|
|
|
@ -27,126 +27,37 @@ class AbstractWrapper(object):
|
|||
'''Any real Appy-managed Zope object has a companion object that is an
|
||||
instance of this class.'''
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 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
|
||||
# Buttons for going to next/previous objects if this one is among bunch of
|
||||
# referenced or searched objects. currentNumber starts with 1.
|
||||
pxObjectNavigate = Px('''
|
||||
<div if="req.get('nav', None)"
|
||||
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']">
|
||||
|
||||
pxNavigateSiblings = Px('''
|
||||
<div if="req.get('nav', None)" var2="ni=ztool.getNavigationInfo()">
|
||||
<!-- 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');
|
||||
goBack=backText and ('%s - %s' % (backText, gotoSource)) \
|
||||
goBack=ni.backText and ('%s - %s' % (ni.backText, gotoSource)) \
|
||||
or gotoSource"
|
||||
src=":url('gotoSource')" title=":goBack"/></a>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Go to the previous page -->
|
||||
<a if="previousUrl" href=":previousUrl"><img title=":_('goto_previous')"
|
||||
src=":url('arrowLeftSimple')"/></a>
|
||||
<a if="ni.previousUrl" href=":ni.previousUrl"><img
|
||||
title=":_('goto_previous')" src=":url('arrowLeftSimple')"/></a>
|
||||
|
||||
<!-- Explain which element is currently shown -->
|
||||
<span class="discreet">
|
||||
<x>:currentNumber</x> <b>//</b>
|
||||
<x>:totalNumber</x>
|
||||
<x>:ni.currentNumber</x> <b>//</b>
|
||||
<x>:ni.totalNumber</x>
|
||||
</span>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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>
|
||||
</div>''')
|
||||
|
||||
|
@ -164,200 +75,21 @@ class AbstractWrapper(object):
|
|||
</x>
|
||||
</td>
|
||||
<!-- Object navigation -->
|
||||
<td align=":dright">:self.pxObjectNavigate</td>
|
||||
<td align=":dright">:obj.pxNavigateSiblings</td>
|
||||
</tr>
|
||||
</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.
|
||||
pxTemplate = Px('''
|
||||
<html var="tool=self.tool; ztool=tool.o; user=tool.user;
|
||||
isAnon=ztool.userIsAnon(); app=ztool.getApp();
|
||||
<html var="ztool=tool.o; user=tool.user;
|
||||
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;
|
||||
appName=ztool.getAppName(); _=ztool.translate;
|
||||
req=ztool.REQUEST; resp=req.RESPONSE;
|
||||
lang=ztool.getUserLanguage(); q=ztool.quote;
|
||||
layoutType=ztool.getLayoutType();
|
||||
zobj=ztool.getPublishedObject(layoutType) or \
|
||||
ztool.getHomeObject();
|
||||
obj = zobj and zobj.appy() or None;
|
||||
showPortlet=ztool.showPortlet(zobj, layoutType);
|
||||
dir=ztool.getLanguageDirection(lang);
|
||||
discreetLogin=ztool.getProductConfig(True).discreetLogin;
|
||||
|
@ -436,7 +168,7 @@ class AbstractWrapper(object):
|
|||
</a>
|
||||
|
||||
<!-- Additional links -->
|
||||
<x>:self.pxLinks</x>
|
||||
<x>:tool.pxLinks</x>
|
||||
|
||||
<!-- Top-level pages -->
|
||||
<a for="page in tool.pages" class="pageLink"
|
||||
|
@ -461,7 +193,7 @@ class AbstractWrapper(object):
|
|||
|
||||
<!-- The message strip -->
|
||||
<tr valign="top">
|
||||
<td><div style="position: relative">:self.pxMessage</div></td>
|
||||
<td><div style="position: relative">:tool.pxMessage</div></td>
|
||||
</tr>
|
||||
|
||||
<!-- The user strip -->
|
||||
|
@ -509,7 +241,7 @@ class AbstractWrapper(object):
|
|||
title=":_('%sTool' % appName)">
|
||||
<img src=":url('appyConfig.gif')"/></a>
|
||||
<!-- Additional icons -->
|
||||
<x>:self.pxIcons</x>
|
||||
<x>:tool.pxIcons</x>
|
||||
<!-- Log out -->
|
||||
<a href=":tool.url + '/performLogout'" title=":_('app_logout')">
|
||||
<img src=":url('logout.gif')"/></a>
|
||||
|
@ -530,14 +262,14 @@ class AbstractWrapper(object):
|
|||
|
||||
<!-- The navigation strip -->
|
||||
<tr if="zobj and showPortlet and (layoutType != 'edit')">
|
||||
<td>:self.pxNavigationStrip</td>
|
||||
<td>:obj.pxNavigationStrip</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr valign="top">
|
||||
<!-- The portlet -->
|
||||
<td if="showPortlet" class="portlet">:self.pxPortlet</td>
|
||||
<td if="showPortlet" class="portlet">:tool.pxPortlet</td>
|
||||
<!-- Page content -->
|
||||
<td class="content">:content</td>
|
||||
</tr>
|
||||
|
@ -545,7 +277,7 @@ class AbstractWrapper(object):
|
|||
</td>
|
||||
</tr>
|
||||
<!-- Footer -->
|
||||
<tr><td>:self.pxFooter</td></tr>
|
||||
<tr><td>:tool.pxFooter</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>''', prologue=Px.xhtmlPrologue)
|
||||
|
@ -555,7 +287,7 @@ class AbstractWrapper(object):
|
|||
# --------------------------------------------------------------------------
|
||||
|
||||
# This PX displays an object's history.
|
||||
pxObjectHistory = Px('''
|
||||
pxHistory = Px('''
|
||||
<x var="startNumber=req.get'startNumber', 0);
|
||||
startNumber=int(startNumber);
|
||||
batchSize=int(req.get('maxPerPage', 5));
|
||||
|
@ -568,7 +300,7 @@ class AbstractWrapper(object):
|
|||
(q(ajaxHookId), q(zobj.absolute_url()), batchSize)">
|
||||
|
||||
<!-- Navigate between history pages -->
|
||||
<x>:self.pxAppyNavigate</x>
|
||||
<x>:tool.pxNavigate</x>
|
||||
<!-- History -->
|
||||
<table width="100%" class="history">
|
||||
<tr>
|
||||
|
@ -610,8 +342,8 @@ class AbstractWrapper(object):
|
|||
<th align=":dleft" width="70%">:_('previous_value')</th>
|
||||
</tr>
|
||||
<tr for="change in event['changes'].items()" valign="top"
|
||||
var2="appyType=zobj.getAppyType(change[0], asDict=True)">
|
||||
<td>::_(appyType['labelId'])</td>
|
||||
var2="field=zobj.getAppyType(change[0])">
|
||||
<td>::_(field.labelId)</td>
|
||||
<td>::change[1][0]</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -655,7 +387,7 @@ class AbstractWrapper(object):
|
|||
|
||||
# Displays header information about an object: title, workflow-related info,
|
||||
# history...
|
||||
pxObjectHeader = Px('''
|
||||
pxHeader = Px('''
|
||||
<div if="not zobj.isTemporary()"
|
||||
var2="hasHistory=zobj.hasHistory();
|
||||
historyMaxPerPage=req.get('maxPerPage', 5);
|
||||
|
@ -710,16 +442,16 @@ class AbstractWrapper(object):
|
|||
</div>''')
|
||||
|
||||
# Shows the range of buttons (next, previous, save,...) and the workflow
|
||||
# transitions.
|
||||
pxObjectButtons = Px('''
|
||||
# transitions for a given object.
|
||||
pxButtons = Px('''
|
||||
<table cellpadding="2" cellspacing="0" style="margin-top: 7px"
|
||||
var="previousPage=zobj.getPreviousPage(phaseInfo, page)[0];
|
||||
nextPage=zobj.getNextPage(phaseInfo, page)[0];
|
||||
var="previousPage=phaseObj.getPreviousPage(page)[0];
|
||||
nextPage=phaseObj.getNextPage(page)[0];
|
||||
isEdit=layoutType == 'edit';
|
||||
pageInfo=phaseInfo['pagesInfo'][page]">
|
||||
pageInfo=phaseObj.pagesInfo[page]">
|
||||
<tr>
|
||||
<!-- Previous -->
|
||||
<td if="previousPage and pageInfo['showPrevious']">
|
||||
<td if="previousPage and pageInfo.showPrevious">
|
||||
<!-- Button on the edit page -->
|
||||
<x if="isEdit">
|
||||
<input type="button" class="button" value=":_('page_previous')"
|
||||
|
@ -735,20 +467,20 @@ class AbstractWrapper(object):
|
|||
</td>
|
||||
|
||||
<!-- Save -->
|
||||
<td if="isEdit and pageInfo['showSave']">
|
||||
<td if="isEdit and pageInfo.showSave">
|
||||
<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>
|
||||
|
||||
<!-- Cancel -->
|
||||
<td if="isEdit and pageInfo['showCancel']">
|
||||
<td if="isEdit and pageInfo.showCancel">
|
||||
<input type="button" class="button" onClick="submitAppyForm('cancel')"
|
||||
style=":url('buttonCancel', bg=True)" value=":_('object_cancel')"/>
|
||||
</td>
|
||||
|
||||
<td if="not isEdit"
|
||||
var2="locked=zobj.isLocked(user, page);
|
||||
editable=pageInfo['showOnEdit'] and zobj.mayEdit()">
|
||||
editable=pageInfo.showOnEdit and zobj.mayEdit()">
|
||||
|
||||
<!-- Edit -->
|
||||
<input type="button" class="button" if="editable and not locked"
|
||||
|
@ -765,7 +497,7 @@ class AbstractWrapper(object):
|
|||
</td>
|
||||
|
||||
<!-- Next -->
|
||||
<td if="nextPage and pageInfo['showNext']">
|
||||
<td if="nextPage and pageInfo.showNext">
|
||||
<!-- Button on the edit page -->
|
||||
<x if="isEdit">
|
||||
<input type="button" class="button" onClick="submitAppyForm('next')"
|
||||
|
@ -780,7 +512,7 @@ class AbstractWrapper(object):
|
|||
|
||||
<!-- Workflow transitions -->
|
||||
<td var="targetObj=zobj"
|
||||
if="targetObj.showTransitions(layoutType)">:self.pxTransitions</td>
|
||||
if="targetObj.showTransitions(layoutType)">:obj.pxTransitions</td>
|
||||
|
||||
<!-- Refresh -->
|
||||
<td if="zobj.isDebug()">
|
||||
|
@ -791,21 +523,29 @@ class AbstractWrapper(object):
|
|||
</tr>
|
||||
</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('''
|
||||
<x var="x=zobj.allows('View', raiseError=True);
|
||||
errors=req.get('errors', {});
|
||||
layout=zobj.getPageLayout(layoutType);
|
||||
phaseInfo=zobj.getAppyPhases(currentOnly=True, layoutType='view');
|
||||
phase=phaseInfo['name'];
|
||||
phaseObj=zobj.getAppyPhases(currentOnly=True, layoutType='view');
|
||||
phase=phaseObj.name;
|
||||
cssJs={};
|
||||
page=req.get('page',None) or zobj.getDefaultViewPage();
|
||||
x=zobj.removeMyLock(user, page);
|
||||
groupedWidgets=zobj.getGroupedAppyTypes(layoutType, page, \
|
||||
cssJs=cssJs)">
|
||||
<x>:self.pxPagePrologue</x>
|
||||
<x var="tagId='pageLayout'">:self.pxLayoutedObject</x>
|
||||
<x>:self.pxPageBottom</x>
|
||||
groupedFields=zobj.getGroupedFields(layoutType, page,cssJs=cssJs)">
|
||||
<x>:tool.pxPagePrologue</x>
|
||||
<x var="tagId='pageLayout'; tagName=''; tagCss='';
|
||||
layoutTarget=obj">:tool.pxLayoutedObject</x>
|
||||
<x>:tool.pxPageBottom</x>
|
||||
</x>''', template=pxTemplate, hook='content')
|
||||
|
||||
pxEdit = Px('''
|
||||
|
@ -813,15 +553,14 @@ class AbstractWrapper(object):
|
|||
errors=req.get('errors', None) or {};
|
||||
layout=zobj.getPageLayout(layoutType);
|
||||
cssJs={};
|
||||
phaseInfo=zobj.getAppyPhases(currentOnly=True, \
|
||||
layoutType=layoutType);
|
||||
phase=phaseInfo['name'];
|
||||
phaseObj=zobj.getAppyPhases(currentOnly=True, \
|
||||
layoutType=layoutType);
|
||||
phase=phaseObj.name;
|
||||
page=req.get('page', None) or zobj.getDefaultEditPage();
|
||||
x=zobj.setLock(user, page);
|
||||
confirmMsg=req.get('confirmMsg', None);
|
||||
groupedWidgets=zobj.getGroupedAppyTypes(layoutType, page, \
|
||||
cssJs=cssJs)">
|
||||
<x>:self.pxPagePrologue</x>
|
||||
groupedFields=zobj.getGroupedFields(layoutType,page, cssJs=cssJs)">
|
||||
<x>:tool.pxPagePrologue</x>
|
||||
<!-- Warn the user that the form should be left via buttons -->
|
||||
<script type="text/javascript"><![CDATA[
|
||||
window.onbeforeunload = function(e){
|
||||
|
@ -840,14 +579,45 @@ class AbstractWrapper(object):
|
|||
<input type="hidden" name="page" value=":page"/>
|
||||
<input type="hidden" name="nav" value=":req.get('nav', None)"/>
|
||||
<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>
|
||||
<script type="text/javascript"
|
||||
if="confirmMsg">:'askConfirm(%s,%s,%s)' % \
|
||||
(q('script'), q('postConfirmedEditForm()'), q(confirmMsg))</script>
|
||||
<x>:self.pxPageBottom</x>
|
||||
<x>:tool.pxPageBottom</x>
|
||||
</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
|
||||
# --------------------------------------------------------------------------
|
||||
|
@ -936,15 +706,7 @@ class AbstractWrapper(object):
|
|||
o = self.o
|
||||
key = o.workflow_history.keys()[0]
|
||||
return o.workflow_history[key]
|
||||
elif name == 'user':
|
||||
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 == 'user': return self.o.getTool().getUser()
|
||||
elif name == 'fields': return self.o.getAllAppyTypes()
|
||||
elif name == 'siteUrl': return self.o.getTool().getSiteUrl()
|
||||
# Now, let's try to return a real attribute.
|
||||
|
|
|
@ -77,11 +77,25 @@ class BufferAction:
|
|||
dumpTb=dumpTb)
|
||||
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):
|
||||
'''Evaluates expression p_expr with the current p_context. Returns a
|
||||
tuple (result, errorOccurred).'''
|
||||
try:
|
||||
res = eval(expr, context)
|
||||
res = self._evalExpr(expr, context)
|
||||
error = False
|
||||
except Exception, e:
|
||||
res = None
|
||||
|
@ -169,7 +183,10 @@ class ForAction(BufferAction):
|
|||
self.iter = iter # Name of the iterator variable used in the each loop
|
||||
|
||||
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
|
||||
# about all currently walked loops. For every walked loop, a specific
|
||||
# object, le'ts name it curLoop, accessible at getattr(loop, self.iter),
|
||||
|
@ -195,11 +212,17 @@ class ForAction(BufferAction):
|
|||
context['loop'] = Object()
|
||||
try:
|
||||
total = len(elems)
|
||||
except:
|
||||
except Exception:
|
||||
total = 0
|
||||
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)
|
||||
return curLoop
|
||||
return curLoop, outerLoop
|
||||
|
||||
def do(self, result, context, elems):
|
||||
'''Performs the "for" action. p_elems is the list of elements to
|
||||
|
@ -229,7 +252,7 @@ class ForAction(BufferAction):
|
|||
if not elems:
|
||||
result.dumpElement(Cell.OD.elem)
|
||||
# Enter the "for" loop.
|
||||
loop = self.initialiseLoop(context, elems)
|
||||
loop, outerLoop = self.initialiseLoop(context, elems)
|
||||
i = -1
|
||||
for item in elems:
|
||||
i += 1
|
||||
|
@ -277,11 +300,13 @@ class ForAction(BufferAction):
|
|||
context[self.iter] = ''
|
||||
for i in range(nbOfMissingCellsLastLine):
|
||||
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:
|
||||
delattr(context['loop'], self.iter)
|
||||
except AttributeError:
|
||||
pass
|
||||
if outerLoop:
|
||||
setattr(context['loop'], self.iter, outerLoop)
|
||||
# Restore hidden variable if any
|
||||
if hasHiddenVariable:
|
||||
context[self.iter] = hiddenVariable
|
||||
|
|
|
@ -19,6 +19,7 @@ import re
|
|||
from xml.sax.saxutils import quoteattr
|
||||
from appy.shared.xml_parser import xmlPrologue, escapeXml
|
||||
from appy.pod import PodError
|
||||
from appy.shared.utils import Traceback
|
||||
from appy.pod.elements import *
|
||||
from appy.pod.actions import IfAction, ElseAction, ForAction, VariablesAction, \
|
||||
NullAction
|
||||
|
|
|
@ -74,17 +74,32 @@ class Table(PodElement):
|
|||
class Expression(PodElement):
|
||||
'''Represents a Python expression that is found in a pod or px.'''
|
||||
OD = None
|
||||
def __init__(self, pyExpr, pod):
|
||||
# The Python expression
|
||||
self.expr = pyExpr.strip()
|
||||
self.pod = pod # True if I work for pod, False if I work for px.
|
||||
# Must we, when evaluating the expression, escape XML special chars
|
||||
# or not?
|
||||
if self.expr.startswith(':'):
|
||||
self.expr = self.expr[1:]
|
||||
self.escapeXml = False
|
||||
def extractInfo(self, py):
|
||||
'''Within p_py, several elements can be included:
|
||||
- the fact that XML chars must be escaped or not (leading ":")
|
||||
- the "normal" Python expression,
|
||||
- an optional "error" expression, that is evaluated when the normal
|
||||
expression raises an exception.
|
||||
This method return a tuple (escapeXml, normaExpr, errorExpr).'''
|
||||
# Determine if we must escape XML chars or not.
|
||||
escapeXml = True
|
||||
if py.startswith(':'):
|
||||
py = py[1:]
|
||||
escapeXml = False
|
||||
# Extract normal and error expression
|
||||
if '|' not in py:
|
||||
expr = py
|
||||
errorExpr = None
|
||||
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:
|
||||
# pod-only: store here the expression's true result (before being
|
||||
# converted to a string).
|
||||
|
@ -98,6 +113,18 @@ class Expression(PodElement):
|
|||
# self.result and self.evaluated are not used by PX, because they
|
||||
# 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):
|
||||
'''Evaluates the Python expression (self.expr) with a given
|
||||
p_context, and returns the result. More precisely, it returns a
|
||||
|
@ -114,8 +141,7 @@ class Expression(PodElement):
|
|||
# with another context.
|
||||
self.evaluated = False
|
||||
else:
|
||||
# Evaluates the Python expression
|
||||
res = eval(self.expr, context)
|
||||
res = self._eval(context)
|
||||
# pod-only: cache the expression result.
|
||||
if self.pod: self.result = res
|
||||
# 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