Improved SAP interface and added historization of fields.
This commit is contained in:
parent
b888f8149b
commit
d320a369c9
|
@ -47,7 +47,7 @@ class Type:
|
|||
def __init__(self, validator, multiplicity, index, default, optional,
|
||||
editDefault, show, page, group, move, indexed, searchable,
|
||||
specificReadPermission, specificWritePermission, width,
|
||||
height, master, masterValue, focus):
|
||||
height, master, masterValue, focus, historized):
|
||||
# The validator restricts which values may be defined. It can be an
|
||||
# interval (1,None), a list of string values ['choice1', 'choice2'],
|
||||
# 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.
|
||||
# It will be rendered in a special way.
|
||||
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.type = self.__class__.__name__
|
||||
self.pythonType = None # The True corresponding Python type
|
||||
|
@ -128,11 +131,11 @@ class Integer(Type):
|
|||
page='main', group=None, move=0, indexed=False,
|
||||
searchable=False, specificReadPermission=False,
|
||||
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,
|
||||
editDefault, show, page, group, move, indexed, searchable,
|
||||
specificReadPermission, specificWritePermission, width,
|
||||
height, master, masterValue, focus)
|
||||
height, master, masterValue, focus, historized)
|
||||
self.pythonType = long
|
||||
|
||||
class Float(Type):
|
||||
|
@ -141,11 +144,11 @@ class Float(Type):
|
|||
page='main', group=None, move=0, indexed=False,
|
||||
searchable=False, specificReadPermission=False,
|
||||
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,
|
||||
editDefault, show, page, group, move, indexed, False,
|
||||
specificReadPermission, specificWritePermission, width,
|
||||
height, master, masterValue, focus)
|
||||
height, master, masterValue, focus, historized)
|
||||
self.pythonType = float
|
||||
|
||||
class String(Type):
|
||||
|
@ -249,11 +252,11 @@ class String(Type):
|
|||
show=True, page='main', group=None, move=0, indexed=False,
|
||||
searchable=False, specificReadPermission=False,
|
||||
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,
|
||||
editDefault, show, page, group, move, indexed, searchable,
|
||||
specificReadPermission, specificWritePermission, width,
|
||||
height, master, masterValue, focus)
|
||||
height, master, masterValue, focus, historized)
|
||||
self.format = format
|
||||
self.isSelect = self.isSelection()
|
||||
def isSelection(self):
|
||||
|
@ -276,11 +279,11 @@ class Boolean(Type):
|
|||
page='main', group=None, move=0, indexed=False,
|
||||
searchable=False, specificReadPermission=False,
|
||||
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,
|
||||
editDefault, show, page, group, move, indexed, searchable,
|
||||
specificReadPermission, specificWritePermission, width,
|
||||
height, master, masterValue, focus)
|
||||
height, master, masterValue, focus, historized)
|
||||
self.pythonType = bool
|
||||
|
||||
class Date(Type):
|
||||
|
@ -294,11 +297,11 @@ class Date(Type):
|
|||
group=None, move=0, indexed=False, searchable=False,
|
||||
specificReadPermission=False, specificWritePermission=False,
|
||||
width=None, height=None, master=None, masterValue=None,
|
||||
focus=False):
|
||||
focus=False, historized=False):
|
||||
Type.__init__(self, validator, multiplicity, index, default, optional,
|
||||
editDefault, show, page, group, move, indexed, searchable,
|
||||
specificReadPermission, specificWritePermission, width,
|
||||
height, master, masterValue, focus)
|
||||
height, master, masterValue, focus, historized)
|
||||
self.format = format
|
||||
self.startYear = startYear
|
||||
self.endYear = endYear
|
||||
|
@ -309,11 +312,12 @@ class File(Type):
|
|||
page='main', group=None, move=0, indexed=False,
|
||||
searchable=False, specificReadPermission=False,
|
||||
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,
|
||||
editDefault, show, page, group, move, indexed, False,
|
||||
specificReadPermission, specificWritePermission, width,
|
||||
height, master, masterValue, focus)
|
||||
height, master, masterValue, focus, historized)
|
||||
self.isImage = isImage
|
||||
|
||||
class Ref(Type):
|
||||
|
@ -325,11 +329,11 @@ class Ref(Type):
|
|||
maxPerPage=30, move=0, indexed=False, searchable=False,
|
||||
specificReadPermission=False, specificWritePermission=False,
|
||||
width=None, height=None, master=None, masterValue=None,
|
||||
focus=False):
|
||||
focus=False, historized=False):
|
||||
Type.__init__(self, validator, multiplicity, index, default, optional,
|
||||
editDefault, show, page, group, move, indexed, False,
|
||||
specificReadPermission, specificWritePermission, width,
|
||||
height, master, masterValue, focus)
|
||||
height, master, masterValue, focus, historized)
|
||||
self.klass = klass
|
||||
self.attribute = attribute
|
||||
self.add = add # May the user add new objects through this ref ?
|
||||
|
@ -356,11 +360,11 @@ class Computed(Type):
|
|||
searchable=False, specificReadPermission=False,
|
||||
specificWritePermission=False, width=None, height=None,
|
||||
method=None, plainText=True, master=None, masterValue=None,
|
||||
focus=False):
|
||||
focus=False, historized=False):
|
||||
Type.__init__(self, None, multiplicity, index, default, optional,
|
||||
False, show, page, group, move, indexed, False,
|
||||
specificReadPermission, specificWritePermission, width,
|
||||
height, master, masterValue, focus)
|
||||
height, master, masterValue, focus, historized)
|
||||
self.method = method # The method used for computing the field value
|
||||
self.plainText = plainText # Does field computation produce pain text
|
||||
# or XHTML?
|
||||
|
@ -376,11 +380,11 @@ class Action(Type):
|
|||
searchable=False, specificReadPermission=False,
|
||||
specificWritePermission=False, width=None, height=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,
|
||||
False, show, page, group, move, indexed, False,
|
||||
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.result = result # 'computation' means that the action will simply
|
||||
# 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,
|
||||
searchable=False, specificReadPermission=False,
|
||||
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,
|
||||
False, show, page, group, move, indexed, False,
|
||||
specificReadPermission, specificWritePermission, width,
|
||||
height, master, masterValue, focus)
|
||||
height, master, masterValue, focus, historized)
|
||||
|
||||
# Workflow-specific types ------------------------------------------------------
|
||||
class State:
|
||||
|
|
|
@ -91,6 +91,9 @@ class Generator(AbstractGenerator):
|
|||
poMsg = msg(app, '', app); poMsg.produceNiceDefault()
|
||||
self.labels += [poMsg,
|
||||
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('root_type', '', msg.ROOT_TYPE),
|
||||
msg('workflow_comment', '', msg.WORKFLOW_COMMENT),
|
||||
|
|
|
@ -79,7 +79,14 @@ def afterTest(test):
|
|||
exec 'from Products.%s import numberOfExecutedTests' % appName
|
||||
if cov and (numberOfExecutedTests == totalNumberOfTests):
|
||||
cov.stop()
|
||||
# Dumps the coverage report
|
||||
appModules = test.getNonEmptySubModules(appName)
|
||||
# Dumps the coverage report
|
||||
# HTML version
|
||||
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)
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
|
@ -31,16 +31,14 @@ class AbstractMixin:
|
|||
if created:
|
||||
obj = self.portal_factory.doCreate(self, self.id) # portal_factory
|
||||
# creates the final object from the temp object.
|
||||
if created and (obj._appy_meta_type == 'tool'):
|
||||
# We are in the special case where the tool itself is being created.
|
||||
# In this case, we do not process form data.
|
||||
pass
|
||||
else:
|
||||
obj.processForm()
|
||||
|
||||
# Get the current language and put it in the request
|
||||
#if rq.form.has_key('current_lang'):
|
||||
# rq.form['language'] = rq.form.get('current_lang')
|
||||
previousData = None
|
||||
if not created: previousData = self.rememberPreviousData()
|
||||
# We do not process form data (=real update on the object) if the tool
|
||||
# itself is being created.
|
||||
if obj._appy_meta_type != 'tool': obj.processForm()
|
||||
if previousData:
|
||||
# Keep in history potential changes on historized fields
|
||||
self.historizeData(previousData)
|
||||
|
||||
# Manage references
|
||||
obj._appy_manageRefs(created)
|
||||
|
@ -145,15 +143,79 @@ class AbstractMixin:
|
|||
self.plone_utils.addPortalMessage(msg)
|
||||
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):
|
||||
'''Brings the user to some p_url after an action has been executed.'''
|
||||
return self.REQUEST.RESPONSE.redirect(url)
|
||||
|
||||
def getAppyAttribute(self, name):
|
||||
'''Returns method or attribute value corresponding to p_name.'''
|
||||
return eval('self.%s' % name)
|
||||
def getAppyValue(self, name, appyType=None, useParamValue=False,value=None):
|
||||
'''Returns the value of field (or method) p_name for this object
|
||||
(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
|
||||
get the Appy type corresponding to a backward field, set p_forward
|
||||
to False and specify the corresponding Archetypes relationship in
|
||||
|
@ -166,22 +228,27 @@ class AbstractMixin:
|
|||
try:
|
||||
# If I get the attr on self instead of baseClass, I get the
|
||||
# property field that is redefined at the wrapper level.
|
||||
appyType = getattr(baseClass, fieldName)
|
||||
res = self._appy_getTypeAsDict(fieldName, appyType, baseClass)
|
||||
res = appyType = getattr(baseClass, fieldName)
|
||||
if asDict:
|
||||
res = self._appy_getTypeAsDict(
|
||||
fieldName, appyType, baseClass)
|
||||
except AttributeError:
|
||||
# Check for another parent
|
||||
if self.wrapperClass.__bases__[0].__bases__:
|
||||
baseClass = self.wrapperClass.__bases__[0].__bases__[-1]
|
||||
try:
|
||||
appyType = getattr(baseClass, fieldName)
|
||||
res = self._appy_getTypeAsDict(fieldName, appyType,
|
||||
baseClass)
|
||||
res = appyType = getattr(baseClass, fieldName)
|
||||
if asDict:
|
||||
res = self._appy_getTypeAsDict(
|
||||
fieldName, appyType, baseClass)
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
referers = self.getProductConfig().referers
|
||||
for appyType, rel in referers[self.__class__.__name__]:
|
||||
if rel == fieldName:
|
||||
res = appyType
|
||||
if asDict:
|
||||
res = appyType.__dict__
|
||||
res['backd'] = appyType.back.__dict__
|
||||
return res
|
||||
|
@ -357,8 +424,7 @@ class AbstractMixin:
|
|||
groups = {} # The already encountered groups
|
||||
for fieldDescr in self._appy_getOrderedFields(isEdit):
|
||||
# Select only widgets shown on current page
|
||||
if fieldDescr.page != page:
|
||||
continue
|
||||
if fieldDescr.page != page: continue
|
||||
# Do not take into account hidden fields and fields that can't be
|
||||
# edited through the edit view
|
||||
if not self.showField(fieldDescr, isEdit): continue
|
||||
|
@ -561,6 +627,27 @@ class AbstractMixin:
|
|||
res = '%s_%s' % (wf.id, 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):
|
||||
'''Computes on p_self the value of the Computed field corresponding to
|
||||
p_appyType.'''
|
||||
|
@ -1080,7 +1167,7 @@ class AbstractMixin:
|
|||
params = ''
|
||||
rq = self.REQUEST
|
||||
for k, v in kwargs.iteritems(): params += '&%s=%s' % (k, v)
|
||||
params = params[1:]
|
||||
if params: params = params[1:]
|
||||
if t == 'showRef':
|
||||
chunk = '/skyn/ajax?objectUid=%s&page=ref&' \
|
||||
'macro=showReferenceContent&' % self.UID()
|
||||
|
@ -1088,6 +1175,11 @@ class AbstractMixin:
|
|||
if rq.has_key(startKey) and not kwargs.has_key(startKey):
|
||||
params += '&%s=%s' % (startKey, rq[startKey])
|
||||
return baseUrl + chunk + params
|
||||
elif t == 'showHistory':
|
||||
chunk = '/skyn/ajax?objectUid=%s&page=macros¯o=history' % \
|
||||
self.UID()
|
||||
if params: params = '&' + params
|
||||
return baseUrl + chunk + params
|
||||
else: # We consider t=='view'
|
||||
return baseUrl + '/skyn/view' + params
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
dummy2 python:response.setHeader('Expires', 'Mon, 11 Dec 1975 12:05:05 GMT');
|
||||
dummy3 python:response.setHeader('CacheControl', 'no-cache')">
|
||||
<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>
|
||||
<metal:callMacro use-macro="python: context.get(page).macros.get(macro)"/>
|
||||
</tal:ajax>
|
||||
|
|
|
@ -12,4 +12,4 @@ else:
|
|||
from Products.CMFCore.utils import getToolByName
|
||||
portal = getToolByName(obj, 'portal_url').getPortalObject()
|
||||
obj = portal.get('portal_%s' % obj.id.lower()) # The tool
|
||||
return obj.getAppyAttribute('on'+action)()
|
||||
return obj.getAppyValue('on'+action)()
|
||||
|
|
|
@ -116,15 +116,13 @@
|
|||
</div>
|
||||
|
||||
<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="v" tal:content="python: v.strftime('%d/%m/') + str(v.year())"></span>
|
||||
<span tal:condition="python: v and (appyType['format'] == 0)"
|
||||
tal:content="python: v.strftime('%H:%M')"></span>
|
||||
<span tal:replace="v"></span>
|
||||
</metal:showDate>
|
||||
|
||||
<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'];
|
||||
maxMult python: appyType['multiplicity'][1];
|
||||
severalValues python: (maxMult == None) or (maxMult > 1)">
|
||||
|
@ -132,30 +130,12 @@
|
|||
<span tal:condition="showLabel" tal:content="label" class="appyLabel"
|
||||
tal:attributes="class python: 'appyLabel ' + contextObj.getCssClasses(appyType, asSlave=False);
|
||||
id python: v"></span>
|
||||
<tal:severalValues condition="python: v and severalValues">
|
||||
<ul class="appyList">
|
||||
<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 class="appyList" tal:condition="python: v and severalValues">
|
||||
<li class="appyBullet" tal:repeat="sv v"><i tal:content="structure sv"></i></li>
|
||||
</ul>
|
||||
</tal:severalValues>
|
||||
<tal:singleValue condition="python: v and not severalValues">
|
||||
<tal:select condition="appyType/isSelect">
|
||||
<span tal:replace="python: tool.translate('%s_%s_list_%s' % (contextObj.meta_type, field.getName(), v))"/>
|
||||
</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>
|
||||
<span tal:condition="python: fmt != 3" tal:replace="structure v"/>
|
||||
<span tal:condition="python: fmt == 3">********</span>
|
||||
</tal:singleValue>
|
||||
</tal:simpleString>
|
||||
<tal:formattedString condition="python: fmt not in (0, 3)">
|
||||
|
@ -275,7 +255,6 @@
|
|||
|
||||
<metal:fields define-macro="listFields"
|
||||
tal:repeat="widgetDescr python: contextObj.getAppyFields(isEdit, pageName)">
|
||||
|
||||
<tal:displayArchetypesField condition="python: widgetDescr['widgetType'] == 'field'">
|
||||
<tal:atField condition="python: widgetDescr['page'] == pageName">
|
||||
<metal:field use-macro="here/skyn/macros/macros/showArchetypesField" />
|
||||
|
@ -293,63 +272,65 @@
|
|||
</tal:displayGroup>
|
||||
</metal:fields>
|
||||
|
||||
<span metal:define-macro="byline"
|
||||
tal:condition="python: site_properties.allowAnonymousViewAbout or not isAnon"
|
||||
tal:define="creator here/Creator;" class="documentByLine">
|
||||
<tal:name tal:condition="creator"
|
||||
tal: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"/>
|
||||
—
|
||||
</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>
|
||||
<metal:history define-macro="history"
|
||||
tal:define="startNumber request/startNumber|python:0;
|
||||
startNumber python: int(startNumber);
|
||||
historyInfo python: contextObj.getHistory(startNumber);
|
||||
objs historyInfo/events;
|
||||
batchSize historyInfo/batchSize;
|
||||
totalNumber historyInfo/totalNumber;
|
||||
ajaxHookId python:'appyHistory';
|
||||
baseUrl python: contextObj.getUrl('showHistory', startNumber='**v**');
|
||||
tool contextObj/getTool">
|
||||
|
||||
<span metal:define-macro="workflowHistory" class="reviewHistory"
|
||||
tal:define="history contextObj/getWorkflowHistory" tal:condition="history">
|
||||
<dl id="history" class="collapsible inline collapsedOnLoad">
|
||||
<dt class="collapsibleHeader" i18n:translate="label_history" i18n:domain="plone">History</dt>
|
||||
<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">
|
||||
<tal:comment replace="nothing">Table containing the history</tal:comment>
|
||||
<tal:history condition="objs">
|
||||
<metal:nav use-macro="here/skyn/macros/macros/appyNavigate"/>
|
||||
<table width="100%" class="listing nosort">
|
||||
<tr i18n:domain="plone">
|
||||
<th i18n:translate="listingheader_action"/>
|
||||
<th i18n:translate="listingheader_performed_by"/>
|
||||
<th i18n:translate="listingheader_date_and_time"/>
|
||||
<th i18n:translate="listingheader_comment"/>
|
||||
</tr>
|
||||
<metal:block tal:define="review_history python: portal.reverseList(review_history);"
|
||||
tal:repeat="items review_history">
|
||||
<tr tal:define="odd repeat/items/odd;
|
||||
rhComments items/comments|nothing;
|
||||
state items/review_state|nothing"
|
||||
tal:condition="python: items['action'] and (rhComments != '_invisible_')"
|
||||
tal:attributes="class python:test(odd, 'even', 'odd')">
|
||||
|
||||
<td tal:content="python: tool.translate(contextObj.getWorkflowLabel(items['action']))"
|
||||
<tal:event repeat="event objs">
|
||||
<tr tal:define="odd repeat/event/odd;
|
||||
rhComments event/comments|nothing;
|
||||
state event/review_state|nothing;
|
||||
isDataChange python: event['action'] == '_datachange_'"
|
||||
tal:attributes="class python:test(odd, 'even', 'odd')" valign="top">
|
||||
<td tal:condition="isDataChange" tal:content="python: tool.translate('data_change')"></td>
|
||||
<td tal:condition="not: isDataChange"
|
||||
tal:content="python: tool.translate(contextObj.getWorkflowLabel(event['action']))"
|
||||
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);
|
||||
fullname actor/fullname|nothing;
|
||||
username actor/username|nothing"
|
||||
tal:content="python:fullname or username or actorid"/>
|
||||
<td tal:content="python:toLocalizedTime(items['time'],long_format=True)"/>
|
||||
<td><tal:comment condition="rhComments" tal:content="structure rhComments"/>
|
||||
<td tal:content="python:contextObj.toLocalizedTime(event['time'],long_format=True)"/>
|
||||
<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>
|
||||
<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>
|
||||
</metal:block>
|
||||
</table>
|
||||
</dd>
|
||||
</dl>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tal:event>
|
||||
</table>
|
||||
</tal:history>
|
||||
</metal:history>
|
||||
|
||||
<div metal:define-macro="pagePrologue">
|
||||
<tal:comment replace="nothing">Global elements used in every page.</tal:comment>
|
||||
|
@ -468,6 +449,32 @@
|
|||
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>
|
||||
<tal:comment replace="nothing">Global form for deleting an object</tal:comment>
|
||||
|
@ -479,7 +486,10 @@
|
|||
|
||||
<div metal:define-macro="showPageHeader"
|
||||
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: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"/>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<metal:byLine use-macro="here/skyn/macros/macros/byline"/>
|
||||
<tal:showWorkflow condition="showWorkflow">
|
||||
<metal:workflowHistory use-macro="here/skyn/macros/macros/workflowHistory"/>
|
||||
</tal:showWorkflow>
|
||||
<td class="documentByLine">
|
||||
<tal:comment replace="nothing">Creator and last modification date</tal:comment>
|
||||
<tal:comment replace="nothing">Plus/minus icon for accessing history</tal:comment>
|
||||
<tal:accessHistory condition="hasHistory">
|
||||
<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"/>
|
||||
<span i18n:translate="label_history" i18n:domain="plone" class="appyHistory"></span>
|
||||
</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"/>
|
||||
—
|
||||
</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 valign="top"><metal:pod use-macro="here/skyn/macros/macros/listPodTemplates"/>
|
||||
</td>
|
||||
</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>
|
||||
<tr tal:condition="python: showWorkflow and contextObj.getWorkflowLabel()">
|
||||
|
@ -819,35 +861,6 @@
|
|||
tal:define="queryUrl python: '%s/skyn/query' % appFolder.absolute_url();
|
||||
currentSearch request/search|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>
|
||||
<dt class="portletHeader">
|
||||
<tal:comment replace="nothing">If there is only one flavour, clicking on the portlet
|
||||
|
@ -907,7 +920,7 @@
|
|||
<img align="left" style="cursor:pointer"
|
||||
tal:attributes="id python: '%s_img' % group['labelId'];
|
||||
src python:test(expanded, 'skyn/collapse.gif', 'skyn/expand.gif');
|
||||
onClick python:'javascript:toggleSearchGroup(\'%s\')' % group['labelId']"/>
|
||||
onClick python:'javascript:toggleCookie(\'%s\')' % group['labelId']"/>
|
||||
<span tal:replace="group/label"/>
|
||||
</dt>
|
||||
<tal:comment replace="nothing">Group searches</tal:comment>
|
||||
|
|
|
@ -27,10 +27,10 @@
|
|||
phase request/phase|phaseInfo/name;
|
||||
pageName python: contextObj.getAppyPage(isEdit, phaseInfo);
|
||||
showWorkflow python: flavour.getAttr('showWorkflowFor' + contextObj.meta_type)">
|
||||
<div metal:use-macro="here/skyn/macros/macros/pagePrologue"/>
|
||||
<div metal:use-macro="here/skyn/macros/macros/showPageHeader"/>
|
||||
<div metal:use-macro="here/skyn/macros/macros/listFields" />
|
||||
<div metal:use-macro="here/skyn/macros/macros/showPageFooter"/>
|
||||
<metal:prologue use-macro="here/skyn/macros/macros/pagePrologue"/>
|
||||
<metal:header use-macro="here/skyn/macros/macros/showPageHeader"/>
|
||||
<metal:fields use-macro="here/skyn/macros/macros/listFields" />
|
||||
<metal:footer use-macro="here/skyn/macros/macros/showPageFooter"/>
|
||||
</metal:fill>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
tal:define="tool python: context.<!toolInstanceName!>"
|
||||
tal:condition="tool/showPortlet">
|
||||
<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;
|
||||
appName string:<!applicationName!>;
|
||||
appFolder tool/getAppFolder;
|
||||
|
|
|
@ -72,6 +72,31 @@
|
|||
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. */
|
||||
.stepDone {
|
||||
background-color: #cde2a7;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
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.utils import sequenceTypes
|
||||
from appy.shared.utils import getOsTempFolder
|
||||
|
@ -153,7 +153,8 @@ class AbstractWrapper:
|
|||
objId = kwargs['id']
|
||||
del kwargs['id']
|
||||
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
|
||||
externalData = None
|
||||
if kwargs.has_key('_data'):
|
||||
|
|
|
@ -59,6 +59,9 @@ class PoMessage:
|
|||
IMPORT_DONE = 'Import terminated successfully.'
|
||||
WORKFLOW_COMMENT = 'Optional comment'
|
||||
WORKFLOW_STATE = 'state'
|
||||
DATA_CHANGE = 'Data change'
|
||||
MODIFIED_FIELD = 'Modified field'
|
||||
PREVIOUS_VALUE = 'Previous value'
|
||||
PHASE = 'phase'
|
||||
ROOT_TYPE = 'type'
|
||||
CHOOSE_A_VALUE = ' - '
|
||||
|
|
|
@ -14,6 +14,12 @@ SAP_FUNCTION_ERROR = 'Error while calling function "%s". %s'
|
|||
SAP_DISCONNECT_ERROR = 'Error while disconnecting from SAP. %s'
|
||||
SAP_TABLE_PARAM_ERROR = 'Param "%s" does not correspond to a valid table ' \
|
||||
'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_INFO_ERROR = 'Error while asking information about function ' \
|
||||
'"%s". %s'
|
||||
|
@ -52,6 +58,32 @@ class Sap:
|
|||
connNoPasswd = params[:params.index('PASSWD')] + 'PASSWD=********'
|
||||
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):
|
||||
'''Calls a function on the SAP server.'''
|
||||
try:
|
||||
|
@ -60,8 +92,8 @@ class Sap:
|
|||
for name, value in params.iteritems():
|
||||
if type(value) == dict:
|
||||
# The param corresponds to a SAP/C "struct"
|
||||
v = self.sap.get_structure(name)()
|
||||
v.from_dict(value)
|
||||
v = self.createStructure(
|
||||
self.sap.get_structure(name),value, name)
|
||||
elif type(value) in sequenceTypes:
|
||||
# The param must be a SAP/C "table" (a list of structs)
|
||||
# Retrieve the name of the struct type related to this
|
||||
|
@ -78,8 +110,7 @@ class Sap:
|
|||
SAP_TABLE_PARAM_ERROR % (name, functionName))
|
||||
v = self.sap.get_table(tableTypeName)
|
||||
for dValue in value:
|
||||
v.append_from_dict(dValue)
|
||||
#v = v.handle
|
||||
v.append(self.createStructure(v.struc, dValue, name))
|
||||
else:
|
||||
v = value
|
||||
function[name] = v
|
||||
|
|
Loading…
Reference in a new issue