[gen] More work ZPT->PX.

This commit is contained in:
Gaetan Delannay 2013-08-21 12:35:30 +02:00
parent 2e9a832463
commit 34e3a3083e
31 changed files with 3287 additions and 3067 deletions

View file

@ -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)
# ------------------------------------------------------------------------------

View file

@ -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')

View file

@ -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] &gt; 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

View file

@ -41,14 +41,14 @@ class Boolean(Field):
pxSearch = Px('''
<x var="typedWidget='%s*bool' % widgetName">
<label lfor=":widgetName">:_(field.labelId)"></label><br/>&nbsp;&nbsp;
<label lfor=":widgetName">:_(field.labelId)</label><br/>&nbsp;&nbsp;
<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"

View file

@ -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)' % \

View file

@ -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>

View file

@ -91,7 +91,7 @@ class Date(Field):
monthFromName='%s_from_month' % name;
yearFromName='%s*date' % widgetName">
<td width="10px">&nbsp;</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>&nbsp;&nbsp;&nbsp;&nbsp;</td>
<td><label>_('search_to')</label>&nbsp;&nbsp;&nbsp;&nbsp;</td>
<td height="20px">
<select id=":dayToName" name=":dayToName">
<option value="">--</option>

View file

@ -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>&nbsp;&nbsp;-
<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 = ''

View file

@ -36,10 +36,10 @@ class Float(Field):
value=":inRequest and requestValue or value" type="text"/>''')
pxSearch = Px('''<x>
<label>:_(field.labelId)"></label><br/>&nbsp;&nbsp;
<label>:_(field.labelId)</label><br/>&nbsp;&nbsp;
<!-- 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>

View file

@ -33,7 +33,7 @@ class Integer(Field):
value=":inRequest and requestValue or value" type="text"/>''')
pxSearch = Px('''<x>
<label>:_(field.labelId)"></label><br/>&nbsp;&nbsp;
<label>:_(field.labelId)</label><br/>&nbsp;&nbsp;
<!-- 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/>

View file

@ -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()

View file

@ -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)&gt;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 &gt; 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&amp;className=%s&amp;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&amp;ref=%s:%s' % \
(ztool.absolute_url(), linkedPortalType, zobj.UID(), \
q('%s/search?className=%s&amp;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/>&nbsp;&nbsp;
<label lfor=":widgetName">:_(field.labelId)</label><br/>&nbsp;&nbsp;
<!-- 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:

View file

@ -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/>&nbsp;&nbsp;
<label lfor="widgetName">:_(field.labelId)</label><br/>&nbsp;&nbsp;
<!-- 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)

View file

@ -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&amp;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.

View file

@ -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'):

View file

@ -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']:

View file

@ -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 '&quot;%s&quot;' % 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()

View file

@ -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

View file

@ -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

View file

@ -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: "&param1=blabla&param2=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:
"&param1=blabla&param2=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 + '&macro=' + 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

View file

@ -3,7 +3,7 @@
&quot;-//W3C//DTD XHTML 1.0 Strict//EN&quot;
&quot;http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd&quot;&gt;" />
<html tal:define="user tool/getUser;
isAnon tool/userIsAnon;
isAnon python: user.login == 'anon';
app tool/getApp;
appUrl app/absolute_url;
appFolder app/data;

View file

@ -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);
}

View file

@ -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&amp;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='/')
# ------------------------------------------------------------------------------

View file

@ -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 &gt; 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">&nbsp;
<x>:startNumber + 1</x><img src=":url('to')"/>
<x>:startNumber + batchNumber</x>&nbsp;<b>//</b>
<x>:totalNumber</x>&nbsp;&nbsp;</td>
<!-- Go to the next page -->
<td var="sNumber=startNumber + batchSize"
if="sNumber &lt; 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&amp;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&amp;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')">&nbsp;&mdash;
&nbsp;<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]">

View file

@ -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('<','&lt;').replace('>','&gt;')
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):

View file

@ -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:

View file

@ -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 &gt; 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">&nbsp;
<x>:startNumber + 1</x><img src=":url('to')"/>
<x>:startNumber + batchNumber</x>&nbsp;<b>//</b>
<x>:totalNumber</x>&nbsp;&nbsp;</td>
<!-- Go to the next page -->
<td var="sNumber=startNumber + batchSize"
if="sNumber &lt; 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">&nbsp;
<x>:currentNumber</x>&nbsp;<b>//</b>
<x>:totalNumber</x>&nbsp;&nbsp;
<x>:ni.currentNumber</x>&nbsp;<b>//</b>
<x>:ni.totalNumber</x>&nbsp;&nbsp;
</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&amp;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&amp;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.

View file

@ -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

View file

@ -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

View file

@ -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

File diff suppressed because it is too large Load diff