Work in progress: assign target to node using a search form

git-svn-id: svn://svn.cy55.de/Zope3/src/loops/trunk@1097 fd906abe-77d9-0310-91a1-e0d9ade77398
This commit is contained in:
helmutm 2006-02-25 18:11:24 +00:00
parent e86c3e331a
commit a0948469d4
10 changed files with 346 additions and 149 deletions

View file

@ -390,113 +390,44 @@ Node Views
... print item.url, view.selected(item) ... print item.url, view.selected(item)
http://127.0.0.1/loops/views/m1/m11 True http://127.0.0.1/loops/views/m1/m11 True
Node Configuration A Node and its Target
------------------ ---------------------
When configuring a node you may specify what you want to do with respect When configuring a node you may specify what you want to do with respect
to the node's target: associate an existing one or create a new one. to the node's target: associate an existing one or create a new one.
These options are provided via the INodeConfigSchema that is provided
by a NodeConfigAdapter; in addition the attributes of the node (like the
title) may be changed via the NodeConfigAdapter.
>>> from loops.interfaces import INodeConfigSchema >>> from loops.browser.node import ConfigureView
>>> from loops.view import NodeConfigAdapter >>> form = {'action': 'create', 'create.title': 'New Resource',
>>> ztapi.provideAdapter(INode, INodeConfigSchema, NodeConfigAdapter) ... 'create.type': 'loops.resource.MediaAsset',}
>>> nodeConfig = INodeConfigSchema(m111) >>> view = ConfigureView(m111, TestRequest(form = form))
>>> sorted((t.token, t.title) for t in view.targetTypes())
>>> nodeConfig.title = u'New title for m111' [('loops.concept.Concept', u'Concept'),
>>> nodeConfig.title ('loops.resource.Document', u'Document'),
u'New title for m111' ('loops.resource.MediaAsset', u'Media Asset')]
>>> m111.title >>> view.update()
u'New title for m111'
>>> nodeConfig.target = doc1
>>> m111.target is doc1
True True
>>> m111 in doc1.getClients()
True
The targetUri and targetType fields are only relevant when creating
a new target object:
>>> nodeConfig.targetUri
''
>>> nodeConfig.targetType
'loops.resource.Document'
The node configuration form provides a target assignment field using
a vocabulary (source) for selecting the target. (In a future version this form
will be extended by a widget that lets you search for potential target
objects.) The source is basically a source list:
>>> from loops.target import TargetSourceList
>>> source = TargetSourceList(m111)
>>> len(source)
1
>>> sorted([zapi.getName(s) for s in source])
[u'doc1']
The form then uses a sort of browser view providing the ITerms interface
based on this source list:
>>> terms = LoopsTerms(source, TestRequest())
>>> term = terms.getTerm(doc1)
>>> term.token, term.title, term.value
('.loops/resources/doc1', u'Zope Info', <loops.resource.Document...>)
>>> term = terms.getTerm(cc1)
>>> term.token, term.title, term.value
('.loops/concepts/cc1', u'cc1', <loops.concept.Concept...>)
>>> terms.getValue('.loops/concepts/cc1') is cc1
True
There is a special edit view class that can be used to configure a node
in a way that allows the creation of a target object on the fly.
(We here use the base class providing the method for this action; the real
application uses a subclass that does all the other stuff for form handling.)
When creating a new target object you may specify a uri that determines
the location of the new target object and its name.
>>> from loops.browser.node import ConfigureBaseView
>>> view = ConfigureBaseView(INodeConfigSchema(m111), TestRequest())
>>> view.checkCreateTarget()
>>> sorted(resources.keys()) >>> sorted(resources.keys())
[u'doc1'] [u'doc1', u'm1.m11.m111']
>>> form = {'field.createTarget': True,
... 'field.targetUri': '.loops/resources/ma07',
... 'field.targetType': 'loops.resource.MediaAsset'}
>>> view = ConfigureBaseView(m111, TestRequest(form=form))
>>> m111.target = view.checkCreateTarget()
>>> sorted(resources.keys())
[u'doc1', u'ma07']
>>> isinstance(resources['ma07'], MediaAsset)
True
>>> form = {'field.createTarget': True, >>> view.target.title, view.target.token
... 'field.targetType': 'loops.resource.Document'} ('New Resource', '.loops/resources/m1.m11.m111')
>>> view = ConfigureBaseView(m111, TestRequest(form=form))
>>> m111.target = view.checkCreateTarget()
>>> sorted(resources.keys())
[u'doc1', u'm1.m11.m111', u'ma07']
>>> isinstance(resources['m1.m11.m111'], Document)
True
A node object provides the targetSchema of its target: A node object provides the targetSchema of its target:
>>> from loops.interfaces import IDocumentView >>> from loops.interfaces import IDocumentView
>>> from loops.interfaces import IMediaAssetView >>> from loops.interfaces import IMediaAssetView
>>> IDocumentView.providedBy(m111) >>> IDocumentView.providedBy(m111)
True
>>> IMediaAssetView.providedBy(m111)
False False
>>> IMediaAssetView.providedBy(m111)
True
>>> m111.target = None >>> m111.target = None
>>> IDocumentView.providedBy(m111) >>> IDocumentView.providedBy(m111)
False False
>>> m111.target = resources['ma07'] >>> m111.target = resources['doc1']
>>> IDocumentView.providedBy(m111) >>> IDocumentView.providedBy(m111)
False
>>> IMediaAssetView.providedBy(m111)
True True
>>> IMediaAssetView.providedBy(m111)
False
A node's target is rendered using the NodeView's renderTargetBody() A node's target is rendered using the NodeView's renderTargetBody()
method. This makes use of a browser view registered for the target interface, method. This makes use of a browser view registered for the target interface,
@ -535,7 +466,7 @@ edit form provided by the node:
>>> proxy = zapi.getAdapter(m111, IDocumentView) >>> proxy = zapi.getAdapter(m111, IDocumentView)
>>> proxy.title = u'Set via proxy' >>> proxy.title = u'Set via proxy'
>>> resources['ma07'].title >>> resources['doc1'].title
u'Set via proxy' u'Set via proxy'
If the target object is removed from its container all references If the target object is removed from its container all references
@ -549,7 +480,7 @@ cybertools.relation package.)
>>> ztapi.subscribe([Interface, IObjectRemovedEvent], None, >>> ztapi.subscribe([Interface, IObjectRemovedEvent], None,
... invalidateRelations) ... invalidateRelations)
>>> del resources['ma07'] >>> del resources['doc1']
>>> m111.target >>> m111.target
>>> IMediaAssetView.providedBy(m111) >>> IMediaAssetView.providedBy(m111)
False False

View file

@ -29,6 +29,9 @@ from zope.cachedescriptors.property import Lazy
from zope.interface import implements from zope.interface import implements
from zope.security.proxy import removeSecurityProxy from zope.security.proxy import removeSecurityProxy
from loops import util
from loops.target import getTargetTypes
class BaseView(object): class BaseView(object):
def __init__(self, context, request): def __init__(self, context, request):
@ -66,12 +69,20 @@ class BaseView(object):
@Lazy @Lazy
def typeTitle(self): def typeTitle(self):
return self.context.conceptType.title voc = util.KeywordVocabulary(getTargetTypes())
token = '.'.join((self.context.__module__,
self.context.__class__.__name__))
term = voc.getTermByToken(token)
return term.title
@Lazy @Lazy
def typeUrl(self): def typeUrl(self):
return zapi.absoluteURL(self.context.conceptType, self.request) return None
def viewIterator(self, objs):
request = self.request
for o in objs:
yield BaseView(o, request)
class LoopsTerms(object): class LoopsTerms(object):

View file

@ -134,6 +134,14 @@ class ConceptView(BaseView):
result = [r for r in result if r.conceptType == type] result = [r for r in result if r.conceptType == type]
return self.viewIterator(result) return self.viewIterator(result)
@Lazy
def typeTitle(self):
return self.context.conceptType.title
@Lazy
def typeUrl(self):
return zapi.absoluteURL(self.context.conceptType, self.request)
def viewIterator(self, objs): def viewIterator(self, objs):
request = self.request request = self.request
for o in objs: for o in objs:

View file

@ -35,6 +35,13 @@
permission="zope.View" permission="zope.View"
/> />
<page
for="*"
name="target_macros"
template="target_macros.pt"
permission="zope.View"
/>
<!-- loops top-level container --> <!-- loops top-level container -->
<addform <addform
@ -331,10 +338,10 @@
<editform <editform
label="Configure Node" label="Configure Node"
name="configure.html" name="configure.html"
schema="loops.interfaces.INodeConfigSchema" schema="loops.interfaces.INode"
fields="title description nodeType target createTarget targetUri targetType" fields="title description nodeType target"
for="loops.interfaces.INode" for="loops.interfaces.INode"
template="edit.pt" template="node_target.pt"
class="loops.browser.node.ConfigureView" class="loops.browser.node.ConfigureView"
permission="zope.ManageContent"> permission="zope.ManageContent">

View file

@ -64,7 +64,8 @@
<div class="separator"></div> <div class="separator"></div>
</div> </div>
<div class="row"> <div metal:define-macro="submit_button"
class="row">
<div class="controls"> <div class="controls">
<input type="submit" name="UPDATE_SUBMIT" value="Change" <input type="submit" name="UPDATE_SUBMIT" value="Change"
i18n:attributes="value submit-button;" /> i18n:attributes="value submit-button;" />

View file

@ -24,16 +24,24 @@ $Id$
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
from zope.app import zapi from zope.app import zapi
from zope.app.catalog.interfaces import ICatalog
from zope.app.container.browser.contents import JustContents from zope.app.container.browser.contents import JustContents
from zope.app.dublincore.interfaces import ICMFDublinCore from zope.app.dublincore.interfaces import ICMFDublinCore
from zope.app.event.objectevent import ObjectCreatedEvent
#import zope.configuration.name #import zope.configuration.name
from zope.dottedname.resolve import resolve from zope.dottedname.resolve import resolve
from zope.event import notify
from zope.proxy import removeAllProxies from zope.proxy import removeAllProxies
from zope.security import canAccess, canWrite from zope.security import canAccess, canWrite
from zope.security.proxy import removeSecurityProxy from zope.security.proxy import removeSecurityProxy
from loops.interfaces import IConcept, IDocument, IMediaAsset from loops.interfaces import IConcept, IDocument, IMediaAsset
from loops.resource import MediaAsset from loops.resource import MediaAsset
from loops.target import getTargetTypes
from loops import util
from loops.browser.common import BaseView
from loops.browser.concept import ConceptView
class NodeView(object): class NodeView(object):
@ -124,12 +132,13 @@ class NodeView(object):
return item.context == self.context return item.context == self.context
class ConfigureBaseView(object): class ConfigureView(BaseView):
""" Helper view class for editing/configuring a node, providing the """ An editing view for configuring a node, optionally creating
stuff needed for creating a target object. a target object.
""" """
def __init__(self, context, request): def __init__(self, context, request):
#self.context = context
self.context = removeSecurityProxy(context) self.context = removeSecurityProxy(context)
self.request = request self.request = request
@ -137,53 +146,84 @@ class ConfigureBaseView(object):
def loopsRoot(self): def loopsRoot(self):
return self.context.getLoopsRoot() return self.context.getLoopsRoot()
def checkCreateTarget(self): @property
form = self.request.form def target(self):
if form.get('field.createTarget', False): target = self.context.target
root = self.loopsRoot if target is not None:
type = self.request.form.get('field.targetType', if IConcept.providedBy(target):
'loops.resource.MediaAsset') return ConceptView(target, self.request)
factory = resolve(type) return BaseView(target, self.request)
uri = self.request.form.get('field.targetUri', None) return None
if uri:
path = uri.split('/')
# TODO: check for .loops prefix
containerName = path[-2]
name = path[-1]
container = root[containerName]
else:
container = ('.resource.' in type and root.getResourceManager()
or root.getConceptManager())
viewManagerPath = zapi.getPath(root.getViewManager())
name = zapi.getPath(self.context)[len(viewManagerPath)+1:]
name = name.replace('/', '.')
# check for duplicates:
num = 1
basename = name
while name in container:
name = '%s-%d' % (basename, num)
num += 1
# create target:
container[name] = factory()
target = container[name]
# set possibly new target uri in request for further processing:
targetUri = self.loopsRoot.getLoopsUri(target)
form['field.target'] = targetUri
return target
class ConfigureView(object):
""" An editing view for configuring a node, optionally creating
a target object.
"""
def __init__(self, context, request):
super(ConfigureView, self).__init__(context, request)
self.delegate = ConfigureBaseView(context, request)
def update(self): def update(self):
if self.update_status is not None: request = self.request
return self.update_status action = request.get('action')
self.delegate.checkCreateTarget() if action is None or action == 'search':
return super(ConfigureView, self).update() return True
if action == 'create':
return self.createAndAssign()
if action == 'assign':
token = request.get('token')
if token:
target = self.loopsRoot.loopsTraverse(token)
else:
target = None
self.context.target = target
# TODO: raise error
return True
def createAndAssign(self):
form = self.request.form
root = self.loopsRoot
type = self.request.form.get('create.type',
'loops.resource.MediaAsset')
factory = resolve(type)
if '.resource.' in type:
container = root.getResourceManager()
else:
container = root.getConceptManager()
name = form.get('create.name', '')
if not name:
viewManagerPath = zapi.getPath(root.getViewManager())
name = zapi.getPath(self.context)[len(viewManagerPath)+1:]
name = name.replace('/', '.')
# check for duplicates:
num = 1
basename = name
while name in container:
name = '%s-%d' % (basename, num)
num += 1
container[name] = removeSecurityProxy(factory())
target = container[name]
target.title = form.get('create.title', u'')
notify(ObjectCreatedEvent(target))
self.context.target = target
return True
def targetTypes(self):
return util.KeywordVocabulary(getTargetTypes())
@Lazy
def search(self):
request = self.request
if request.get('action') != 'search':
return []
searchTerm = request.get('searchTerm', None)
if searchTerm:
cat = zapi.getUtility(ICatalog)
result = cat.searchResults(loops_searchableText=searchTerm)
else:
result = (list(self.loopsRoot.getConceptManager().values())
+ list(self.loopsRoot.getResourceManager().values()))
return list(self.viewIterator(result))
def viewIterator(self, objs):
request = self.request
for o in objs:
if o == self.context.target:
continue
if IConcept.providedBy(o):
yield ConceptView(o, request)
else:
yield BaseView(o, request)

43
browser/node_target.pt Normal file
View file

@ -0,0 +1,43 @@
<tal:tag condition="view/update" />
<html metal:use-macro="context/@@standard_macros/view"
i18n:domain="zope">
<head></head>
<body>
<div metal:fill-slot="body"
tal:define="target view/target">
<h1 tal:content="context/title">Node Title</h1><br />
<div>
<span i18n:translate=""
tal:condition="not:target">No target assigned</span>
<tal:target condition="target">
<span i18n:translate="">Currently assigned target</span>:
<a href="#"
tal:attributes="href string:${target/url}/@@SelectedManagementView.html"
tal:content="target/title">Document xy</a>
</tal:target>
</div>
<div style="padding-right:20px">
<metal:create use-macro="views/target_macros/create" />
</div>
<div tal:define="items view/search;
action string:assign;
target nocall:context/target;
summary string:Assignment candidates;
legend string:Search;
buttonText string:Assign Target;"
style="padding-right:20px">
<metal:assign use-macro="views/target_macros/listing">
<metal:search fill-slot="topActions">
<metal:block use-macro="views/target_macros/search" />
</metal:search>
</metal:assign>
</div>
</div>
</body>
</html>

146
browser/target_macros.pt Normal file
View file

@ -0,0 +1,146 @@
<html i18n:domain="loops">
<metal:assignments define-macro="listing">
<fieldset>
<legend tal:content="legend"
i18n:translate="">Listing</legend>
<metal:top define-slot="topActions" />
<form metal:define-macro="listing_form"
method="post" name="listing" action="."
tal:attributes="action request/URL"
tal:condition="items">
<input type="hidden" name="action" value="assign"
tal:attributes="value action" />
<table class="listing" summary="Currently assigned"
i18n:attributes="summary"
tal:attributes="summary summary">
<thead>
<tr>
<th>&nbsp;</th>
<th i18n:translate="label_title">Title</th>
<th i18n:translate="label_type">Type</th>
</tr>
</thead>
<tbody tal:define="target view/target;
targetToken python: target and target.token or None">
<tal:none define="item nothing; title string:None; token nothing;
type nothing">
<metal:none use-macro="views/target_macros/list_item_tr" />
</tal:none>
<tal:current define="item target"
condition="item">
<metal:current use-macro="views/target_macros/list_item" />
</tal:current>
<tal:items repeat="item items">
<metal:item define-macro="list_item"
tal:define="title item/title;
token item/token;
type item/typeTitle">
<tr metal:define-macro="list_item_tr">
<td class="field">
<input class="formSelection"
type="radio" name="token" id="#" value=""
tal:attributes="value token;
checked python: targetToken == token" />
</td>
<td>
<a href="#"
tal:omit-tag="not:item"
tal:content="title"
tal:attributes="href
string:${item/url|nothing}/@@SelectedManagementView.html">
Title
</a>
</td>
<td>
<a tal:condition="type"
href="#"
tal:attributes="href
string:${item/typeUrl}/@@SelectedManagementView.html"
tal:omit-tag="not:item/typeUrl">
<span tal:replace="type">Type</span>
</a>
</td>
</tr>
</metal:item>
</tal:items>
</tbody>
</table>
<div class="formControls">
<input class="context" type="submit" name="form.button.submit"
value="Change assignment"
i18n:attributes="value"
tal:attributes="value buttonText" />
<metal:buttons define-slot="specialButtons" />
</div>
</form>
</fieldset>
</metal:assignments>
<metal:create define-macro="create">
<fieldset>
<legend i18n:translate="">Create Target</legend>
<form method="post" name="listing" action="."
tal:attributes="action request/URL">
<input type="hidden" name="action" value="create" />
<div class="row">
<span i18n:translate="">Name</span>
<input name="create.name" size="15"
tal:attributes="value nothing" />&nbsp;
<span i18n:translate="">Title</span>
<input name="create.title" size="30"
tal:attributes="value nothing" />&nbsp;
<span i18n:translate="">Type</span>
<select name="create.type">
<tal:types repeat="type view/targetTypes">
<option value=".loops/concepts/topic"
i18n:translate=""
tal:attributes="value type/token"
tal:content="type/title">Topic</option>
</tal:types>
</select>
</div><br />
<div class="formControls">
<input class="context" type="submit" name="form.button.submit"
value="Create Target"
i18n:attributes="value" />
</div>
</form>
</fieldset>
</metal:create>
<metal:search define-macro="search">
<form method="post" name="listing" action="."
tal:attributes="action request/URL">
<input type="hidden" name="action" value="search" />
<div class="row"
tal:define="searchTerm request/searchTerm | nothing;
searchType request/searchType | nothing;">
<span i18n:translate="">Search Term</span>
<input name="searchTerm"
tal:attributes="value searchTerm" />
<span i18n:translate="">Type</span>
<select name="searchType">
<option value="*">Any</option>
<tal:types repeat="type view/targetTypes">
<option value=".loops/concepts/topic"
i18n:translate=""
tal:attributes="value type/token;
selected python: type.token == searchType"
tal:content="type/title">Topic</option>
</tal:types>
</select>
</div>
<div class="formControls">
<input class="context" type="submit" name="form.button.submit"
value="Search"
i18n:attributes="value" />
</div>
</form>
</metal:search>
</html>

View file

@ -49,7 +49,8 @@
<require <require
permission="zope.View" permission="zope.View"
attributes="getLoopsUri loopsTraverse getConceptManager" /> attributes="getLoopsUri loopsTraverse getConceptManager
getResourceManager getViewManager" />
</content> </content>

View file

@ -26,6 +26,7 @@ $Id$
from zope.app import zapi from zope.app import zapi
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
from zope.component import adapts from zope.component import adapts
from zope.i18nmessageid import MessageFactory
from zope.interface import implements from zope.interface import implements
from zope import schema from zope import schema
from zope.security.proxy import removeSecurityProxy from zope.security.proxy import removeSecurityProxy
@ -36,6 +37,8 @@ from loops.interfaces import IDocumentView, IMediaAssetView
from loops.interfaces import IView from loops.interfaces import IView
from loops.interfaces import IConcept, IConceptView from loops.interfaces import IConcept, IConceptView
_ = MessageFactory('loops')
# proxies for accessing target objects from views/nodes # proxies for accessing target objects from views/nodes
@ -141,3 +144,9 @@ class QueryableTargetSource(object):
def __contains__(self, value): def __contains__(self, value):
return value in self.resources.values() or value in self.concepts.values() return value in self.resources.values() or value in self.concepts.values()
def getTargetTypes():
return (('loops.concept.Concept', _(u'Concept')),
('loops.resource.Document', _(u'Document')),
('loops.resource.MediaAsset', _(u'Media Asset')),
)