diff --git a/README.txt b/README.txt index a5e35f8..f28ca4d 100755 --- a/README.txt +++ b/README.txt @@ -18,6 +18,7 @@ with lower-level aspects like type or state management. >>> from zope.app.testing.setup import placefulSetUp, placefulTearDown >>> site = placefulSetUp(True) + >>> from zope import component >>> from zope.app import zapi >>> from zope.app.tests import ztapi >>> from zope.interface import Interface @@ -122,7 +123,7 @@ type manager. >>> from cybertools.typology.interfaces import ITypeManager >>> from loops.interfaces import ILoopsObject - >>> from loops.type import LoopsTypeManager + >>> from loops.type import LoopsTypeManager, LoopsType >>> ztapi.provideAdapter(ILoopsObject, ITypeManager, LoopsTypeManager) >>> from loops.concept import ConceptTypeSourceList @@ -545,6 +546,8 @@ and of a lot of other stuff needed for the rendering machine. ... with=(IBrowserRequest,)) >>> m112.target = doc1 + + >>> component.provideAdapter(LoopsType) >>> view = NodeView(m112, TestRequest()) >>> view.renderTarget() u'' diff --git a/browser/common.py b/browser/common.py index 1275df8..a783787 100644 --- a/browser/common.py +++ b/browser/common.py @@ -42,6 +42,7 @@ from cybertools.browser.view import GenericView from cybertools.relation.interfaces import IRelationRegistry from cybertools.typology.interfaces import IType, ITypeManager from loops.interfaces import IView +from loops.type import ITypeConcept from loops import util from loops.util import _ @@ -135,18 +136,39 @@ class BaseView(GenericView): def value(self): return self.context + @Lazy + def type(self): + return IType(self.context) + + @Lazy + def typeProvider(self): + type = self.type + if type is not None: + return type.typeProvider + + @Lazy + def typeInterface(self): + provider = self.typeProvider + if provider is not None: + tc = ITypeConcept(provider) + return tc.typeInterface + + @Lazy + def typeAdapter(self): + ifc = self.typeInterface + if ifc is not None: + return ifc(self.context) + @Lazy def typeTitle(self): - type = IType(self.context) + type = self.type return type is not None and type.title or None @Lazy def typeUrl(self): - type = IType(self.context) - if type is not None: - provider = type.typeProvider - if provider is not None: - return zapi.absoluteURL(provider, self.request) + provider = self.typeProvider + if provider is not None: + return zapi.absoluteURL(provider, self.request) return None def viewIterator(self, objs): @@ -161,6 +183,12 @@ class BaseView(GenericView): for t in ITypeManager(self.context).types]) + [('loops:*', 'Any')]) + def conceptTypesForSearch(self): + general = [('loops:concept:*', 'Any Concept'),] + return util.KeywordVocabulary(general + sorted([(t.tokenForSearch, t.title) + for t in ITypeManager(self.context).types + if 'concept' in t.qualifiers])) + @Lazy def uniqueId(self): return zapi.getUtility(IRelationRegistry).getUniqueIdForObject(self.context) diff --git a/browser/configure.zcml b/browser/configure.zcml index e1fef37..4e3079a 100644 --- a/browser/configure.zcml +++ b/browser/configure.zcml @@ -324,6 +324,15 @@ permission="zope.View" /> + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + +
+ Create Information Object +
+ + +
+ +
+
+ error +
+
Assign Concept(s)
+    + + +
+ + +
+
+
+ diff --git a/browser/loops.css b/browser/loops.css index a25fce8..53fb481 100644 --- a/browser/loops.css +++ b/browser/loops.css @@ -84,3 +84,34 @@ div.image { font-weight: bold; } +/* dojo stuff */ + +.dojoComboBox { + width: 200px; +} + +.dojoDialog { + background: #eee; + border: 1px solid #999; + -moz-border-radius: 5px; + padding: 4px; +} + +.dojoDialog th { + font-size: 120%; + padding: 0 5px 8px 5px; +} + +.dojoDialog .headline { + font-weight: bold; +} + +.dojoDialog input.text { + width: 100%; + margin-right: 10px; +} + +.dojoDialog input.submit { + font-weight: bold; +} + diff --git a/browser/loops.js b/browser/loops.js index 1a5c06d..3166cb2 100644 --- a/browser/loops.js +++ b/browser/loops.js @@ -13,9 +13,9 @@ function focusOpener() { } } -function listConceptsForComboBox() { +/*function listConceptsForComboBox() { return [['Zope', 'zope'], ] -} +}*/ function submitReplacing(targetId, formId, actionUrl) { dojo.io.updateNode(targetId, { @@ -27,17 +27,21 @@ function submitReplacing(targetId, formId, actionUrl) { } function inlineEdit(id, saveUrl) { - var iconNode = dojo.byId("inlineedit_icon"); - iconNode.style.visibility = "hidden"; - var editor = dojo.widget.fromScript("Editor", - {items: ["save", "|", "formatblock", "|", - "insertunorderedlist", "insertorderedlist", "|", - "bold", "italic", "|", "createLink", "insertimage"], + //dojo.require('dojo.widget.Editor'); + var iconNode = dojo.byId('inlineedit_icon'); + iconNode.style.visibility = 'hidden'; + var editor = dojo.widget.fromScript('Editor', + {items: ['save', '|', 'formatblock', '|', + 'insertunorderedlist', 'insertorderedlist', '|', + 'bold', 'italic', '|', 'createLink', 'insertimage'], saveUrl: saveUrl, closeOnSave: true, - onSave: function(){ + htmlEditing: true, + onSave: function() { this.disableToolbar(true); - iconNode.style.visibility = "visible";} + iconNode.style.visibility = 'visible'; + //window.location.reload(); + } }, dojo.byId(id)); return false; } @@ -50,3 +54,19 @@ function setConceptTypeForComboBox(typeId, cbId) { dp.searchUrl = newUrl; } +var createObjectDlg = false; + +function createObjectDialog() { + //createObjectDlg = dojo.widget.byId('createObject'); + //createObjectDlg = false; + dojo.require('dojo.widget.Dialog'); + dojo.require('dojo.widget.ComboBox'); + if (!createObjectDlg) { + createObjectDlg = dojo.widget.fromScript('Dialog', + {bgColor: 'white', bgOpacity: 0.5, toggle: 'fade', toggleDuration: 250, + executeScripts: true, + href: 'create_object.html' + }, dojo.byId('createObject')); + } + createObjectDlg.show(); +} diff --git a/browser/node.py b/browser/node.py index ee90dc8..4bb563c 100644 --- a/browser/node.py +++ b/browser/node.py @@ -22,7 +22,7 @@ View class for Node objects. $Id$ """ -from zope import component, interface +from zope import component, interface, schema from zope.cachedescriptors.property import Lazy from zope.app import zapi from zope.app.annotation.interfaces import IAnnotations @@ -34,6 +34,7 @@ from zope.app.pagetemplate import ViewPageTemplateFile from zope.app.security.interfaces import IUnauthenticatedPrincipal from zope.dottedname.resolve import resolve from zope.event import notify +from zope.formlib.form import Form, FormFields from zope.formlib.namedtemplate import NamedTemplate from zope.proxy import removeAllProxies from zope.security import canAccess, canWrite @@ -50,6 +51,7 @@ from loops.interfaces import IConcept, IResource, IDocument, IMediaAsset, INode from loops.interfaces import IViewConfiguratorSchema from loops.resource import MediaAsset from loops import util +from loops.util import _ from loops.browser.common import BaseView from loops.browser.concept import ConceptView @@ -70,16 +72,16 @@ class NodeView(BaseView): resourceName='loops.css', media='all') cm.register('js', 'loops.js', resourceName='loops.js') cm.register('portlet_left', 'navigation', title='Navigation', - subMacro=self.template.macros['menu']) + subMacro=self.template.macros['menu']) if not IUnauthenticatedPrincipal.providedBy(self.request.principal): cm.register('portlet_right', 'clipboard', title='Clipboard', - subMacro=self.template.macros['clipboard']) + subMacro=self.template.macros['clipboard']) # this belongs to loops.organize; how to register portlets # from sub- (other) packages? # see controller / configurator: use multiple configurators; # register additional configurators (adapters) from within package. - cm.register('portlet_right', 'worklist', title='Worklist', - subMacro=self.template.macros['worklist']) + cm.register('portlet_right', 'actions', title='Actions', + subMacro=self.template.macros['actions']) @Lazy def view(self): @@ -156,6 +158,7 @@ class NodeView(BaseView): def renderTarget(self): target = self.target + #targetAdapter = target.typeAdapter return target is not None and target.render() or u'' @Lazy @@ -164,9 +167,10 @@ class NodeView(BaseView): @Lazy def bodyMacro(self): - # TODO: replace by: return self.target.macroName + # TODO: replace by something like: return self.target.macroName target = self.targetObject - if target is None or IDocument.providedBy(target): + if (target is None or IDocument.providedBy(target) + or (IResource.providedBy(target) and target.contentType.startswith('text/'))): return 'textbody' if IConcept.providedBy(target): return 'conceptbody' @@ -295,6 +299,12 @@ class NodeView(BaseView): cm.register('js-execute', jsCall, jsCall=jsCall) return 'return inlineEdit("%s", "%s/inline_save")' % (id, self.virtualTargetUrl) + def registerDojoDialog(self): + self.registerDojo() + cm = self.controller.macros + jsCall = 'dojo.require("dojo.widget.Dialog")' + cm.register('js-execute', jsCall, jsCall=jsCall) + # inner HTML views @@ -314,10 +324,43 @@ class InlineEdit(NodeView): def save(self): target = self.virtualTargetObject - target.data = self.request.form['editorContent'] + data = self.request.form['editorContent'] + if type(data) != unicode: + try: + data = data.decode('ISO-8859-15') # IE hack + except UnicodeDecodeError: + print 'loops.browser.node.InlineEdit.save():', data + return + # data = data.decode('UTF-8') + target.data = data notify(ObjectModifiedEvent(target, Attributes(IResource, 'data'))) +class CreateObject(NodeView, Form): + + template = ViewPageTemplateFile('form_macros.pt') + + @property + def macro(self): return self.template.macros['create'] + + form_fields = FormFields( + schema.TextLine(__name__='title', title=_(u'Title')), + schema.Text(__name__='body', title=_(u'Body Text')), + schema.TextLine(__name__='linkUrl', title=_(u'Link'), required=False), + ) + + title = _(u'Enter Note') + form_action = 'create_note' + + def __init__(self, context, request): + super(CreateObject, self).__init__(context, request) + self.setUpWidgets() + self.widgets['body'].height = 3 + + def __call__(self): + return innerHtml(self) + + # special (named) views for nodes class SpecialNodeView(NodeView): diff --git a/browser/node_macros.pt b/browser/node_macros.pt index 6f28dc7..df694f8 100644 --- a/browser/node_macros.pt +++ b/browser/node_macros.pt @@ -194,12 +194,20 @@ - + - - + + diff --git a/browser/resource.py b/browser/resource.py index b90320c..adc5f63 100644 --- a/browser/resource.py +++ b/browser/resource.py @@ -23,6 +23,7 @@ $Id$ """ from zope.cachedescriptors.property import Lazy +from zope import component from zope.app import zapi from zope.app.catalog.interfaces import ICatalog from zope.app.dublincore.interfaces import ICMFDublinCore @@ -39,6 +40,7 @@ from loops.interfaces import IBaseResource, IDocument, IMediaAsset from loops.browser.common import EditForm, BaseView from loops.browser.concept import ConceptRelationView, ConceptConfigureView from loops.browser.node import NodeView +from loops.interfaces import ITypeConcept renderingFactories = { 'text/plain': 'zope.source.plaintext', @@ -60,7 +62,8 @@ class ResourceEditForm(EditForm): fields = FormFields(IBaseResource) typeInterface = self.typeInterface if typeInterface is not None: - fields = FormFields(fields, typeInterface) + omit = [f for f in typeInterface if f in IBaseResource] + fields = FormFields(fields.omit(*omit), typeInterface) return fields @@ -94,6 +97,20 @@ class ResourceView(BaseView): subMacro=self.template.macros['related'], position=0, info=self) + @Lazy + def view(self): + context = self.context + tp = IType(context).typeProvider + if tp: + viewName = ITypeConcept(tp).viewName + if viewName: + return component.queryMultiAdapter((context, self.request), + name=viewName) + if context.contentType.startswith('text/'): + # TODO: This should be controlled by resourceType + return DocumentView(context, self.request) + return self + def show(self): data = self.context.data response = self.request.response @@ -173,6 +190,9 @@ class DocumentView(ResourceView): def macro(self): return ResourceView.template.macros['render'] + @Lazy + def view(self): return self + def render(self): """ Return the rendered content (data) of the context object. """ @@ -189,3 +209,16 @@ class DocumentView(ResourceView): return (self.inlineEditingActive and self.context.contentType == 'text/html' and canWrite(self.context, 'data')) + + +class NoteView(DocumentView): + + @property + def macro(self): + return ResourceView.template.macros['render_note'] + + @property + def linkUrl(self): + ad = self.typeAdapter + return ad and ad.linkUrl or '' + diff --git a/browser/resource_macros.pt b/browser/resource_macros.pt index d3ab3a2..46d7792 100644 --- a/browser/resource_macros.pt +++ b/browser/resource_macros.pt @@ -17,6 +17,16 @@ + + + + + +

Title

diff --git a/configure.zcml b/configure.zcml index 6563dba..a12a0f2 100644 --- a/configure.zcml +++ b/configure.zcml @@ -319,6 +319,14 @@ set_schema="loops.interfaces.IFile" /> + + + + + + diff --git a/interfaces.py b/interfaces.py index d78c07c..781b73d 100644 --- a/interfaces.py +++ b/interfaces.py @@ -532,7 +532,13 @@ class ITypeConcept(Interface): source="loops.TypeInterfaceSource", required=False) - # viewName = schema.TextLine() + viewName = schema.TextLine( + title=_(u'View name'), + description=_(u'Name of a special view be used for presenting ' + 'objects of this type.'), + default=u'', + required=False) + # storage = schema.Choice() @@ -554,13 +560,24 @@ class IImage(IResourceAdapter): """ -class ITextDocument(IResourceAdapter): +class ITextDocument(IResourceAdapter, IDocumentSchema): """ A resource containing some sort of plain text that may be rendered and edited without necessarily involving a special external application (like e.g. OpenOffice); typical content types are text/html, text/xml, text/restructured, etc. """ +class INote(ITextDocument): + """ Typically a short piece of text; in addition a note may contain + a URL linking it to more information. + """ + + linkUrl = schema.TextLine( + title=_(u'Link URL'), + description=_(u'An (optional) link associated with this note'), + default=u'', + required=False) + # view configurator stuff diff --git a/organize/interfaces.py b/organize/interfaces.py index ab2da6c..02e9994 100644 --- a/organize/interfaces.py +++ b/organize/interfaces.py @@ -32,8 +32,7 @@ from zope.security.proxy import removeSecurityProxy from cybertools.organize.interfaces import IPerson as IBasePerson from loops.organize.util import getPrincipalFolder, authPluginId - -_ = MessageFactory('zope') +from loops.util import _ ANNOTATION_KEY = 'loops.organize.person' @@ -49,7 +48,7 @@ def raiseValidationError(info): class UserId(schema.TextLine): - + def _validate(self, userId): from loops.organize.party import getPersonForUser if not userId: diff --git a/resource.py b/resource.py index b431079..82093d7 100644 --- a/resource.py +++ b/resource.py @@ -45,7 +45,7 @@ from cybertools.relation.interfaces import IRelatable from cybertools.typology.interfaces import ITypeManager from interfaces import IBaseResource, IResource -from interfaces import IFile +from interfaces import IFile, INote from interfaces import IDocument, IDocumentSchema, IDocumentView from interfaces import IMediaAsset, IMediaAssetView from interfaces import IResourceManager, IResourceManagerContained @@ -196,6 +196,16 @@ class FileAdapter(ResourceAdapterBase): implements(IFile) +class NoteAdapter(ResourceAdapterBase): + """ A type adapter for providing note functionality for resources. + """ + + implements(INote) + _schemas = list(INote) + list(IBaseResource) + + #contentType = u'text/restructured' + + class DocumentWriteFileAdapter(object): implements(IWriteFile) diff --git a/search/browser.py b/search/browser.py index 2f01439..c6d7376 100644 --- a/search/browser.py +++ b/search/browser.py @@ -63,12 +63,6 @@ class Search(BaseView): self.maxRowNum = n return n - def conceptTypesForSearch(self): - general = [('loops:concept:*', 'Any Concept'),] - return util.KeywordVocabulary(general + sorted([(t.tokenForSearch, t.title) - for t in ITypeManager(self.context).types - if 'concept' in t.qualifiers])) - def initDojo(self): self.registerDojo() cm = self.controller.macros diff --git a/type.py b/type.py index f2c4bfd..afa547e 100644 --- a/type.py +++ b/type.py @@ -33,7 +33,7 @@ from cybertools.typology.type import BaseType, TypeManager from cybertools.typology.interfaces import ITypeManager from loops.interfaces import ILoopsObject, IConcept, IResource from loops.interfaces import ITypeConcept -from loops.interfaces import IResourceAdapter, IFile, IImage, ITextDocument +from loops.interfaces import IResourceAdapter, IFile, IImage, ITextDocument, INote from loops.concept import Concept from loops.resource import Resource, Document, MediaAsset from loops.common import AdapterBase @@ -43,7 +43,7 @@ class LoopsType(BaseType): adapts(ILoopsObject) - factoryMapping = dict(concept=Concept, resource=Resource) + factoryMapping = dict(concept=Concept, resource=Resource, document=Document) containerMapping = dict(concept='concepts', resource='resources') @Lazy @@ -83,6 +83,11 @@ class LoopsType(BaseType): @Lazy def factory(self): + ti = self.typeInterface + #if ti is not None: + # fn = getattr(ti, 'factoryName', None) + # if fn: + # return self.factoryMapping.get(fn, Concept) return self.factoryMapping.get(self.qualifiers[0], Concept) @Lazy @@ -241,7 +246,7 @@ class TypeInterfaceSourceList(object): implements(schema.interfaces.IIterableSource) - typeInterfaces = (ITypeConcept, IFile, ITextDocument) + typeInterfaces = (ITypeConcept, IFile, ITextDocument, INote) def __init__(self, context): self.context = context