[gen] Display action fields with layout 'buttons' in query results and tied objects for refs, besides transitions. [gen] Show delete button when allowed on the view layout. [gen] Improved method AbstractWrapper::createFrom.

This commit is contained in:
Gaetan Delannay 2015-01-06 21:13:30 +01:00
parent 225ea927a4
commit 746a6cd52d
7 changed files with 171 additions and 131 deletions

View file

@ -30,21 +30,23 @@ class Action(Field):
<form var="formId='%s_%s_form' % (zobj.id, name);
label=_(field.labelId);
descr=field.hasDescr and _(field.descrId) or None;
buttonWidth=ztool.getButtonWidth(label)"
buttonWidth=ztool.getButtonWidth(label);
smallButtons=smallButtons|False;
css=smallButtons and 'buttonSmall button' or 'button'"
id=":formId" action=":ztool.absolute_url() + '/do'"
style="display:inline">
<input type="hidden" name="action" value="ExecuteAction"/>
<input type="hidden" name="objectUid" value=":zobj.id"/>
<input type="hidden" name="fieldName" value=":name"/>
<input type="hidden" name="comment" value=""/>
<input if="field.confirm" type="button" class="button" title=":descr"
<input if="field.confirm" type="button" class=":css" title=":descr"
var="labelConfirm=_(field.labelId + '_confirm');
commentParam=(field.confirm == 'text') and 'true' or 'false'"
value=":label"
style=":'%s; %s' % (url(field.icon, bg=True), buttonWidth)"
onclick=":'askConfirm(%s,%s,%s,%s)' % (q('form'), q(formId), \
q(labelConfirm), commentParam)"/>
<input if="not field.confirm" type="submit" class="button" name="do"
<input if="not field.confirm" type="submit" class=":css" name="do"
value=":label" title=":descr"
style=":'%s; %s' % (url(field.icon, bg=True), buttonWidth)"/>
</form>''')

View file

@ -86,10 +86,10 @@ class FileInfo:
self.uploadName = uploadName # The name of the uploaded file
self.size = 0 # Its size, in bytes
self.mimeType = None # Its MIME type
self.modified = None # The last modification date for this file.
self.modified = None # The last modification date for this file
# Complete metadata if p_inDb is False
if not inDb:
self.fsName = '' # Already included in self.fsPath.
self.fsName = '' # Already included in self.fsPath
# We will not store p_inDb. Checking if self.fsName is the empty
# string is equivalent.
fileInfo = os.stat(self.fsPath)
@ -166,20 +166,25 @@ class FileInfo:
'''Writes to the filesystem the p_fileObj file, that can be:
- a Zope FileUpload (coming from a HTTP post);
- a OFS.Image.File object (legacy within-ZODB file object);
- another ("not-in-DB") FileInfo instance;
- a tuple (fileName, fileContent, mimeType)
(see doc in method File.store below).'''
# Determine p_fileObj's type
fileType = fileObj.__class__.__name__
# Set MIME type
# Determine the MIME type and the base name of the file to store
if fileType == 'FileUpload':
mimeType = self.getMimeTypeFromFileUpload(fileObj)
fileName = fileObj.filename
elif fileType == 'File':
mimeType = fileObj.content_type
fileName = fileObj.filename
elif fileType == 'FileInfo':
mimeType = fileObj.mimeType
fileName = fileObj.uploadName
else:
mimeType = fileObj[2]
fileName = fileObj[0]
self.mimeType = mimeType or File.defaultMimeType
# Determine the original name of the file to store.
fileName= fileType.startswith('File') and fileObj.filename or fileObj[0]
if not fileName:
# Name it according to field name. Deduce file extension from the
# MIME type.
@ -195,12 +200,12 @@ class FileInfo:
fsName = osPathJoin(dbFolder, self.fsPath, self.fsName)
f = file(fsName, 'wb')
if fileType == 'FileUpload':
# Write the FileUpload instance on disk.
# Write the FileUpload instance on disk
self.size = self.replicateFile(fileObj, f)
elif fileType == 'File':
# Write the File instance on disk.
# Write the File instance on disk
if fileObj.data.__class__.__name__ == 'Pdata':
# The file content is splitted in several chunks.
# The file content is splitted in several chunks
f.write(fileObj.data.data)
nextPart = fileObj.data.next
while nextPart:
@ -210,10 +215,14 @@ class FileInfo:
# Only one chunk
f.write(fileObj.data)
self.size = fileObj.size
elif fileType == 'FileInfo':
src = file(fileObj.fsPath, 'rb')
self.size = self.replicateFile(src, f)
src.close()
else:
# Write fileObj[1] on disk.
# Write fileObj[1] on disk
if fileObj[1].__class__.__name__ == 'file':
# It is an open file handler.
# It is an open file handler
self.size = self.replicateFile(fileObj[1], f)
else:
# We have file content directly in fileObj[1]
@ -361,7 +370,17 @@ class File(Field):
name = requestName or self.name
return obj.REQUEST.get('%s_file' % name)
def getCopyValue(self, obj): raise Exception('Not implemented yet.')
def getCopyValue(self, obj):
'''Create a copy of the FileInfo instance stored for p_obj for this
field. This copy will contain the absolute path to the file on the
filesystem. This way, the file may be read independently from p_obj
(and copied somewhere else).'''
info = self.getValue(obj)
if not info: return
# Create a "not-in-DB", temporary FileInfo
return FileInfo(info.getFilePath(obj), inDb=False,
uploadName=info.uploadName)
def getDefaultLayouts(self): return {'view':'l-f','edit':'lrv-f'}
def isEmptyValue(self, obj, value):
@ -405,13 +424,15 @@ class File(Field):
f. a 3-tuple (fileName, fileContent, mimeType) where
- fileName and fileContent have the same meaning than above;
- mimeType is the MIME type of the file.
g. a FileInfo instance, that must be "not-in-DB", ie, with an
absolute path in attribute fsPath.
'''
zobj = obj.o
if value:
# There is a new value to store. Get the folder on disk where to
# store the new file.
dbFolder, folder = zobj.getFsFolder(create=True)
# Remove the previous file if it existed.
# Remove the previous file if it existed
info = getattr(obj.aq_base, self.name, None)
if info:
# The previous file can be a legacy File object in an old
@ -432,6 +453,9 @@ class File(Field):
elif isinstance(value, basestring):
# Case d
info.copyFile(self.name, value, dbFolder)
elif isinstance(value, FileInfo):
# Case g
info.writeFile(self.name, value, dbFolder)
else:
# Cases e, f. Extract file name, content and MIME type.
fileName = mimeType = None
@ -444,7 +468,7 @@ class File(Field):
mimeType = mimeType or guessMimeType(fileName)
info.writeFile(self.name, (fileName, fileContent, mimeType),
dbFolder)
# Store the FileInfo instance in the database.
# Store the FileInfo instance in the database
setattr(obj, self.name, info)
else:
# I store value "None", excepted if I find in the request the desire

View file

@ -77,10 +77,9 @@ class Ref(Field):
# This PX displays icons for triggering actions on a given referenced object
# (edit, delete, etc).
pxObjectActions = Px('''
<table class="noStyle">
<tr>
<div>
<!-- Arrows for moving objects up or down -->
<td if="(totalNumber &gt;1) and changeOrder and not inPickList \
<x if="(totalNumber &gt;1) and changeOrder and not inPickList \
and not inMenu"
var2="ajaxBaseCall=navBaseCall.replace('**v**','%s,%s,{%s:%s,%s:%s}'%\
(q(startNumber), q('doChangeOrder'), q('refObjectUid'),
@ -101,41 +100,44 @@ class Ref(Field):
<img if="objectIndex &lt; (totalNumber-1)" class="clickable"
src=":url('arrowDown')" title=":_('move_down')"
onclick=":ajaxBaseCall.replace('**v**', 'down')"/>
</td>
</x>
<!-- Edit -->
<td if="not field.noForm and tied.o.mayEdit()">
<a var="navInfo=field.getNavInfo(zobj, loop.tied.nb + 1 + startNumber, \
<a if="not field.noForm and tied.o.mayEdit()"
var2="navInfo=field.getNavInfo(zobj, loop.tied.nb + 1 + startNumber, \
totalNumber);
linkInPopup=inPopup or (target.target != '_self')"
href=":tied.o.getUrl(mode='edit', page='main', nav=navInfo, \
inPopup=linkInPopup)"
target=":target.target" onclick=":target.openPopup">
<img src=":url('edit')" title=":_('object_edit')"/></a>
</td>
<img src=":url('edit')" title=":_('object_edit')"/>
</a>
<!-- Delete -->
<td if="mayEdit and field.delete and tied.o.mayDelete()">
<img class="clickable" title=":_('object_delete')" src=":url('delete')"
<img if="mayEdit and field.delete and tied.o.mayDelete()"
class="clickable" title=":_('object_delete')" src=":url('delete')"
onclick=":'onDeleteObject(%s)' % q(tiedUid)"/>
</td>
<!-- Unlink -->
<td if="mayUnlink and field.mayUnlinkElement(obj, tied)">
<img var="imgName=linkList and 'unlinkUp' or 'unlink'; action='unlink'"
<img if="mayUnlink and field.mayUnlinkElement(obj, tied)"
var2="imgName=linkList and 'unlinkUp' or 'unlink'; action='unlink'"
class="clickable" title=":_('object_unlink')" src=":url(imgName)"
onclick=":'onLink(%s,%s,%s,%s)' % (q(action), q(zobj.id), \
q(field.name), q(tiedUid))"/>
</td>
<!-- Insert (if in pick list) -->
<td if="inPickList">
<img var="action='link'" class="clickable" title=":_('object_link')"
src=":url(action)"
<img if="inPickList" var2="action='link'" class="clickable"
title=":_('object_link')" src=":url(action)"
onclick=":'onLink(%s,%s,%s,%s)' % (q(action), q(zobj.id), \
q(field.name), q(tiedUid))"/>
</td>
<!-- Workflow transitions -->
<td if="tied.o.showTransitions('result')"
var2="targetObj=tied.o; buttonsMode='small'">:tied.pxTransitions</td>
</tr>
</table>''')
<x if="tied.o.showTransitions('result')"
var2="targetObj=tied.o; buttonsMode='small'">:tied.pxTransitions</x>
<!-- Fields (actions) defined with layout "buttons" -->
<x if="not inPopup"
var2="fields=tied.o.getAppyTypes('buttons', 'main', type='Action');
layoutType='view'">
<!-- Call pxView and not pxRender to avoid having a table -->
<x for="field in fields"
var2="name=field.name; smallButtons=True">:field.pxView</x>
</x>
</div>''')
# Displays the button allowing to add a new object through a Ref field, if
# it has been declared as addable and if multiplicities allow it.
@ -797,7 +799,7 @@ class Ref(Field):
UIDs, because m_store on the destination object can store tied
objects based on such a list.'''
res = getattr(obj.aq_base, self.name, ())
# Return a copy: it can be dangerous to give the real database value.
# Return a copy: it can be dangerous to give the real database value
if res: return list(res)
def getXmlValue(self, obj, value):
@ -830,7 +832,7 @@ class Ref(Field):
paginated = startNumber != None
isSearch = False
if 'masterValues' in req:
# Convert masterValue(s) from id(s) to real object(s).
# Convert masterValue(s) from id(s) to real object(s)
masterValues = req['masterValues'].strip()
if not masterValues: masterValues = None
else:
@ -865,11 +867,11 @@ class Ref(Field):
objects.objects = [o.appy() for o in objects.objects]
else:
objects = self.select(obj)
# Remove already linked objects if required.
# Remove already linked objects if required
if removeLinked:
uids = getattr(obj.o.aq_base, self.name, None)
if uids:
# Browse objects in reverse order and remove linked objects.
# Browse objects in reverse order and remove linked objects
if isSearch: objs = objects.objects
else: objs = objects
i = len(objs) - 1
@ -884,7 +886,7 @@ class Ref(Field):
if paginated and not isSearch:
total = len(objects)
objects = objects[startNumber:startNumber + self.maxPerPage]
# Return the result, wrapped in a SomeObjects instance if required.
# Return the result, wrapped in a SomeObjects instance if required
if not someObjects:
if isSearch: return objects.objects
return objects
@ -909,7 +911,7 @@ class Ref(Field):
for tied in objects:
menuId = self.menuIdMethod(obj, tied)
if menuId in menuIds:
# We have already encountered this menu.
# We have already encountered this menu
menuIndex = menuIds[menuId]
res[menuIndex].objects.append(tied)
else:
@ -947,7 +949,7 @@ class Ref(Field):
'''This method returns the index of the first linked object that must be
shown, or None if all linked objects must be shown at once (it
happens when p_render is "menus").'''
# When using 'menus' render mode, all linked objects must be shown.
# When using 'menus' render mode, all linked objects must be shown
if render == 'menus': return
# When using 'list' (=default) render mode, the index of the first
# object to show is in the request.

View file

@ -328,7 +328,7 @@ class Transition:
# functions return True.
hasRole = None
for condition in self.condition:
# "Unwrap" role names from Role instances.
# "Unwrap" role names from Role instances
if isinstance(condition, Role): condition = condition.name
if isinstance(condition, basestring): # It is a role
if hasRole == None:
@ -337,7 +337,7 @@ class Transition:
hasRole = True
else: # It is a method
res = condition(wf, obj.appy())
if not res: return res # False or a No instance.
if not res: return res # False or a No instance
if hasRole != False:
return True
@ -345,13 +345,13 @@ class Transition:
'''Executes the action related to this transition.'''
msg = ''
obj = obj.appy()
wf = wf.__instance__ # We need the prototypical instance here.
wf = wf.__instance__ # We need the prototypical instance here
if type(self.action) in (tuple, list):
# We need to execute a list of actions
for act in self.action:
msgPart = act(wf, obj)
if msgPart: msg += msgPart
else: # We execute a single action only.
else: # We execute a single action only
msgPart = self.action(wf, obj)
if msgPart: msg += msgPart
return msg

View file

@ -365,30 +365,34 @@ class ToolWrapper(AbstractWrapper):
if="sub">::zobj.highlight(sub)</span>
<!-- Actions -->
<table class="noStyle" if="not inPopup and zobj.mayAct()">
<tr>
<div if="not inPopup and zobj.mayAct()">
<!-- Edit -->
<td if="zobj.mayEdit()">
<a var="navInfo='search.%s.%s.%d.%d' % \
<a if="zobj.mayEdit()"
var2="navInfo='search.%s.%s.%d.%d' % \
(className, searchName, loop.zobj.nb+1+startNumber, totalNumber);
linkInPopup=inPopup or (target.target != '_self')"
target=":target.target" onclick=":target.openPopup"
href=":zobj.getUrl(mode='edit', page=zobj.getDefaultEditPage(), \
nav=navInfo, inPopup=linkInPopup)">
<img src=":url('edit')" title=":_('object_edit')"/></a>
</td>
<td>
<img src=":url('edit')" title=":_('object_edit')"/>
</a>
<!-- Delete -->
<img if="zobj.mayDelete()" class="clickable" src=":url('delete')"
title=":_('object_delete')"
onClick=":'onDeleteObject(%s)' % q(zobj.id)"/>
</td>
<!-- Workflow transitions -->
<td if="zobj.showTransitions('result')"
<x if="zobj.showTransitions('result')"
var2="targetObj=zobj;
buttonsMode='small'">:targetObj.appy().pxTransitions</td>
</tr>
</table>
buttonsMode='small'">:targetObj.appy().pxTransitions</x>
<!-- Fields (actions) defined with layout "buttons" -->
<x if="not inPopup"
var2="fields=zobj.getAppyTypes('buttons', 'main', type='Action');
layoutType='view'">
<!-- Call pxView and not pxRender to avoid having a table -->
<x for="field in fields"
var2="name=field.name; smallButtons=True">:field.pxView</x>
</x>
</div>
</x>
<x if="not mayView">
<img src=":url('fake')" style="margin-right: 5px"/>

View file

@ -304,7 +304,7 @@ class UserWrapper(AbstractWrapper):
rq = self.request
if (self.user == self) and hasattr(rq, 'userLogins'):
return rq.userLogins
# Compute it.
# Compute it
res = [group.login for group in self.groups]
if not groupsOnly: res.append(self.login)
return res

View file

@ -30,7 +30,7 @@ class AbstractWrapper(object):
'(%s).click()' % q(gotoName)"/><img
id=":gotoName" name=":gotoName"
class="clickable" src=":url('gotoNumber')" title=":label"
onClick=":'gotoTied(%s,%s,this.previousSibling,%s)' % \
onclick=":'gotoTied(%s,%s,this.previousSibling,%s)' % \
(q(sourceUrl), q(field.name), totalNumber)"/></x>''')
pxNavigationStrip = Px('''
@ -469,7 +469,7 @@ class AbstractWrapper(object):
<!-- Button on the edit page -->
<x if="isEdit">
<input type="button" class="button" value=":label"
onClick="submitAppyForm('previous')"
onclick="submitAppyForm('previous')"
style=":'%s; %s' % (url('previous', bg=True), buttonWidth)"/>
<input type="hidden" name="previousPage" value=":previousPage"/>
</x>
@ -479,26 +479,22 @@ class AbstractWrapper(object):
onclick=":'goto(%s)' % q(zobj.getUrl(page=previousPage, \
inPopup=inPopup))"/>
</x>
<!-- Save -->
<input if="isEdit and pageInfo.showSave"
type="button" class="button" onClick="submitAppyForm('save')"
type="button" class="button" onclick="submitAppyForm('save')"
var2="label=_('object_save')" value=":label"
style=":'%s; %s' % (url('save', bg=True), \
ztool.getButtonWidth(label))" />
<!-- Cancel -->
<input if="isEdit and pageInfo.showCancel"
type="button" class="button" onClick="submitAppyForm('cancel')"
type="button" class="button" onclick="submitAppyForm('cancel')"
var2="label=_('object_cancel')" value=":label"
style=":'%s; %s' % (url('cancel', bg=True), \
ztool.getButtonWidth(label))"/>
<x if="not isEdit"
var2="locked=zobj.isLocked(user, page);
editable=pageInfo.showOnEdit and pageInfo.showEdit and \
mayAct and zobj.mayEdit()">
<!-- Edit -->
<input type="button" class="button" if="editable and not locked"
var="label=_('object_edit')" value=":label"
@ -506,7 +502,6 @@ class AbstractWrapper(object):
ztool.getButtonWidth(label))"
onclick=":'goto(%s)' % q(zobj.getUrl(mode='edit', page=page, \
inPopup=inPopup))"/>
<!-- Locked -->
<a if="editable and locked">
<img style="cursor: help"
@ -520,14 +515,20 @@ class AbstractWrapper(object):
src=":url('unlockBig')"
onclick=":'onUnlockPage(%s,%s)' % (q(zobj.id), q(page))"/></a>
</x>
<!-- Delete -->
<input if="not isEdit and not inPopup and zobj.mayDelete()"
type="button" class="button"
onclick=":'onDeleteObject(%s)' % q(zobj.id)"
var2="label=_('object_delete')" value=":label"
style=":'%s; %s' % (url('delete', bg=True), \
ztool.getButtonWidth(label))"/>
<!-- Next -->
<x if="nextPage and pageInfo.showNext"
var2="label=_('page_next');
buttonWidth=ztool.getButtonWidth(label)">
<!-- Button on the edit page -->
<x if="isEdit">
<input type="button" class="button" onClick="submitAppyForm('next')"
<input type="button" class="button" onclick="submitAppyForm('next')"
style=":'%s; %s' % (url('next', bg=True), buttonWidth)"
value=":label"/>
<input type="hidden" name="nextPage" value=":nextPage"/>
@ -538,12 +539,10 @@ class AbstractWrapper(object):
onclick=":'goto(%s)' % q(zobj.getUrl(page=nextPage, \
inPopup=inPopup))"/>
</x>
<!-- Workflow transitions -->
<x var="targetObj=zobj; buttonsMode='normal'"
if="mayAct and \
targetObj.showTransitions(layoutType)">:obj.pxTransitions</x>
<!-- Fields (actions) defined with layout "buttons" -->
<x if="layoutType != 'edit'"
var2="fields=zobj.getAppyTypes('buttons', page, type='Action');
@ -929,13 +928,17 @@ class AbstractWrapper(object):
return appyObj
def createFrom(self, fieldNameOrClass, other, noSecurity=False,
executeMethods=True, exclude=()):
executeMethods=True, exclude=(), keepBase=False):
'''Similar to m_create above, excepted that we will use another object
(p_other) as base for filling in data for the object to create.
p_exclude can list fields (by their names) that will not be
copied on p_other. Note that this method does not perform a deep
copy: objects linked via Ref fields from p_self will be
referenced by the clone, but not themselves copied.'''
p_exclude can list fields (by their names) that will not be copied on
p_other. If p_keepBase is True, basic attributes will be kept on the
new object: creator and dates "created" and "modified". Else, the
new object's creator will be the logged user.
Note that this method does not perform a deep copy: objects linked
via Ref fields from p_self will be referenced by the clone, but not
themselves copied.'''
# Get the field values to set from p_other and store it in a dict.
# p_other may not be of the same class as p_self.
params = {}
@ -944,9 +947,14 @@ class AbstractWrapper(object):
if not field.persist or (field.name in exclude) or \
((field.type == 'Ref') and field.isBack): continue
params[field.name] = field.getCopyValue(other.o)
return self.create(fieldNameOrClass, noSecurity=noSecurity,
res = self.create(fieldNameOrClass, noSecurity=noSecurity,
raiseOnWrongAttribute=False,
executeMethods=executeMethods, **params)
# Propagate base attributes if required
if keepBase:
for name in ('creator', 'created', 'modified'):
setattr(res.o, name, getattr(other.o, name))
return res
def freeze(self, fieldName, template=None, format='pdf', noSecurity=True,
freezeOdtOnError=True):