diff --git a/README.txt b/README.txt index 25f1ed7..3041d14 100755 --- a/README.txt +++ b/README.txt @@ -549,10 +549,10 @@ view for rendering.) >>> component.provideAdapter(LoopsType) >>> view = NodeView(m112, TestRequest()) >>> view.renderTarget() - u'' + u'
'
   >>> doc1.data = u'Test data\n\nAnother paragraph'
   >>> view.renderTarget()
-  u'Test data\n\nAnother paragraph'
+  u'
Test data\n\nAnother paragraph
' >>> doc1.contentType = 'text/restructured' >>> view.renderTarget() u'

Test data

\n

Another paragraph

\n' @@ -667,6 +667,18 @@ created object: >>> sorted(t.__name__ for t in note.getConcepts()) [u'note', u'topic'] +When creating an object its name is automatically generated using the title +of the object. Let's make sure that the name chooser also handles special +and possibly critcal cases: + + >>> nc = ResourceNameChooser(resources) + >>> nc.chooseName(u'abc: (cde)', None) + u'abc_cde' + >>> nc.chooseName(u'\xdcml\xe4ut', None) + u'uemlaeut' + >>> nc.chooseName(u'A very very loooooong title', None) + u'a_title' + Editing an object ----------------- diff --git a/browser/form.py b/browser/form.py index 285a91d..c3fb02a 100644 --- a/browser/form.py +++ b/browser/form.py @@ -235,12 +235,36 @@ class CreateObject(EditObject): return True +specialCharacters = { + '\xc4': 'Ae', '\xe4': 'ae', '\xd6': 'Oe', '\xf6': 'oe', + '\xdc': 'Ue', '\xfc': 'ue', '\xdf': 'ss'} + class ResourceNameChooser(NameChooser): adapts(IResourceManager) def chooseName(self, title, obj): - name = title.replace(' ', '_').lower() - name = super(ResourceNameChooser, self).chooseName(name, obj) - return name + result = [] + if len(title) > 15: + words = title.split() + if len(words) > 1: + title = '_'.join((words[0], words[-1])) + for c in title: + try: + c = c.encode('ISO8859-15') + except UnicodeEncodeError: + continue + if c in specialCharacters: + result.append(specialCharacters[c].lower()) + continue + if ord(c) > 127: + c = chr(ord(c) & 127) + if c in ('_., '): + result.append('_') + elif not c.isalpha() and not c.isdigit(): + continue + else: + result.append(c.lower()) + name = unicode(''.join(result)) + return super(ResourceNameChooser, self).chooseName(name, obj) diff --git a/browser/node.py b/browser/node.py index 3da4acb..5bb4fcb 100644 --- a/browser/node.py +++ b/browser/node.py @@ -288,6 +288,8 @@ class NodeView(BaseView): targetId = self.targetId if targetId is not None: return '%s/.target%s' % (self.url, targetId) + else: + return self.url @Lazy def realTargetUrl(self): diff --git a/browser/resource.py b/browser/resource.py index 41907b0..9548777 100644 --- a/browser/resource.py +++ b/browser/resource.py @@ -34,6 +34,7 @@ from zope.formlib.interfaces import DISPLAY_UNWRITEABLE from zope.proxy import removeAllProxies from zope.security import canAccess, canWrite from zope.security.proxy import removeSecurityProxy +from zope.documenttemplate.dt_util import html_quote from cybertools.typology.interfaces import IType from loops.interfaces import IBaseResource, IDocument, IMediaAsset @@ -112,10 +113,14 @@ class ResourceView(BaseView): return self def show(self): - data = self.context.data + context = self.context + data = context.data response = self.request.response - response.setHeader('Content-Type', self.context.contentType) + response.setHeader('Content-Type', context.contentType) response.setHeader('Content-Length', len(data)) + if not context.contentType.startswith('image/'): + response.setHeader('Content-Disposition', + 'attachment; filename=%s' % zapi.getName(context)) return data def concepts(self): @@ -202,9 +207,12 @@ class DocumentView(ResourceView): """ Return the rendered content (data) of the context object. """ text = self.context.data - typeKey = renderingFactories.get(self.context.contentType, None) + contentType = self.context.contentType + typeKey = renderingFactories.get(contentType, None) if typeKey is None: - return text + if contentType == 'text/html': + return text + return u'
%s
' % html_quote(text) source = zapi.createObject(typeKey, text) view = zapi.getMultiAdapter((removeAllProxies(source), self.request)) return view.render() diff --git a/configure.zcml b/configure.zcml index a12a0f2..34517f0 100644 --- a/configure.zcml +++ b/configure.zcml @@ -340,6 +340,11 @@ + + >> img1_type.title u'File' +Using the type machinery we can also specify options that may be used +for controlling e.g. storage for external files. + + >>> extfile = concepts['extfile'] = Concept(u'External File') + >>> ef1 = resources['ef1'] = Resource(u'Extfile #1') + >>> ef1.resourceType = extfile + >>> ef1_type = IType(ef1) + >>> IType(ef1).options + [] + >>> extfile_ad = TypeConcept(extfile) + >>> extfile_ad.options = ['dummy', 'storage:varsubdir', + ... 'storage_parameters:extfiles'] + >>> IType(ef1).options + ['dummy', 'storage:varsubdir', 'storage_parameters:extfiles'] + >>> IType(ef1).optionsDict + {'default': ['dummy'], 'storage_parameters': 'extfiles', 'storage': 'varsubdir'} + Can we find out somehow which types are available? This is the time to look for a type manager. This could be a utility; but in the loops package it is again an adapter, now for the loops root object. Nevertheless one can diff --git a/interfaces.py b/interfaces.py index 4adc68f..6b20e79 100644 --- a/interfaces.py +++ b/interfaces.py @@ -539,6 +539,13 @@ class ITypeConcept(Interface): default=u'', required=False) + options = schema.List( + title=_(u'Options'), + description=_(u'Additional settings.'), + value_type=schema.TextLine(), + default=[], + required=False) + # storage = schema.Choice() @@ -555,6 +562,12 @@ class IFile(IResourceAdapter, IResourceSchema): """ +class IExternalFile(IFile): + """ A file whose content (data attribute) is not stored in the ZODB + but somewhere else, typically in the file system. + """ + + class IImage(IResourceAdapter): """ A media asset that may be embedded in a (web) page as an image. """ @@ -590,26 +603,3 @@ class IViewConfiguratorSchema(Interface): required=False) -# the next two interfaces are obsolete, they will be replaced by IResourceStorage: - -class IFileSystemResource(Interface): - - fsPath = schema.BytesLine( - title=_(u'Filesystem Path'), - description=_(u'Optional path to a file in the filesystem ' - 'to be used for storing the resource'), - default='', - missing_value='', - required=False) - - -class IControlledResource(Interface): - - readOnly = schema.Bool( - title=_(u'Read only'), - description=_(u'Check this if resource may not be modified ' - 'after being first filled with non-empty content'), - default=False, - required=False) - - diff --git a/resource.py b/resource.py index c67e3a3..bb75018 100644 --- a/resource.py +++ b/resource.py @@ -22,6 +22,7 @@ Definition of the Concept class. $Id$ """ +from zope import component from zope.app import zapi from zope.app.container.btree import BTreeContainer from zope.app.container.contained import Contained @@ -42,6 +43,7 @@ from zope.event import notify from cybertools.relation.registry import getRelations from cybertools.relation.interfaces import IRelatable +from cybertools.storage.interfaces import IExternalStorage from cybertools.text.interfaces import ITextTransform from cybertools.typology.interfaces import IType, ITypeManager @@ -208,6 +210,38 @@ class FileAdapter(ResourceAdapterBase): data = property(getData, setData) +class ExternalFileAdapter(FileAdapter): + + @Lazy + def externalAddress(self): + # or is this an editable attribute? + # or some sort of subpath set during import? + # anyway: an attribute of the context object. + return self.context.__name__ + + @Lazy + def options(self): + return IType(self.context).optionsDict + + @Lazy + def storageName(self): + return self.options.get('storage') + + @Lazy + def storageParams(self): + return self.options.get('storage_parameters') + + def setData(self, data): + storage = component.getUtility(IExternalStorage, name=self.storageName) + storage.setData(self.externalAddress, data, params=self.storageParams) + + def getData(self): + storage = component.getUtility(IExternalStorage) + return storage.getData(self.externalAddress, params=self.storageParams) + + data = property(getData, setData) + + class DocumentAdapter(ResourceAdapterBase): """ Common base class for all resource types with a text-like data attribute. @@ -280,8 +314,8 @@ class IndexAttributes(object): ti = IType(context).typeInterface if ti is not None: adapted = ti(context) - transform = component.queryAdapter( - adapted, ITextTransform, name=context.contentType) + transform = component.queryAdapter(adapted, ITextTransform, + name=context.contentType) if transform is not None: rfa = component.queryAdapter(IReadFile, adapted) if rfa is None: diff --git a/type.py b/type.py index 3f748af..d49e52d 100644 --- a/type.py +++ b/type.py @@ -99,6 +99,21 @@ class LoopsType(BaseType): # TODO: unify this type attribute naming... return self.context.resourceType + @Lazy + def options(self): + return ITypeConcept(self.typeProvider).options or [] + + @Lazy + def optionsDict(self): + result = {'default': []} + for opt in self.options: + if ':' in opt: + key, value = opt.split(':', 1) + result[key] = value + else: + result['default'].append(opt) + return result + class LoopsTypeInfo(LoopsType): """ The type info class used by the type manager for listing types. @@ -237,6 +252,14 @@ class TypeConcept(AdapterBase): self.context._typeInterface = ifc typeInterface = property(getTypeInterface, setTypeInterface) + def getOptions(self): + return getattr(self.context, '_options', []) + #return super(TypeConcept, self).options or [] + def setOptions(self, value): + self.context._options = value + #super(TypeConcept, self).options = value + options = property(getOptions, setOptions) + class TypeInterfaceSourceList(object): diff --git a/view.py b/view.py index db6ddc4..a99d2cd 100644 --- a/view.py +++ b/view.py @@ -196,4 +196,4 @@ class NodeTraverser(ItemTraverser): request.annotations['loops.view'] = viewAnnotations return self.context return super(NodeTraverser, self).publishTraverse(request, name) - +