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,
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:

View file

@ -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),

View file

@ -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)
# ------------------------------------------------------------------------------

View file

@ -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,24 +228,29 @@ 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.__dict__
res['backd'] = appyType.back.__dict__
res = appyType
if asDict:
res = appyType.__dict__
res['backd'] = appyType.back.__dict__
return res
def _appy_getRefs(self, fieldName, ploneObjects=False,
@ -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&macro=history' % \
self.UID()
if params: params = '&' + params
return baseUrl + chunk + params
else: # We consider t=='view'
return baseUrl + '/skyn/view' + params

View file

@ -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>

View file

@ -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)()

View file

@ -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 &gt; 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"/>
&mdash;
</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">
<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:attributes="class string:state-${state}"/>
<td tal:define="actorid python:items.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"/>
<tal:noComment condition="not: rhComments" i18n:translate="no_comments" i18n:domain="plone"/></td>
</tr>
</metal:block>
</table>
</dd>
</dl>
</span>
<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>
<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: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: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>
</table>
</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"/>&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 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,12 +920,12 @@
<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']"/>&nbsp;
onClick python:'javascript:toggleCookie(\'%s\')' % group['labelId']"/>&nbsp;
<span tal:replace="group/label"/>
</dt>
<tal:comment replace="nothing">Group searches</tal:comment>
<span tal:attributes="id group/labelId;
style python:test(expanded, 'display:block', 'display:none')">
style python:test(expanded, 'display:block', 'display:none')">
<dt class="portletAppyItem portletSearch portletGroupItem" tal:repeat="search group/searches">
<a tal:attributes="href python: '%s?type_name=%s&flavourNumber=%s&search=%s' % (queryUrl, rootClass, flavourNumber, search['name']);
title search/descr;

View file

@ -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>

View file

@ -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;

View file

@ -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;

View file

@ -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'):

View file

@ -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 = ' - '

View file

@ -8,15 +8,21 @@ from appy.gen.utils import sequenceTypes
class SapError(Exception): pass
SAP_MODULE_ERROR = 'Module pysap was not found (you can get it at ' \
'http://pysaprfc.sourceforge.net)'
'http://pysaprfc.sourceforge.net)'
SAP_CONNECT_ERROR = 'Error while connecting to SAP (conn_string: %s). %s'
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".'
'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'
'"%s". %s'
SAP_GROUP_NOT_FOUND = 'Group of functions "%s" does not exist or is empty.'
# Is the pysap module present or not ?
@ -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