[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
# made to it is shown.
pxRender = Px('''
<x var="showChanges=showChanges|False;
<x var="showChanges=showChanges|req.get('showChanges',False);
layoutType=layoutType|req.get('layoutType');
layout=field.layouts[layoutType];
name=fieldName|field.name;
sync=field.sync[layoutType];
@ -65,8 +66,7 @@ class Field:
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 '';
slaveCss=field.getSlaveCss();
tagCss=tagCss|'';
tagCss=('%s %s' % (slaveCss, tagCss)).strip();
tagId='%s_%s' % (zobj.UID(), name);
@ -170,7 +170,12 @@ class Field:
# The behaviour of this field may depend on another, "master" field
self.master = master
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)
# If a field must retain attention in a particular way, set focus=True.
# It will be rendered in a special way.
@ -323,6 +328,7 @@ class Field:
if not masterData: return True
else:
master, masterValue = masterData
if masterValue and callable(masterValue): return True
reqValue = master.getRequestValue(obj.REQUEST)
# reqValue can be a list or not
if type(reqValue) not in sutils.sequenceTypes:
@ -567,6 +573,38 @@ class Field:
if self.master: return (self.master, self.masterValue)
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):
'''Returns True if the p_value must be considered as an empty value.'''
return value in self.nullValues

View file

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

View file

@ -15,7 +15,7 @@
# 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 appy import Object
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 ' \
'2-tuple (fileName, fileContent) or a 3-tuple (fileName, fileContent, ' \
'mimeType).'
CONVERSION_ERROR = 'An error occurred. %s'
# ------------------------------------------------------------------------------
class FileInfo:
@ -44,6 +45,12 @@ class FileInfo:
self.mimeType = None # Its MIME type
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):
'''Removes the file from the filesystem.'''
try:
@ -176,6 +183,39 @@ class FileInfo:
response.write(chunk)
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):

View file

@ -282,13 +282,11 @@ class Ref(Field):
pxEdit = Px('''
<select if="field.link"
var2="requestValue=req.get(name, []);
inRequest=req.has_key(name);
zobjects=field.getSelectableObjects(obj);
var2="zobjects=field.getSelectableObjects(obj);
uids=[o.UID() for o in \
field.getLinkedObjects(zobj).objects];
isBeingCreated=zobj.isTemporary()"
name=":name" size=":isMultiple and field.height or ''"
field.getLinkedObjects(zobj).objects]"
name=":name" id=":name" size=":isMultiple and field.height or ''"
onchange=":field.getOnChange(name, zobj, layoutType)"
multiple=":isMultiple">
<option value="" if="not isMultiple">:_('choose_a_value')</option>
<option for="ztied in zobjects" var2="uid=ztied.o.UID()"
@ -743,13 +741,31 @@ class Ref(Field):
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.'''
if not self.select:
# No select method has been defined: we must retrieve all objects
# of the referred type that the user is allowed to access.
return obj.search(self.klass)
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:
return self.select(obj)
if not self.select:
# No select method has been defined: we must retrieve all
# objects of the referred type that the user is allowed to
# access.
return obj.search(self.klass)
else:
return self.select(obj)
xhtmlToText = re.compile('<.*?>', re.S)
def getReferenceLabel(self, refObject):

View file

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