diff --git a/browser/action.py b/browser/action.py index 710e124..56d1204 100644 --- a/browser/action.py +++ b/browser/action.py @@ -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', +) diff --git a/browser/common.py b/browser/common.py index e7826b3..f10d066 100644 --- a/browser/common.py +++ b/browser/common.py @@ -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"); ' diff --git a/browser/concept.py b/browser/concept.py index 2357c12..3127bfc 100644 --- a/browser/concept.py +++ b/browser/concept.py @@ -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): diff --git a/browser/concept_macros.pt b/browser/concept_macros.pt index b1fdc34..4c4ec11 100644 --- a/browser/concept_macros.pt +++ b/browser/concept_macros.pt @@ -114,6 +114,7 @@ Size Modification Date Author(s) + St 2007-03-30 John + +
+ diff --git a/browser/configure.zcml b/browser/configure.zcml index 7019d2f..d468568 100644 --- a/browser/configure.zcml +++ b/browser/configure.zcml @@ -565,7 +565,14 @@ - + + + + @@ -216,6 +217,34 @@ + + States + + + loops.simple_publishing + draft + + + + + + + + +
- -
- - Edit - - - Edit - - -
-
+ +
+ + + +
+
+ + @@ -230,6 +217,11 @@ + +
Object Information
+
+ +
@@ -262,4 +254,3 @@ tal:attributes="src context/++resource++edit.gif" border="0" /> - diff --git a/browser/resource.py b/browser/resource.py index a08347f..74ed77b 100644 --- a/browser/resource.py +++ b/browser/resource.py @@ -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): diff --git a/browser/resource_macros.pt b/browser/resource_macros.pt index f9fc6b8..e31540a 100644 --- a/browser/resource_macros.pt +++ b/browser/resource_macros.pt @@ -2,10 +2,7 @@
- -
- +

Title

@@ -40,6 +37,7 @@

+

Title


@@ -50,6 +48,7 @@
+

Title

Description

diff --git a/organize/stateful/README.txt b/organize/stateful/README.txt index a84f0ba..7c08fb8 100644 --- a/organize/stateful/README.txt +++ b/organize/stateful/README.txt @@ -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 ============= diff --git a/organize/stateful/base.py b/organize/stateful/base.py index a81284c..9a8f77a 100644 --- a/organize/stateful/base.py +++ b/organize/stateful/base.py @@ -41,6 +41,9 @@ class StatefulLoopsObject(Stateful, StatefulAdapter): adapts(ILoopsObject) + def getAvailableTransitionsForUser(self): + return self.getAvailableTransitions() + class SimplePublishable(StatefulLoopsObject): diff --git a/organize/stateful/browser.py b/organize/stateful/browser.py new file mode 100644 index 0000000..a4b1093 --- /dev/null +++ b/organize/stateful/browser.py @@ -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', + ) diff --git a/organize/stateful/quality.py b/organize/stateful/quality.py index 7513eef..f2c3dc8 100644 --- a/organize/stateful/quality.py +++ b/organize/stateful/quality.py @@ -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)