[gen] Master-slave fields: slave values can now ajax-change when the user modifies master values.

This commit is contained in:
Gaetan Delannay 2014-03-03 18:54:21 +01:00
parent f7172be6ee
commit b8ceb66a49
15 changed files with 253 additions and 74 deletions

View file

@ -49,7 +49,8 @@ class Field:
# * showChanges If True, a variant of the field showing successive changes # * showChanges If True, a variant of the field showing successive changes
# made to it is shown. # made to it is shown.
pxRender = Px(''' pxRender = Px('''
<x var="showChanges=showChanges|False; <x var="showChanges=showChanges|req.get('showChanges',False);
layoutType=layoutType|req.get('layoutType');
layout=field.layouts[layoutType]; layout=field.layouts[layoutType];
name=fieldName|field.name; name=fieldName|field.name;
sync=field.sync[layoutType]; sync=field.sync[layoutType];
@ -65,8 +66,7 @@ class Field:
isMultiple=(field.multiplicity[1] == None) or \ isMultiple=(field.multiplicity[1] == None) or \
(field.multiplicity[1] &gt; 1); (field.multiplicity[1] &gt; 1);
masterCss=field.slaves and ('master_%s' % name) or ''; masterCss=field.slaves and ('master_%s' % name) or '';
slaveCss=field.master and ('slave_%s_%s' % \ slaveCss=field.getSlaveCss();
(field.masterName, '_'.join(field.masterValue))) or '';
tagCss=tagCss|''; tagCss=tagCss|'';
tagCss=('%s %s' % (slaveCss, tagCss)).strip(); tagCss=('%s %s' % (slaveCss, tagCss)).strip();
tagId='%s_%s' % (zobj.UID(), name); tagId='%s_%s' % (zobj.UID(), name);
@ -170,7 +170,12 @@ class Field:
# The behaviour of this field may depend on another, "master" field # The behaviour of this field may depend on another, "master" field
self.master = master self.master = master
if master: self.master.slaves.append(self) if master: self.master.slaves.append(self)
# When master has some value(s), there is impact on this field. # The semantics of attribute "masterValue" below is as follows:
# - if "masterValue" is anything but a method, the field will be shown
# only when the master has this value, or one of it if multivalued;
# - if "masterValue" is a method, the value(s) of the slave field will
# be returned by this method, depending on the master value(s) that
# are given to it, as its unique parameter.
self.masterValue = gutils.initMasterValue(masterValue) self.masterValue = gutils.initMasterValue(masterValue)
# If a field must retain attention in a particular way, set focus=True. # If a field must retain attention in a particular way, set focus=True.
# It will be rendered in a special way. # It will be rendered in a special way.
@ -323,6 +328,7 @@ class Field:
if not masterData: return True if not masterData: return True
else: else:
master, masterValue = masterData master, masterValue = masterData
if masterValue and callable(masterValue): return True
reqValue = master.getRequestValue(obj.REQUEST) reqValue = master.getRequestValue(obj.REQUEST)
# reqValue can be a list or not # reqValue can be a list or not
if type(reqValue) not in sutils.sequenceTypes: if type(reqValue) not in sutils.sequenceTypes:
@ -567,6 +573,38 @@ class Field:
if self.master: return (self.master, self.masterValue) if self.master: return (self.master, self.masterValue)
if self.group: return self.group.getMasterData() if self.group: return self.group.getMasterData()
def getSlaveCss(self):
'''Gets the CSS class that must apply to this field in the web UI when
this field is the slave of another field.'''
if not self.master: return ''
res = 'slave*%s*' % self.masterName
if not callable(self.masterValue):
res += '*'.join(self.masterValue)
else:
res += '+'
return res
def getOnChange(self, name, zobj, layoutType):
'''When this field is a master, this method computes the call to the
Javascript function that will be called when its value changes (in
order to update slaves).'''
if not self.slaves: return ''
q = zobj.getTool().quote
# Create the dict of request values for slave fields.
rvs = {}
req = zobj.REQUEST
for slave in self.slaves:
name = slave.name
if not req.has_key(name): continue
if not req[name]: continue
rvs[name] = req[name]
if rvs:
rvs = ',%s' % sutils.getStringDict(rvs)
else:
rvs = ''
return 'updateSlaves(this,null,%s,%s%s)' % \
(q(zobj.absolute_url()), q(layoutType), rvs)
def isEmptyValue(self, value, obj=None): def isEmptyValue(self, value, obj=None):
'''Returns True if the p_value must be considered as an empty value.''' '''Returns True if the p_value must be considered as an empty value.'''
return value in self.nullValues return value in self.nullValues

View file

@ -33,8 +33,9 @@ class Boolean(Field):
<x var="isChecked=field.isChecked(zobj, rawValue)"> <x var="isChecked=field.isChecked(zobj, rawValue)">
<input type="checkbox" name=":name + '_visible'" id=":name" <input type="checkbox" name=":name + '_visible'" id=":name"
class=":masterCss" checked=":isChecked" class=":masterCss" checked=":isChecked"
onclick=":'toggleCheckbox(%s, %s); updateSlaves(this)' % \ onclick=":'toggleCheckbox(%s, %s); %s' % (q(name), \
(q(name), q('%s_hidden' % name))"/> q('%s_hidden' % name), field.getOnChange(name, zobj, \
layoutType))"/>
<input type="hidden" name=":name" id=":'%s_hidden' % name" <input type="hidden" name=":name" id=":'%s_hidden' % name"
value=":isChecked and 'True' or 'False'"/> value=":isChecked and 'True' or 'False'"/>
</x>''') </x>''')

View file

@ -15,7 +15,7 @@
# Appy. If not, see <http://www.gnu.org/licenses/>. # Appy. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import time, os.path, mimetypes import time, os.path, mimetypes, shutil
from DateTime import DateTime from DateTime import DateTime
from appy import Object from appy import Object
from appy.fields import Field from appy.fields import Field
@ -27,6 +27,7 @@ from appy.shared import UnmarshalledFile, mimeTypesExts
WRONG_FILE_TUPLE = 'This is not the way to set a file. You can specify a ' \ WRONG_FILE_TUPLE = 'This is not the way to set a file. You can specify a ' \
'2-tuple (fileName, fileContent) or a 3-tuple (fileName, fileContent, ' \ '2-tuple (fileName, fileContent) or a 3-tuple (fileName, fileContent, ' \
'mimeType).' 'mimeType).'
CONVERSION_ERROR = 'An error occurred. %s'
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class FileInfo: class FileInfo:
@ -44,6 +45,12 @@ class FileInfo:
self.mimeType = None # Its MIME type 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.
def getFilePath(self, obj):
'''Returns the absolute file name of the file on disk that corresponds
to this FileInfo instance.'''
dbFolder, folder = obj.o.getFsFolder()
return os.path.join(dbFolder, folder, self.fsName)
def removeFile(self, dbFolder, removeEmptyFolders=False): def removeFile(self, dbFolder, removeEmptyFolders=False):
'''Removes the file from the filesystem.''' '''Removes the file from the filesystem.'''
try: try:
@ -176,6 +183,39 @@ class FileInfo:
response.write(chunk) response.write(chunk)
f.close() f.close()
def dump(self, obj, filePath=None, format=None):
'''Exports this file to disk (outside the db-controller filesystem).
The tied Appy p_obj(ect) is required. If p_filePath is specified, it
is the path name where the file will be dumped; folders mentioned in
it must exist. If not, the file will be dumped in the OS temp folder.
The absolute path name of the dumped file is returned. If an error
occurs, the method returns None. If p_format is specified,
LibreOffice will be called for converting the dumped file to the
desired format.'''
if not filePath:
filePath = '%s/file%f.%s' % (sutils.getOsTempFolder(), time.time(),
self.fsName)
# Copies the file to disk.
shutil.copyfile(self.getFilePath(obj), filePath)
if format:
# Convert the dumped file using LibreOffice
errorMessage = obj.tool.convert(filePath, format)
# Even if we have an "error" message, it could be a simple warning.
# So we will continue here and, as a subsequent check for knowing if
# an error occurred or not, we will test the existence of the
# converted file (see below).
os.remove(filePath)
# Return the name of the converted file.
baseName, ext = os.path.splitext(filePath)
if (ext == '.%s' % format):
filePath = '%s.res.%s' % (baseName, format)
else:
filePath = '%s.%s' % (baseName, format)
if not os.path.exists(filePath):
obj.log(CONVERSION_ERROR % errorMessage, type='error')
return
return filePath
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class File(Field): class File(Field):

View file

@ -282,13 +282,11 @@ class Ref(Field):
pxEdit = Px(''' pxEdit = Px('''
<select if="field.link" <select if="field.link"
var2="requestValue=req.get(name, []); var2="zobjects=field.getSelectableObjects(obj);
inRequest=req.has_key(name);
zobjects=field.getSelectableObjects(obj);
uids=[o.UID() for o in \ uids=[o.UID() for o in \
field.getLinkedObjects(zobj).objects]; field.getLinkedObjects(zobj).objects]"
isBeingCreated=zobj.isTemporary()" name=":name" id=":name" size=":isMultiple and field.height or ''"
name=":name" size=":isMultiple and field.height or ''" onchange=":field.getOnChange(name, zobj, layoutType)"
multiple=":isMultiple"> multiple=":isMultiple">
<option value="" if="not isMultiple">:_('choose_a_value')</option> <option value="" if="not isMultiple">:_('choose_a_value')</option>
<option for="ztied in zobjects" var2="uid=ztied.o.UID()" <option for="ztied in zobjects" var2="uid=ztied.o.UID()"
@ -743,10 +741,28 @@ class Ref(Field):
def getSelectableObjects(self, obj): def getSelectableObjects(self, obj):
'''This method returns the list of all objects that can be selected to '''This method returns the list of all objects that can be selected to
be linked as references to p_obj via p_self.''' be linked as references to p_obj via p_self. If master values are
present in the request, we use field.masterValues method instead of
self.select.'''
req = obj.request
if 'masterValues' in req:
# Convert masterValue(s) from UID(s) to real object(s).
masterValues = req['masterValues'].strip()
if not masterValues: masterValues = None
else:
masterValues = masterValues.split('*')
tool = obj.tool
if len(masterValues) == 1:
masterValues = tool.getObject(masterValues[0])
else:
masterValues = [tool.getObject(v) for v in masterValues]
res = self.masterValue(obj, masterValues)
return res
else:
if not self.select: if not self.select:
# No select method has been defined: we must retrieve all objects # No select method has been defined: we must retrieve all
# of the referred type that the user is allowed to access. # objects of the referred type that the user is allowed to
# access.
return obj.search(self.klass) return obj.search(self.klass)
else: else:
return self.select(obj) return self.select(obj)

View file

@ -104,21 +104,19 @@ class String(Field):
pxEdit = Px(''' pxEdit = Px('''
<x var="fmt=field.format; <x var="fmt=field.format;
isSelect=field.isSelect;
isMaster=field.slaves;
isOneLine=fmt in (0,3,4)"> isOneLine=fmt in (0,3,4)">
<select if="isSelect" <select if="field.isSelect"
var2="possibleValues=field.getPossibleValues(zobj, \ var2="possibleValues=field.getPossibleValues(zobj, \
withTranslations=True, withBlankValue=True)" withTranslations=True, withBlankValue=True)"
name=":name" id=":name" class=":masterCss" name=":name" id=":name" class=":masterCss"
multiple=":isMultiple and 'multiple' or ''" multiple=":isMultiple and 'multiple' or ''"
onchange=":isMaster and 'updateSlaves(this)' or ''" onchange=":field.getOnChange(name, zobj, layoutType)"
size=":isMultiple and field.height or 1"> size=":isMultiple and field.height or 1">
<option for="val in possibleValues" value=":val[0]" <option for="val in possibleValues" value=":val[0]"
selected=":field.isSelected(zobj, name, val[0], rawValue)" selected=":field.isSelected(zobj, name, val[0], rawValue)"
title=":val[1]">:ztool.truncateValue(val[1],field.width)</option> title=":val[1]">:ztool.truncateValue(val[1],field.width)</option>
</select> </select>
<x if="isOneLine and not isSelect"> <x if="isOneLine and not field.isSelect">
<input id=":name" name=":name" size=":field.width" <input id=":name" name=":name" size=":field.width"
maxlength=":field.maxChars" maxlength=":field.maxChars"
value=":inRequest and requestValue or value" value=":inRequest and requestValue or value"

View file

@ -171,6 +171,9 @@ class Config:
# People having one of these roles will be able to create instances # People having one of these roles will be able to create instances
# of classes defined in your application. # of classes defined in your application.
defaultCreators = ['Manager'] defaultCreators = ['Manager']
# The "root" classes are those that will get their menu in the user
# interface. Put their names in the list below.
rootClasses = []
# Number of translations for every page on a Translation object # Number of translations for every page on a Translation object
translationsPerPage = 30 translationsPerPage = 30
# Language that will be used as a basis for translating to other # Language that will be used as a basis for translating to other

View file

@ -553,9 +553,6 @@ class ZopeGenerator(Generator):
if theImport not in imports: if theImport not in imports:
imports.append(theImport) imports.append(theImport)
repls['imports'] = '\n'.join(imports) repls['imports'] = '\n'.join(imports)
# Compute root classes
repls['rootClasses'] = ','.join(["'%s'" % c.name \
for c in classesButTool if c.isRoot()])
# Compute list of class definitions # Compute list of class definitions
repls['appClasses'] = ','.join(['%s.%s' % (c.klass.__module__, \ repls['appClasses'] = ','.join(['%s.%s' % (c.klass.__module__, \
c.klass.__name__) for c in classes]) c.klass.__name__) for c in classes])
@ -564,6 +561,9 @@ class ZopeGenerator(Generator):
for c in classes]) for c in classes])
repls['allClassNames'] = ','.join(['"%s"' % c.name \ repls['allClassNames'] = ','.join(['"%s"' % c.name \
for c in classesButTool]) for c in classesButTool])
allShortClassNames = ['"%s":"%s"' % (c.name.split('_')[-1], c.name) \
for c in classesAll]
repls['allShortClassNames'] = ','.join(allShortClassNames)
# Compute the list of ordered attributes (forward and backward, # Compute the list of ordered attributes (forward and backward,
# inherited included) for every Appy class. # inherited included) for every Appy class.
attributes = [] attributes = []

View file

@ -209,8 +209,12 @@ class ToolMixin(BaseMixin):
def getRootClasses(self): def getRootClasses(self):
'''Returns the list of root classes for this application.''' '''Returns the list of root classes for this application.'''
cfg = self.getProductConfig() cfg = self.getProductConfig().appConfig
return [self.getAppyClass(k) for k in cfg.rootClasses] rootClasses = cfg.rootClasses
if not rootClasses:
# We consider every class as being a root class.
rootClasses = cfg.appClassNames
return [self.getAppyClass(k) for k in rootClasses]
def getSearchInfo(self, className, refInfo=None): def getSearchInfo(self, className, refInfo=None):
'''Returns, as an object: '''Returns, as an object:
@ -466,6 +470,10 @@ class ToolMixin(BaseMixin):
'''Gets the Appy class corresponding to the Zope class named p_name. '''Gets the Appy class corresponding to the Zope class named p_name.
If p_wrapper is True, it returns the Appy wrapper. Else, it returns If p_wrapper is True, it returns the Appy wrapper. Else, it returns
the user-defined class.''' the user-defined class.'''
# p_zopeName may be the name of the Zope class *or* the name of the Appy
# class (shorter, not prefixed with the underscored package path).
classes = self.getProductConfig().allShortClassNames
if zopeName in classes: zopeName = classes[zopeName]
zopeClass = self.getZopeClass(zopeName) zopeClass = self.getZopeClass(zopeName)
if wrapper: return zopeClass.wrapperClass if wrapper: return zopeClass.wrapperClass
else: return zopeClass.wrapperClass.__bases__[-1] else: return zopeClass.wrapperClass.__bases__[-1]

View file

@ -814,6 +814,20 @@ class BaseMixin:
res.append(field) res.append(field)
return res return res
def getSlaveFieldsRequestValues(self, pageName):
'''Returns the list of slave fields having a masterValue being a
method.'''
res = {}
req = self.REQUEST
for field in self.getAllAppyTypes():
if field.page.name != pageName: continue
if field.masterValue and callable(field.masterValue):
# We have such a field
name = field.name
if req.has_key(name) and req[name]:
res[name] = req[name]
return sutils.getStringDict(res)
def getCssJs(self, fields, layoutType, res): def getCssJs(self, fields, layoutType, res):
'''Gets, in p_res ~{'css':[s_css], 'js':[s_js]}~ the lists of '''Gets, in p_res ~{'css':[s_css], 'js':[s_js]}~ the lists of
Javascript and CSS files required by Appy types p_fields when shown Javascript and CSS files required by Appy types p_fields when shown

View file

@ -20,10 +20,10 @@ PROJECTNAME = '<!applicationName!>'
diskFolder = os.path.dirname(<!applicationName!>.__file__) diskFolder = os.path.dirname(<!applicationName!>.__file__)
# Applications classes, in various formats # Applications classes, in various formats
rootClasses = [<!rootClasses!>]
appClasses = [<!appClasses!>] appClasses = [<!appClasses!>]
appClassNames = [<!appClassNames!>] appClassNames = [<!appClassNames!>]
allClassNames = [<!allClassNames!>] allClassNames = [<!allClassNames!>]
allShortClassNames = {<!allShortClassNames!>}
# In the following dict, we store, for every Appy class, the ordered list of # In the following dict, we store, for every Appy class, the ordered list of
# fields. # fields.

View file

@ -228,10 +228,13 @@ function askComputedField(hookId, objectUrl, fieldName) {
askAjaxChunk(hookId, 'GET', objectUrl, fieldName+':pxViewContent'); askAjaxChunk(hookId, 'GET', objectUrl, fieldName+':pxViewContent');
} }
function askField(hookId, objectUrl, layoutType, showChanges){ function askField(hookId, objectUrl, layoutType, showChanges, masterValues,
requestValue){
// Sends an Ajax request for getting the content of any field. // Sends an Ajax request for getting the content of any field.
var fieldName = hookId.split('_')[1]; var fieldName = hookId.split('_')[1];
var params = {'layoutType': layoutType, 'showChanges': showChanges}; var params = {'layoutType': layoutType, 'showChanges': showChanges};
if (masterValues) params['masterValues'] = masterValues.join('*');
if (requestValue) params[fieldName] = requestValue;
askAjaxChunk(hookId, 'GET', objectUrl, fieldName+':pxRender', params, null, askAjaxChunk(hookId, 'GET', objectUrl, fieldName+':pxRender', params, null,
evalInnerScripts); evalInnerScripts);
} }
@ -286,14 +289,15 @@ function toggleSubTitles(tag) {
// Functions used for master/slave relationships between widgets // Functions used for master/slave relationships between widgets
function getSlaveInfo(slave, infoType) { function getSlaveInfo(slave, infoType) {
// Returns the appropriate info about slavery, depending on p_infoType. // Returns the appropriate info about slavery, depending on p_infoType.
cssClasses = slave.className.split(' '); var cssClasses = slave.className.split(' ');
var masterInfo = null;
// Find the CSS class containing master-related info. // Find the CSS class containing master-related info.
for (var j=0; j < cssClasses.length; j++) { for (var j=0; j < cssClasses.length; j++) {
if (cssClasses[j].indexOf('slave_') == 0) { if (cssClasses[j].indexOf('slave*') == 0) {
// Extract, from this CSS class, master name or master values. // Extract, from this CSS class, master name or master values.
masterInfo = cssClasses[j].split('_'); masterInfo = cssClasses[j].split('*');
if (infoType == 'masterName') return masterInfo[1]; if (infoType == 'masterName') return masterInfo[1];
else return masterInfo.slice(2); // Master values else return masterInfo.slice(2);
} }
} }
} }
@ -337,7 +341,7 @@ function getSlaves(master) {
if (master.type == 'checkbox') { if (master.type == 'checkbox') {
masterName = masterName.substr(0, masterName.length-8); masterName = masterName.substr(0, masterName.length-8);
} }
slavePrefix = 'slave_' + masterName + '_'; slavePrefix = 'slave*' + masterName + '*';
for (var i=0; i < allSlaves.length; i++){ for (var i=0; i < allSlaves.length; i++){
cssClasses = allSlaves[i].className.split(' '); cssClasses = allSlaves[i].className.split(' ');
for (var j=0; j < cssClasses.length; j++) { for (var j=0; j < cssClasses.length; j++) {
@ -350,37 +354,52 @@ function getSlaves(master) {
return res; return res;
} }
function updateSlaves(master, slave) { function updateSlaves(master, slave, objectUrl, layoutType, requestValues){
// Given the value(s) in a master field, we must update slave's visibility. /* Given the value(s) in a master field, we must update slave's visibility or
// If p_slave is given, it updates only this slave. Else, it updates all value(s). If p_slave is given, it updates only this slave. Else, it updates
// slaves of p_master. all slaves of p_master. */
var slaves = null; var slaves = null;
if (slave) { slaves = [slave]; } if (slave) { slaves = [slave]; }
else { slaves = getSlaves(master); } else { slaves = getSlaves(master); }
masterValues = getMasterValues(master); masterValues = getMasterValues(master);
for (var i=0; i < slaves.length; i++) { for (var i=0; i < slaves.length; i++) {
showSlave = false;
slaveryValues = getSlaveInfo(slaves[i], 'masterValues'); slaveryValues = getSlaveInfo(slaves[i], 'masterValues');
if (slaveryValues[0] != '+') {
// Update slaves visibility depending on master values.
var showSlave = false;
for (var j=0; j < slaveryValues.length; j++) { for (var j=0; j < slaveryValues.length; j++) {
for (var k=0; k< masterValues.length; k++) { for (var k=0; k< masterValues.length; k++) {
if (slaveryValues[j] == masterValues[k]) showSlave = true; if (slaveryValues[j] == masterValues[k]) showSlave = true;
} }
} }
if (showSlave) slaves[i].style.display = ""; if (showSlave) slaves[i].style.display = '';
else slaves[i].style.display = "none"; else slaves[i].style.display = 'none';
}
else {
// Update slaves' values depending on master values.
var slaveId = slaves[i].id;
var slaveName = slaveId.split('_')[1];
var reqValue = null;
if (requestValues && (slaveName in requestValues))
reqValue = requestValues[slaveName];
askField(slaveId, objectUrl, layoutType, false, masterValues, reqValue);
}
} }
} }
function initSlaves() { function initSlaves(objectUrl, layoutType, requestValues) {
// When the current page is loaded, we must set the correct state for all /* When the current page is loaded, we must set the correct state for all
// slave fields. slave fields. p_requestValues are those from the slave fields that must
be ajax-updated. */
slaves = getElementsHavingName('table', 'slave'); slaves = getElementsHavingName('table', 'slave');
i = slaves.length -1; i = slaves.length -1;
while (i >= 0) { while (i >= 0) {
masterName = getSlaveInfo(slaves[i], 'masterName'); masterName = getSlaveInfo(slaves[i], 'masterName');
master = document.getElementById(masterName); master = document.getElementById(masterName);
// If master is not here, we can't hide its slaves when appropriate. // If master is not here, we can't hide its slaves when appropriate.
if (master) updateSlaves(master, slaves[i]); if (master) {
updateSlaves(master, slaves[i], objectUrl, layoutType, requestValues);
}
i -= 1; i -= 1;
} }
} }

View file

@ -211,7 +211,8 @@ def writeCookie(login, password, request):
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
def initMasterValue(v): def initMasterValue(v):
'''Standardizes p_v as a list of strings.''' '''Standardizes p_v as a list of strings, excepted if p_v is a method.'''
if callable(v): return v
if not isinstance(v, bool) and not v: res = [] if not isinstance(v, bool) and not v: res = []
elif type(v) not in sutils.sequenceTypes: res = [v] elif type(v) not in sutils.sequenceTypes: res = [v]
else: res = v else: res = v

View file

@ -141,7 +141,9 @@ class ToolWrapper(AbstractWrapper):
</x>''') </x>''')
pxPageBottom = Px(''' pxPageBottom = Px('''
<script type="text/javascript">initSlaves();</script>''') <script type="text/javascript">:'initSlaves(%s,%s,%s)' % \
(q(zobj.absolute_url()), q(layoutType), \
zobj.getSlaveFieldsRequestValues(page))</script>''')
pxPortlet = Px(''' pxPortlet = Px('''
<x var="toolUrl=tool.url; <x var="toolUrl=tool.url;
@ -260,11 +262,11 @@ class ToolWrapper(AbstractWrapper):
# Hook for defining a PX that proposes additional links, after the links # Hook for defining a PX that proposes additional links, after the links
# corresponding to top-level pages. # corresponding to top-level pages.
pxLinks = '' pxLinks = Px('')
# Hook for defining a PX that proposes additional icons after standard # Hook for defining a PX that proposes additional icons after standard
# icons in the user strip. # icons in the user strip.
pxIcons = '' pxIcons = Px('')
# Displays the content of a layouted object (a page or a field). If the # 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) # layouted object is a page, the "layout target" (where to look for PXs)
@ -310,7 +312,7 @@ class ToolWrapper(AbstractWrapper):
</table>''', template=AbstractWrapper.pxTemplate, hook='content') </table>''', template=AbstractWrapper.pxTemplate, hook='content')
# Show on query list or grid, the field content for a given object. # Show on query list or grid, the field content for a given object.
pxQueryField = Px('''<x> pxQueryField = Px('''
<!-- Title --> <!-- Title -->
<x if="field.name == 'title'" <x if="field.name == 'title'"
var2="navInfo='search.%s.%s.%d.%d' % \ var2="navInfo='search.%s.%s.%d.%d' % \
@ -350,7 +352,6 @@ class ToolWrapper(AbstractWrapper):
<x if="field.name != 'title'"> <x if="field.name != 'title'">
<x var="layoutType='cell'; innerRef=True" <x var="layoutType='cell'; innerRef=True"
if="zobj.showField(field.name, 'result')">:field.pxRender</x> if="zobj.showField(field.name, 'result')">:field.pxRender</x>
</x>
</x>''') </x>''')
# Show query results as a list. # Show query results as a list.

View file

@ -253,6 +253,17 @@ def keepDigits(s):
if c.isdigit(): res += c if c.isdigit(): res += c
return res return res
def getStringDict(d):
'''Gets the string literal corresponding to dict p_d.'''
res = []
for k, v in d.iteritems():
if type(v) not in sequenceTypes:
value = "'%s':'%s'" % (k, v)
else:
value = "'%s':%s" % (k, v)
res.append(value)
return '{%s}' % ','.join(res)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
def formatNumber(n, sep=',', precision=2, tsep=' '): def formatNumber(n, sep=',', precision=2, tsep=' '):
'''Returns a string representation of number p_n, which can be a float '''Returns a string representation of number p_n, which can be a float

View file

@ -596,31 +596,48 @@ class XmlMarshaller:
def dumpFile(self, res, v): def dumpFile(self, res, v):
'''Dumps a file into the result.''' '''Dumps a file into the result.'''
if not v: return if not v: return
w = res.write
# p_value contains the (possibly binary) content of a file. We will # p_value contains the (possibly binary) content of a file. We will
# encode it in Base64, in one or several parts. # encode it in Base64, in one or several parts.
partTag = self.getTagName('part') partTag = self.getTagName('part')
res.write('<%s type="base64" number="1">' % partTag) res.write('<%s type="base64" number="1">' % partTag)
if hasattr(v, 'data'): if hasattr(v, 'data'):
# The file is an Archetypes file. # The file is an Archetypes file.
valueType = v.data.__class__.__name__ if v.data.__class__.__name__ == 'Pdata':
if valueType == 'Pdata':
# There will be several parts. # There will be several parts.
res.write(v.data.data.encode('base64')) w(v.data.data.encode('base64'))
# Write subsequent parts # Write subsequent parts
nextPart = v.data.next nextPart = v.data.next
nextPartNumber = 2 nextPartNb = 2
while nextPart: while nextPart:
res.write('</%s>' % partTag) # Close the previous part w('</%s>' % partTag) # Close the previous part
res.write('<%s type="base64" number="%d">' % \ w('<%s type="base64" number="%d">' % (partTag, nextPartNb))
(partTag, nextPartNumber)) w(nextPart.data.encode('base64'))
res.write(nextPart.data.encode('base64'))
nextPart = nextPart.next nextPart = nextPart.next
nextPartNumber += 1 nextPartNb += 1
else: else:
res.write(v.data.encode('base64')) w(v.data.encode('base64'))
w('</%s>' % partTag)
elif hasattr(v, 'uploadName'):
# The file is a Appy FileInfo instance. Read the file from disk.
filePath = v.getFilePath(self.instance)
f = file(filePath, 'rb')
partNb = 1
while True:
chunk = f.read(v.BYTES)
if not chunk: break
# We have one more chunk. Dump the start tag (excepted if it is
# the first chunk: the start tag has already been dumped, see
# above).
if partNb > 1:
w('<%s type="base64" number="%d">' % (partTag, partNb))
w(chunk.encode('base64'))
w('</%s>' % partTag) # Close the tag
partNb += 1
f.close()
else: else:
res.write(v.encode('base64')) w(v.encode('base64'))
res.write('</%s>' % partTag) w('</%s>' % partTag)
def dumpDict(self, res, v): def dumpDict(self, res, v):
'''Dumps the XML version of dict p_v.''' '''Dumps the XML version of dict p_v.'''
@ -704,11 +721,22 @@ class XmlMarshaller:
if fieldValue: length = len(fieldValue) if fieldValue: length = len(fieldValue)
res.write(' count="%d"' % length) res.write(' count="%d"' % length)
if fType == 'file': if fType == 'file':
# Get the MIME type
mimeType = None
if hasattr(fieldValue, 'content_type'): if hasattr(fieldValue, 'content_type'):
res.write(' mimeType="%s"' % fieldValue.content_type) mimeType = fieldValue.content_type
elif hasattr(fieldValue, 'mimeType'):
mimeType = fieldValue.mimeType
if mimeType: res.write(' mimeType="%s"' % mimeType)
# Get the file name
fileName = None
if hasattr(fieldValue, 'filename'): if hasattr(fieldValue, 'filename'):
fileName = fieldValue.filename
elif hasattr(fieldValue, 'uploadName'):
fileName = fieldValue.uploadName
if fileName:
res.write(' name="') res.write(' name="')
self.dumpString(res, fieldValue.filename) self.dumpString(res, fileName)
res.write('"') res.write('"')
res.write('>') res.write('>')
# Dump the field value # Dump the field value
@ -724,6 +752,8 @@ class XmlMarshaller:
an instance at all, but another Python data structure or basic type, an instance at all, but another Python data structure or basic type,
p_objectType is ignored.''' p_objectType is ignored.'''
self.objectType = objectType self.objectType = objectType
# The Appy object is needed to marshall its File fields.
if objectType == 'appy': self.instance = instance
# Call the XmlMarshaller constructor if it hasn't been called yet. # Call the XmlMarshaller constructor if it hasn't been called yet.
if not hasattr(self, 'cdata'): if not hasattr(self, 'cdata'):
XmlMarshaller.__init__(self) XmlMarshaller.__init__(self)
@ -789,7 +819,6 @@ class XmlMarshaller:
if field.type == 'File': if field.type == 'File':
fieldType = 'file' fieldType = 'file'
v = field.getValue(instance) v = field.getValue(instance)
if v: v = v._zopeFile
elif field.type == 'Ref': elif field.type == 'Ref':
fieldType = 'ref' fieldType = 'ref'
v = field.getValue(instance, type='zobjects') v = field.getValue(instance, type='zobjects')