Improved SAP interface and added historization of fields.

This commit is contained in:
Gaetan Delannay 2009-12-14 20:22:55 +01:00
parent b888f8149b
commit d320a369c9
13 changed files with 362 additions and 182 deletions

View file

@ -47,7 +47,7 @@ class Type:
def __init__(self, validator, multiplicity, index, default, optional, def __init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, indexed, searchable, editDefault, show, page, group, move, indexed, searchable,
specificReadPermission, specificWritePermission, width, specificReadPermission, specificWritePermission, width,
height, master, masterValue, focus): height, master, masterValue, focus, historized):
# The validator restricts which values may be defined. It can be an # The validator restricts which values may be defined. It can be an
# interval (1,None), a list of string values ['choice1', 'choice2'], # interval (1,None), a list of string values ['choice1', 'choice2'],
# a regular expression, a custom function, a Selection instance, etc. # a regular expression, a custom function, a Selection instance, etc.
@ -107,6 +107,9 @@ class Type:
# 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.
self.focus = focus self.focus = focus
# If we must keep track of changes performed on a field, "historized"
# must be set to True.
self.historized = historized
self.id = id(self) self.id = id(self)
self.type = self.__class__.__name__ self.type = self.__class__.__name__
self.pythonType = None # The True corresponding Python type self.pythonType = None # The True corresponding Python type
@ -128,11 +131,11 @@ class Integer(Type):
page='main', group=None, move=0, indexed=False, page='main', group=None, move=0, indexed=False,
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
master=None, masterValue=None, focus=False): master=None, masterValue=None, focus=False, historized=False):
Type.__init__(self, validator, multiplicity, index, default, optional, Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, indexed, searchable, editDefault, show, page, group, move, indexed, searchable,
specificReadPermission, specificWritePermission, width, specificReadPermission, specificWritePermission, width,
height, master, masterValue, focus) height, master, masterValue, focus, historized)
self.pythonType = long self.pythonType = long
class Float(Type): class Float(Type):
@ -141,11 +144,11 @@ class Float(Type):
page='main', group=None, move=0, indexed=False, page='main', group=None, move=0, indexed=False,
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
master=None, masterValue=None, focus=False): master=None, masterValue=None, focus=False, historized=False):
Type.__init__(self, validator, multiplicity, index, default, optional, Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, indexed, False, editDefault, show, page, group, move, indexed, False,
specificReadPermission, specificWritePermission, width, specificReadPermission, specificWritePermission, width,
height, master, masterValue, focus) height, master, masterValue, focus, historized)
self.pythonType = float self.pythonType = float
class String(Type): class String(Type):
@ -249,11 +252,11 @@ class String(Type):
show=True, page='main', group=None, move=0, indexed=False, show=True, page='main', group=None, move=0, indexed=False,
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
master=None, masterValue=None, focus=False): master=None, masterValue=None, focus=False, historized=False):
Type.__init__(self, validator, multiplicity, index, default, optional, Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, indexed, searchable, editDefault, show, page, group, move, indexed, searchable,
specificReadPermission, specificWritePermission, width, specificReadPermission, specificWritePermission, width,
height, master, masterValue, focus) height, master, masterValue, focus, historized)
self.format = format self.format = format
self.isSelect = self.isSelection() self.isSelect = self.isSelection()
def isSelection(self): def isSelection(self):
@ -276,11 +279,11 @@ class Boolean(Type):
page='main', group=None, move=0, indexed=False, page='main', group=None, move=0, indexed=False,
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
master=None, masterValue=None, focus=False): master=None, masterValue=None, focus=False, historized=False):
Type.__init__(self, validator, multiplicity, index, default, optional, Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, indexed, searchable, editDefault, show, page, group, move, indexed, searchable,
specificReadPermission, specificWritePermission, width, specificReadPermission, specificWritePermission, width,
height, master, masterValue, focus) height, master, masterValue, focus, historized)
self.pythonType = bool self.pythonType = bool
class Date(Type): class Date(Type):
@ -294,11 +297,11 @@ class Date(Type):
group=None, move=0, indexed=False, searchable=False, group=None, move=0, indexed=False, searchable=False,
specificReadPermission=False, specificWritePermission=False, specificReadPermission=False, specificWritePermission=False,
width=None, height=None, master=None, masterValue=None, width=None, height=None, master=None, masterValue=None,
focus=False): focus=False, historized=False):
Type.__init__(self, validator, multiplicity, index, default, optional, Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, indexed, searchable, editDefault, show, page, group, move, indexed, searchable,
specificReadPermission, specificWritePermission, width, specificReadPermission, specificWritePermission, width,
height, master, masterValue, focus) height, master, masterValue, focus, historized)
self.format = format self.format = format
self.startYear = startYear self.startYear = startYear
self.endYear = endYear self.endYear = endYear
@ -309,11 +312,12 @@ class File(Type):
page='main', group=None, move=0, indexed=False, page='main', group=None, move=0, indexed=False,
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
master=None, masterValue=None, focus=False, isImage=False): master=None, masterValue=None, focus=False, historized=False,
isImage=False):
Type.__init__(self, validator, multiplicity, index, default, optional, Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, indexed, False, editDefault, show, page, group, move, indexed, False,
specificReadPermission, specificWritePermission, width, specificReadPermission, specificWritePermission, width,
height, master, masterValue, focus) height, master, masterValue, focus, historized)
self.isImage = isImage self.isImage = isImage
class Ref(Type): class Ref(Type):
@ -325,11 +329,11 @@ class Ref(Type):
maxPerPage=30, move=0, indexed=False, searchable=False, maxPerPage=30, move=0, indexed=False, searchable=False,
specificReadPermission=False, specificWritePermission=False, specificReadPermission=False, specificWritePermission=False,
width=None, height=None, master=None, masterValue=None, width=None, height=None, master=None, masterValue=None,
focus=False): focus=False, historized=False):
Type.__init__(self, validator, multiplicity, index, default, optional, Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, indexed, False, editDefault, show, page, group, move, indexed, False,
specificReadPermission, specificWritePermission, width, specificReadPermission, specificWritePermission, width,
height, master, masterValue, focus) height, master, masterValue, focus, historized)
self.klass = klass self.klass = klass
self.attribute = attribute self.attribute = attribute
self.add = add # May the user add new objects through this ref ? self.add = add # May the user add new objects through this ref ?
@ -356,11 +360,11 @@ class Computed(Type):
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
method=None, plainText=True, master=None, masterValue=None, method=None, plainText=True, master=None, masterValue=None,
focus=False): focus=False, historized=False):
Type.__init__(self, None, multiplicity, index, default, optional, Type.__init__(self, None, multiplicity, index, default, optional,
False, show, page, group, move, indexed, False, False, show, page, group, move, indexed, False,
specificReadPermission, specificWritePermission, width, specificReadPermission, specificWritePermission, width,
height, master, masterValue, focus) height, master, masterValue, focus, historized)
self.method = method # The method used for computing the field value self.method = method # The method used for computing the field value
self.plainText = plainText # Does field computation produce pain text self.plainText = plainText # Does field computation produce pain text
# or XHTML? # or XHTML?
@ -376,11 +380,11 @@ class Action(Type):
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
action=None, result='computation', master=None, action=None, result='computation', master=None,
masterValue=None, focus=False): masterValue=None, focus=False, historized=False):
Type.__init__(self, None, (0,1), index, default, optional, Type.__init__(self, None, (0,1), index, default, optional,
False, show, page, group, move, indexed, False, False, show, page, group, move, indexed, False,
specificReadPermission, specificWritePermission, width, specificReadPermission, specificWritePermission, width,
height, master, masterValue, focus) height, master, masterValue, focus, historized)
self.action = action # Can be a single method or a list/tuple of methods self.action = action # Can be a single method or a list/tuple of methods
self.result = result # 'computation' means that the action will simply self.result = result # 'computation' means that the action will simply
# compute things and redirect the user to the same page, with some # compute things and redirect the user to the same page, with some
@ -425,11 +429,11 @@ class Info(Type):
page='main', group=None, move=0, indexed=False, page='main', group=None, move=0, indexed=False,
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
master=None, masterValue=None, focus=False): master=None, masterValue=None, focus=False, historized=False):
Type.__init__(self, None, (0,1), index, default, optional, Type.__init__(self, None, (0,1), index, default, optional,
False, show, page, group, move, indexed, False, False, show, page, group, move, indexed, False,
specificReadPermission, specificWritePermission, width, specificReadPermission, specificWritePermission, width,
height, master, masterValue, focus) height, master, masterValue, focus, historized)
# Workflow-specific types ------------------------------------------------------ # Workflow-specific types ------------------------------------------------------
class State: class State:

View file

@ -91,6 +91,9 @@ class Generator(AbstractGenerator):
poMsg = msg(app, '', app); poMsg.produceNiceDefault() poMsg = msg(app, '', app); poMsg.produceNiceDefault()
self.labels += [poMsg, self.labels += [poMsg,
msg('workflow_state', '', msg.WORKFLOW_STATE), msg('workflow_state', '', msg.WORKFLOW_STATE),
msg('data_change', '', msg.DATA_CHANGE),
msg('modified_field', '', msg.MODIFIED_FIELD),
msg('previous_value', '', msg.PREVIOUS_VALUE),
msg('phase', '', msg.PHASE), msg('phase', '', msg.PHASE),
msg('root_type', '', msg.ROOT_TYPE), msg('root_type', '', msg.ROOT_TYPE),
msg('workflow_comment', '', msg.WORKFLOW_COMMENT), msg('workflow_comment', '', msg.WORKFLOW_COMMENT),

View file

@ -79,7 +79,14 @@ def afterTest(test):
exec 'from Products.%s import numberOfExecutedTests' % appName exec 'from Products.%s import numberOfExecutedTests' % appName
if cov and (numberOfExecutedTests == totalNumberOfTests): if cov and (numberOfExecutedTests == totalNumberOfTests):
cov.stop() cov.stop()
# Dumps the coverage report
appModules = test.getNonEmptySubModules(appName) appModules = test.getNonEmptySubModules(appName)
# Dumps the coverage report
# HTML version
cov.html_report(directory=covFolder, morfs=appModules) cov.html_report(directory=covFolder, morfs=appModules)
# Summary in a text file
f = file('%s/summary.txt' % covFolder, 'w')
cov.report(file=f, morfs=appModules)
f.close()
# Annotated modules
cov.annotate(directory=covFolder, morfs=appModules)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -31,16 +31,14 @@ class AbstractMixin:
if created: if created:
obj = self.portal_factory.doCreate(self, self.id) # portal_factory obj = self.portal_factory.doCreate(self, self.id) # portal_factory
# creates the final object from the temp object. # creates the final object from the temp object.
if created and (obj._appy_meta_type == 'tool'): previousData = None
# We are in the special case where the tool itself is being created. if not created: previousData = self.rememberPreviousData()
# In this case, we do not process form data. # We do not process form data (=real update on the object) if the tool
pass # itself is being created.
else: if obj._appy_meta_type != 'tool': obj.processForm()
obj.processForm() if previousData:
# Keep in history potential changes on historized fields
# Get the current language and put it in the request self.historizeData(previousData)
#if rq.form.has_key('current_lang'):
# rq.form['language'] = rq.form.get('current_lang')
# Manage references # Manage references
obj._appy_manageRefs(created) obj._appy_manageRefs(created)
@ -145,15 +143,79 @@ class AbstractMixin:
self.plone_utils.addPortalMessage(msg) self.plone_utils.addPortalMessage(msg)
self.goto(rq['HTTP_REFERER']) self.goto(rq['HTTP_REFERER'])
def rememberPreviousData(self):
'''This method is called before updating an object and remembers, for
every historized field, the previous value. Result is a dict
~{s_fieldName: previousFieldValue}~'''
res = {}
for atField in self.Schema().filterFields(isMetadata=0):
fieldName = atField.getName()
appyType = self.getAppyType(fieldName, asDict=False)
if appyType and appyType.historized:
res[fieldName] = (getattr(self, fieldName),
atField.widget.label_msgid)
return res
def historizeData(self, previousData):
'''Records in the object history potential changes on historized fields.
p_previousData contains the values, before an update, of the
historized fields, while p_self already contains the (potentially)
modified values.'''
# Remove from previousData all values that were not changed
for fieldName in previousData.keys():
if getattr(self, fieldName) == previousData[fieldName][0]:
del previousData[fieldName]
if previousData:
# Create the event to add in the history
DateTime = self.getProductConfig().DateTime
state = self.portal_workflow.getInfoFor(self, 'review_state')
user = self.portal_membership.getAuthenticatedMember()
event = {'action': '_datachange_', 'changes': previousData,
'review_state': state, 'actor': user.id,
'time': DateTime(), 'comments': ''}
# Add the event to the history
histKey = self.workflow_history.keys()[0]
self.workflow_history[histKey] += (event,)
def goto(self, url): def goto(self, url):
'''Brings the user to some p_url after an action has been executed.''' '''Brings the user to some p_url after an action has been executed.'''
return self.REQUEST.RESPONSE.redirect(url) return self.REQUEST.RESPONSE.redirect(url)
def getAppyAttribute(self, name): def getAppyValue(self, name, appyType=None, useParamValue=False,value=None):
'''Returns method or attribute value corresponding to p_name.''' '''Returns the value of field (or method) p_name for this object
return eval('self.%s' % name) (p_self). If p_appyType (the corresponding Appy type) is provided,
it gives additional information about the way to render the value.
If p_useParamValue is True, the method uses p_value instead of the
real field value (useful for rendering a value from the object
history, for example).'''
# Which value will we use ?
if useParamValue: v = value
else: v = eval('self.%s' % name)
if not appyType: return v
if (v == None) or (v == ''): return v
vType = appyType['type']
if vType == 'Date':
res = v.strftime('%d/%m/') + str(v.year())
if appyType['format'] == 0:
res += ' %s' % v.strftime('%H:%M')
return res
elif vType == 'String':
if not v: return v
if appyType['isSelect']:
maxMult = appyType['multiplicity'][1]
t = self.translate
if (maxMult == None) or (maxMult > 1):
return [t('%s_%s_list_%s' % (self.meta_type, name, e)) \
for e in v]
else:
return t('%s_%s_list_%s' % (self.meta_type, name, v))
return v
elif vType == 'Boolean':
if v: return self.translate('yes', domain='plone')
else: return self.translate('no', domain='plone')
return v
def getAppyType(self, fieldName, forward=True): def getAppyType(self, fieldName, forward=True, asDict=True):
'''Returns the Appy type corresponding to p_fieldName. If you want to '''Returns the Appy type corresponding to p_fieldName. If you want to
get the Appy type corresponding to a backward field, set p_forward get the Appy type corresponding to a backward field, set p_forward
to False and specify the corresponding Archetypes relationship in to False and specify the corresponding Archetypes relationship in
@ -166,22 +228,27 @@ class AbstractMixin:
try: try:
# If I get the attr on self instead of baseClass, I get the # If I get the attr on self instead of baseClass, I get the
# property field that is redefined at the wrapper level. # property field that is redefined at the wrapper level.
appyType = getattr(baseClass, fieldName) res = appyType = getattr(baseClass, fieldName)
res = self._appy_getTypeAsDict(fieldName, appyType, baseClass) if asDict:
res = self._appy_getTypeAsDict(
fieldName, appyType, baseClass)
except AttributeError: except AttributeError:
# Check for another parent # Check for another parent
if self.wrapperClass.__bases__[0].__bases__: if self.wrapperClass.__bases__[0].__bases__:
baseClass = self.wrapperClass.__bases__[0].__bases__[-1] baseClass = self.wrapperClass.__bases__[0].__bases__[-1]
try: try:
appyType = getattr(baseClass, fieldName) res = appyType = getattr(baseClass, fieldName)
res = self._appy_getTypeAsDict(fieldName, appyType, if asDict:
baseClass) res = self._appy_getTypeAsDict(
fieldName, appyType, baseClass)
except AttributeError: except AttributeError:
pass pass
else: else:
referers = self.getProductConfig().referers referers = self.getProductConfig().referers
for appyType, rel in referers[self.__class__.__name__]: for appyType, rel in referers[self.__class__.__name__]:
if rel == fieldName: if rel == fieldName:
res = appyType
if asDict:
res = appyType.__dict__ res = appyType.__dict__
res['backd'] = appyType.back.__dict__ res['backd'] = appyType.back.__dict__
return res return res
@ -357,8 +424,7 @@ class AbstractMixin:
groups = {} # The already encountered groups groups = {} # The already encountered groups
for fieldDescr in self._appy_getOrderedFields(isEdit): for fieldDescr in self._appy_getOrderedFields(isEdit):
# Select only widgets shown on current page # Select only widgets shown on current page
if fieldDescr.page != page: if fieldDescr.page != page: continue
continue
# Do not take into account hidden fields and fields that can't be # Do not take into account hidden fields and fields that can't be
# edited through the edit view # edited through the edit view
if not self.showField(fieldDescr, isEdit): continue if not self.showField(fieldDescr, isEdit): continue
@ -561,6 +627,27 @@ class AbstractMixin:
res = '%s_%s' % (wf.id, res) res = '%s_%s' % (wf.id, res)
return res return res
def hasHistory(self):
'''Has this object an history?'''
if hasattr(self.aq_base, 'workflow_history') and self.workflow_history:
key = self.workflow_history.keys()[0]
for event in self.workflow_history[key]:
if event['action'] and (event['comments'] != '_invisible_'):
return True
return False
def getHistory(self, startNumber=0, reverse=True, includeInvisible=False):
'''Returns the history for this object, sorted in reverse order (most
recent change first) if p_reverse is True.'''
batchSize = 3
key = self.workflow_history.keys()[0]
history = list(self.workflow_history[key][1:])
if not includeInvisible:
history = [e for e in history if e['comments'] != '_invisible_']
if reverse: history.reverse()
return {'events': history[startNumber:startNumber+batchSize],
'totalNumber': len(history), 'batchSize':batchSize}
def getComputedValue(self, appyType): def getComputedValue(self, appyType):
'''Computes on p_self the value of the Computed field corresponding to '''Computes on p_self the value of the Computed field corresponding to
p_appyType.''' p_appyType.'''
@ -1080,7 +1167,7 @@ class AbstractMixin:
params = '' params = ''
rq = self.REQUEST rq = self.REQUEST
for k, v in kwargs.iteritems(): params += '&%s=%s' % (k, v) for k, v in kwargs.iteritems(): params += '&%s=%s' % (k, v)
params = params[1:] if params: params = params[1:]
if t == 'showRef': if t == 'showRef':
chunk = '/skyn/ajax?objectUid=%s&page=ref&' \ chunk = '/skyn/ajax?objectUid=%s&page=ref&' \
'macro=showReferenceContent&' % self.UID() 'macro=showReferenceContent&' % self.UID()
@ -1088,6 +1175,11 @@ class AbstractMixin:
if rq.has_key(startKey) and not kwargs.has_key(startKey): if rq.has_key(startKey) and not kwargs.has_key(startKey):
params += '&%s=%s' % (startKey, rq[startKey]) params += '&%s=%s' % (startKey, rq[startKey])
return baseUrl + chunk + params return baseUrl + chunk + params
elif t == 'showHistory':
chunk = '/skyn/ajax?objectUid=%s&page=macros&macro=history' % \
self.UID()
if params: params = '&' + params
return baseUrl + chunk + params
else: # We consider t=='view' else: # We consider t=='view'
return baseUrl + '/skyn/view' + params return baseUrl + '/skyn/view' + params

View file

@ -19,7 +19,7 @@
dummy2 python:response.setHeader('Expires', 'Mon, 11 Dec 1975 12:05:05 GMT'); dummy2 python:response.setHeader('Expires', 'Mon, 11 Dec 1975 12:05:05 GMT');
dummy3 python:response.setHeader('CacheControl', 'no-cache')"> dummy3 python:response.setHeader('CacheControl', 'no-cache')">
<tal:executeAction condition="action"> <tal:executeAction condition="action">
<tal:do define="dummy python: contextObj.getAppyAttribute('on'+action)()" omit-tag=""/> <tal:do define="dummy python: contextObj.getAppyValue('on'+action)()" omit-tag=""/>
</tal:executeAction> </tal:executeAction>
<metal:callMacro use-macro="python: context.get(page).macros.get(macro)"/> <metal:callMacro use-macro="python: context.get(page).macros.get(macro)"/>
</tal:ajax> </tal:ajax>

View file

@ -12,4 +12,4 @@ else:
from Products.CMFCore.utils import getToolByName from Products.CMFCore.utils import getToolByName
portal = getToolByName(obj, 'portal_url').getPortalObject() portal = getToolByName(obj, 'portal_url').getPortalObject()
obj = portal.get('portal_%s' % obj.id.lower()) # The tool obj = portal.get('portal_%s' % obj.id.lower()) # The tool
return obj.getAppyAttribute('on'+action)() return obj.getAppyValue('on'+action)()

View file

@ -116,15 +116,13 @@
</div> </div>
<metal:showDate define-macro="showDateField" <metal:showDate define-macro="showDateField"
tal:define="v python: field.getAccessor(contextObj)()"> tal:define="v python: contextObj.getAppyValue(field.getName(), appyType)">
<span tal:condition="showLabel" tal:content="label" class="appyLabel"></span> <span tal:condition="showLabel" tal:content="label" class="appyLabel"></span>
<span tal:condition="v" tal:content="python: v.strftime('%d/%m/') + str(v.year())"></span> <span tal:replace="v"></span>
<span tal:condition="python: v and (appyType['format'] == 0)"
tal:content="python: v.strftime('%H:%M')"></span>
</metal:showDate> </metal:showDate>
<metal:showString define-macro="showStringField" <metal:showString define-macro="showStringField"
tal:define="v python: field.getAccessor(contextObj)(); tal:define="v python: contextObj.getAppyValue(field.getName(), appyType);
fmt python: appyType['format']; fmt python: appyType['format'];
maxMult python: appyType['multiplicity'][1]; maxMult python: appyType['multiplicity'][1];
severalValues python: (maxMult == None) or (maxMult &gt; 1)"> severalValues python: (maxMult == None) or (maxMult &gt; 1)">
@ -132,30 +130,12 @@
<span tal:condition="showLabel" tal:content="label" class="appyLabel" <span tal:condition="showLabel" tal:content="label" class="appyLabel"
tal:attributes="class python: 'appyLabel ' + contextObj.getCssClasses(appyType, asSlave=False); tal:attributes="class python: 'appyLabel ' + contextObj.getCssClasses(appyType, asSlave=False);
id python: v"></span> id python: v"></span>
<tal:severalValues condition="python: v and severalValues"> <ul class="appyList" tal:condition="python: v and severalValues">
<ul class="appyList"> <li class="appyBullet" tal:repeat="sv v"><i tal:content="structure sv"></i></li>
<tal:items repeat="sv v">
<tal:select condition="appyType/isSelect">
<li class="appyBullet">
<i tal:content="python: tool.translate('%s_%s_list_%s' % (contextObj.meta_type, field.getName(), sv))"></i>
</li>
</tal:select>
<tal:string condition="not: appyType/isSelect">
<li class="appyBullet"><i tal:content="sv"></i></li>
</tal:string>
</tal:items>
</ul> </ul>
</tal:severalValues>
<tal:singleValue condition="python: v and not severalValues"> <tal:singleValue condition="python: v and not severalValues">
<tal:select condition="appyType/isSelect"> <span tal:condition="python: fmt != 3" tal:replace="structure v"/>
<span tal:replace="python: tool.translate('%s_%s_list_%s' % (contextObj.meta_type, field.getName(), v))"/> <span tal:condition="python: fmt == 3">********</span>
</tal:select>
<tal:noSelect condition="python: not appyType['isSelect'] and (fmt != 3)">
<span tal:replace="structure v"/>
</tal:noSelect>
<tal:password condition="python: not appyType['isSelect'] and (fmt == 3)">
********
</tal:password>
</tal:singleValue> </tal:singleValue>
</tal:simpleString> </tal:simpleString>
<tal:formattedString condition="python: fmt not in (0, 3)"> <tal:formattedString condition="python: fmt not in (0, 3)">
@ -275,7 +255,6 @@
<metal:fields define-macro="listFields" <metal:fields define-macro="listFields"
tal:repeat="widgetDescr python: contextObj.getAppyFields(isEdit, pageName)"> tal:repeat="widgetDescr python: contextObj.getAppyFields(isEdit, pageName)">
<tal:displayArchetypesField condition="python: widgetDescr['widgetType'] == 'field'"> <tal:displayArchetypesField condition="python: widgetDescr['widgetType'] == 'field'">
<tal:atField condition="python: widgetDescr['page'] == pageName"> <tal:atField condition="python: widgetDescr['page'] == pageName">
<metal:field use-macro="here/skyn/macros/macros/showArchetypesField" /> <metal:field use-macro="here/skyn/macros/macros/showArchetypesField" />
@ -293,63 +272,65 @@
</tal:displayGroup> </tal:displayGroup>
</metal:fields> </metal:fields>
<span metal:define-macro="byline" <metal:history define-macro="history"
tal:condition="python: site_properties.allowAnonymousViewAbout or not isAnon" tal:define="startNumber request/startNumber|python:0;
tal:define="creator here/Creator;" class="documentByLine"> startNumber python: int(startNumber);
<tal:name tal:condition="creator" historyInfo python: contextObj.getHistory(startNumber);
tal:define="author python:contextObj.portal_membership.getMemberInfo(creator)"> objs historyInfo/events;
<span class="documentAuthor" i18n:domain="plone" i18n:translate="label_by_author"> batchSize historyInfo/batchSize;
by <a tal:attributes="href string:${portal_url}/author/${creator}" totalNumber historyInfo/totalNumber;
tal:content="python:author and author['fullname'] or creator" ajaxHookId python:'appyHistory';
tal:omit-tag="not:author" i18n:name="author"/> baseUrl python: contextObj.getUrl('showHistory', startNumber='**v**');
&mdash; tool contextObj/getTool">
</span>
</tal:name>
<span class="documentModified">
<span i18n:translate="box_last_modified" i18n:domain="plone"/>
<span tal:replace="python:toLocalizedTime(here.ModificationDate(),long_format=1)"/>
</span>
</span>
<span metal:define-macro="workflowHistory" class="reviewHistory" <tal:comment replace="nothing">Table containing the history</tal:comment>
tal:define="history contextObj/getWorkflowHistory" tal:condition="history"> <tal:history condition="objs">
<dl id="history" class="collapsible inline collapsedOnLoad"> <metal:nav use-macro="here/skyn/macros/macros/appyNavigate"/>
<dt class="collapsibleHeader" i18n:translate="label_history" i18n:domain="plone">History</dt> <table width="100%" class="listing nosort">
<dd class="collapsibleContent">
<table width="100%" class="listing nosort" i18n:attributes="summary summary_review_history"
tal:define="review_history python:contextObj.portal_workflow.getInfoFor(contextObj, 'review_history', []);
review_history python:[review for review in review_history if review.get('action','')]"
tal:condition="review_history">
<tr i18n:domain="plone"> <tr i18n:domain="plone">
<th i18n:translate="listingheader_action"/> <th i18n:translate="listingheader_action"/>
<th i18n:translate="listingheader_performed_by"/> <th i18n:translate="listingheader_performed_by"/>
<th i18n:translate="listingheader_date_and_time"/> <th i18n:translate="listingheader_date_and_time"/>
<th i18n:translate="listingheader_comment"/> <th i18n:translate="listingheader_comment"/>
</tr> </tr>
<metal:block tal:define="review_history python: portal.reverseList(review_history);" <tal:event repeat="event objs">
tal:repeat="items review_history"> <tr tal:define="odd repeat/event/odd;
<tr tal:define="odd repeat/items/odd; rhComments event/comments|nothing;
rhComments items/comments|nothing; state event/review_state|nothing;
state items/review_state|nothing" isDataChange python: event['action'] == '_datachange_'"
tal:condition="python: items['action'] and (rhComments != '_invisible_')" tal:attributes="class python:test(odd, 'even', 'odd')" valign="top">
tal:attributes="class python:test(odd, 'even', 'odd')"> <td tal:condition="isDataChange" tal:content="python: tool.translate('data_change')"></td>
<td tal:condition="not: isDataChange"
<td tal:content="python: tool.translate(contextObj.getWorkflowLabel(items['action']))" tal:content="python: tool.translate(contextObj.getWorkflowLabel(event['action']))"
tal:attributes="class string:state-${state}"/> tal:attributes="class string:state-${state}"/>
<td tal:define="actorid python:items.get('actor'); <td tal:define="actorid python:event.get('actor');
actor python:contextObj.portal_membership.getMemberInfo(actorid); actor python:contextObj.portal_membership.getMemberInfo(actorid);
fullname actor/fullname|nothing; fullname actor/fullname|nothing;
username actor/username|nothing" username actor/username|nothing"
tal:content="python:fullname or username or actorid"/> tal:content="python:fullname or username or actorid"/>
<td tal:content="python:toLocalizedTime(items['time'],long_format=True)"/> <td tal:content="python:contextObj.toLocalizedTime(event['time'],long_format=True)"/>
<td><tal:comment condition="rhComments" tal:content="structure rhComments"/> <td tal:condition="not: isDataChange"><tal:comment condition="rhComments" tal:content="structure rhComments"/>
<tal:noComment condition="not: rhComments" i18n:translate="no_comments" i18n:domain="plone"/></td> <tal:noComment condition="not: rhComments" i18n:translate="no_comments" i18n:domain="plone"/></td>
<td tal:condition="isDataChange">
<tal:comment replace="nothing">
Display the previous values of the fields whose value were modified in this change.</tal:comment>
<table class="appyChanges" width="100%">
<tr>
<th tal:content="python: tool.translate('modified_field')"></th>
<th tal:content="python: tool.translate('previous_value')"></th>
</tr>
<tr tal:repeat="change event/changes/items">
<td tal:content="python: tool.translate(change[1][1])"></td>
<td tal:define="appyType python:contextObj.getAppyType(change[0])"
tal:content="python: contextObj.getAppyValue(change[0], appyType, True, change[1][0])"></td>
</tr> </tr>
</metal:block>
</table> </table>
</dd> </td>
</dl> </tr>
</span> </tal:event>
</table>
</tal:history>
</metal:history>
<div metal:define-macro="pagePrologue"> <div metal:define-macro="pagePrologue">
<tal:comment replace="nothing">Global elements used in every page.</tal:comment> <tal:comment replace="nothing">Global elements used in every page.</tal:comment>
@ -468,6 +449,32 @@
f.submit(); f.submit();
} }
} }
function toggleCookie(cookieId) {
// What is the state of this boolean (expanded/collapsed) cookie?
var state = readCookie(cookieId);
if ((state != 'collapsed') && (state != 'expanded')) {
// No cookie yet, create it.
createCookie(cookieId, 'collapsed');
state = 'collapsed';
}
var hook = document.getElementById(cookieId); // The hook is the part of
// the HTML document that needs to be shown or hidden.
var displayValue = 'none';
var newState = 'collapsed';
var imgSrc = 'skyn/expand.gif';
if (state == 'collapsed') {
// Show the HTML zone
displayValue = 'block';
imgSrc = 'skyn/collapse.gif';
newState = 'expanded';
}
// Update the corresponding HTML element
hook.style.display = displayValue;
var img = document.getElementById(cookieId + '_img');
img.src = imgSrc;
// Inverse the cookie value
createCookie(cookieId, newState);
}
--> -->
</script> </script>
<tal:comment replace="nothing">Global form for deleting an object</tal:comment> <tal:comment replace="nothing">Global form for deleting an object</tal:comment>
@ -479,7 +486,10 @@
<div metal:define-macro="showPageHeader" <div metal:define-macro="showPageHeader"
tal:define="appyPages python: contextObj.getAppyPages(phase); tal:define="appyPages python: contextObj.getAppyPages(phase);
showCommonInfo python: not isEdit" showCommonInfo python: not isEdit;
hasHistory contextObj/hasHistory;
historyExpanded python: tool.getCookieValue('appyHistory', default='collapsed') == 'expanded';
creator contextObj/Creator"
tal:condition="not: contextObj/isTemporary"> tal:condition="not: contextObj/isTemporary">
<tal:comment replace="nothing">Information that is common to all tabs (object title, state, etc)</tal:comment> <tal:comment replace="nothing">Information that is common to all tabs (object title, state, etc)</tal:comment>
@ -509,15 +519,47 @@
<td colspan="2" class="discreet" tal:content="descrLabel"/> <td colspan="2" class="discreet" tal:content="descrLabel"/>
</tr> </tr>
<tr> <tr>
<td> <td class="documentByLine">
<metal:byLine use-macro="here/skyn/macros/macros/byline"/> <tal:comment replace="nothing">Creator and last modification date</tal:comment>
<tal:showWorkflow condition="showWorkflow"> <tal:comment replace="nothing">Plus/minus icon for accessing history</tal:comment>
<metal:workflowHistory use-macro="here/skyn/macros/macros/workflowHistory"/> <tal:accessHistory condition="hasHistory">
</tal:showWorkflow> <img align="left" style="cursor:pointer" onClick="javascript:toggleCookie('appyHistory')"
tal:attributes="src python:test(historyExpanded, 'skyn/collapse.gif', 'skyn/expand.gif');"
id="appyHistory_img"/>&nbsp;
<span i18n:translate="label_history" i18n:domain="plone" class="appyHistory"></span>&nbsp;
</tal:accessHistory>
<tal:comment replace="nothing">Show document creator</tal:comment>
<tal:creator condition="creator"
define="author python:contextObj.portal_membership.getMemberInfo(creator)">
<span class="documentAuthor" i18n:domain="plone" i18n:translate="label_by_author">
by <a tal:attributes="href string:${portal_url}/author/${creator}"
tal:content="python:author and author['fullname'] or creator"
tal:omit-tag="not:author" i18n:name="author"/>
&mdash;
</span>
</tal:creator>
<tal:comment replace="nothing">Show last modification date</tal:comment>
<span i18n:translate="box_last_modified" i18n:domain="plone"></span>
<span tal:replace="python:contextObj.toLocalizedTime(contextObj.ModificationDate(),long_format=1)"></span>
</td> </td>
<td valign="top"><metal:pod use-macro="here/skyn/macros/macros/listPodTemplates"/> <td valign="top"><metal:pod use-macro="here/skyn/macros/macros/listPodTemplates"/>
</td> </td>
</tr> </tr>
<tal:comment replace="nothing">Object history</tal:comment>
<tr tal:condition="hasHistory">
<td colspan="2">
<span id="appyHistory"
tal:attributes="style python:test(historyExpanded, 'display:block', 'display:none')">
<div tal:define="ajaxHookId python: contextObj.UID() + '_history';
ajaxUrl python: contextObj.getUrl('showHistory')"
tal:attributes="id ajaxHookId">
<script language="javascript" tal:content="python: 'askAjaxChunk(\'%s\',\'%s\')' % (ajaxHookId, ajaxUrl)">
</script>
</div>
</span>
</td>
</tr>
<tal:comment replace="nothing">Workflow-related information and actions</tal:comment> <tal:comment replace="nothing">Workflow-related information and actions</tal:comment>
<tr tal:condition="python: showWorkflow and contextObj.getWorkflowLabel()"> <tr tal:condition="python: showWorkflow and contextObj.getWorkflowLabel()">
@ -819,35 +861,6 @@
tal:define="queryUrl python: '%s/skyn/query' % appFolder.absolute_url(); tal:define="queryUrl python: '%s/skyn/query' % appFolder.absolute_url();
currentSearch request/search|nothing; currentSearch request/search|nothing;
currentType request/type_name|nothing;"> currentType request/type_name|nothing;">
<script language="javascript">
<!--
function toggleSearchGroup(groupId) {
// What is the state of this toggle?
var state = readCookie(groupId);
if ((state != 'collapsed') && (state != 'expanded')) {
// No cookie yet, create it.
createCookie(groupId, 'collapsed');
state = 'collapsed';
}
var group = document.getElementById(groupId);
var displayValue = 'none';
var newState = 'collapsed';
var imgSrc = 'skyn/expand.gif';
if (state == 'collapsed') {
// Expand the group
displayValue = 'block';
imgSrc = 'skyn/collapse.gif';
newState = 'expanded';
}
// Update group visibility and img
group.style.display = displayValue;
var img = document.getElementById(groupId + '_img');
img.src = imgSrc;
// Inverse the cookie value
createCookie(groupId, newState);
}
-->
</script>
<tal:comment replace="nothing">Portlet title, with link to tool.</tal:comment> <tal:comment replace="nothing">Portlet title, with link to tool.</tal:comment>
<dt class="portletHeader"> <dt class="portletHeader">
<tal:comment replace="nothing">If there is only one flavour, clicking on the portlet <tal:comment replace="nothing">If there is only one flavour, clicking on the portlet
@ -907,7 +920,7 @@
<img align="left" style="cursor:pointer" <img align="left" style="cursor:pointer"
tal:attributes="id python: '%s_img' % group['labelId']; tal:attributes="id python: '%s_img' % group['labelId'];
src python:test(expanded, 'skyn/collapse.gif', 'skyn/expand.gif'); src python:test(expanded, 'skyn/collapse.gif', 'skyn/expand.gif');
onClick python:'javascript:toggleSearchGroup(\'%s\')' % group['labelId']"/>&nbsp; onClick python:'javascript:toggleCookie(\'%s\')' % group['labelId']"/>&nbsp;
<span tal:replace="group/label"/> <span tal:replace="group/label"/>
</dt> </dt>
<tal:comment replace="nothing">Group searches</tal:comment> <tal:comment replace="nothing">Group searches</tal:comment>

View file

@ -27,10 +27,10 @@
phase request/phase|phaseInfo/name; phase request/phase|phaseInfo/name;
pageName python: contextObj.getAppyPage(isEdit, phaseInfo); pageName python: contextObj.getAppyPage(isEdit, phaseInfo);
showWorkflow python: flavour.getAttr('showWorkflowFor' + contextObj.meta_type)"> showWorkflow python: flavour.getAttr('showWorkflowFor' + contextObj.meta_type)">
<div metal:use-macro="here/skyn/macros/macros/pagePrologue"/> <metal:prologue use-macro="here/skyn/macros/macros/pagePrologue"/>
<div metal:use-macro="here/skyn/macros/macros/showPageHeader"/> <metal:header use-macro="here/skyn/macros/macros/showPageHeader"/>
<div metal:use-macro="here/skyn/macros/macros/listFields" /> <metal:fields use-macro="here/skyn/macros/macros/listFields" />
<div metal:use-macro="here/skyn/macros/macros/showPageFooter"/> <metal:footer use-macro="here/skyn/macros/macros/showPageFooter"/>
</metal:fill> </metal:fill>
</body> </body>
</html> </html>

View file

@ -6,6 +6,7 @@
tal:define="tool python: context.<!toolInstanceName!>" tal:define="tool python: context.<!toolInstanceName!>"
tal:condition="tool/showPortlet"> tal:condition="tool/showPortlet">
<metal:block metal:use-macro="here/global_defines/macros/defines" /> <metal:block metal:use-macro="here/global_defines/macros/defines" />
<metal:prologue use-macro="here/skyn/macros/macros/pagePrologue"/>
<dl tal:define="rootClasses tool/getRootClasses; <dl tal:define="rootClasses tool/getRootClasses;
appName string:<!applicationName!>; appName string:<!applicationName!>;
appFolder tool/getAppFolder; appFolder tool/getAppFolder;

View file

@ -72,6 +72,31 @@
padding: 0.1em 1em 0.1em 1.3em; padding: 0.1em 1em 0.1em 1.3em;
} }
.appyChanges th {
font-style: italic;
background-color: transparent;
border-bottom: 1px dashed #8CACBB;
border-top: 0 none transparent;
border-left: 0 none transparent;
border-right: 0 none transparent;
padding: 0.1em 0.1em 0.1em 0.1em;
}
.appyChanges td {
padding: 0.1em 0.1em 0.1em 0.1em !important;
border-right: 0 none transparent !important;
border-top: 0 none transparent;
border-left: 0 none transparent;
border-right: 0 none transparent;
}
.appyHistory {
font-variant: small-caps;
font-weight: bold;
color: black;
font-size: 105%;
}
/* stepxx classes are used for displaying status of a phase or state. */ /* stepxx classes are used for displaying status of a phase or state. */
.stepDone { .stepDone {
background-color: #cde2a7; background-color: #cde2a7;

View file

@ -2,7 +2,7 @@
developer the real classes used by the underlying web framework.''' developer the real classes used by the underlying web framework.'''
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import time, os.path, mimetypes, unicodedata import time, os.path, mimetypes, unicodedata, random
from appy.gen import Search from appy.gen import Search
from appy.gen.utils import sequenceTypes from appy.gen.utils import sequenceTypes
from appy.shared.utils import getOsTempFolder from appy.shared.utils import getOsTempFolder
@ -153,7 +153,8 @@ class AbstractWrapper:
objId = kwargs['id'] objId = kwargs['id']
del kwargs['id'] del kwargs['id']
else: else:
objId = '%s.%f' % (idPrefix, time.time()) objId = '%s.%f.%s' % (idPrefix, time.time(),
str(random.random()).split('.')[1])
# Determine if object must be created from external data # Determine if object must be created from external data
externalData = None externalData = None
if kwargs.has_key('_data'): if kwargs.has_key('_data'):

View file

@ -59,6 +59,9 @@ class PoMessage:
IMPORT_DONE = 'Import terminated successfully.' IMPORT_DONE = 'Import terminated successfully.'
WORKFLOW_COMMENT = 'Optional comment' WORKFLOW_COMMENT = 'Optional comment'
WORKFLOW_STATE = 'state' WORKFLOW_STATE = 'state'
DATA_CHANGE = 'Data change'
MODIFIED_FIELD = 'Modified field'
PREVIOUS_VALUE = 'Previous value'
PHASE = 'phase' PHASE = 'phase'
ROOT_TYPE = 'type' ROOT_TYPE = 'type'
CHOOSE_A_VALUE = ' - ' CHOOSE_A_VALUE = ' - '

View file

@ -14,6 +14,12 @@ SAP_FUNCTION_ERROR = 'Error while calling function "%s". %s'
SAP_DISCONNECT_ERROR = 'Error while disconnecting from SAP. %s' SAP_DISCONNECT_ERROR = 'Error while disconnecting from SAP. %s'
SAP_TABLE_PARAM_ERROR = 'Param "%s" does not correspond to a valid table ' \ SAP_TABLE_PARAM_ERROR = 'Param "%s" does not correspond to a valid table ' \
'parameter for function "%s".' 'parameter for function "%s".'
SAP_STRUCT_ELEM_NOT_FOUND = 'Structure used by parameter "%s" does not define '\
'an attribute named "%s."'
SAP_STRING_REQUIRED = 'Type mismatch for attribute "%s" used in parameter ' \
'"%s": a string value is expected (SAP type is %s).'
SAP_STRING_OVERFLOW = 'A string value for attribute "%s" used in parameter ' \
'"%s" is too long (SAP type is %s).'
SAP_FUNCTION_NOT_FOUND = 'Function "%s" does not exist.' SAP_FUNCTION_NOT_FOUND = 'Function "%s" does not exist.'
SAP_FUNCTION_INFO_ERROR = 'Error while asking information about function ' \ SAP_FUNCTION_INFO_ERROR = 'Error while asking information about function ' \
'"%s". %s' '"%s". %s'
@ -52,6 +58,32 @@ class Sap:
connNoPasswd = params[:params.index('PASSWD')] + 'PASSWD=********' connNoPasswd = params[:params.index('PASSWD')] + 'PASSWD=********'
raise SapError(SAP_CONNECT_ERROR % (connNoPasswd, str(se))) raise SapError(SAP_CONNECT_ERROR % (connNoPasswd, str(se)))
def createStructure(self, structDef, userData, paramName):
'''Create a struct corresponding to SAP/C structure definition
p_structDef and fills it with dict p_userData.'''
res = structDef()
for name, value in userData.iteritems():
if name not in structDef._sfield_names_:
raise SapError(SAP_STRUCT_ELEM_NOT_FOUND % (paramName, name))
sapType = structDef._sfield_sap_types_[name]
# Check if the value is valid according to the required type
if sapType[0] == 'C':
sType = '%s%d' % (sapType[0], sapType[1])
# "None" value is tolerated.
if value == None: value = ''
if not isinstance(value, basestring):
raise SapError(
SAP_STRING_REQUIRED % (name, paramName, sType))
if len(value) > sapType[1]:
raise SapError(
SAP_STRING_OVERFLOW % (name, paramName, sType))
# Left-fill the string with blanks.
v = value.ljust(sapType[1])
else:
v = value
res[name.lower()] = v
return res
def call(self, functionName, **params): def call(self, functionName, **params):
'''Calls a function on the SAP server.''' '''Calls a function on the SAP server.'''
try: try:
@ -60,8 +92,8 @@ class Sap:
for name, value in params.iteritems(): for name, value in params.iteritems():
if type(value) == dict: if type(value) == dict:
# The param corresponds to a SAP/C "struct" # The param corresponds to a SAP/C "struct"
v = self.sap.get_structure(name)() v = self.createStructure(
v.from_dict(value) self.sap.get_structure(name),value, name)
elif type(value) in sequenceTypes: elif type(value) in sequenceTypes:
# The param must be a SAP/C "table" (a list of structs) # The param must be a SAP/C "table" (a list of structs)
# Retrieve the name of the struct type related to this # Retrieve the name of the struct type related to this
@ -78,8 +110,7 @@ class Sap:
SAP_TABLE_PARAM_ERROR % (name, functionName)) SAP_TABLE_PARAM_ERROR % (name, functionName))
v = self.sap.get_table(tableTypeName) v = self.sap.get_table(tableTypeName)
for dValue in value: for dValue in value:
v.append_from_dict(dValue) v.append(self.createStructure(v.struc, dValue, name))
#v = v.handle
else: else:
v = value v = value
function[name] = v function[name] = v