[gen] Added an index 'Modified' on every object (it represents the date of the last modification); removed obsolete workflow-related code; removed attributes Tool.showAllStatesInPhaseFor...; changed the way to display the object's current state in the UI.

This commit is contained in:
Gaetan Delannay 2012-11-05 10:21:27 +01:00
parent f31cbc4d12
commit 21585df6a1
12 changed files with 69 additions and 123 deletions

View file

@ -1547,9 +1547,15 @@ class Date(Type):
def getFormattedValue(self, obj, value): def getFormattedValue(self, obj, value):
if self.isEmptyValue(value): return '' if self.isEmptyValue(value): return ''
res = value.strftime('%d/%m/') + str(value.year()) tool = obj.getTool().appy()
# A problem may occur with some extreme year values. Replace the "year"
# part "by hand".
dateFormat = tool.dateFormat
if '%Y' in dateFormat:
dateFormat = dateFormat.replace('%Y', str(value.year()))
res = value.strftime(dateFormat)
if self.format == Date.WITH_HOUR: if self.format == Date.WITH_HOUR:
res += ' %s' % value.strftime('%H:%M') res += ' %s' % value.strftime(tool.hourFormat)
return res return res
def getRequestValue(self, request, requestName=None): def getRequestValue(self, request, requestName=None):

View file

@ -539,18 +539,6 @@ class ToolClassDescriptor(ClassDescriptor):
fieldType = gen.Boolean(default=True, page='userInterface', fieldType = gen.Boolean(default=True, page='userInterface',
group=groupName) group=groupName)
self.addField(fieldName, fieldType) self.addField(fieldName, fieldType)
# Adds the boolean field for showing all states in current state or not.
# If this boolean is True but the current phase counts only one state,
# we will not show the state at all: the fact of knowing in what phase
# we are is sufficient. If this boolean is False, we simply show the
# current state.
defaultValue = False
if len(classDescr.getPhases()) > 1:
defaultValue = True
fieldName = 'showAllStatesInPhaseFor%s' % className
fieldType = gen.Boolean(default=defaultValue, page='userInterface',
group=groupName)
self.addField(fieldName, fieldType)
class UserClassDescriptor(ClassDescriptor): class UserClassDescriptor(ClassDescriptor):
'''Appy-specific class for representing a user.''' '''Appy-specific class for representing a user.'''

View file

@ -502,6 +502,7 @@ class ZopeGenerator(Generator):
msg('object_history', '', msg.OBJECT_HISTORY), msg('object_history', '', msg.OBJECT_HISTORY),
msg('object_created_by', '', msg.OBJECT_CREATED_BY), msg('object_created_by', '', msg.OBJECT_CREATED_BY),
msg('object_created_on', '', msg.OBJECT_CREATED_ON), msg('object_created_on', '', msg.OBJECT_CREATED_ON),
msg('object_modified_on', '', msg.OBJECT_MODIFIED_ON),
msg('object_action', '', msg.OBJECT_ACTION), msg('object_action', '', msg.OBJECT_ACTION),
msg('object_author', '', msg.OBJECT_AUTHOR), msg('object_author', '', msg.OBJECT_AUTHOR),
msg('action_date', '', msg.ACTION_DATE), msg('action_date', '', msg.ACTION_DATE),

View file

@ -9,8 +9,8 @@ from appy.shared.utils import normalizeText
defaultIndexes = { defaultIndexes = {
'State': 'ListIndex', 'UID': 'FieldIndex', 'Title': 'TextIndex', 'State': 'ListIndex', 'UID': 'FieldIndex', 'Title': 'TextIndex',
'SortableTitle': 'FieldIndex', 'SearchableText': 'TextIndex', 'SortableTitle': 'FieldIndex', 'SearchableText': 'TextIndex',
'Creator': 'FieldIndex', 'Created': 'DateIndex', 'ClassName': 'FieldIndex', 'Creator': 'FieldIndex', 'Created': 'DateIndex', 'Modified': 'DateIndex',
'Allowed': 'KeywordIndex'} 'ClassName': 'FieldIndex', 'Allowed': 'KeywordIndex'}
# Stuff for creating or updating the indexes ----------------------------------- # Stuff for creating or updating the indexes -----------------------------------
class TextIndexInfo: class TextIndexInfo:

View file

@ -87,6 +87,10 @@ class BaseMixin:
appyObject = obj.appy() appyObject = obj.appy()
if hasattr(appyObject, 'onEdit'): if hasattr(appyObject, 'onEdit'):
msg = appyObject.onEdit(created) msg = appyObject.onEdit(created)
# Update last modification date
if not created:
from DateTime import DateTime
obj.modified = DateTime()
obj.reindex() obj.reindex()
return obj, msg return obj, msg
@ -700,29 +704,6 @@ class BaseMixin:
res.append({'field':field, 'width':width, 'align': align}) res.append({'field':field, 'width':width, 'align': align})
return res return res
def getAppyStates(self, phase, currentOnly=False):
'''Returns information about the states that are related to p_phase.
If p_currentOnly is True, we return the current state, even if not
related to p_phase.'''
currentState = self.State()
if currentOnly:
return [StateDescr(currentState, 'current').get()]
res = []
workflow = self.getWorkflow()
stateStatus = 'done'
for stateName in dir(workflow):
if getattr(workflow, stateName).__class__.__name__ != 'State':
continue
if stateName == currentState:
stateStatus = 'current'
elif stateStatus != 'done':
stateStatus = 'future'
state = getattr(workflow, stateName)
if (state.phase == phase) and \
(self._appy_showState(workflow, state.show)):
res.append(StateDescr(stateName, stateStatus).get())
return res
def getAppyTransitions(self, includeFake=True, includeNotShowable=False): def getAppyTransitions(self, includeFake=True, includeNotShowable=False):
'''This method returns info about transitions that one can trigger from '''This method returns info about transitions that one can trigger from
the user interface. the user interface.
@ -778,8 +759,7 @@ class BaseMixin:
for appyType in self.getAllAppyTypes(): for appyType in self.getAllAppyTypes():
typePhase = appyType.page.phase typePhase = appyType.page.phase
if typePhase not in phases: if typePhase not in phases:
states = self.getAppyStates(typePhase) phase = PhaseDescr(typePhase, self)
phase = PhaseDescr(typePhase, states, self)
res.append(phase.__dict__) res.append(phase.__dict__)
phases[typePhase] = phase phases[typePhase] = phase
else: else:
@ -1213,7 +1193,7 @@ class BaseMixin:
return wrapper return wrapper
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
# Standard methods for computing values of standard Appy indexes # Methods for computing values of standard Appy indexes
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
def UID(self): def UID(self):
'''Returns the unique identifier for this object.''' '''Returns the unique identifier for this object.'''
@ -1246,6 +1226,11 @@ class BaseMixin:
'''When was this object created ?''' '''When was this object created ?'''
return self.created return self.created
def Modified(self):
'''When was this object last modified ?'''
if hasattr(self.aq_base, 'modified'): return self.modified
return self.created
def State(self, name=True, initial=False): def State(self, name=True, initial=False):
'''Returns information about the current object state. If p_name is '''Returns information about the current object state. If p_name is
True, the returned info is the state name. Else, it is the State True, the returned info is the state name. Else, it is the State
@ -1291,11 +1276,12 @@ class BaseMixin:
res.add('user:%s' % id) res.add('user:%s' % id)
return list(res) return list(res)
def _appy_showState(self, workflow, stateShow): def showState(self):
'''Must I show a state whose "show value" is p_stateShow?''' '''Must I show self's current state ?'''
stateShow = self.State(name=False).show
if callable(stateShow): if callable(stateShow):
return stateShow(workflow, self.appy()) return stateShow(self.getWorkflow(), self.appy())
else: return stateShow return stateShow
def _appy_listStates(self): def _appy_listStates(self):
'''Lists the possible states for this object.''' '''Lists the possible states for this object.'''

View file

@ -210,7 +210,7 @@ setattr(Page, Page.pages.back.attribute, Page.pages.back)
# Prefixes of the fields generated on the Tool. # Prefixes of the fields generated on the Tool.
toolFieldPrefixes = ('podTemplate', 'formats', 'resultColumns', toolFieldPrefixes = ('podTemplate', 'formats', 'resultColumns',
'enableAdvancedSearch', 'numberOfSearchColumns', 'enableAdvancedSearch', 'numberOfSearchColumns',
'searchFields', 'showWorkflow', 'showAllStatesInPhase') 'searchFields', 'showWorkflow')
defaultToolFields = ('title', 'mailHost', 'mailEnabled', 'mailFrom', defaultToolFields = ('title', 'mailHost', 'mailEnabled', 'mailFrom',
'appyVersion', 'dateFormat', 'hourFormat', 'users', 'appyVersion', 'dateFormat', 'hourFormat', 'users',
'connectedUsers', 'groups', 'translations', 'connectedUsers', 'groups', 'translations',

View file

@ -36,7 +36,6 @@ class PoMessage:
MSG_numberOfSearchColumns = "Number of search columns" MSG_numberOfSearchColumns = "Number of search columns"
MSG_searchFields = "Search fields" MSG_searchFields = "Search fields"
MSG_showWorkflow = 'Show workflow-related information' MSG_showWorkflow = 'Show workflow-related information'
MSG_showAllStatesInPhase = 'Show all states in phase'
POD_ASKACTION = 'Trigger related action' POD_ASKACTION = 'Trigger related action'
REF_NO = 'No object.' REF_NO = 'No object.'
REF_ADD = 'Add a new one' REF_ADD = 'Add a new one'
@ -163,6 +162,7 @@ class PoMessage:
OBJECT_HISTORY = 'History' OBJECT_HISTORY = 'History'
OBJECT_CREATED_BY = 'By' OBJECT_CREATED_BY = 'By'
OBJECT_CREATED_ON = 'On' OBJECT_CREATED_ON = 'On'
OBJECT_MODIFIED_ON = 'Last updated on'
OBJECT_ACTION = 'Action' OBJECT_ACTION = 'Action'
OBJECT_AUTHOR = 'Author' OBJECT_AUTHOR = 'Author'
ACTION_DATE = 'Date' ACTION_DATE = 'Date'

View file

@ -115,7 +115,7 @@ img { border: 0; vertical-align: middle}
.summary {margin-bottom: 5px;} .summary {margin-bottom: 5px;}
.objectTitle { font-size: 11pt; border-bottom: 3px solid grey; .objectTitle { font-size: 11pt; border-bottom: 3px solid grey;
font-weight: bold;} font-weight: bold;}
.by { padding-top: 5px;} .by { padding-top: 5px; color: grey; font-size: 97% }
.workflow { text-align: center; border-top: 1px solid grey } .workflow { text-align: center; border-top: 1px solid grey }
.underTitle { background-color: #e9e9e9 } .underTitle { background-color: #e9e9e9 }
.objectNavigate { margin-top: 3px;} .objectNavigate { margin-top: 3px;}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 B

View file

@ -137,27 +137,6 @@
</tal:history> </tal:history>
</metal:history> </metal:history>
<tal:comment replace="nothing">
This macro displays an object's state(s). It is used by macro "header" below.
</tal:comment>
<metal:states define-macro="states"
tal:define="showAllStatesInPhase python: tool.getAttr('showAllStatesInPhaseFor' + contextObj.meta_type);
states python: contextObj.getAppyStates(phase, currentOnly=not showAllStatesInPhase)"
tal:condition="python: test(showAllStatesInPhase, len(states)&gt;1, True)">
<table>
<tr>
<tal:state repeat="stateInfo states">
<td class="state"
tal:content="python: _(contextObj.getWorkflowLabel(stateInfo['name']))">
</td>
<td tal:condition="python: stateInfo['name'] != states[-1]['name']">
<img tal:attributes="src string: $appUrl/ui/nextState.png"/>
</td>
</tal:state>
</tr>
</table>
</metal:states>
<tal:comment replace="nothing"> <tal:comment replace="nothing">
This macro displays an object's transitions(s). It is used by macro "header" below. This macro displays an object's transitions(s). It is used by macro "header" below.
</tal:comment> </tal:comment>
@ -223,15 +202,28 @@
<span tal:replace="python: _('object_history')"></span> ||&nbsp; <span tal:replace="python: _('object_history')"></span> ||&nbsp;
</tal:accessHistory> </tal:accessHistory>
<tal:comment replace="nothing">Show document creator</tal:comment> <tal:comment replace="nothing">Document creator</tal:comment>
<span class="by" tal:condition="python: creator != 'Anonymous User'"> <tal:creator condition="python: creator != 'Anonymous User'">
<tal:by replace="python: _('object_created_by')"/> <tal:by replace="python: _('object_created_by')"/>
<tal:creator replace="python: tool.getUserName(creator)"/> &mdash; <tal:creator replace="python: tool.getUserName(creator)"/>
</span> </tal:creator>
<tal:comment replace="nothing">Show creation date</tal:comment> <tal:comment replace="nothing">Creation and last modification dates</tal:comment>
<tal:by replace="python: _('object_created_on')"/> <tal:by replace="python: _('object_created_on')"/>
<span tal:replace="python: tool.formatDate(contextObj.created, withHour=True)"></span> <tal:dates define="creationDate contextObj/Created;
</td> modificationDate contextObj/Modified">
<tal:date replace="python: tool.formatDate(creationDate, withHour=True)"/>
<tal:modified condition="python: modificationDate != creationDate">&mdash;
<tal:by replace="python: _('object_modified_on')"/>
<tal:date replace="python: tool.formatDate(modificationDate, withHour=True)"/>
</tal:modified>
</tal:dates>
<tal:comment replace="nothing">State</tal:comment>
<tal:state condition="contextObj/showState">
&mdash;
<tal:label replace="python: _('workflow_state')"/>:
<b tal:content="python: _(contextObj.getWorkflowLabel())"></b>
</tal:state>
</td>
</tr> </tr>
<tal:comment replace="nothing">Object history</tal:comment> <tal:comment replace="nothing">Object history</tal:comment>
<tr tal:condition="hasHistory" class="underTitle"> <tr tal:condition="hasHistory" class="underTitle">
@ -246,15 +238,10 @@
</span> </span>
</td> </td>
</tr> </tr>
<tal:comment replace="nothing">Workflow-related information and actions</tal:comment> <tal:comment replace="nothing">Possible transitions</tal:comment>
<tr tal:condition="python: showWorkflow and contextObj.getWorkflowLabel()" class="workflow"> <tr tal:condition="python: showWorkflow and contextObj.getWorkflowLabel()" class="workflow">
<td colspan="2"> <td colspan="2" tal:attributes="align dright">
<table width="100%"> <metal:states use-macro="here/ui/page/macros/transitions"/>
<tr>
<td><metal:states use-macro="here/ui/page/macros/states"/></td>
<td tal:attributes="align dright"><metal:states use-macro="here/ui/page/macros/transitions"/></td>
</tr>
</table>
</td> </td>
</tr> </tr>
</table> </table>

View file

@ -48,6 +48,7 @@ def createObject(folder, id, className, appName, wf=True, noSecurity=False):
obj.creator = userId or 'Anonymous User' obj.creator = userId or 'Anonymous User'
from DateTime import DateTime from DateTime import DateTime
obj.created = DateTime() obj.created = DateTime()
obj.modified = obj.created
obj.__ac_local_roles__ = { userId: ['Owner'] } # userId can be None (anon). obj.__ac_local_roles__ = { userId: ['Owner'] } # userId can be None (anon).
if wf: obj.notifyWorkflowCreated() if wf: obj.notifyWorkflowCreated()
return obj return obj
@ -115,9 +116,8 @@ class GroupDescr(Descr):
groupDict['widgets'].append(newRow) groupDict['widgets'].append(newRow)
class PhaseDescr(Descr): class PhaseDescr(Descr):
def __init__(self, name, states, obj): def __init__(self, name, obj):
self.name = name self.name = name
self.states = states
self.obj = obj self.obj = obj
self.phaseStatus = None self.phaseStatus = None
# The list of names of pages in this phase # The list of names of pages in this phase
@ -163,42 +163,26 @@ class PhaseDescr(Descr):
self.hiddenPages.append(appyType.page.name) self.hiddenPages.append(appyType.page.name)
def computeStatus(self, allPhases): def computeStatus(self, allPhases):
'''Compute status of whole phase based on individual status of states '''Compute status of this phase, which depends on the page currently
in this phase. If this phase includes no state, the concept of phase
is simply used as a tab, and its status depends on the page currently
shown. This method also fills fields "previousPhase" and "nextPhase" shown. This method also fills fields "previousPhase" and "nextPhase"
if relevant, based on list of p_allPhases.''' if relevant, based on list of p_allPhases.'''
res = 'Current' res = 'Current'
if self.states: # Compute status based on current page
# Compute status base on states page = self.obj.REQUEST.get('page', 'main')
res = self.states[0]['stateStatus'] if page in self.pages:
if len(self.states) > 1: res = 'Current'
for state in self.states[1:]:
if res != state['stateStatus']:
res = 'Current'
break
else: else:
# Compute status based on current page res = 'Deselected'
page = self.obj.REQUEST.get('page', 'main') # Identify previous and next phases
if page in self.pages: for phaseInfo in allPhases:
res = 'Current' if phaseInfo['name'] == self.name:
else: i = allPhases.index(phaseInfo)
res = 'Deselected' if i > 0:
# Identify previous and next phases self.previousPhase = allPhases[i-1]
for phaseInfo in allPhases: if i < (len(allPhases)-1):
if phaseInfo['name'] == self.name: self.nextPhase = allPhases[i+1]
i = allPhases.index(phaseInfo)
if i > 0:
self.previousPhase = allPhases[i-1]
if i < (len(allPhases)-1):
self.nextPhase = allPhases[i+1]
self.phaseStatus = res self.phaseStatus = res
class StateDescr(Descr):
def __init__(self, name, stateStatus):
self.name = name
self.stateStatus = stateStatus.capitalize()
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
upperLetter = re.compile('[A-Z]') upperLetter = re.compile('[A-Z]')
def produceNiceMessage(msg): def produceNiceMessage(msg):

View file

@ -113,12 +113,6 @@ class ToolWrapper(AbstractWrapper):
"showWorkflow" "showWorkflow"
Stores the boolean field indicating if we must show workflow- Stores the boolean field indicating if we must show workflow-
related information for p_klass or not. related information for p_klass or not.
"showAllStatesInPhase"
Stores the boolean field indicating if we must show all states
linked to the current phase or not. If this field is False, we
simply show the current state, be it linked to the current phase
or not.
''' '''
fullClassName = self.o.getPortalType(klass) fullClassName = self.o.getPortalType(klass)
res = '%sFor%s' % (attributeType, fullClassName) res = '%sFor%s' % (attributeType, fullClassName)