work in progress: object actions, stateful resources: classification quality

git-svn-id: svn://svn.cy55.de/Zope3/src/loops/trunk@2538 fd906abe-77d9-0310-91a1-e0d9ade77398
This commit is contained in:
helmutm 2008-04-23 08:39:17 +00:00
parent c1e94af299
commit 828dfda5c5
16 changed files with 321 additions and 100 deletions

View file

@ -27,7 +27,8 @@ from zope import component
from zope.app.pagetemplate import ViewPageTemplateFile
from zope.cachedescriptors.property import Lazy
from cybertools.browser.action import Action
from cybertools.browser.action import Action, actions
from loops.util import _
class TargetAction(Action):
@ -68,7 +69,6 @@ class DialogAction(Action):
if self.fixedType:
urlParams['fixed_type'] = 'yes'
urlParams.update(self.addParams)
#url = self.page.virtualTargetUrlWithSkin
url = self.page.virtualTargetUrl
return self.jsOnClick % (self.dialogName, url, self.viewName,
urlencode(urlParams))
@ -77,3 +77,20 @@ class DialogAction(Action):
def innerHtmlId(self):
return 'dialog.' + self.dialogName
# standard actions
actions.register('info', 'object', DialogAction,
description=_(u'Information about this object.'),
viewName='object_info.html',
dialogName='object_info',
icon='cybertools.icons/info.png',
cssClass='icon-action',
)
actions.register('external_edit', 'object', TargetAction,
description=_(u'Edit with external editor.'),
viewName='external_edit?version=this',
icon='edit.gif',
cssClass='icon-action',
)

View file

@ -50,6 +50,7 @@ from zope.traversing.api import getName, getParent
from cybertools.ajax.dojo import dojoMacroTemplate
from cybertools.browser.view import GenericView
from cybertools.relation.interfaces import IRelationRegistry
from cybertools.stateful.interfaces import IStateful
from cybertools.text import mimetypes
from cybertools.typology.interfaces import IType, ITypeManager
from loops.common import adapted
@ -370,6 +371,21 @@ class BaseView(GenericView, I18NView):
addInfo = u', '.join(e for e in (current, released) if e)
return u'%s (%s)' % (versionId, addInfo)
# states
@Lazy
def states(self):
result = []
if not checkPermission('loops.ManageSite', self.context):
# TODO: replace by more sensible permission
return result
target = self.virtualTargetObject
for std in ['loops.classification_quality',
'loops.simple_publishing']:
stf = component.getAdapter(target, IStateful, name=std)
result.append(stf)
return result
# controlling editing
@Lazy
@ -431,6 +447,17 @@ class BaseView(GenericView, I18NView):
#cm.register('css', identifier='dojo.css', position=1,
# resourceName='ajax.dojo/dojo/resources/dojo.css', media='all')
def registerDojoDialog(self):
self.registerDojo()
jsCall = 'dojo.require("dijit.Dialog")'
self.controller.macros.register('js-execute', jsCall, jsCall=jsCall)
def registerDojoTooltipDialog(self):
self.registerDojo()
jsCall = ('dojo.require("dijit.Dialog");'
'dojo.require("dijit.form.Button");')
self.controller.macros.register('js-execute', jsCall, jsCall=jsCall)
def registerDojoDateWidget(self):
self.registerDojo()
jsCall = ('dojo.require("dijit.form.DateTextBox"); '

View file

@ -211,25 +211,6 @@ class ConceptView(BaseView):
def description(self):
return self.adapted.description
def xxx_fieldData(self):
# obsolete - use getData() instead
# TODO: change view macros accordingly
ti = IType(self.context).typeInterface
if not ti:
return
adapter = self.adapted
for n, f in schema.getFieldsInOrder(ti):
if n in ('title', 'description',): # already shown in header
continue
value = getattr(adapter, n, '')
if not value:
continue
bound = f.bind(adapter)
widget = component.getMultiAdapter(
(bound, self.request), IDisplayWidget)
widget.setRenderedValue(value)
yield dict(title=f.title, value=value, id=n, widget=widget)
def getData(self, omit=('title', 'description')):
data = self.instance.applyTemplate()
for k in omit:
@ -328,7 +309,9 @@ class ConceptView(BaseView):
def resources(self):
rels = self.context.getResourceRelations()
for r in rels:
yield self.childViewFactory(r, self.request, contextIsSecond=True)
#yield self.childViewFactory(r, self.request, contextIsSecond=True)
from loops.browser.resource import ResourceRelationView
yield ResourceRelationView(r, self.request, contextIsSecond=True)
@Lazy
def view(self):

View file

@ -114,6 +114,7 @@
<th i18n:translate="">Size</th>
<th i18n:translate="">Modification Date</th>
<th i18n:translate="">Author(s)</th>
<th i18n:translate="">St</th>
</tr>
<tal:items repeat="related resources">
<tal:item define="class python: repeat['related'].odd() and 'even' or 'odd';
@ -141,6 +142,10 @@
</td>
<td><span tal:replace="related/modified">2007-03-30</span></td>
<td><span tal:replace="related/creators">John</span></td>
<td style="white-space: nowrap"
tal:define="target nocall:related">
<div metal:use-macro="views/node_macros/object_actions" />
</td>
</tr>
</tal:item>
</tal:items>

View file

@ -565,7 +565,14 @@
</pages>
<!-- forms (end-user view) -->
<!-- dialogs/forms (end-user views) -->
<page
name="object_info.html"
for="loops.interfaces.INode"
class="loops.browser.node.ObjectInfo"
permission="zope.View"
/>
<page
name="create_object.html"

View file

@ -44,6 +44,7 @@ from cybertools.composer.interfaces import IInstance
from cybertools.composer.schema.interfaces import ISchemaFactory
from cybertools.composer.schema.browser.common import schema_macros, schema_edit_macros
from cybertools.composer.schema.schema import FormState
from cybertools.stateful.interfaces import IStateful
from cybertools.typology.interfaces import IType, ITypeManager
from loops.common import adapted
from loops.concept import Concept, ConceptRelation, ResourceRelation
@ -437,18 +438,31 @@ class EditObject(FormController, I18NView):
formState = instance.applyTemplate(data=form, fieldHandlers=self.fieldHandlers)
self.selected = []
self.old = []
stateKeys = []
for k in form.keys():
if k.startswith(self.prefix):
fn = k[len(self.prefix):]
value = form[k]
if fn.startswith(self.conceptPrefix) and value:
self.collectConcepts(fn[len(self.conceptPrefix):], value)
if k.startswith('state.'):
stateKeys.append(k)
self.collectAutoConcepts()
if self.old or self.selected:
self.assignConcepts(obj)
for k in stateKeys:
self.updateState(k)
notify(ObjectModifiedEvent(obj))
return formState
def updateState(self, key):
trans = self.request.form.get(key, '-')
if trans == '-':
return
stdName = key[len('state.'):]
stf = component.getAdapter(self.object, IStateful, name=stdName)
stf.doTransition(trans)
def handleFileUpload(self, context, value, fieldInstance, formState):
""" Special handler for fileupload fields;
value is a FileUpload instance.

View file

@ -55,6 +55,7 @@
<tr metal:use-macro="customMacro" />
</tal:custom>
<tr metal:use-macro="view/template/macros/versioning" />
<tr metal:use-macro="view/template/macros/states" />
<tr metal:use-macro="view/template/macros/buttons" />
</tbody>
</table>
@ -216,6 +217,34 @@
</metal:versioning>
<metal:states define-macro="states"
tal:define="states view/states"
tal:condition="states">
<tr><td colspan="5" i18n:translate="" class="headline">States</td></tr>
<tr tal:repeat="st states">
<tal:state define="stObj st/getStateObject;
stDef st/statesDefinition;
stTrans st/getAvailableTransitionsForUser">
<td colspan="2" i18n:translate=""
tal:content="stDef">loops.simple_publishing</td>
<td i18n:translate=""
tal:content="stObj/title">draft</td>
<td>
<select name="state.loops.simple_publishing"
tal:condition="stTrans"
tal:attributes="name string:state.$stDef">
<option i18n:translate="" value="-">No change</option>
<option tal:repeat="trans stTrans"
tal:attributes="value trans/name"
tal:content="trans/title">publish</option>
</select>
</td>
<td />
</tal:state>
</tr>
</metal:states>
<tr metal:define-macro="buttons" i18n:domain="" class="buttons">
<td colspan="5">
<input value="Save" type="submit"

View file

@ -175,6 +175,10 @@ div.menu-1, div.menu-2 {
padding-top: 6px;
}
.icon-action {
display: inline;
}
.flow-left {
float: left;
}
@ -251,7 +255,6 @@ table.listing th {
padding: 1em 0 1em 0;
}
/* search stuff */
.searchForm input.button, input.submit {

View file

@ -383,11 +383,12 @@ class NodeView(BaseView):
return ('%s/.target%s' %
(self.url, util.getUidForObject(target)))
def getActions(self, category='object'):
def getActions(self, category='object', target=None):
actions = []
self.registerDojo()
if category in self.actions:
actions.extend(self.actions[category](self))
if target is None:
target = self.virtualTarget
if target is not None:
actions.extend(target.getActions(category, page=self))
@ -456,17 +457,23 @@ class NodeView(BaseView):
url = self.virtualTargetUrl
return ExternalEditorView(target, self.request).load(url=url)
# helper methods
def registerDojoDialog(self):
self.registerDojo()
cm = self.controller.macros
jsCall = 'dojo.require("dijit.Dialog")'
cm.register('js-execute', jsCall, jsCall=jsCall)
# inner HTML views
class ObjectInfo(NodeView):
__call__ = innerHtml
@property
def macro(self):
return self.template.macros['object_info']
@Lazy
def dialog_name(self):
return self.request.get('dialog', 'object_info')
class InlineEdit(NodeView):
""" Provides inline editor as inner HTML"""

View file

@ -25,29 +25,6 @@
</div>
<div tal:define="target nocall:item/targetObjectView"
tal:condition="nocall:target">
<tal:ignore condition="nothing">
<div metal:define-macro="editicons"
style="float: right;">
<span id="xedit_icon"
tal:condition="target/xeditable | nothing">
<a href="#" title="Edit"
tal:attributes="href string:${item/realTargetUrl}/external_edit?version=this;
title string:Edit '${target/title}' with External Editor"><img
src="edit.gif" alt="Edit"
tal:attributes="src context/++resource++edit.gif" /></a>
</span>
<span id="inlineedit_icon"
xtal:condition="item/inlineEditable"
tal:condition="nothing">
<a href="#" title="Edit" style="padding: 5px"
tal:attributes="title string:Edit '${target/title}' with Inline Editor;
onclick python: item.inlineEdit(id)"><img
src="edit.gif" alt="Edit"
tal:attributes="src context/++resource++edit.gif" /></a>
</span>
<tal:rte define="dummy item/checkRTE" />
</div>
</tal:ignore>
<div class="content-1 subcolumn" id="1.body"
tal:define="node nocall:item;
item nocall:target"
@ -208,6 +185,16 @@
</metal:actions>
<metal:actions define-macro="object_actions">
<div style="float: right;"
tal:define="target nocall:target|nothing">
<tal:actions repeat="action python:view.getActions('object', target=target)">
<metal:action use-macro="action/macro" />
</tal:actions>
</div>
</metal:actions>
<metal:actions define-macro="clipboard">
<div class="menu-2">loops Development</div>
</metal:actions>
@ -230,6 +217,11 @@
<!-- inner HTML macros -->
<metal:info define-macro="object_info">
<div>Object Information</div>
</metal:info>
<div metal:define-macro="inline_edit"
class="content-1" id="1.body">
<form action="." method="post" id="1.form">
@ -262,4 +254,3 @@
tal:attributes="src context/++resource++edit.gif" border="0" />
</a>
</metal:editlink>

View file

@ -39,6 +39,7 @@ from zope.security.proxy import removeSecurityProxy
from zope.traversing.api import getName, getParent
from zope.traversing.browser import absoluteURL
from cybertools.browser.action import actions
from cybertools.typology.interfaces import IType
from cybertools.xedit.browser import ExternalEditorView, fromUnicode
from loops.browser.action import DialogAction, TargetAction
@ -48,6 +49,7 @@ from loops.browser.node import NodeView, node_macros
from loops.common import adapted, NameChooser
from loops.interfaces import IBaseResource, IDocument, IMediaAsset, ITextDocument
from loops.interfaces import ITypeConcept
from loops.organize.stateful.browser import statefulActions
from loops.versioning.browser import version_macros
from loops.versioning.interfaces import IVersionable
from loops.util import _
@ -182,14 +184,11 @@ class ResourceView(BaseView):
return actions
def getObjectActions(self, page=None):
actions = []
if page is None:
factory, view = Action, self
else:
factory, view = TargetAction, page
#if self.xeditable:
# actions.append(factory(self, page=view,))
return actions
acts = ['info']
acts.extend('state.' + st for st in statefulActions)
if self.xeditable:
acts.append('external_edit')
return actions.get('object', acts, view=self, page=page)
actions = dict(portlet=getPortletActions, object=getObjectActions)
@ -209,6 +208,12 @@ class ResourceView(BaseView):
yield NodeView(node, self.request)
class ResourceRelationView(ResourceView, ConceptRelationView):
def __init__(self, relation, request, contextIsSecond=False):
ConceptRelationView.__init__(self, relation, request, contextIsSecond)
class ResourceConfigureView(ResourceView, ConceptConfigureView):
#def __init__(self, context, request):

View file

@ -2,10 +2,7 @@
<div tal:attributes="ondblclick python: item.openEditWindow('configure.html')">
<tal:body define="itemNum view/itemNum;
id string:$itemNum.body;">
<tal:edit define="target nocall:item;
item nocall:node|nocall:view;">
<div metal:use-macro="views/node_macros/editicons" />
</tal:edit>
<div metal:use-macro="views/node_macros/object_actions" />
<h1 tal:content="item/title">Title</h1>
<p tal:define="description description|item/description"
tal:condition="description">
@ -40,6 +37,7 @@
<metal:block define-macro="image">
<div tal:attributes="ondblclick python: item.openEditWindow('edit.html')">
<div metal:use-macro="views/node_macros/object_actions" />
<h3 tal:content="item/title">Title</h3><br />
<img src="#"
tal:attributes="src string:${view/url}/.target${view/targetId}/view" />
@ -50,6 +48,7 @@
<metal:block define-macro="download">
<div tal:attributes="ondblclick python: item.openEditWindow('edit.html')">
<div metal:use-macro="views/node_macros/object_actions" />
<h3 tal:content="item/title">Title</h3>
<p><i tal:content="item/description">Description</i></p>
<p>

View file

@ -64,6 +64,8 @@ not just kept in the adapter.
Controlling classification quality
----------------------------------
We again first have to register states definitions and adapter classes.
>>> from loops.organize.stateful.quality import classificationQuality
>>> component.provideUtility(classificationQuality(),
... name='loops.classification_quality')
@ -74,10 +76,15 @@ Controlling classification quality
>>> component.provideHandler(assign)
>>> component.provideHandler(deassign)
Now we can get a stateful adapter for a resource.
>>> qcheckedDoc01 = component.getAdapter(doc01, IStateful,
... name='loops.classification_quality')
>>> qcheckedDoc01.state
'unclassified'
'new'
Let's create two customer objects to be used for classification of resources
later.
>>> tCustomer = concepts['customer']
>>> from loops.concept import Concept
@ -87,6 +94,9 @@ Controlling classification quality
>>> c02 = addAndConfigureObject(concepts, Concept, 'c02', conceptType=tCustomer,
... title='DocFive')
When we change the concept assignments of the resource - i.e. its classification
- the classification quality state changes automaticalls
>>> c01.assignResource(doc01)
>>> qcheckedDoc01 = component.getAdapter(doc01, IStateful,
... name='loops.classification_quality')
@ -97,10 +107,15 @@ Controlling classification quality
>>> qcheckedDoc01.state
'classified'
In order to mark the classification as "verified" (i.e. quality-checked)
we have to perform the corresponding transition explicitly.
>>> qcheckedDoc01.doTransition('verify')
>>> qcheckedDoc01.state
'verified'
Upon later changes of classification the "verified" state gets lost again.
>>> c02.deassignResource(doc01)
>>> qcheckedDoc01.state
'classified'
@ -109,6 +124,39 @@ Controlling classification quality
>>> qcheckedDoc01.state
'unclassified'
Changing states when editing
----------------------------
We first need a node that provides us access to the resource as its target
>>> from loops.view import Node
>>> node = addAndConfigureObject(views, Node, 'node', target=doc01)
>>> from loops.browser.form import EditObjectForm, EditObject
>>> from zope.publisher.browser import TestRequest
The form view gives us access to the states of the object.
>>> form = EditObjectForm(node, TestRequest())
>>> for st in form.states:
... sto = st.getStateObject()
... transitions = st.getAvailableTransitions()
... userTrans = st.getAvailableTransitionsForUser()
... print st.statesDefinition, sto.title, [t.title for t in transitions],
... print [t.title for t in userTrans]
loops.classification_quality unclassified ['classify', 'verify'] ['verify']
loops.simple_publishing published ['retract'] ['retract']
Let's now update the form.
>>> input = {'state.loops.classification_quality': 'verify'}
>>> proc = EditObject(form, TestRequest(form=input))
>>> proc.update()
False
>>> qcheckedDoc01.state
'verified'
Fin de partie
=============

View file

@ -41,6 +41,9 @@ class StatefulLoopsObject(Stateful, StatefulAdapter):
adapts(ILoopsObject)
def getAvailableTransitionsForUser(self):
return self.getAvailableTransitions()
class SimplePublishable(StatefulLoopsObject):

View file

@ -0,0 +1,64 @@
#
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
"""
Views and actions for states management.
$Id$
"""
from zope import component
from zope.cachedescriptors.property import Lazy
from cybertools.browser.action import Action, actions
from cybertools.stateful.interfaces import IStateful
from loops.util import _
statefulActions = ('loops.classification_quality',)
class StateAction(Action):
url = None
definition = None
@Lazy
def stateful(self):
return component.getAdapter(self.view.context, IStateful,
name=self.definition)
@Lazy
def description(self):
return (u'State information for %s: %s' %
(self.definition, self.stateObject.title))
@Lazy
def stateObject(self):
return self.stateful.getStateObject()
@Lazy
def icon(self):
return 'cybertools.icons/led%s.png' % self.stateObject.color
for std in statefulActions:
actions.register('state.' + std, 'object', StateAction,
definition = std,
cssClass='icon-action',
)

View file

@ -39,51 +39,70 @@ from loops.organize.stateful.base import StatefulLoopsObject
@implementer(IStatesDefinition)
def classificationQuality():
return StatesDefinition('classificationQuality',
State('unclassified', 'unclassified', ('classify',)),
State('new', 'new', ('classify', 'verify',
'change_classification', 'remove_classification'),
color='red'),
State('unclassified', 'unclassified', ('classify', 'verify'),
color='red'),
State('classified', 'classified',
('verify', 'change_classification', 'remove_classification')),
('verify', 'change_classification', 'remove_classification'),
color='yellow'),
State('verified', 'verified',
('change_classification', 'remove_classification')),
('change_classification', 'remove_classification'),
color='green'),
Transition('classify', 'classify', 'classified'),
Transition('verify', 'verify', 'verified'),
Transition('change_classification', 'change classification', 'classified'),
Transition('remove_classification', 'remove classification', 'unclassified'),
initialState='unclassified')
initialState='new')
class ClassificationQualityCheckable(StatefulLoopsObject):
statesDefinition = 'loops.classification_quality'
def getAvailableTransitionsForUser(self):
return [tr for tr in self.getAvailableTransitions()
if tr.name == 'verify']
# automatic transitions
def assign(self, relation):
if not self.isRelevant(relation):
return
if self.state in ('new', 'unclassified'):
self.doTransition('classify')
else:
self.doTransition('change_classification')
def deassign(self, relation):
if not self.isRelevant(relation):
return
if self.state in ('new', 'classified', 'verified'):
old = self.context.getParentRelations()
if len(old) > 2: # the hasType relation always remains
self.doTransition('change_classification')
else:
self.doTransition('remove_classification')
def isRelevant(self, relation):
""" Return True if the relation given is relevant for changing
the quality state.
"""
return (IResource.providedBy(self.context) and
getName(relation.predicate) != 'hasType')
# event handlers
@adapter(ILoopsObject, IAssignmentEvent)
def assign(obj, event):
target = event.relation.second
if not IResource.providedBy(target):
return
pred = event.relation.predicate
if getName(pred) == 'hasType':
return
stf = component.getAdapter(target, IStateful, name='loops.classification_quality')
if stf.state == 'unclassified':
stf.doTransition('classify')
else:
stf.doTransition('change_classification')
stf = component.getAdapter(event.relation.second, IStateful,
name='loops.classification_quality')
stf.assign(event.relation)
@adapter(ILoopsObject, IDeassignmentEvent)
def deassign(obj, event):
target = event.relation.second
if not IResource.providedBy(target):
return
pred = event.relation.predicate
if getName(pred) == 'hasType':
return
stf = component.getAdapter(target, IStateful, name='loops.classification_quality')
if stf.state in ('classified', 'verified'):
old = target.getParentRelations()
if len(old) > 2: # the hasType relation always remains
stf.doTransition('change_classification')
else:
stf.doTransition('remove_classification')
stf = component.getAdapter(event.relation.second, IStateful,
name='loops.classification_quality')
stf.deassign(event.relation)