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
\nAnother 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)
-
+