[Products.Silva][Sylvain Viollon] Add Ghost Asset. Refactor Ghos...
[email protected] Fri, 08 Nov 2013 16:36:53 +0100
| Newsgroups | gmane.comp.web.zope.silva.cvs |
|---|---|
| Message-ID | <[email protected]> |
author: Sylvain Viollon
date: Fri Nov 08 16:36:46 2013 +0100
revision: 11875:13f06e1c78b7 in Products.Silva
branch: 2.4
details: https://hg.infrae.com/Products.Silva?cmd=changeset;node=13f06e1c78b7
modified: Products/Silva/Asset.py Products/Silva/EmailMessageService.py Products/Silva/ExtensionService_templates/configureextensions.cpt Products/Silva/File/content.py Products/Silva/File/smi_templates/infoportlet.cpt Products/Silva/File/views.py Products/Silva/Folder/management.py Products/Silva/Ghost.py Products/Silva/Ghost/__init__.py Products/Silva/Ghost/base.py Products/Silva/Ghost/content.py Products/Silva/Ghost/haunted.py Products/Silva/Ghost/metadata.py Products/Silva/Ghost/smi.py Products/Silva/Ghost/views.py Products/Silva/GhostAsset/__init__.py Products/Silva/GhostAsset/content.py Products/Silva/GhostAsset/smi.py Products/Silva/GhostAsset/views.py Products/Silva/GhostFolder.py Products/Silva/GhostFolder/__init__.py Products/Silva/GhostFolder/addables.py Products/Silva/GhostFol
der/content.py Products/Silva/GhostFolder/smi.py Products/Silva/Image/content.py Products/Silva/Image/views.py Products/Silva/adapters/haunted.py Products/Silva/install.py Products/Silva/sil
vaxml/xmlexport.py Products/Silva/silvaxml/xmlimport.py Products/Silva/tests/data/test_export_ghost_asset.silvaxml Products/Silva/tests/data/test_export_ghost_folder.silvaxml Products/Silva/tests/data/test_export_ghostfolder.silvaxml Products/Silva/tests/data/test_import_ghost_asset.zip Products/Silva/tests/mockers.py Products/Silva/tests/test_extension.py Products/Silva/tests/test_file.py Products/Silva/tests/test_folder_addables.py Products/Silva/tests/test_folder_quota.py Products/Silva/tests/test_ghost_asset.py Products/Silva/tests/test_ghost_folder.py Products/Silva/tests/test_image.py Products/Silva/tests/test_xml_export.py Products/Silva/tests/test_xml_import.py
added: Products/Silva/Ghost/__init__.py Products/Silva/Ghost/base.py Products/Silva/Ghost/content.py Products/Silva/Ghost/haunted.py Products/Silva/Ghost/metadata.py Products/Silva/Ghost/smi.py Products/Silva/Ghost/views.py Products/Silva/GhostAsset/__init__.py Products/Silva/GhostAsset/content.py Products/Silva/GhostAsset/smi.py Products/Silva/GhostAsset/views.py Products/Silva/GhostFolder/__init__.py Products/Silva/GhostFolder/addables.py Products/Silva/GhostFolder/content.py Products/Silva/GhostFolder/smi.py Products/Silva/tests/data/test_export_ghost_asset.silvaxml Products/Silva/tests/data/test_export_ghost_folder.silvaxml Products/Silva/tests/data/test_import_ghost_asset.zip Products/Silva/tests/test_ghost_asset.py
removed: Products/Silva/Ghost.py Products/Silva/GhostFolder.py Products/Silva/adapters/haunted.py Products/Silva/tests/data/test_export_ghostfolder.silvaxml
log: Add Ghost Asset. Refactor Ghost code.
diffstat:
Products/Silva/Asset.py | 3 +-
Products/Silva/EmailMessageService.py | 4 +-
Products/Silva/ExtensionService_templates/configureextensions.cpt | 1 +
Products/Silva/File/content.py | 59 +-
Products/Silva/File/smi_templates/infoportlet.cpt | 33 +-
Products/Silva/File/views.py | 3 +-
Products/Silva/Folder/management.py | 11 +-
Products/Silva/Ghost.py | 356 --------
Products/Silva/Ghost/__init__.py | 27 +
Products/Silva/Ghost/base.py | 185 ++++
Products/Silva/Ghost/content.py | 148 +++
Products/Silva/Ghost/haunted.py | 36 +
Products/Silva/Ghost/metadata.py | 22 +
Products/Silva/Ghost/smi.py | 78 +
Products/Silva/Ghost/views.py | 38 +
Products/Silva/GhostAsset/__init__.py | 1 +
Products/Silva/GhostAsset/content.py | 116 ++
Products/Silva/GhostAsset/smi.py | 56 +
Products/Silva/GhostAsset/views.py | 73 +
Products/Silva/GhostFolder.py | 423 ----------
Products/Silva/GhostFolder/__init__.py | 6 +
Products/Silva/GhostFolder/addables.py | 24 +
Products/Silva/GhostFolder/content.py | 264 ++++++
Products/Silva/GhostFolder/smi.py | 79 +
Products/Silva/Image/content.py | 36 +-
Products/Silva/Image/views.py | 9 +-
Products/Silva/adapters/haunted.py | 36 -
Products/Silva/install.py | 1 +
Products/Silva/silvaxml/xmlexport.py | 13 +
Products/Silva/silvaxml/xmlimport.py | 71 +-
Products/Silva/tests/data/test_export_ghost_asset.silvaxml | 100 ++
Products/Silva/tests/data/test_export_ghost_folder.silvaxml | 254 ++++++
Products/Silva/tests/data/test_export_ghostfolder.silvaxml | 254 ------
Products/Silva/tests/data/test_import_ghost_asset.zip | Bin
Products/Silva/tests/mockers.py | 26 +-
Products/Silva/tests/test_extension.py | 4 +
Products/Silva/tests/test_file.py | 11 +-
Products/Silva/tests/test_folder_addables.py | 7 +-
Products/Silva/tests/test_folder_quota.py | 6 +-
Products/Silva/tests/test_ghost_asset.py | 123 ++
Products/Silva/tests/test_ghost_folder.py | 200 +++-
Products/Silva/tests/test_image.py | 8 +-
Products/Silva/tests/test_xml_export.py | 28 +-
Products/Silva/tests/test_xml_import.py | 29 +
44 files changed, 2072 insertions(+), 1190 deletions(-)
diffs (3896 lines):
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/Asset.py
--- a/Products/Silva/Asset.py Mon Nov 04 17:03:35 2013 +0100
+++ b/Products/Silva/Asset.py Fri Nov 08 16:36:46 2013 +0100
@@ -63,8 +63,7 @@
except (AttributeError, NotImplementedError):
# Well, not all content respect its interface.
path = '/'.join(self.getPhysicalPath())
- klass = str(self.__class__)
- logger.error('bad asset object %s - %s' % (path, klass))
+ logger.error('Invalid asset %s - %s', path, str(self.__class__))
return -1
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/EmailMessageService.py
--- a/Products/Silva/EmailMessageService.py Mon Nov 04 17:03:35 2013 +0100
+++ b/Products/Silva/EmailMessageService.py Fri Nov 08 16:36:46 2013 +0100
@@ -296,7 +296,7 @@
grok.require('zope2.ViewManagementScreens')
grok.name('manage_settings')
- label = _(u"Messaging Configuration")
+ label = _(u"Messaging configuration")
description = _(u"Configure settings for email messaging between members. "
u"The default MailHost service is used to send messages.")
ignoreContent = False
@@ -307,7 +307,7 @@
class EmailMessageConfiguration(silvaforms.ConfigurationForm):
grok.context(EmailMessageService)
- label = _(u"Messaging Configuration")
+ label = _(u"Messaging configuration")
description = _(u"Configure settings for email messaging between members. "
u"The default MailHost service is used to send messages.")
fields = silvaforms.Fields(IEmailMessageSettings)
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/ExtensionService_templates/configureextensions.cpt
--- a/Products/Silva/ExtensionService_templates/configureextensions.cpt Mon Nov 04 17:03:35 2013 +0100
+++ b/Products/Silva/ExtensionService_templates/configureextensions.cpt Fri Nov 08 16:36:46 2013 +0100
@@ -30,6 +30,7 @@
Cancel
</a>
<a class="form-control" name="refresh_all"
+ data-confirmation="Are you sure ?"
i18n:translate="">
<ins class="icon form_check"></ins>
Refresh all
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/File/content.py
--- a/Products/Silva/File/content.py Mon Nov 04 17:03:35 2013 +0100
+++ b/Products/Silva/File/content.py Fri Nov 08 16:36:46 2013 +0100
@@ -2,17 +2,19 @@
# Copyright (c) 2002-2013 Infrae. All rights reserved.
# See also LICENSE.txt
+from cStringIO import StringIO
+from cgi import escape
+from types import StringTypes
+import logging
import os
import os.path
-import logging
-from types import StringTypes
-from cgi import escape
-from cStringIO import StringIO
+import time
+import warnings
# Zope 3
from ZODB import blob
from five import grok
-from zope.component import getUtility
+from zope.component import getUtility, getMultiAdapter
from zope.event import notify
from zope.lifecycleevent import ObjectCreatedEvent, ObjectModifiedEvent
from zope.lifecycleevent.interfaces import IObjectMovedEvent
@@ -31,11 +33,12 @@
from Products.Silva.File.converters import get_converter_for_mimetype
from silva.core import conf as silvaconf
+from silva.core import interfaces
from silva.core.conf.utils import ISilvaFactoryDispatcher
-from silva.core import interfaces
+from silva.core.interfaces import ContentError
from silva.core.interfaces import IMimeTypeClassifier, ISilvaNameChooser
-from silva.core.interfaces import ContentError
from silva.core.services.interfaces import IFilesService
+from silva.core.views.interfaces import IContentURL
from silva.translations import translate as _
logger = logging.getLogger('silva.file')
@@ -151,12 +154,27 @@
security.declareProtected(
SilvaPermissions.View, 'get_download_url')
- def get_download_url(self):
- return self.absolute_url()
+ def get_download_url(self, preview=False, request=None):
+ if request is None:
+ request = self.REQUEST
+ url = getMultiAdapter((self, request), IContentURL).url(preview=preview)
+ if preview:
+ # In case of preview we add something that change at the
+ # end of the url to prevent caching from the browser.
+ url += '?' + str(int(time.time()))
+ return url
security.declareProtected(
SilvaPermissions.View, 'tag')
def tag(self, **kw):
+ warnings.warn(
+ 'tag have been replaced with get_html_tag. '
+ 'It will be removed, please update your code.',
+ DeprecationWarning, stacklevel=2)
+ return self.get_html_tag(**kw)
+
+ security.declareProtected(SilvaPermissions.View, 'get_html_tag')
+ def get_html_tag(self, preview=False, request=None, **extra_attributes):
""" return xhtml tag
Since 'class' is a Python reserved word, it cannot be passed in
@@ -165,19 +183,20 @@
will accept a 'css_class' argument that will be converted to
'class' in the output tag to work around this.
"""
- src = self.get_download_url()
- named = []
- tooltip = unicode(_('download'))
+ src = self.get_download_url(preview, request)
+ title = self.get_title_or_id()
- if kw.has_key('css_class'):
- kw['class'] = kw['css_class']
- del kw['css_class']
+ if extra_attributes.has_key('css_class'):
+ extra_attributes['class'] = extra_attributes['css_class']
+ del extra_attributes['css_class']
- for name, value in kw.items():
- named.append('%s="%s"' % (escape(name), escape(value)))
- named = ' '.join(named)
- return '<a href="%s" title="%s %s" %s>%s</a>' % (
- src, tooltip, self.id, named, self.get_title_or_id())
+ extra_html_attributes = [
+ u'{name}="{value}"'.format(name=escape(name, 1),
+ value=escape(value, 1))
+ for name, value in extra_attributes.iteritems()]
+
+ return '<a href="%s" title="Download %s" %s>%s</a>' % (
+ src, self.get_filename(), extra_html_attributes, title)
# checks where the mime type is text/* or javascript
security.declareProtected(
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/File/smi_templates/infoportlet.cpt
--- a/Products/Silva/File/smi_templates/infoportlet.cpt Mon Nov 04 17:03:35 2013 +0100
+++ b/Products/Silva/File/smi_templates/infoportlet.cpt Fri Nov 08 16:36:46 2013 +0100
@@ -1,22 +1,23 @@
-<div class="portlet"
- i18n:domain="silva">
- <tal:mime_type tal:condition="viewlet.mime_type">
- <h4 i18n:translate="">File mime type</h4>
- <p tal:content="viewlet.mime_type">mime type</p>
- </tal:mime_type>
-
- <tal:content_encoding tal:condition="viewlet.content_encoding">
- <h4 i18n:translate="">File encoding</h4>
- <p tal:content="viewlet.content_encoding">content encoding</p>
- </tal:content_encoding>
-
- <h4 i18n:translate="">Link to file</h4>
+<tal:info i18n:domain="silva">
+ <div class="portlet">
+ <h4 i18n:translate="">Link to file</h4>
<p>
<a title="Download" target="_blank"
- tal:content="context.get_title_or_id()"
- tal:attributes="href path:context/@@absolute_url"
+ tal:content="context.get_filename()"
+ tal:attributes="href context.get_download_url(preview=True)"
i18n:attributes="title">
link
</a>
</p>
-</div>
+ </div>
+
+ <div class="portlet" tal:condition="viewlet.mime_type">
+ <h4 i18n:translate="">File mime type</h4>
+ <p tal:content="viewlet.mime_type">mime type</p>
+ </div>
+
+ <div class="portlet" tal:condition="viewlet.content_encoding">
+ <h4 i18n:translate="">File encoding</h4>
+ <p tal:content="viewlet.content_encoding">content encoding</p>
+ </div>
+</tal:info>
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/File/views.py
--- a/Products/Silva/File/views.py Mon Nov 04 17:03:35 2013 +0100
+++ b/Products/Silva/File/views.py Fri Nov 08 16:36:46 2013 +0100
@@ -125,7 +125,8 @@
grok.require('zope2.View')
def render(self):
- return self.content.tag()
+ return self.content.get_html_tag(
+ request=self.request, preview=self.is_preview)
def parse_datetime(value):
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/Folder/management.py
--- a/Products/Silva/Folder/management.py Mon Nov 04 17:03:35 2013 +0100
+++ b/Products/Silva/Folder/management.py Fri Nov 08 16:36:46 2013 +0100
@@ -5,7 +5,7 @@
import re
from five import grok
-from zope.cachedescriptors.property import CachedProperty
+from zope.cachedescriptors.property import Lazy
from zope.container.contained import notifyContainerModified
from zope.event import notify
from zope.lifecycleevent import ObjectCopiedEvent
@@ -20,7 +20,7 @@
from OFS.subscribers import compatibilityCall
from Products.Silva import helpers
-from Products.Silva.Ghost import ghost_factory
+from Products.Silva.Ghost import get_ghost_factory
from infrae.comethods import cofunction
from silva.core import conf as silvaconf
@@ -88,7 +88,7 @@
self.context, to_identifier))
return content
- @CachedProperty
+ @Lazy
def __addables(self):
return set(IAddableContents(self.context).get_authorized_addables())
@@ -233,12 +233,13 @@
while content is not None:
result = self.__verify_copyable(content)
if result is None:
- if IAsset.providedBy(content):
+ factory = get_ghost_factory(self.context, content)
+ if factory is None:
identifier = self.__make_id('copy', content.getId())
result = self.__copy(content, identifier)
else:
identifier = self.__make_id('ghost', content.getId())
- result = ghost_factory(self.context, identifier, content)
+ result = factory(identifier)
content = yield result
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/Ghost.py
--- a/Products/Silva/Ghost.py Mon Nov 04 17:03:35 2013 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,356 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2002-2013 Infrae. All rights reserved.
-# See also LICENSE.txt
-
-# Zope 3
-from five import grok
-from zope.component import getUtility, queryMultiAdapter
-
-# Zope 2
-from Acquisition import aq_inner, aq_base
-from AccessControl import ClassSecurityInfo, getSecurityManager
-from AccessControl import Unauthorized
-from App.class_init import InitializeClass
-
-# Silva
-from Products.SilvaMetadata.Binding import DefaultMetadataBindingFactory
-from Products.Silva.VersionedContent import VersionedContent
-from Products.Silva.Version import Version
-from Products.Silva import SilvaPermissions
-
-from zeam.form import silva as silvaforms
-from zeam.form.base.errors import Error
-from zeam.form.base.widgets import widgetId
-
-from silva.core.views.interfaces import IPreviewLayer
-from silva.core import conf as silvaconf
-from silva.core.conf.interfaces import IIdentifiedContent
-from silva.core.interfaces import errors
-from silva.core.interfaces.errors import ContentError
-from silva.core.interfaces import (
- IContainer, IContent, IGhost, IGhostAware, IGhostVersion)
-from silva.core.references.reference import Reference
-from silva.core.references.reference import DeleteSourceReferenceValue
-from silva.core.references.reference import WeakReferenceValue
-from silva.core.references.reference import get_content_id, get_content_from_id
-from silva.core.references.interfaces import IReferenceService
-from silva.core.views import views as silvaviews
-from silva.core.views.interfaces import IView
-from silva.translations import translate as _
-
-
-def validate_target(ghost, target, is_folderish=False, adding=False):
- """Validate a ghost for its given target.
- """
- if target is None:
- return errors.EmptyInvalidTarget()
- # Check for cicular reference. You cannot select an ancestor
- # or descandant of the ghost (or the ghost)
- target_path = target.getPhysicalPath()
- ghost_path = ghost.getPhysicalPath()
- if len(target_path) > len(ghost_path) and not adding:
- if ghost_path == target_path[:len(ghost_path)]:
- return errors.CircularInvalidTarget()
- elif target_path == ghost_path[:len(target_path)]:
- return errors.CircularInvalidTarget()
- if IGhostAware.providedBy(target):
- return errors.GhostInvalidTarget()
- if is_folderish:
- # If we are a container, we expect to have a container as
- # target.
- if IContent.providedBy(target):
- return errors.ContainerInvalidTarget()
- if not IContainer.providedBy(target):
- return errors.ContainerInvalidTarget()
- else:
- # If we are not a container, we expect to have a content
- # as target.
- if IContainer.providedBy(target):
- return errors.ContentInvalidTarget()
- if not IContent.providedBy(target):
- return errors.ContentInvalidTarget()
-
-
-class GhostBase(object):
- """baseclass for Ghosts (or Ghost versions if it's versioned)
- """
- security = ClassSecurityInfo()
-
- security.declareProtected(
- SilvaPermissions.ChangeSilvaContent, 'set_title')
- def set_title(self, title):
- """You cannot change the title of a ghost.
- """
- if title is not None:
- raise ContentError(
- _(u"A ghost title is immutable."),
- self.get_silva_object())
-
- security.declareProtected(
- SilvaPermissions.AccessContentsInformation, 'get_title')
- def get_title(self):
- """Get title.
- """
- content = self.get_haunted()
- if content is not None and content.is_published():
- return content.get_title()
- return _(u"Ghost target is broken")
-
- security.declareProtected(
- SilvaPermissions.AccessContentsInformation, 'get_short_title')
- def get_short_title(self):
- """Get short title.
- """
- content = self.get_haunted()
- if content is not None and content.is_published():
- return content.get_short_title()
- return _(u"Ghost target is broken")
-
- security.declareProtected(
- SilvaPermissions.AccessContentsInformation, 'get_title_editable')
- def get_title_editable(self):
- """Get title.
- """
- content = self.get_haunted()
- if content is not None:
- return content.get_title_editable()
- return _(u"Ghost target is broken")
-
- security.declareProtected(
- SilvaPermissions.AccessContentsInformation, 'get_short_title_editable')
- def get_short_title_editable(self):
- """Get title.
- """
- content = self.get_haunted()
- if content is not None:
- return content.get_short_title_editable()
- return _(u"Ghost target is broken")
-
- security.declareProtected(
- SilvaPermissions.ChangeSilvaContent, 'set_haunted')
- def set_haunted(self, content, auto_delete=False):
- """ Set the content as the haunted object
- """
- service = getUtility(IReferenceService)
- factory = WeakReferenceValue
- if auto_delete:
- factory = DeleteSourceReferenceValue
- reference = service.get_reference(
- aq_inner(self), name=u"haunted", add=True, factory=factory)
- if not isinstance(content, int):
- content = get_content_id(content)
- reference.set_target_id(content)
-
- security.declareProtected(SilvaPermissions.View, 'get_haunted')
- def get_haunted(self):
- service = getUtility(IReferenceService)
- reference = service.get_reference(
- aq_inner(self), name=u"haunted")
- if reference is not None:
- return reference.target
-
- security.declareProtected(SilvaPermissions.View, 'get_link_status')
- def get_link_status(self):
- """Return an error code if this version of the ghost is broken.
- returning None means the ghost is Ok.
- """
- return validate_target(
- self,
- self.get_haunted(),
- IContainer.providedBy(self))
-
-
-class GhostMetadataBindingFactory(DefaultMetadataBindingFactory):
- grok.context(IGhostAware)
- read_only = True
-
- def get_content(self):
- haunted = self.context.get_haunted()
- if haunted is not None:
- if IPreviewLayer.providedBy(haunted.REQUEST):
- return haunted.get_previewable()
- return haunted.get_viewable()
- return None
-
-
-class Ghost(VersionedContent):
- __doc__ = _("""Ghosts are special documents that function as a
- placeholder for an item in another location (like an alias,
- symbolic link, shortcut). Unlike a hyperlink, which takes the
- Visitor to another location, a ghost object keeps the Visitor in the
- current publication, and presents the content of the ghosted item.
- The ghost inherits properties from its location (e.g. layout
- and stylesheets).
- """)
-
- meta_type = "Silva Ghost"
- security = ClassSecurityInfo()
-
- grok.implements(IGhost)
- silvaconf.icon('icons/ghost.png')
- silvaconf.version_class('GhostVersion')
-
- security.declareProtected(
- SilvaPermissions.AccessContentsInformation, 'get_haunted')
- def get_haunted(self):
- version = self.get_previewable()
- if version is not None:
- return version.get_haunted()
- return None
-
- security.declareProtected(
- SilvaPermissions.AccessContentsInformation, 'is_published')
- def is_published(self):
- public = self.get_viewable()
- if public is None:
- return False
- haunted = public.get_haunted()
- if haunted is None:
- return False
- return haunted.is_published()
-
- security.declareProtected(
- SilvaPermissions.AccessContentsInformation, 'get_modification_datetime')
- def get_modification_datetime(self):
- """Return modification datetime.
- """
- version = self.get_viewable()
- if version is not None:
- content = version.get_haunted()
- if content is not None:
- return content.get_modification_datetime()
- return super(Ghost, self).get_modification_datetime()
-
-InitializeClass(Ghost)
-
-
-class GhostVersion(GhostBase, Version):
- """Ghost version.
- """
- meta_type = 'Silva Ghost Version'
- grok.implements(IGhostVersion)
-
- security = ClassSecurityInfo()
-
- security.declareProtected(
- SilvaPermissions.AccessContentsInformation, 'fulltext')
- def fulltext(self):
- target = self.get_haunted()
- if target is not None:
- public_version = target.get_viewable()
- if public_version and hasattr(aq_base(public_version), 'fulltext'):
- return public_version.fulltext()
- return ""
-
-
-class IGhostSchema(IIdentifiedContent):
-
- haunted = Reference(
- IContent,
- title=_(u"Target"),
- description=_(u"The internal item the ghost is mirroring."),
- required=True)
-
-
-def TargetValidator(field_name, is_folderish=False, adding=False):
-
- class Validator(object):
-
- def __init__(self, form, fields):
- self.form = form
- self.fields = fields
-
- def validate(self, data):
- """Validate ghost target before setting it.
- """
- # This is not beauty, but it works.
- content_id = data.get(field_name)
- if content_id is silvaforms.NO_VALUE:
- # If there value is required it is already checked
- return []
- error = validate_target(
- self.form.context,
- get_content_from_id(content_id),
- is_folderish,
- adding)
- if error is not None:
- identifier = widgetId(self.form, self.fields[field_name])
- return [Error(error.doc(), identifier)]
- return []
-
- return Validator
-
-
-class GhostAddForm(silvaforms.SMIAddForm):
- """Add form for a ghost
- """
- grok.name(u"Silva Ghost")
- grok.context(IGhost)
-
- fields = silvaforms.Fields(IGhostSchema)
- fields['haunted'].referenceNotSetLabel = _(
- u"Click the Lookup button to select an item to haunt.")
- dataValidators = [
- TargetValidator('haunted', is_folderish=False, adding=True)]
-
- def _add(self, parent, data):
- factory = parent.manage_addProduct['Silva']
- return factory.manage_addGhost(
- data['id'], None, haunted=data['haunted'])
-
-
-class GhostEditForm(silvaforms.SMIEditForm):
- """ Edit form for Ghost
- """
- grok.context(IGhost)
- fields = silvaforms.Fields(IGhostSchema).omit('id')
- dataValidators = [
- TargetValidator('haunted', is_folderish=False, adding=False)]
-
-
-class GhostView(silvaviews.View):
- grok.context(IGhost)
-
- broken_message = _(u"This content is unavailable. "
- u"Please inform the site manager.")
-
- def render(self):
- haunted = self.content.get_haunted()
- if haunted is None:
- return self.broken_message
- permission = self.is_preview and 'Read Silva content' or 'View'
- if not getSecurityManager().checkPermission(permission, haunted):
- raise Unauthorized(
- u"You do not have permission to "
- u"see the target of this ghost")
- view = queryMultiAdapter((haunted, self.request), name="content.html")
- if view is None:
- return self.broken_message
- if IView.providedBy(view) and view.content is None:
- return self.broken_message
- return view()
-
-
-def ghost_factory(container, identifier, target):
- """add new ghost to container
-
- container: container to add ghost to (must be acquisition wrapped)
- id: (str) id for new ghost in container
- target: object to be haunted (ghosted), acquisition wrapped
- returns created ghost
-
- actual ghost created depends on haunted object
- on IContainer a GhostFolder is created
- on IVersionedContent a Ghost is created
- """
- factory = container.manage_addProduct['Silva']
- if IGhostAware.providedBy(target):
- target = target.get_haunted()
- if target is not None:
- if IContainer.providedBy(target):
- factory = factory.manage_addGhostFolder
- elif IContent.providedBy(target):
- factory = factory.manage_addGhost
- factory(identifier, None, haunted=target)
- return container._getOb(identifier)
- return None
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/Ghost/__init__.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Products/Silva/Ghost/__init__.py Fri Nov 08 16:36:46 2013 +0100
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2002-2013 Infrae. All rights reserved.
+# See also LICENSE.txt
+
+
+from zeam.component import getComponent
+from silva.core.interfaces import IGhostManager
+
+# For BBB
+from .base import GhostBase, GhostBaseManager, GhostBaseManipulator
+from .content import Ghost, GhostVersion
+from .smi import TargetValidator
+
+
+def get_ghost_factory(container, target):
+ """add new ghost to container
+ """
+ if target is None:
+ return None
+
+ get_manager = getComponent((target,), IGhostManager, default=None)
+ if get_manager is None:
+ return None
+
+ return lambda identifier: get_manager(
+ container=container).modify(target, identifier).create()
+
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/Ghost/base.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Products/Silva/Ghost/base.py Fri Nov 08 16:36:46 2013 +0100
@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2002-2013 Infrae. All rights reserved.
+# See also LICENSE.txt
+
+# Zope 3
+from five import grok
+from zeam.component import getComponent
+from zope.component import getUtility
+
+# Zope 2
+from Acquisition import aq_inner
+from AccessControl import ClassSecurityInfo
+from App.class_init import InitializeClass
+
+# Silva
+from Products.Silva import SilvaPermissions
+
+from silva.core.interfaces import errors
+from silva.core.interfaces.errors import ContentError
+from silva.core.interfaces import IGhostAware, IGhostManager, IPublishable
+from silva.core.references.reference import DeleteSourceReferenceValue
+from silva.core.references.reference import WeakReferenceValue
+from silva.core.references.reference import get_content_id
+from silva.core.references.interfaces import IReferenceService
+from silva.translations import translate as _
+
+
+class GhostBaseManipulator(object):
+
+ def __init__(self, manager, target, identifier):
+ self.manager = manager
+ self.target = target
+ self.identifier = identifier
+
+ def create(self):
+ raise NotImplementedError
+
+ def update(self):
+ raise NotImplementedError
+
+ def need_update(self):
+ raise NotImplementedError
+
+ def verify(self):
+ if self.manager.ghost is None:
+ if self.target is not None:
+ return self.create()
+ elif self.need_update():
+ return self.update()
+ return self.manager.ghost
+
+
+class GhostBaseManager(object):
+ grok.implements(IGhostManager)
+ manipulator = GhostBaseManipulator
+
+ def __init__(self, ghost=None, container=None,
+ auto_delete=False, auto_publish=False):
+ self.ghost = ghost
+ if container is None:
+ assert ghost is not None, 'Need to provide ghost or a container'
+ container = ghost.get_container()
+ self.container = container
+ self.auto_delete = auto_delete
+ self.auto_publish = auto_publish
+
+ def modify(self, target, identifier=None):
+ if identifier is None:
+ identifier = target
+ if IGhostAware.providedBy(target):
+ target = target.get_haunted()
+ return self.manipulator(self, target, identifier)
+
+ def validate(self, target, adding=False):
+ if target is None:
+ return errors.EmptyInvalidTarget()
+ if IGhostAware.providedBy(target):
+ return errors.GhostInvalidTarget()
+ # Check for cicular reference. You cannot select an ancestor
+ # or descandant of the ghost (or the ghost)
+ target_path = target.getPhysicalPath()
+ if adding:
+ test_path = self.container.getPhysicalPath()
+ else:
+ test_path = self.ghost.getPhysicalPath()
+
+ # XXX !!!!
+ if not adding and len(target_path) > len(test_path):
+ if test_path == target_path[:len(test_path)]:
+ return errors.CircularInvalidTarget()
+ elif target_path == test_path[:len(target_path)]:
+ return errors.CircularInvalidTarget()
+
+
+class GhostBase(object):
+ """baseclass for Ghosts (or Ghost versions if it's versioned)
+ """
+ security = ClassSecurityInfo()
+
+ security.declareProtected(
+ SilvaPermissions.ChangeSilvaContent, 'set_title')
+ def set_title(self, title):
+ """You cannot change the title of a ghost.
+ """
+ if title is not None:
+ raise ContentError(
+ _(u"A ghost title is immutable."),
+ self.get_silva_object())
+
+ security.declareProtected(
+ SilvaPermissions.AccessContentsInformation, 'get_title')
+ def get_title(self):
+ """Get title.
+ """
+ content = self.get_haunted()
+ if content is not None:
+ if not IPublishable.providedBy(content) or content.is_published():
+ return content.get_title()
+ return _(u"Ghost target is broken")
+
+ security.declareProtected(
+ SilvaPermissions.AccessContentsInformation, 'get_short_title')
+ def get_short_title(self):
+ """Get short title.
+ """
+ content = self.get_haunted()
+ if content is not None:
+ if not IPublishable.providedBy(content) or content.is_published():
+ return content.get_short_title()
+ return _(u"Ghost target is broken")
+
+ security.declareProtected(
+ SilvaPermissions.AccessContentsInformation, 'get_title_editable')
+ def get_title_editable(self):
+ """Get title.
+ """
+ content = self.get_haunted()
+ if content is not None:
+ return content.get_title_editable()
+ return _(u"Ghost target is broken")
+
+ security.declareProtected(
+ SilvaPermissions.AccessContentsInformation, 'get_short_title_editable')
+ def get_short_title_editable(self):
+ """Get title.
+ """
+ content = self.get_haunted()
+ if content is not None:
+ return content.get_short_title_editable()
+ return _(u"Ghost target is broken")
+
+ security.declareProtected(
+ SilvaPermissions.ChangeSilvaContent, 'set_haunted')
+ def set_haunted(self, content, auto_delete=False):
+ """ Set the content as the haunted object
+ """
+ service = getUtility(IReferenceService)
+ factory = WeakReferenceValue
+ if auto_delete:
+ factory = DeleteSourceReferenceValue
+ reference = service.get_reference(
+ aq_inner(self), name=u"haunted", add=True, factory=factory)
+ if not isinstance(content, int):
+ content = get_content_id(content)
+ reference.set_target_id(content)
+
+ security.declareProtected(SilvaPermissions.View, 'get_haunted')
+ def get_haunted(self):
+ service = getUtility(IReferenceService)
+ reference = service.get_reference(aq_inner(self), name=u"haunted")
+ if reference is not None:
+ return reference.target
+ return None
+
+ security.declareProtected(SilvaPermissions.View, 'get_link_status')
+ def get_link_status(self):
+ """Return an error code if this version of the ghost is broken.
+ returning None means the ghost is Ok.
+ """
+ ghost = self.get_silva_object()
+ get_manager = getComponent((ghost,), IGhostManager)
+ return get_manager(ghost=ghost).validate(self.get_haunted())
+
+
+InitializeClass(GhostBase)
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/Ghost/content.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Products/Silva/Ghost/content.py Fri Nov 08 16:36:46 2013 +0100
@@ -0,0 +1,148 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2002-2013 Infrae. All rights reserved.
+# See also LICENSE.txt
+
+# Zope 3
+from five import grok
+from zeam.component import component
+
+# Zope 2
+from Acquisition import aq_base
+from AccessControl import ClassSecurityInfo
+from App.class_init import InitializeClass
+
+# Silva
+from Products.Silva.VersionedContent import VersionedContent
+from Products.Silva.Version import Version
+from Products.Silva import SilvaPermissions
+
+from silva.core import conf as silvaconf
+from silva.core.interfaces.errors import ContentInvalidTarget
+from silva.core.interfaces import IContent, IGhost, IGhostVersion
+from silva.core.interfaces import IPublicationWorkflow, IGhostManager
+from silva.translations import translate as _
+
+from .base import GhostBase, GhostBaseManipulator, GhostBaseManager
+
+
+class GhostVersion(GhostBase, Version):
+ """Ghost version.
+ """
+ meta_type = 'Silva Ghost Version'
+ grok.implements(IGhostVersion)
+
+ security = ClassSecurityInfo()
+ security.declareProtected(
+ SilvaPermissions.AccessContentsInformation, 'fulltext')
+ def fulltext(self):
+ target = self.get_haunted()
+ if target is not None:
+ public_version = target.get_viewable()
+ if public_version and hasattr(aq_base(public_version), 'fulltext'):
+ return public_version.fulltext()
+ return ""
+
+
+InitializeClass(GhostVersion)
+
+
+class Ghost(VersionedContent):
+ __doc__ = _("""Ghosts are special documents that function as a
+ placeholder for an item in another location (like an alias,
+ symbolic link, shortcut). Unlike a hyperlink, which takes the
+ Visitor to another location, a ghost object keeps the Visitor in the
+ current publication, and presents the content of the ghosted item.
+ The ghost inherits properties from its location (e.g. layout
+ and stylesheets).
+ """)
+
+ meta_type = "Silva Ghost"
+ security = ClassSecurityInfo()
+
+ grok.implements(IGhost)
+ silvaconf.icon('icons/ghost.png')
+ silvaconf.version_class(GhostVersion)
+
+ security.declareProtected(
+ SilvaPermissions.AccessContentsInformation, 'get_haunted')
+ def get_haunted(self):
+ version = self.get_previewable()
+ if version is not None:
+ return version.get_haunted()
+ return None
+
+ security.declareProtected(
+ SilvaPermissions.AccessContentsInformation, 'is_published')
+ def is_published(self):
+ public = self.get_viewable()
+ if public is None:
+ return False
+ haunted = public.get_haunted()
+ if haunted is None:
+ return False
+ return haunted.is_published()
+
+ security.declareProtected(
+ SilvaPermissions.AccessContentsInformation, 'get_modification_datetime')
+ def get_modification_datetime(self):
+ """Return modification datetime.
+ """
+ version = self.get_viewable()
+ if version is not None:
+ content = version.get_haunted()
+ if content is not None:
+ return content.get_modification_datetime()
+ return super(Ghost, self).get_modification_datetime()
+
+InitializeClass(Ghost)
+
+
+class GhostManipulator(GhostBaseManipulator):
+
+ def create(self):
+ assert self.manager.ghost is None
+ ghost = None
+ factory = self.manager.container.manage_addProduct['Silva']
+ factory.manage_addGhost(self.identifier, None)
+ ghost = self.manager.container._getOb(self.identifier)
+ version = ghost.get_editable()
+ version.set_haunted(
+ self.target, auto_delete=self.manager.auto_delete)
+ if self.manager.auto_publish:
+ IPublicationWorkflow(ghost).publish()
+ self.manager.ghost = ghost
+ return ghost
+
+ def update(self):
+ assert self.manager.ghost is not None
+ if IGhost.providedBy(self.manager.ghost):
+ publication = IPublicationWorkflow(self.manager.ghost)
+ if self.manager.ghost.get_editable() is None:
+ publication.new_version()
+ version = self.manager.ghost.get_editable()
+ version.set_haunted(
+ self.target, auto_delete=self.manager.auto_delete)
+ if self.manager.auto_publish:
+ publication.publish()
+ return self.manager.ghost
+ self.manager.container.manage_delObjects([self.identifier])
+ self.manager.ghost = None
+ return self.create()
+
+ def need_update(self):
+ if IGhost.providedBy(self.manager.ghost):
+ viewable = self.manager.ghost.get_viewable()
+ return self.target != viewable.get_haunted()
+ return IContent.providedBy(self.manager.ghost)
+
+
+@component(IContent, provides=IGhostManager)
+class GhostManager(GhostBaseManager):
+ manipulator = GhostManipulator
+
+ def validate(self, target, adding=False):
+ error = super(GhostManager, self).validate(target, adding)
+ if error is None:
+ if not IContent.providedBy(target):
+ return ContentInvalidTarget()
+ return error
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/Ghost/haunted.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Products/Silva/Ghost/haunted.py Fri Nov 08 16:36:46 2013 +0100
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2002-2013 Infrae. All rights reserved.
+# See also LICENSE.txt
+
+from five import grok
+from silva.core import interfaces
+from silva.core.references.interfaces import IReferenceService
+from zope.component import getUtility
+
+
+class Haunted(grok.Adapter):
+ """Adapted content for retrieving the 'iterator' of haunting
+ objects (Ghosts).
+ """
+ grok.implements(interfaces.IHaunted)
+ grok.context(interfaces.IContent)
+
+ def getHaunting(self):
+ service = getUtility(IReferenceService)
+ for reference in service.get_references_to(
+ self.context, name=u'haunted'):
+ yield reference.source.get_silva_object()
+
+
+class HauntedGhost(Haunted):
+ """Adapted content for retrieving the 'iterator' of haunting
+ objects (Ghosts).
+ """
+ grok.context(interfaces.IGhost)
+
+ def getHaunting(self):
+ # Nothing to look for - Ghost cannot be haunted. Don't yield anything
+ # XXX how to not yield anything??
+ if None:
+ yield None
+
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/Ghost/metadata.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Products/Silva/Ghost/metadata.py Fri Nov 08 16:36:46 2013 +0100
@@ -0,0 +1,22 @@
+
+
+from five import grok
+
+from Products.SilvaMetadata.Binding import DefaultMetadataBindingFactory
+
+from silva.core.interfaces import IGhostAware
+from silva.core.views.interfaces import IPreviewLayer
+
+
+class GhostMetadataBindingFactory(DefaultMetadataBindingFactory):
+ grok.context(IGhostAware)
+ read_only = True
+
+ def get_content(self):
+ haunted = self.context.get_haunted()
+ if haunted is not None:
+ if IPreviewLayer.providedBy(haunted.REQUEST):
+ return haunted.get_previewable()
+ return haunted.get_viewable()
+ return None
+
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/Ghost/smi.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Products/Silva/Ghost/smi.py Fri Nov 08 16:36:46 2013 +0100
@@ -0,0 +1,78 @@
+
+# Zope 3
+from five import grok
+from zeam.form import silva as silvaforms
+from zeam.form.base.errors import Error
+from zeam.form.base.widgets import widgetId
+from zeam.component import getComponent
+
+# Silva
+from silva.core.conf.interfaces import IIdentifiedContent
+from silva.core.interfaces import IGhost, IContent, IGhostManager
+from silva.core.references.reference import Reference
+from silva.core.references.reference import get_content_from_id
+from silva.translations import translate as _
+
+
+class IGhostSchema(IIdentifiedContent):
+
+ haunted = Reference(
+ IContent,
+ title=_(u"Target"),
+ description=_(u"The internal item the ghost is mirroring."),
+ required=True)
+
+
+def TargetValidator(field_name, wanted, adding=False):
+
+ class Validator(object):
+
+ def __init__(self, form, fields):
+ self.form = form
+ self.fields = fields
+
+ def validate(self, data):
+ """Validate ghost target before setting it.
+ """
+ # This is not beauty, but it works.
+ content_id = data.get(field_name)
+ if content_id is silvaforms.NO_VALUE:
+ # If there value is required it is already checked
+ return []
+ getManager = getComponent((wanted,), IGhostManager)
+ if adding:
+ manager = getManager(container=self.form.context)
+ else:
+ manager = getManager(ghost=self.form.context)
+ error = manager.validate(get_content_from_id(content_id), adding)
+ if error is not None:
+ identifier = widgetId(self.form, self.fields[field_name])
+ return [Error(error.doc(), identifier)]
+ return []
+
+ return Validator
+
+
+class GhostAddForm(silvaforms.SMIAddForm):
+ """Add form for a ghost
+ """
+ grok.name(u"Silva Ghost")
+ grok.context(IGhost)
+
+ fields = silvaforms.Fields(IGhostSchema)
+ fields['haunted'].referenceNotSetLabel = _(
+ u"Click the Lookup button to select an item to haunt.")
+ dataValidators = [TargetValidator('haunted', IContent, adding=True)]
+
+ def _add(self, parent, data):
+ factory = parent.manage_addProduct['Silva']
+ return factory.manage_addGhost(
+ data['id'], None, haunted=data['haunted'])
+
+
+class GhostEditForm(silvaforms.SMIEditForm):
+ """ Edit form for Ghost
+ """
+ grok.context(IGhost)
+ fields = silvaforms.Fields(IGhostSchema).omit('id')
+ dataValidators = [TargetValidator('haunted', IContent, adding=False)]
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/Ghost/views.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Products/Silva/Ghost/views.py Fri Nov 08 16:36:46 2013 +0100
@@ -0,0 +1,38 @@
+
+# Zope 3
+from five import grok
+from zope.component import queryMultiAdapter
+
+# Zope 2
+from AccessControl import getSecurityManager
+from AccessControl import Unauthorized
+
+# Silva
+from silva.core.interfaces import IGhost
+from silva.core.views import views as silvaviews
+from silva.core.views.interfaces import IView
+from silva.translations import translate as _
+
+
+class GhostView(silvaviews.View):
+ grok.context(IGhost)
+
+ broken_message = _(u"This content is unavailable. "
+ u"Please inform the site manager.")
+
+ def render(self):
+ haunted = self.content.get_haunted()
+ if haunted is None:
+ return self.broken_message
+ permission = self.is_preview and 'Read Silva content' or 'View'
+ if not getSecurityManager().checkPermission(permission, haunted):
+ raise Unauthorized(
+ u"You do not have permission to "
+ u"see the target of this ghost")
+ view = queryMultiAdapter((haunted, self.request), name="content.html")
+ if view is None:
+ return self.broken_message
+ if IView.providedBy(view) and view.content is None:
+ return self.broken_message
+ return view()
+
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/GhostAsset/__init__.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Products/Silva/GhostAsset/__init__.py Fri Nov 08 16:36:46 2013 +0100
@@ -0,0 +1,1 @@
+# This is a package.
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/GhostAsset/content.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Products/Silva/GhostAsset/content.py Fri Nov 08 16:36:46 2013 +0100
@@ -0,0 +1,116 @@
+
+# Zope 3
+from five import grok
+from zeam.component import component
+
+# Zope 2
+from AccessControl import ClassSecurityInfo
+from App.class_init import InitializeClass
+
+# Silva
+from Products.Silva import SilvaPermissions
+from Products.Silva.Asset import Asset
+from Products.Silva.Ghost import GhostBase
+from Products.Silva.Ghost import GhostBaseManipulator, GhostBaseManager
+
+from silva.core.interfaces import IGhostAsset, IAsset
+from silva.core.interfaces import IAssetPayload, IGhostManager
+from silva.core.interfaces.errors import AssetInvalidTarget
+
+
+class GhostAsset(GhostBase, Asset):
+ grok.implements(IGhostAsset)
+ security = ClassSecurityInfo()
+ meta_type = "Silva Ghost Asset"
+
+ security.declareProtected(
+ SilvaPermissions.AccessContentsInformation, 'get_filename')
+ def get_filename(self):
+ asset = self.get_haunted()
+ if asset is not None:
+ return asset.get_filename()
+ return ''
+
+ security.declareProtected(
+ SilvaPermissions.AccessContentsInformation, 'get_file_size')
+ def get_file_size(self):
+ asset = self.get_haunted()
+ if asset is not None:
+ return asset.get_file_size()
+ return 0
+
+ security.declareProtected(
+ SilvaPermissions.AccessContentsInformation, 'get_mime_type')
+ def get_mime_type(self):
+ asset = self.get_haunted()
+ if asset is not None:
+ return asset.get_mime_type()
+ return 'application/octet-stream'
+
+ def get_quota_usage(self):
+ # Ghost Assets don't use any quota.
+ return -1
+
+ security.declareProtected(
+ SilvaPermissions.AccessContentsInformation, 'get_modification_datetime')
+ def get_modification_datetime(self):
+ """Return modification datetime.
+ """
+ asset = self.get_haunted()
+ if asset is not None:
+ return asset.get_modification_datetime()
+ return super(GhostAsset, self).get_modification_datetime()
+
+
+InitializeClass(GhostAsset)
+
+
+class GhostAssetManipulator(GhostBaseManipulator):
+
+ def create(self):
+ assert self.manager.ghost is None
+ factory = self.manager.container.manage_addProduct['Silva']
+ factory.manage_addGhostAsset(self.identifier, None)
+ ghost = self.manager.container._getOb(self.identifier)
+ ghost.set_haunted(self.target, auto_delete=self.manager.auto_delete)
+ self.manager.ghost = ghost
+ return ghost
+
+ def update(self):
+ assert self.manager.ghost is not None
+ if IGhostAsset.providedBy(self.manager.ghost):
+ self.manager.ghost.set_haunted(
+ self.target, auto_delete=self.manager.auto_delete)
+ return self.manager.ghost
+ self.manager.container.manage_delObjects([self.identifier])
+ self.manager.ghost = None
+ return self.create()
+
+ def need_update(self):
+ if IGhostAsset.providedBy(self.manager.ghost):
+ return self.target != self.manager.ghost.get_haunted()
+ # Only update if the invalid ghost is an asset.
+ return IAsset.providedBy(self.manager.ghost)
+
+
+@component(IAsset, provides=IGhostManager)
+class GhostAssetManager(GhostBaseManager):
+ manipulator = GhostAssetManipulator
+
+ def validate(self, target, adding=False):
+ error = super(GhostAssetManager, self).validate(target, adding)
+ if error is None:
+ if not IAsset.providedBy(target):
+ return AssetInvalidTarget()
+ return error
+
+
+class GhostAssetPayload(grok.Adapter):
+ grok.implements(IAssetPayload)
+ grok.context(IGhostAsset)
+
+ def get_payload(self):
+ asset = self.context.get_haunted()
+ if asset is not None:
+ return IAssetPayload(asset).get_payload()
+ return None
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/GhostAsset/smi.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Products/Silva/GhostAsset/smi.py Fri Nov 08 16:36:46 2013 +0100
@@ -0,0 +1,56 @@
+
+
+# Zope 3
+from five import grok
+
+# Silva
+from Products.Silva.Ghost import TargetValidator
+from Products.Silva.Asset import AssetEditTab
+
+from silva.core.conf.interfaces import IIdentifiedContent
+from silva.core.references.reference import Reference
+from silva.core.interfaces import IAsset, IGhostAsset
+from silva.translations import translate as _
+from zeam.form import silva as silvaforms
+
+
+class IGhostAssetSchema(IIdentifiedContent):
+
+ haunted = Reference(IAsset,
+ title=_(u"Target"),
+ description=_(u"The asset the ghost is mirroring"),
+ required=True)
+
+
+class GhostAssetAddForm(silvaforms.SMIAddForm):
+ """ Add form for ghost folders
+ """
+ grok.name(u'Silva Ghost Asset')
+
+ fields = silvaforms.Fields(IGhostAssetSchema)
+ fields['haunted'].referenceNotSetLabel = _(
+ u"Click the Lookup button to select an asset to haunt.")
+ dataValidators = [TargetValidator('haunted', IAsset, adding=True)]
+
+ def _add(self, parent, data):
+ factory = parent.manage_addProduct['Silva']
+ return factory.manage_addGhostAsset(
+ data['id'], None, haunted=data['haunted'])
+
+
+class GhostAssetEditForm(silvaforms.SMISubForm):
+ """ Edit form Ghost Folder
+ """
+ grok.context(IGhostAsset)
+ grok.view(AssetEditTab)
+ grok.order(10)
+
+ label = _(u'Edit ghost')
+ ignoreContent = False
+ dataManager = silvaforms.SilvaDataManager
+ dataValidators = [TargetValidator('haunted', IAsset, adding=False)]
+ fields = silvaforms.Fields(IGhostAssetSchema).omit('id')
+ actions = silvaforms.Actions(
+ silvaforms.CancelEditAction(),
+ silvaforms.EditAction())
+
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/GhostAsset/views.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Products/Silva/GhostAsset/views.py Fri Nov 08 16:36:46 2013 +0100
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2002-2013 Infrae. All rights reserved.
+# See also LICENSE.txt
+
+# Zope 3
+from five import grok
+from zope.component import getMultiAdapter
+from zope.publisher.interfaces.browser import IBrowserRequest
+
+# Zope 2
+from AccessControl import getSecurityManager
+from AccessControl import Unauthorized
+
+# Silva
+from silva.core.views import views as silvaviews
+from silva.core.interfaces import IGhostAsset, IDownloableAsset
+from silva.translations import translate as _
+from silva.core.views.httpheaders import HTTPResponseHeaders
+from silva.core.views.interfaces import IHTTPResponseHeaders
+
+
+class GhostAssetView(silvaviews.View):
+ grok.context(IGhostAsset)
+ grok.require('zope2.View')
+
+ def render(self):
+ haunted = self.context.get_haunted()
+ if haunted is None:
+ return _(u"This content is unavailable. "
+ u"Please inform the site manager.")
+ permission = self.is_preview and 'Read Silva content' or 'View'
+ if not getSecurityManager().checkPermission(permission, haunted):
+ raise Unauthorized(
+ u"You do not have permission to "
+ u"see the target of this ghost")
+ if IDownloableAsset.providedBy(haunted):
+ return haunted.get_html_tag(
+ request=self.request, preview=self.is_preview)
+ return _(u"This content cannot be previewed.")
+
+
+class GhostAssetDownloadView(silvaviews.View):
+ grok.context(IGhostAsset)
+ grok.require('zope2.View')
+ grok.name('index.html')
+
+ def render(self):
+ haunted = self.context.get_haunted()
+ if haunted is None:
+ self.response.setStatus(404)
+ return u''
+ permission = self.is_preview and 'Read Silva content' or 'View'
+ if not getSecurityManager().checkPermission(permission, haunted):
+ raise Unauthorized(
+ u"You do not have permission to "
+ u"see the target of this ghost")
+ view = getMultiAdapter((haunted, self.request), name='index.html')
+ return view()
+
+
+class GhostAssetResponseHeaders(HTTPResponseHeaders):
+ """This reliably set HTTP headers on file serving, for GET and
+ HEAD requests.
+ """
+ grok.adapts(IBrowserRequest, IGhostAsset)
+
+ def other_headers(self, headers):
+ haunted = self.context.get_haunted()
+ if haunted is not None:
+ headers = getMultiAdapter(
+ (self.request, haunted),
+ IHTTPResponseHeaders)
+ headers.other_headers(headers)
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/GhostFolder.py
--- a/Products/Silva/GhostFolder.py Mon Nov 04 17:03:35 2013 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,423 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2003-2013 Infrae. All rights reserved.
-# See also LICENSE.txt
-
-# Zope 3
-from five import grok
-from zope.component import getUtility
-from zope.lifecycleevent.interfaces import IObjectCreatedEvent
-from zope.lifecycleevent.interfaces import IObjectCopiedEvent
-
-# Zope 2
-from AccessControl import ClassSecurityInfo
-from AccessControl.security import checkPermission
-from Acquisition import aq_parent
-from App.class_init import InitializeClass
-from OFS.interfaces import IObjectClonedEvent
-
-# silva
-from Products.Silva.Folder import Folder
-from Products.Silva.Publication import Publication
-from Products.Silva import SilvaPermissions, helpers
-from Products.Silva.Ghost import GhostBase
-from Products.Silva.Ghost import TargetValidator
-from Products.SilvaMetadata.interfaces import IMetadataService
-
-from silva.core import conf as silvaconf
-from silva.core.interfaces import IAddableContents
-from silva.core.interfaces import IOrderManager, IPublicationWorkflow
-from silva.core.interfaces import (
- IContainer, IContent, IGhost,
- IPublication, IGhostFolder)
-from silva.core.conf.interfaces import IIdentifiedContent
-from silva.core.references.reference import Reference
-from silva.ui.menu import ContentMenu, MenuItem
-from silva.translations import translate as _
-
-from zeam.form import silva as silvaforms
-
-
-class Sync(object):
-
- def __init__(self, target_container, target, ghost_container, ghost):
- self.target_container = target_container
- self.target = target
- self.ghost_container = ghost_container
- self.ghost = ghost
-
- self.target_id = target.getId()
- if ghost is not None:
- self.ghost_id = ghost.getId()
-
- def get_real_target(self):
- return self.target
-
- def update(self):
- raise NotImplementedError
-
- def verify(self):
- return True
-
- def create(self):
- raise NotImplementedError
-
- finish = None
-
-
-class SyncContainer(Sync):
-
- def create(self):
- target = self.get_real_target()
- if target is not None:
- factory = self.ghost_container.manage_addProduct['Silva']
- factory.manage_addGhostFolder(self.target_id, None)
- self.ghost = self.ghost_container._getOb(self.target_id)
- self.ghost.set_haunted(target, auto_delete=True)
- return self.ghost
-
- def verify(self):
- return self.get_real_target() == self.ghost.get_haunted()
-
- def update(self):
- self.ghost.set_haunted(self.get_real_target(), auto_delete=True)
- return self.ghost
-
- def finish(self):
- ## XXX: I don't think that works (like ever worked)
- target = self.get_real_target()
- if target is not None:
- orderer = IOrderManager(self.ghost)
- for index, content in enumerate(target.get_ordered_publishables()):
- orderer.move(content, index)
-
-
-class SyncGhostContainer(SyncContainer):
-
- def get_real_target(self):
- return self.target.get_haunted()
-
-
-class SyncContent(Sync):
-
- def create(self):
- target = self.get_real_target()
- if target is not None:
- factory = self.ghost_container.manage_addProduct['Silva']
- factory.manage_addGhost(self.target_id, None)
- self.ghost = self.ghost_container._getOb(self.target_id)
- version = self.ghost.get_editable()
- version.set_haunted(self.get_real_target(), auto_delete=True)
- IPublicationWorkflow(self.ghost).publish()
- return self.ghost
-
- def verify(self):
- return self.get_real_target() == self.ghost.get_haunted()
-
- def update(self):
- publication = IPublicationWorkflow(self.ghost)
- if self.ghost.get_editable() is None:
- publication.new_version()
- version = self.ghost.get_editable()
- version.set_haunted(self.get_real_target(), auto_delete=True)
- publication.publish()
- return self.ghost
-
-
-class SyncGhost(SyncContent):
-
- def get_real_target(self):
- return self.target.get_haunted()
-
-
-class SyncCopy(Sync):
- # this is anything else -- copy it. We cannot check if it was
- # modified and if copying is really necessary.
-
- def create(self):
- ghost = self.target._getCopy(self.ghost_container)
- self.ghost_container._setObject(self.target_id, ghost)
- return self.ghost_container._getOb(self.target_id)
-
- def update(self):
- assert self.ghost is not None
- self.ghost_container.manage_delObjects([self.target_id])
- return self.create()
-
-
-
-
-# sync map... (haunted objects interface, ghost objects interface,
-# update/create class) order is important, i.e. interfaces are
-# checked in this order.
-SYNC_MAP = [
- (IGhostFolder, IGhostFolder, SyncGhostContainer),
- (IContainer, IContainer, SyncContainer),
- (IGhost, IGhost, SyncGhost),
- (IContent, IGhost, SyncContent),
- (None, None, SyncCopy),
- ]
-
-
-class GhostFolder(GhostBase, Folder):
- __doc__ = _("""Ghost Folders are similar to Ghosts, but instead of being a
- placeholder for a document, they create placeholders and/or copies of all
- the contents of the ‘original’ folder. The advantage of Ghost
- Folders is the contents stay in sync with the original, by manual or
- automatic resyncing. Note that when a folder is
- ghosted, assets – such as Images and Files – are copied
- (physically duplicated) while documents are ghosted.""")
-
- meta_type = 'Silva Ghost Folder'
-
- grok.implements(IGhostFolder)
- silvaconf.icon('icons/ghost_folder.png')
- silvaconf.priority(0)
-
- security = ClassSecurityInfo()
-
- security.declareProtected(
- SilvaPermissions.ApproveSilvaContent, 'haunt')
- def haunt(self):
- """populate the the ghost folder with ghosts
- """
- haunted = self.get_haunted()
- if haunted is None:
- return
- stack = self._haunt_diff(haunted, self)
- updaters = [SyncContainer(None, haunted, None, self)]
-
- while stack:
- # breadth first search
- h_container, h_id, g_container, g_id = stack.pop(0)
- if h_id is None:
- # object was removed from haunted, so just remove it and
- # continue
- g_container.manage_delObjects([g_id])
- continue
- h_ob = h_container._getOb(h_id)
-
- if g_id is None:
- # object was added to haunted
- g_ob = None
- else:
- # object is there but may have changed
- g_ob = g_container._getOb(g_id)
- g_ob_new = None
-
- for h_if, g_if, factory in SYNC_MAP:
- if h_if and not h_if.providedBy(h_ob):
- continue
- if g_ob is None:
- # matching haunted interface, no ghost -> create
- updater = factory(h_container, h_ob, g_container, g_ob)
- g_ob_new = updater.create()
- if updater.finish is not None:
- updaters.append(updater)
- break
- if g_if and not g_if.providedBy(g_ob):
- # haunted interface machces but ghost interface doesn't
- continue
- # haunted interface and ghost interface match -> update
- updater = factory(h_container, h_ob, g_container, g_ob)
- # if the object is not uptodate, update it
- if not updater.verify():
- g_ob_new = updater.update()
- if updater.finish is not None:
- updaters.append(updater)
- else:
- g_ob_new = g_ob
- break
- else:
- assert True, "no updater was called for %r" % (
- (h_container, h_ob, g_container, g_ob),)
- if IContainer.providedBy(h_ob) and g_ob_new is not None:
- stack.extend(self._haunt_diff(h_ob, g_ob_new))
- for updater in updaters:
- updater.finish()
-
- def _haunt_diff(self, haunted, ghost):
- """diffes two containers
-
- haunted: IContainer, container to be haunted
- ghost: IContainer, ghost
-
- returns list of tuple:
- [(haunted, h_id, ghost, g_id)]
- whereby
- h_id is the haunted object's id or None if a ghost exists but
- no object to be haunted
- g_id is the ghost's id or None if the ghost doesn't exist but
- has to be created
- haunted and ghost are the objects passed in
- """
- assert IContainer.providedBy(haunted)
- assert IContainer.providedBy(ghost)
- h_ids = list(haunted.objectIds())
- g_ids = list(ghost.objectIds())
- h_ids.sort()
- g_ids.sort()
- ids = []
- while h_ids or g_ids:
- h_id = None
- g_id = None
- if h_ids:
- h_id = h_ids[0]
- if g_ids:
- g_id = g_ids[0]
- if h_id == g_id or h_id is None or g_id is None:
- ids.append((h_id, g_id))
- if h_ids:
- del h_ids[0]
- if g_ids:
- del g_ids[0]
- elif h_id < g_id:
- ids.append((h_id, None))
- del h_ids[0]
- elif h_id > g_id:
- ids.append((None, g_id))
- del g_ids[0]
- return [(haunted, h_id, ghost, g_id) for (h_id, g_id) in ids]
-
- security.declareProtected(
- SilvaPermissions.ApproveSilvaContent, 'to_publication')
- def to_publication(self):
- """replace self with a folder"""
- haunted = self.get_haunted()
- if haunted is not None:
- binding = getUtility(IMetadataService).getMetadata(haunted)
- data_content = binding.get('silva-content', acquire=0)
- data_extra = binding.get('silva-extra', acquire=0)
- helpers.convert_content(self, Publication)
- if haunted is not None:
- binding = getUtility(IMetadataService).getMetadata(self)
- binding.setValues('silva-content', data_content)
- binding.setValues('silva-extra', data_extra)
-
- security.declareProtected(
- SilvaPermissions.ApproveSilvaContent, 'to_folder')
- def to_folder(self):
- """replace self with a folder"""
- haunted = self.get_haunted()
- if haunted is not None:
- binding = getUtility(IMetadataService).getMetadata(haunted)
- data_content = binding.get('silva-content', acquire=0)
- data_extra = binding.get('silva-extra', acquire=0)
- helpers.convert_content(self, Folder)
- if haunted is not None:
- binding = getUtility(IMetadataService).getMetadata(self)
- binding.setValues('silva-content', data_content)
- binding.setValues('silva-extra', data_extra)
-
- # all this is for a nice side bar
- def is_transparent(self):
- """show in subtree? depends on haunted object"""
- content = self.get_haunted()
- if IContainer.providedBy(content):
- return content.is_transparent()
- return 0
-
- security.declareProtected(SilvaPermissions.View,'get_publication')
- def get_publication(self):
- """returns self if haunted object is a publication"""
- content = self.get_haunted()
- if IPublication.providedBy(content):
- return self.aq_inner
- return aq_parent(self).get_publication()
-
- def is_deletable(self):
- pass
-
-
-InitializeClass(GhostFolder)
-
-
-class IGhostFolderSchema(IIdentifiedContent):
-
- haunted = Reference(IContainer,
- title=_(u"Target"),
- description=_(u"The internal folder the ghost is mirroring"),
- required=True)
-
-
-class SyncAction(silvaforms.Action):
- description = _(u"Synchronize target and ghost folder content")
- ignoreRequest = True
-
- def available(self, form):
- return checkPermission('silva.ChangeSilvaContent', form.context)
-
- def __call__(self, form):
- folder = form.context
- if folder.get_link_status() is None:
- folder.haunt()
- form.send_message(
- _(u'Ghost Folder synchronized.'), type='feedback')
- return silvaforms.SUCCESS
- form.send_message(
- _(u'Ghost Folder was not synchronized, because the target is invalid.'),
- type='error')
- return silvaforms.FAILURE
-
-
-class GhostFolderAddForm(silvaforms.SMIAddForm):
- """ Add form for ghost folders
- """
- grok.name(u'Silva Ghost Folder')
-
- fields = silvaforms.Fields(IGhostFolderSchema)
- fields['haunted'].referenceNotSetLabel = _(
- u"Click the Lookup button to select a container to haunt.")
- dataValidators = [
- TargetValidator('haunted', is_folderish=True, adding=True)]
-
- def _add(self, parent, data):
- factory = parent.manage_addProduct['Silva']
- return factory.manage_addGhostFolder(
- data['id'], None, haunted=data['haunted'])
-
-
-class GhostFolderEditForm(silvaforms.SMIEditForm):
- """ Edit form Ghost Folder
- """
- grok.context(IGhostFolder)
- grok.name('silva.ui.edit')
-
- fields = silvaforms.Fields(IGhostFolderSchema).omit('id')
- dataValidators = [
- TargetValidator('haunted', is_folderish=True, adding=False)]
- actions = silvaforms.SMIEditForm.actions + SyncAction(_(u'Synchronize'))
-
-
-class GhostFolderEditMenu(MenuItem):
- grok.adapts(ContentMenu, IGhostFolder)
- grok.order(10.1) # Goes right after the content tab.
- name = _(u'Edit')
- screen = GhostFolderEditForm
-
-
[email protected](IGhostFolder, IObjectCreatedEvent)
[email protected](IGhostFolder, IObjectClonedEvent)
-def haunt_created_folder(folder, event):
- if (folder != event.object or
- IObjectCopiedEvent.providedBy(event)):
- return
- # If the ghost folder is in a valid state after creation or copy,
- # haunt its content.
- if folder.get_link_status() is None:
- folder.haunt()
-
-
-class AddableContents(grok.Adapter):
- grok.context(IGhostFolder)
- grok.implements(IAddableContents)
- grok.provides(IAddableContents)
-
- def get_authorized_addables(self):
- return []
-
- def get_container_addables(self):
- return []
-
- def get_all_addables(self):
- return []
-
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/GhostFolder/__init__.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Products/Silva/GhostFolder/__init__.py Fri Nov 08 16:36:46 2013 +0100
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2003-2013 Infrae. All rights reserved.
+# See also LICENSE.txt
+
+# BBB
+from .content import GhostFolder
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/GhostFolder/addables.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Products/Silva/GhostFolder/addables.py Fri Nov 08 16:36:46 2013 +0100
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013 Infrae. All rights reserved.
+# See also LICENSE.txt
+
+from five import grok
+from silva.core.interfaces import IGhostFolder, IAddableContents
+
+
+class AddableContents(grok.Adapter):
+ grok.context(IGhostFolder)
+ grok.implements(IAddableContents)
+ grok.provides(IAddableContents)
+
+ def get_authorized_addables(self):
+ return []
+
+ def get_container_addables(self):
+ return []
+
+ def get_all_addables(self):
+ return []
+
+
+
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/GhostFolder/content.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Products/Silva/GhostFolder/content.py Fri Nov 08 16:36:46 2013 +0100
@@ -0,0 +1,264 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2003-2013 Infrae. All rights reserved.
+# See also LICENSE.txt
+
+# Zope 3
+from five import grok
+from zeam.component import component, getComponent
+from zope.component import getUtility
+from zope.lifecycleevent.interfaces import IObjectCopiedEvent
+from zope.lifecycleevent.interfaces import IObjectCreatedEvent
+
+# Zope 2
+from AccessControl import ClassSecurityInfo
+from Acquisition import aq_parent
+from App.class_init import InitializeClass
+from OFS.interfaces import IObjectClonedEvent
+
+# silva
+from Products.Silva import SilvaPermissions, helpers
+from Products.Silva.Folder import Folder
+from Products.Silva.Ghost import GhostBase
+from Products.Silva.Ghost import GhostBaseManipulator, GhostBaseManager
+from Products.Silva.Publication import Publication
+from Products.SilvaMetadata.interfaces import IMetadataService
+
+from silva.core import conf as silvaconf
+from silva.core.interfaces import IContainer, IGhostFolder, IGhostManager
+from silva.core.interfaces import IPublication
+from silva.core.interfaces.errors import ContainerInvalidTarget
+from silva.translations import translate as _
+
+
+class CopyManipulator(GhostBaseManipulator):
+
+ def make_copy(self):
+ ghost = self.target._getCopy(self.manager.container)
+ self.manager.container._setObject(self.identifier, ghost)
+ # Publish if needed ?
+ return self.manager.container._getOb(self.identifier)
+
+ def create(self):
+ assert self.manager.ghost is None
+ ghost = self.make_copy()
+ self.manager.ghost = ghost
+ return ghost
+
+ def update(self):
+ assert self.manager.ghost is not None
+ self.manager.container.manage_delObjects([self.identifier])
+ ghost = self.make_copy()
+ self.manager.ghost = ghost
+ return ghost
+
+ def need_update(self):
+ return True
+
+
+class CopyManager(GhostBaseManager):
+ manipulator = CopyManipulator
+
+
+def get_factory(target):
+ return getComponent((target,), IGhostManager, default=CopyManager)
+
+
+class GhostFolder(GhostBase, Folder):
+ __doc__ = _("""Ghost Folders are similar to Ghosts, but instead of being a
+ placeholder for a document, they create placeholders and/or copies of all
+ the contents of the ‘original’ folder. The advantage of Ghost
+ Folders is the contents stay in sync with the original, by manual or
+ automatic resyncing. Note that when a folder is
+ ghosted, assets – such as Images and Files – are copied
+ (physically duplicated) while documents are ghosted.""")
+
+ meta_type = 'Silva Ghost Folder'
+
+ grok.implements(IGhostFolder)
+ silvaconf.icon('icons/ghost_folder.png')
+ silvaconf.priority(0)
+
+ security = ClassSecurityInfo()
+
+ security.declareProtected(
+ SilvaPermissions.ApproveSilvaContent, 'haunt')
+ def haunt(self):
+ """populate the the ghost folder with ghosts
+ """
+ haunted = self.get_haunted()
+ if haunted is None:
+ return False
+ stack = self._haunt_diff(haunted, self)
+
+ while stack:
+ # breadth first search
+ h_container, h_id, g_container, g_id = stack.pop(0)
+
+ if h_id is None:
+ # object was removed from haunted, so just remove it and
+ # continue
+ g_container.manage_delObjects([g_id])
+ continue
+
+ h_ob = h_container._getOb(h_id)
+ g_ob = None
+ if g_id is not None:
+ g_ob = g_container._getOb(g_id)
+
+ g_ob = get_factory(h_ob)(
+ ghost=g_ob,
+ container=g_container,
+ auto_delete=True,
+ auto_publish=True).modify(h_ob, h_id).verify()
+
+ if IContainer.providedBy(h_ob) and g_ob is not None:
+ stack.extend(self._haunt_diff(h_ob, g_ob))
+
+ return True
+
+ def _haunt_diff(self, haunted, ghost):
+ """diffes two containers
+
+ haunted: IContainer, container to be haunted
+ ghost: IContainer, ghost
+
+ returns list of tuple:
+ [(haunted, h_id, ghost, g_id)]
+ whereby
+ h_id is the haunted object's id or None if a ghost exists but
+ no object to be haunted
+ g_id is the ghost's id or None if the ghost doesn't exist but
+ has to be created
+ haunted and ghost are the objects passed in
+ """
+ assert IContainer.providedBy(haunted)
+ assert IContainer.providedBy(ghost)
+ h_ids = list(haunted.objectIds())
+ g_ids = list(ghost.objectIds())
+ h_ids.sort()
+ g_ids.sort()
+ ids = []
+ while h_ids or g_ids:
+ h_id = None
+ g_id = None
+ if h_ids:
+ h_id = h_ids[0]
+ if g_ids:
+ g_id = g_ids[0]
+ if h_id == g_id or h_id is None or g_id is None:
+ ids.append((h_id, g_id))
+ if h_ids:
+ del h_ids[0]
+ if g_ids:
+ del g_ids[0]
+ elif h_id < g_id:
+ ids.append((h_id, None))
+ del h_ids[0]
+ elif h_id > g_id:
+ ids.append((None, g_id))
+ del g_ids[0]
+ return [(haunted, h_id, ghost, g_id) for (h_id, g_id) in ids]
+
+ security.declareProtected(
+ SilvaPermissions.ApproveSilvaContent, 'to_publication')
+ def to_publication(self):
+ """replace self with a folder"""
+ haunted = self.get_haunted()
+ if haunted is not None:
+ binding = getUtility(IMetadataService).getMetadata(haunted)
+ data_content = binding.get('silva-content', acquire=0)
+ data_extra = binding.get('silva-extra', acquire=0)
+ helpers.convert_content(self, Publication)
+ if haunted is not None:
+ binding = getUtility(IMetadataService).getMetadata(self)
+ binding.setValues('silva-content', data_content)
+ binding.setValues('silva-extra', data_extra)
+
+ security.declareProtected(
+ SilvaPermissions.ApproveSilvaContent, 'to_folder')
+ def to_folder(self):
+ """replace self with a folder"""
+ haunted = self.get_haunted()
+ if haunted is not None:
+ binding = getUtility(IMetadataService).getMetadata(haunted)
+ data_content = binding.get('silva-content', acquire=0)
+ data_extra = binding.get('silva-extra', acquire=0)
+ helpers.convert_content(self, Folder)
+ if haunted is not None:
+ binding = getUtility(IMetadataService).getMetadata(self)
+ binding.setValues('silva-content', data_content)
+ binding.setValues('silva-extra', data_extra)
+
+ security.declareProtected(SilvaPermissions.View,'get_publication')
+ def get_publication(self):
+ """returns self if haunted object is a publication"""
+ content = self.get_haunted()
+ if IPublication.providedBy(content):
+ return self.aq_inner
+ return aq_parent(self).get_publication()
+
+ def is_transparent(self):
+ """show in subtree? depends on haunted object"""
+ content = self.get_haunted()
+ if IContainer.providedBy(content):
+ return content.is_transparent()
+ return 0
+
+ def is_deletable(self):
+ pass
+
+
+InitializeClass(GhostFolder)
+
+
+class GhostFolderManipulator(GhostBaseManipulator):
+
+ def create(self):
+ assert self.manager.ghost is None
+ factory = self.manager.container.manage_addProduct['Silva']
+ factory.manage_addGhostFolder(self.identifier, None)
+ ghost = self.manager.container._getOb(self.identifier)
+ ghost.set_haunted(self.target, auto_delete=self.manager.auto_delete)
+ self.manager.ghost = ghost
+ return ghost
+
+ def update(self):
+ assert self.manager.ghost is not None
+ if IGhostFolder.providedBy(self.manager.ghost):
+ self.manager.ghost.set_haunted(
+ self.target, auto_delete=self.manager.auto_delete)
+ return None
+ self.manager.container.manage_delObjects([self.identifier])
+ self.manager.ghost = None
+ return self.create()
+
+ def need_update(self):
+ if IGhostFolder.providedBy(self.manager.ghost):
+ return self.target != self.manager.ghost.get_haunted()
+ # Only update if the invalid ghost is an asset.
+ return IContainer.providedBy(self.manager.ghost)
+
+
+@component(IContainer, provides=IGhostManager)
+class GhostFolderManager(GhostBaseManager):
+ manipulator = GhostFolderManipulator
+
+ def validate(self, target, adding=False):
+ error = super(GhostFolderManager, self).validate(target, adding)
+ if error is None:
+ if not IContainer.providedBy(target):
+ return ContainerInvalidTarget()
+ return error
+
+
[email protected](IGhostFolder, IObjectCreatedEvent)
[email protected](IGhostFolder, IObjectClonedEvent)
+def haunt_created_folder(folder, event):
+ if (folder != event.object or
+ IObjectCopiedEvent.providedBy(event)):
+ return
+ # If the ghost folder is in a valid state after creation or copy,
+ # haunt its content.
+ if folder.get_link_status() is None:
+ folder.haunt()
+
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/GhostFolder/smi.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Products/Silva/GhostFolder/smi.py Fri Nov 08 16:36:46 2013 +0100
@@ -0,0 +1,79 @@
+
+
+# Zope 3
+from five import grok
+
+# Zope 2
+from AccessControl.security import checkPermission
+
+# Silva
+from Products.Silva.Ghost import TargetValidator
+
+from silva.core.conf.interfaces import IIdentifiedContent
+from silva.core.references.reference import Reference
+from silva.core.interfaces import IContainer, IGhostFolder
+from silva.ui.menu import ContentMenu, MenuItem
+from silva.translations import translate as _
+from zeam.form import silva as silvaforms
+
+
+class IGhostFolderSchema(IIdentifiedContent):
+
+ haunted = Reference(IContainer,
+ title=_(u"Target"),
+ description=_(u"The internal folder the ghost is mirroring"),
+ required=True)
+
+
+class SyncAction(silvaforms.Action):
+ description = _(u"Synchronize target and ghost folder content")
+ ignoreRequest = True
+
+ def available(self, form):
+ return checkPermission('silva.ChangeSilvaContent', form.context)
+
+ def __call__(self, form):
+ folder = form.context
+ if folder.get_link_status() is None:
+ folder.haunt()
+ form.send_message(
+ _(u'Ghost Folder synchronized.'), type='feedback')
+ return silvaforms.SUCCESS
+ form.send_message(
+ _(u'Ghost Folder was not synchronized, because the target is invalid.'),
+ type='error')
+ return silvaforms.FAILURE
+
+
+class GhostFolderAddForm(silvaforms.SMIAddForm):
+ """ Add form for ghost folders
+ """
+ grok.name(u'Silva Ghost Folder')
+
+ fields = silvaforms.Fields(IGhostFolderSchema)
+ fields['haunted'].referenceNotSetLabel = _(
+ u"Click the Lookup button to select a container to haunt.")
+ dataValidators = [TargetValidator('haunted', IContainer, adding=True)]
+
+ def _add(self, parent, data):
+ factory = parent.manage_addProduct['Silva']
+ return factory.manage_addGhostFolder(
+ data['id'], None, haunted=data['haunted'])
+
+
+class GhostFolderEditForm(silvaforms.SMIEditForm):
+ """ Edit form Ghost Folder
+ """
+ grok.context(IGhostFolder)
+ grok.name('silva.ui.edit')
+
+ fields = silvaforms.Fields(IGhostFolderSchema).omit('id')
+ dataValidators = [TargetValidator('haunted', IContainer, adding=False)]
+ actions = silvaforms.SMIEditForm.actions + SyncAction(_(u'Synchronize'))
+
+
+class GhostFolderEditMenu(MenuItem):
+ grok.adapts(ContentMenu, IGhostFolder)
+ grok.order(10.1) # Goes right after the content tab.
+ name = _(u'Edit')
+ screen = GhostFolderEditForm
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/Image/content.py
--- a/Products/Silva/Image/content.py Mon Nov 04 17:03:35 2013 +0100
+++ b/Products/Silva/Image/content.py Fri Nov 08 16:36:46 2013 +0100
@@ -9,6 +9,7 @@
import os.path
import re
import time
+import warnings
# Zope 3
from five import grok
@@ -23,13 +24,13 @@
# Silva
from silva.core import conf as silvaconf
+from silva.core import interfaces
from silva.core.conf.utils import ISilvaFactoryDispatcher
-from silva.core import interfaces
+from silva.core.interfaces import ContentError
from silva.core.interfaces import IMimeTypeClassifier, ISilvaNameChooser
-from silva.core.interfaces import ContentError
from silva.core.services.interfaces import IFilesService
+from silva.core.views.interfaces import IContentURL
from silva.translations import translate as _
-from silva.core.views.interfaces import IContentURL
from .. import SilvaPermissions
from ..Asset import Asset
@@ -428,7 +429,18 @@
security.declareProtected(SilvaPermissions.View, 'tag')
def tag(self, hires=False, thumbnail=False,
- request=None, preview=False, **extra_attributes):
+ request=None, preview=False, **extra_attributes):
+ warnings.warn(
+ 'tag have been replaced with get_html_tag. '
+ 'It will be removed, please update your code.',
+ DeprecationWarning, stacklevel=2)
+ return self.get_html_tag(
+ hires=hires, thumbnail=thumbnail,
+ request=request, preview=preview, **extra_attributes)
+
+ security.declareProtected(SilvaPermissions.View, 'get_html_tag')
+ def get_html_tag(self, preview=False, request=None, hires=False,
+ thumbnail=False, **extra_attributes):
""" return xhtml tag
Since 'class' is a Python reserved word, it cannot be passed in
@@ -437,10 +449,8 @@
will accept a 'css_class' argument that will be converted to
'class' in the output tag to work around this.
"""
- url = self.url(request=request,
- preview=preview,
- hires=hires,
- thumbnail=thumbnail)
+ url = self.get_download_url(
+ request=request, preview=preview, hires=hires, thumbnail=thumbnail)
title = self.get_title_or_id()
width, height = self.get_dimensions(thumbnail=thumbnail, hires=hires)
@@ -463,6 +473,16 @@
security.declareProtected(SilvaPermissions.View, 'url')
def url(self, hires=False, thumbnail=False, request=None, preview=False):
+ warnings.warn(
+ 'url have been replaced with get_download_url. '
+ 'It will be removed, please update your code.',
+ DeprecationWarning, stacklevel=2)
+ return self.get_download_url(
+ hires=hires, thumbnail=thumbnail,
+ request=request, preview=preview)
+
+ security.declareProtected(SilvaPermissions.View, 'get_download_url')
+ def get_download_url(self, preview=False, request=None, hires=False, thumbnail=False):
"return url of image"
if request is None:
request = self.REQUEST
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/Image/views.py
--- a/Products/Silva/Image/views.py Mon Nov 04 17:03:35 2013 +0100
+++ b/Products/Silva/Image/views.py Fri Nov 08 16:36:46 2013 +0100
@@ -29,10 +29,11 @@
thumbnail = False
def render(self):
- return self.content.tag(request=self.request,
- hires=self.hires,
- preview=self.is_preview,
- thumbnail=self.thumbnail)
+ return self.content.get_html_tag(
+ request=self.request,
+ hires=self.hires,
+ preview=self.is_preview,
+ thumbnail=self.thumbnail)
class ImageDownloadView(FileDownloadView):
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/adapters/haunted.py
--- a/Products/Silva/adapters/haunted.py Mon Nov 04 17:03:35 2013 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,36 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2002-2013 Infrae. All rights reserved.
-# See also LICENSE.txt
-
-from five import grok
-from silva.core import interfaces
-from silva.core.references.interfaces import IReferenceService
-from zope.component import getUtility
-
-
-class Haunted(grok.Adapter):
- """Adapted content for retrieving the 'iterator' of haunting
- objects (Ghosts).
- """
- grok.implements(interfaces.IHaunted)
- grok.context(interfaces.IContent)
-
- def getHaunting(self):
- service = getUtility(IReferenceService)
- for reference in service.get_references_to(
- self.context, name=u'haunted'):
- yield reference.source.get_silva_object()
-
-
-class HauntedGhost(Haunted):
- """Adapted content for retrieving the 'iterator' of haunting
- objects (Ghosts).
- """
- grok.context(interfaces.IGhost)
-
- def getHaunting(self):
- # Nothing to look for - Ghost cannot be haunted. Don't yield anything
- # XXX how to not yield anything??
- if None:
- yield None
-
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/install.py
--- a/Products/Silva/install.py Mon Nov 04 17:03:35 2013 +0100
+++ b/Products/Silva/install.py Fri Nov 08 16:36:46 2013 +0100
@@ -121,6 +121,7 @@
'Add Silva Folders',
'Add Silva Ghost Versions',
'Add Silva Ghosts',
+ 'Add Silva Ghost Assets',
'Add Silva Links',
'Add Silva Link Versions',
'Add Silva Images',
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/silvaxml/xmlexport.py
--- a/Products/Silva/silvaxml/xmlexport.py Mon Nov 04 17:03:35 2013 +0100
+++ b/Products/Silva/silvaxml/xmlexport.py Fri Nov 08 16:36:46 2013 +0100
@@ -169,6 +169,19 @@
self.endElement('image')
+class GhostAssetProducer(producers.SilvaProducer):
+ """Export a verson of a Silva Asset to XML.
+ """
+ grok.adapts(interfaces.IGhostAsset, Interface)
+
+ def sax(self):
+ self.startElement('ghost_asset', {'id': self.context.id})
+ self.startElement('haunted')
+ self.characters(self.get_reference(u'haunted'))
+ self.endElement('haunted')
+ self.endElement('ghost_asset')
+
+
class IndexerProducer(producers.SilvaProducer):
"""Export an IndexerProducer to XML.
"""
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/silvaxml/xmlimport.py
--- a/Products/Silva/silvaxml/xmlimport.py Mon Nov 04 17:03:35 2013 +0100
+++ b/Products/Silva/silvaxml/xmlimport.py Fri Nov 08 16:36:46 2013 +0100
@@ -3,8 +3,9 @@
# See also LICENSE.txt
from five import grok
+from zope.cachedescriptors.property import Lazy
+from zeam.component import getComponent
-from Products.Silva.Ghost import validate_target
from silva.core import conf as silvaconf
from silva.core import interfaces
from silva.core.interfaces.events import IContentImported
@@ -151,6 +152,10 @@
(NS_SILVA_URI, 'haunted'):
self.handlerFactories.contentHandler('haunted'),}
+ @Lazy
+ def _getManager(self):
+ return getComponent((interfaces.IGhost,), interfaces.IGhostManager)
+
def _createVersion(self, identifier):
factory = self.parent().manage_addProduct['Silva']
factory.manage_addGhostVersion(identifier, None)
@@ -162,20 +167,21 @@
def endElementNS(self, name, qname):
if name == (NS_SILVA_URI, 'content'):
importer = self.getExtra()
- ghost = self.result()
+ version = self.result()
haunted = self.getData('haunted')
if not haunted:
- importer.reportProblem(_(u'Missing ghost target.'), ghost)
+ importer.reportProblem(_(u'Missing ghost target.'), version)
else:
def set_target(target):
- problem = validate_target(ghost, target)
+ problem = self._getManager(
+ ghost=version.get_silva_object()).validate(target)
if problem is not None:
- importer.reportProblem(problem.doc(), ghost)
+ importer.reportProblem(problem.doc(), version)
else:
- ghost.set_haunted(target)
+ version.set_haunted(target)
- importer.resolveImportedPath(ghost, set_target, haunted)
+ importer.resolveImportedPath(version, set_target, haunted)
self.updateVersionCount()
self.storeWorkflow()
@@ -187,6 +193,10 @@
return {
(NS_SILVA_URI, 'haunted'):
self.handlerFactories.contentHandler('haunted'),}
+ @Lazy
+ def _getManager(self):
+ return getComponent(
+ (interfaces.IGhostFolder,), interfaces.IGhostManager)
def _createContent(self, identifier):
factory = self.parent().manage_addProduct['Silva']
@@ -209,7 +219,7 @@
else:
def set_target(target):
- problem = validate_target(folder, target, is_folderish=True)
+ problem = self._getManager(ghost=folder).validate(target)
if problem is not None:
importer.reportProblem(problem.doc(), folder)
else:
@@ -363,6 +373,49 @@
self.notifyImport()
+class GhostAssetHandler(handlers.SilvaHandler):
+ grok.name('ghost_asset')
+
+ def getOverrides(self):
+ return {
+ (NS_SILVA_URI, 'haunted'):
+ self.handlerFactories.contentHandler('haunted'),}
+
+ @Lazy
+ def _getManager(self):
+ return getComponent((interfaces.IGhostAsset,), interfaces.IGhostManager)
+
+ def _createContent(self, identifier):
+ factory = self.parent().manage_addProduct['Silva']
+ factory.manage_addGhostAsset(identifier, None)
+
+ def _verifyContent(self, content):
+ return interfaces.IGhostAsset.providedBy(content)
+
+ def startElementNS(self, name, qname, attrs):
+ if name == (NS_SILVA_URI, 'ghost_asset'):
+ self.createContent(attrs)
+
+ def endElementNS(self, name, qname):
+ if name == (NS_SILVA_URI, 'ghost_asset'):
+ importer = self.getExtra()
+ asset = self.result()
+ haunted = self.getData('haunted')
+ if not haunted:
+ importer.reportProblem(_(u'Missing ghost target.'), asset)
+ else:
+
+ def set_target(target):
+ problem = self._getManager(ghost=asset).validate(target)
+ if problem is not None:
+ importer.reportProblem(problem.doc(), asset)
+ else:
+ asset.set_haunted(target)
+
+ importer.resolveImportedPath(asset, set_target, haunted)
+ self.notifyImport()
+
+
class UnknownContentHandler(handlers.SilvaHandler):
"""Importer for content which have been exported in a ZEXP.
"""
@@ -380,7 +433,7 @@
def endElementNS(self, name, qname):
if name == (NS_SILVA_URI, 'unknown_content'):
extra = self.getExtra()
- # XXX check non
+ # XXX check name
identifier = self.getData('id')
import_file = extra.getFile('zexps/' + self.getData('zexp'))
content = extra.root._p_jar.importFile(import_file)
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/tests/data/test_export_ghost_asset.silvaxml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Products/Silva/tests/data/test_export_ghost_asset.silvaxml Fri Nov 08 16:36:46 2013 +0100
@@ -0,0 +1,100 @@
+<?xml version='1.0' encoding='utf-8'?>
+<silva xmlns="http://infrae.com/namespace/silva" {namespaces} silva_version="{version}">
+ <folder id="folder">
+ <metadata>
+ <set id="silva-content">
+ <silva-content:maintitle>
+ This is <boo>a</boo> folder
+ </silva-content:maintitle>
+ <silva-content:shorttitle>
+ </silva-content:shorttitle>
+ </set>
+ <set id="silva-extra">
+ <silva-extra:comment>
+ </silva-extra:comment>
+ <silva-extra:contactemail>
+ </silva-extra:contactemail>
+ <silva-extra:contactname>
+ </silva-extra:contactname>
+ <silva-extra:content_description>
+ </silva-extra:content_description>
+ <silva-extra:creationtime>
+ YYYY-MM-DDTHH:MM:SS
+ </silva-extra:creationtime>
+ <silva-extra:creator>
+ author
+ </silva-extra:creator>
+ <silva-extra:keywords>
+ </silva-extra:keywords>
+ <silva-extra:language>
+ </silva-extra:language>
+ <silva-extra:lastauthor>
+ editor
+ </silva-extra:lastauthor>
+ <silva-extra:modificationtime>
+ YYYY-MM-DDTHH:MM:SS
+ </silva-extra:modificationtime>
+ <silva-extra:subject>
+ </silva-extra:subject>
+ </set>
+ <set xmlns:silvasettings="http://infrae.com/namespace/metadata/silva-settings" id="silva-settings">
+ <silvasettings:hide_from_tocs>
+ do not hide
+ </silvasettings:hide_from_tocs>
+ </set>
+ </metadata>
+ <content>
+ <ghost_asset id="ghost">
+ <haunted>
+ folder/logo
+ </haunted>
+ </ghost_asset>
+ <file id="logo">
+ <metadata>
+ <set id="silva-content">
+ <silva-content:maintitle>
+ Silva Logo
+ </silva-content:maintitle>
+ <silva-content:shorttitle>
+ </silva-content:shorttitle>
+ </set>
+ <set id="silva-extra">
+ <silva-extra:comment>
+ </silva-extra:comment>
+ <silva-extra:contactemail>
+ </silva-extra:contactemail>
+ <silva-extra:contactname>
+ </silva-extra:contactname>
+ <silva-extra:content_description>
+ </silva-extra:content_description>
+ <silva-extra:creationtime>
+ YYYY-MM-DDTHH:MM:SS
+ </silva-extra:creationtime>
+ <silva-extra:creator>
+ editor
+ </silva-extra:creator>
+ <silva-extra:keywords>
+ </silva-extra:keywords>
+ <silva-extra:language>
+ </silva-extra:language>
+ <silva-extra:lastauthor>
+ editor
+ </silva-extra:lastauthor>
+ <silva-extra:modificationtime>
+ YYYY-MM-DDTHH:MM:SS
+ </silva-extra:modificationtime>
+ <silva-extra:subject>
+ </silva-extra:subject>
+ </set>
+ <set xmlns:silvasettings="http://infrae.com/namespace/metadata/silva-settings" id="silva-settings">
+ <silvasettings:hide_from_tocs>
+ do not hide
+ </silvasettings:hide_from_tocs>
+ </set>
+ </metadata>
+ <asset id="1">
+ </asset>
+ </file>
+ </content>
+ </folder>
+</silva>
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/tests/data/test_export_ghost_folder.silvaxml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Products/Silva/tests/data/test_export_ghost_folder.silvaxml Fri Nov 08 16:36:46 2013 +0100
@@ -0,0 +1,254 @@
+<?xml version='1.0' encoding='utf-8'?>
+<silva xmlns="http://infrae.com/namespace/silva" {namespaces} silva_version="{version}">
+ <folder id="folder">
+ <metadata>
+ <set id="silva-content">
+ <silva-content:maintitle>
+ This is <boo>a</boo> folder
+ </silva-content:maintitle>
+ <silva-content:shorttitle>
+ </silva-content:shorttitle>
+ </set>
+ <set id="silva-extra">
+ <silva-extra:comment>
+ </silva-extra:comment>
+ <silva-extra:contactemail>
+ </silva-extra:contactemail>
+ <silva-extra:contactname>
+ </silva-extra:contactname>
+ <silva-extra:content_description>
+ </silva-extra:content_description>
+ <silva-extra:creationtime>
+ YYYY-MM-DDTHH:MM:SS
+ </silva-extra:creationtime>
+ <silva-extra:creator>
+ author
+ </silva-extra:creator>
+ <silva-extra:keywords>
+ </silva-extra:keywords>
+ <silva-extra:language>
+ </silva-extra:language>
+ <silva-extra:lastauthor>
+ chiefeditor
+ </silva-extra:lastauthor>
+ <silva-extra:modificationtime>
+ YYYY-MM-DDTHH:MM:SS
+ </silva-extra:modificationtime>
+ <silva-extra:subject>
+ </silva-extra:subject>
+ </set>
+ <set xmlns:silvasettings="http://infrae.com/namespace/metadata/silva-settings" id="silva-settings">
+ <silvasettings:hide_from_tocs>
+ do not hide
+ </silvasettings:hide_from_tocs>
+ </set>
+ </metadata>
+ <content>
+ <folder id="container">
+ <metadata>
+ <set id="silva-content">
+ <silva-content:maintitle>
+ Content
+ </silva-content:maintitle>
+ <silva-content:shorttitle>
+ </silva-content:shorttitle>
+ </set>
+ <set id="silva-extra">
+ <silva-extra:comment>
+ </silva-extra:comment>
+ <silva-extra:contactemail>
+ </silva-extra:contactemail>
+ <silva-extra:contactname>
+ </silva-extra:contactname>
+ <silva-extra:content_description>
+ </silva-extra:content_description>
+ <silva-extra:creationtime>
+ YYYY-MM-DDTHH:MM:SS
+ </silva-extra:creationtime>
+ <silva-extra:creator>
+ chiefeditor
+ </silva-extra:creator>
+ <silva-extra:keywords>
+ </silva-extra:keywords>
+ <silva-extra:language>
+ </silva-extra:language>
+ <silva-extra:lastauthor>
+ chiefeditor
+ </silva-extra:lastauthor>
+ <silva-extra:modificationtime>
+ YYYY-MM-DDTHH:MM:SS
+ </silva-extra:modificationtime>
+ <silva-extra:subject>
+ </silva-extra:subject>
+ </set>
+ <set xmlns:silvasettings="http://infrae.com/namespace/metadata/silva-settings" id="silva-settings">
+ <silvasettings:hide_from_tocs>
+ do not hide
+ </silvasettings:hide_from_tocs>
+ </set>
+ </metadata>
+ <content>
+ <default>
+ <auto_toc depth="-1" display_desc_flag="False" id="index" show_icon="False" sort_order="silva" types="Silva Document,Silva Publication,Silva Folder">
+ <metadata>
+ <set id="silva-content">
+ <silva-content:maintitle>
+ Content
+ </silva-content:maintitle>
+ <silva-content:shorttitle>
+ </silva-content:shorttitle>
+ </set>
+ <set id="silva-extra">
+ <silva-extra:comment>
+ </silva-extra:comment>
+ <silva-extra:contactemail>
+ </silva-extra:contactemail>
+ <silva-extra:contactname>
+ </silva-extra:contactname>
+ <silva-extra:content_description>
+ </silva-extra:content_description>
+ <silva-extra:creationtime>
+ YYYY-MM-DDTHH:MM:SS
+ </silva-extra:creationtime>
+ <silva-extra:creator>
+ chiefeditor
+ </silva-extra:creator>
+ <silva-extra:keywords>
+ </silva-extra:keywords>
+ <silva-extra:language>
+ </silva-extra:language>
+ <silva-extra:lastauthor>
+ chiefeditor
+ </silva-extra:lastauthor>
+ <silva-extra:modificationtime>
+ YYYY-MM-DDTHH:MM:SS
+ </silva-extra:modificationtime>
+ <silva-extra:subject>
+ </silva-extra:subject>
+ </set>
+ <set xmlns:silvasettings="http://infrae.com/namespace/metadata/silva-settings" id="silva-settings">
+ <silvasettings:hide_from_tocs>
+ do not hide
+ </silvasettings:hide_from_tocs>
+ </set>
+ </metadata>
+ </auto_toc>
+ </default>
+ <link id="link">
+ <workflow>
+ <version id="0">
+ <status>
+ unapproved
+ </status>
+ <publication_datetime>
+ </publication_datetime>
+ <expiration_datetime>
+ </expiration_datetime>
+ </version>
+ </workflow>
+ <content version_id="0">
+ <metadata>
+ <set id="silva-content">
+ <silva-content:maintitle>
+ Infrae
+ </silva-content:maintitle>
+ <silva-content:shorttitle>
+ </silva-content:shorttitle>
+ </set>
+ <set id="silva-extra">
+ <silva-extra:comment>
+ </silva-extra:comment>
+ <silva-extra:contactemail>
+ </silva-extra:contactemail>
+ <silva-extra:contactname>
+ </silva-extra:contactname>
+ <silva-extra:content_description>
+ </silva-extra:content_description>
+ <silva-extra:creationtime>
+ YYYY-MM-DDTHH:MM:SS
+ </silva-extra:creationtime>
+ <silva-extra:creator>
+ chiefeditor
+ </silva-extra:creator>
+ <silva-extra:keywords>
+ </silva-extra:keywords>
+ <silva-extra:language>
+ </silva-extra:language>
+ <silva-extra:lastauthor>
+ chiefeditor
+ </silva-extra:lastauthor>
+ <silva-extra:modificationtime>
+ YYYY-MM-DDTHH:MM:SS
+ </silva-extra:modificationtime>
+ <silva-extra:subject>
+ </silva-extra:subject>
+ </set>
+ <set xmlns:silvasettings="http://infrae.com/namespace/metadata/silva-settings" id="silva-settings">
+ <silvasettings:hide_from_tocs>
+ do not hide
+ </silvasettings:hide_from_tocs>
+ </set>
+ </metadata>
+ <url>
+ http://infrae.com
+ </url>
+ </content>
+ </link>
+ <file id="file">
+ <metadata>
+ <set id="silva-content">
+ <silva-content:maintitle>
+ Torvald blob
+ </silva-content:maintitle>
+ <silva-content:shorttitle>
+ </silva-content:shorttitle>
+ </set>
+ <set id="silva-extra">
+ <silva-extra:comment>
+ </silva-extra:comment>
+ <silva-extra:contactemail>
+ </silva-extra:contactemail>
+ <silva-extra:contactname>
+ </silva-extra:contactname>
+ <silva-extra:content_description>
+ </silva-extra:content_description>
+ <silva-extra:creationtime>
+ YYYY-MM-DDTHH:MM:SS
+ </silva-extra:creationtime>
+ <silva-extra:creator>
+ chiefeditor
+ </silva-extra:creator>
+ <silva-extra:keywords>
+ </silva-extra:keywords>
+ <silva-extra:language>
+ </silva-extra:language>
+ <silva-extra:lastauthor>
+ chiefeditor
+ </silva-extra:lastauthor>
+ <silva-extra:modificationtime>
+ YYYY-MM-DDTHH:MM:SS
+ </silva-extra:modificationtime>
+ <silva-extra:subject>
+ </silva-extra:subject>
+ </set>
+ <set xmlns:silvasettings="http://infrae.com/namespace/metadata/silva-settings" id="silva-settings">
+ <silvasettings:hide_from_tocs>
+ do not hide
+ </silvasettings:hide_from_tocs>
+ </set>
+ </metadata>
+ <asset id="1">
+ </asset>
+ </file>
+ </content>
+ </folder>
+ <ghost_folder id="ghost">
+ <content>
+ <haunted>
+ folder/container
+ </haunted>
+ </content>
+ </ghost_folder>
+ </content>
+ </folder>
+</silva>
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/tests/data/test_export_ghostfolder.silvaxml
--- a/Products/Silva/tests/data/test_export_ghostfolder.silvaxml Mon Nov 04 17:03:35 2013 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,254 +0,0 @@
-<?xml version='1.0' encoding='utf-8'?>
-<silva xmlns="http://infrae.com/namespace/silva" {namespaces} silva_version="{version}">
- <folder id="folder">
- <metadata>
- <set id="silva-content">
- <silva-content:maintitle>
- This is <boo>a</boo> folder
- </silva-content:maintitle>
- <silva-content:shorttitle>
- </silva-content:shorttitle>
- </set>
- <set id="silva-extra">
- <silva-extra:comment>
- </silva-extra:comment>
- <silva-extra:contactemail>
- </silva-extra:contactemail>
- <silva-extra:contactname>
- </silva-extra:contactname>
- <silva-extra:content_description>
- </silva-extra:content_description>
- <silva-extra:creationtime>
- YYYY-MM-DDTHH:MM:SS
- </silva-extra:creationtime>
- <silva-extra:creator>
- author
- </silva-extra:creator>
- <silva-extra:keywords>
- </silva-extra:keywords>
- <silva-extra:language>
- </silva-extra:language>
- <silva-extra:lastauthor>
- chiefeditor
- </silva-extra:lastauthor>
- <silva-extra:modificationtime>
- YYYY-MM-DDTHH:MM:SS
- </silva-extra:modificationtime>
- <silva-extra:subject>
- </silva-extra:subject>
- </set>
- <set xmlns:silvasettings="http://infrae.com/namespace/metadata/silva-settings" id="silva-settings">
- <silvasettings:hide_from_tocs>
- do not hide
- </silvasettings:hide_from_tocs>
- </set>
- </metadata>
- <content>
- <folder id="container">
- <metadata>
- <set id="silva-content">
- <silva-content:maintitle>
- Content
- </silva-content:maintitle>
- <silva-content:shorttitle>
- </silva-content:shorttitle>
- </set>
- <set id="silva-extra">
- <silva-extra:comment>
- </silva-extra:comment>
- <silva-extra:contactemail>
- </silva-extra:contactemail>
- <silva-extra:contactname>
- </silva-extra:contactname>
- <silva-extra:content_description>
- </silva-extra:content_description>
- <silva-extra:creationtime>
- YYYY-MM-DDTHH:MM:SS
- </silva-extra:creationtime>
- <silva-extra:creator>
- chiefeditor
- </silva-extra:creator>
- <silva-extra:keywords>
- </silva-extra:keywords>
- <silva-extra:language>
- </silva-extra:language>
- <silva-extra:lastauthor>
- chiefeditor
- </silva-extra:lastauthor>
- <silva-extra:modificationtime>
- YYYY-MM-DDTHH:MM:SS
- </silva-extra:modificationtime>
- <silva-extra:subject>
- </silva-extra:subject>
- </set>
- <set xmlns:silvasettings="http://infrae.com/namespace/metadata/silva-settings" id="silva-settings">
- <silvasettings:hide_from_tocs>
- do not hide
- </silvasettings:hide_from_tocs>
- </set>
- </metadata>
- <content>
- <default>
- <auto_toc depth="-1" display_desc_flag="False" id="index" show_icon="False" sort_order="silva" types="Silva Document,Silva Publication,Silva Folder">
- <metadata>
- <set id="silva-content">
- <silva-content:maintitle>
- Content
- </silva-content:maintitle>
- <silva-content:shorttitle>
- </silva-content:shorttitle>
- </set>
- <set id="silva-extra">
- <silva-extra:comment>
- </silva-extra:comment>
- <silva-extra:contactemail>
- </silva-extra:contactemail>
- <silva-extra:contactname>
- </silva-extra:contactname>
- <silva-extra:content_description>
- </silva-extra:content_description>
- <silva-extra:creationtime>
- YYYY-MM-DDTHH:MM:SS
- </silva-extra:creationtime>
- <silva-extra:creator>
- chiefeditor
- </silva-extra:creator>
- <silva-extra:keywords>
- </silva-extra:keywords>
- <silva-extra:language>
- </silva-extra:language>
- <silva-extra:lastauthor>
- chiefeditor
- </silva-extra:lastauthor>
- <silva-extra:modificationtime>
- YYYY-MM-DDTHH:MM:SS
- </silva-extra:modificationtime>
- <silva-extra:subject>
- </silva-extra:subject>
- </set>
- <set xmlns:silvasettings="http://infrae.com/namespace/metadata/silva-settings" id="silva-settings">
- <silvasettings:hide_from_tocs>
- do not hide
- </silvasettings:hide_from_tocs>
- </set>
- </metadata>
- </auto_toc>
- </default>
- <link id="link">
- <workflow>
- <version id="0">
- <status>
- unapproved
- </status>
- <publication_datetime>
- </publication_datetime>
- <expiration_datetime>
- </expiration_datetime>
- </version>
- </workflow>
- <content version_id="0">
- <metadata>
- <set id="silva-content">
- <silva-content:maintitle>
- Infrae
- </silva-content:maintitle>
- <silva-content:shorttitle>
- </silva-content:shorttitle>
- </set>
- <set id="silva-extra">
- <silva-extra:comment>
- </silva-extra:comment>
- <silva-extra:contactemail>
- </silva-extra:contactemail>
- <silva-extra:contactname>
- </silva-extra:contactname>
- <silva-extra:content_description>
- </silva-extra:content_description>
- <silva-extra:creationtime>
- YYYY-MM-DDTHH:MM:SS
- </silva-extra:creationtime>
- <silva-extra:creator>
- chiefeditor
- </silva-extra:creator>
- <silva-extra:keywords>
- </silva-extra:keywords>
- <silva-extra:language>
- </silva-extra:language>
- <silva-extra:lastauthor>
- chiefeditor
- </silva-extra:lastauthor>
- <silva-extra:modificationtime>
- YYYY-MM-DDTHH:MM:SS
- </silva-extra:modificationtime>
- <silva-extra:subject>
- </silva-extra:subject>
- </set>
- <set xmlns:silvasettings="http://infrae.com/namespace/metadata/silva-settings" id="silva-settings">
- <silvasettings:hide_from_tocs>
- do not hide
- </silvasettings:hide_from_tocs>
- </set>
- </metadata>
- <url>
- http://infrae.com
- </url>
- </content>
- </link>
- <file id="file">
- <metadata>
- <set id="silva-content">
- <silva-content:maintitle>
- Torvald blob
- </silva-content:maintitle>
- <silva-content:shorttitle>
- </silva-content:shorttitle>
- </set>
- <set id="silva-extra">
- <silva-extra:comment>
- </silva-extra:comment>
- <silva-extra:contactemail>
- </silva-extra:contactemail>
- <silva-extra:contactname>
- </silva-extra:contactname>
- <silva-extra:content_description>
- </silva-extra:content_description>
- <silva-extra:creationtime>
- YYYY-MM-DDTHH:MM:SS
- </silva-extra:creationtime>
- <silva-extra:creator>
- chiefeditor
- </silva-extra:creator>
- <silva-extra:keywords>
- </silva-extra:keywords>
- <silva-extra:language>
- </silva-extra:language>
- <silva-extra:lastauthor>
- chiefeditor
- </silva-extra:lastauthor>
- <silva-extra:modificationtime>
- YYYY-MM-DDTHH:MM:SS
- </silva-extra:modificationtime>
- <silva-extra:subject>
- </silva-extra:subject>
- </set>
- <set xmlns:silvasettings="http://infrae.com/namespace/metadata/silva-settings" id="silva-settings">
- <silvasettings:hide_from_tocs>
- do not hide
- </silvasettings:hide_from_tocs>
- </set>
- </metadata>
- <asset id="1">
- </asset>
- </file>
- </content>
- </folder>
- <ghost_folder id="ghost">
- <content>
- <haunted>
- folder/container
- </haunted>
- </content>
- </ghost_folder>
- </content>
- </folder>
-</silva>
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/tests/data/test_import_ghost_asset.zip
Binary file Products/Silva/tests/data/test_import_ghost_asset.zip has changed
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/tests/mockers.py
--- a/Products/Silva/tests/mockers.py Mon Nov 04 17:03:35 2013 +0100
+++ b/Products/Silva/tests/mockers.py Fri Nov 08 16:36:46 2013 +0100
@@ -4,6 +4,7 @@
from OFS.SimpleItem import SimpleItem
+from Products.Silva.Asset import Asset
from Products.Silva.Publishable import NonPublishable
from Products.Silva.VersionedContent import VersionedContent
from Products.Silva.Version import Version
@@ -12,7 +13,7 @@
from five import grok
from silva.core import conf as silvaconf
from silva.core.views import views as silvaviews
-from silva.core.interfaces import INonPublishable, IVersionedContent
+from silva.core.interfaces import INonPublishable, IAsset, IVersionedContent
from silva.core.interfaces.adapters import IIndexEntries
from zeam.form import silva as silvaforms
@@ -76,12 +77,25 @@
return self.content.get_title()
-class IMockupAsset(INonPublishable):
+class IMockupNonPublishable(INonPublishable):
pass
-class MockupAsset(NonPublishable, SimpleItem):
- """A mockup asset.
+class MockupNonPublishable(NonPublishable, SimpleItem):
+ """A mockup non-publishable.
+ """
+ grok.implements(IMockupNonPublishable)
+ meta_type = 'Mockup Non Publishable'
+ silvaconf.priority(-10)
+ silvaconf.icon('tests/mockers.png')
+
+
+class IMockupAsset(IAsset):
+ pass
+
+
+class MockupAsset(Asset, SimpleItem):
+ """A mockup non-publishable.
"""
grok.implements(IMockupAsset)
meta_type = 'Mockup Asset'
@@ -93,11 +107,15 @@
root.manage_permission(
'Add Mockup Assets', roleinfo.AUTHOR_ROLES)
root.manage_permission(
+ 'Add Mockup Non Publishables', roleinfo.AUTHOR_ROLES)
+ root.manage_permission(
'Add Mockup Versions', roleinfo.AUTHOR_ROLES)
root.manage_permission(
'Add Mockup VersionedContents', roleinfo.AUTHOR_ROLES)
root.service_metadata.addTypesMapping(
['Mockup Version'], ('silva-content', 'silva-extra', 'silva-settings'))
root.service_metadata.addTypesMapping(
+ ['Mockup Non Publishable'], ('silva-content', 'silva-extra',))
+ root.service_metadata.addTypesMapping(
['Mockup Asset'], ('silva-content', 'silva-extra',))
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/tests/test_extension.py
--- a/Products/Silva/tests/test_extension.py Mon Nov 04 17:03:35 2013 +0100
+++ b/Products/Silva/tests/test_extension.py Fri Nov 08 16:36:46 2013 +0100
@@ -63,6 +63,7 @@
'Silva Folder',
'Silva Ghost',
'Silva Ghost Version',
+ 'Silva Ghost Asset',
'Silva Ghost Folder',
'Silva Image',
'Silva Indexer',
@@ -73,6 +74,7 @@
'Silva Simple Member',
'Silva Filtering Service',
'Mockup Asset',
+ 'Mockup Non Publishable',
'Mockup VersionedContent',
'Mockup Version'])
self.assertEqual(
@@ -97,6 +99,8 @@
'Silva',
'Silva',
'Silva',
+ 'Silva',
+ 'Silva',
'Silva'])
def test_extension_egg(self):
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/tests/test_file.py
--- a/Products/Silva/tests/test_file.py Mon Nov 04 17:03:35 2013 +0100
+++ b/Products/Silva/tests/test_file.py Fri Nov 08 16:36:46 2013 +0100
@@ -66,13 +66,16 @@
with self.layer.open_fixture('photo.tif') as test_file:
factory.manage_addFile('test_file_id', 'Test File 1', test_file)
with self.assertRaises(ValueError) as error:
- factory.manage_addImage('test_file_id', 'Test File 2',
- test_file)
+ factory.manage_addImage(
+ 'test_file_id', 'Test File 2', test_file)
- self.assertEqual(str(error.exception), "Please provide a unique id: ${reason}")
+ self.assertEqual(
+ str(error.exception),
+ "Please provide a unique id: ${reason}")
with self.layer.open_fixture('photo.tif') as test_file:
- factory.manage_addFile('test_file_unique_id', 'Test File 3', test_file)
+ factory.manage_addFile(
+ 'test_file_unique_id', 'Test File 3', test_file)
content = self.root._getOb('test_file_unique_id')
self.assertTrue(verifyObject(interfaces.IAsset, content))
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/tests/test_folder_addables.py
--- a/Products/Silva/tests/test_folder_addables.py Mon Nov 04 17:03:35 2013 +0100
+++ b/Products/Silva/tests/test_folder_addables.py Fri Nov 08 16:36:46 2013 +0100
@@ -11,11 +11,13 @@
DEFAULT_ALL_ADDABLES = [
'Mockup VersionedContent',
'Mockup Asset',
+ 'Mockup Non Publishable',
'Silva Folder',
'Silva Publication',
'Silva Image',
'Silva File',
'Silva Ghost',
+ 'Silva Ghost Asset',
'Silva Ghost Folder',
'Silva Indexer',
'Silva Link',
@@ -23,10 +25,12 @@
AUTHOR_ALL_ADDABLES = [
'Mockup VersionedContent',
'Mockup Asset',
+ 'Mockup Non Publishable',
'Silva Folder',
'Silva Image',
'Silva File',
'Silva Ghost',
+ 'Silva Ghost Asset',
'Silva Link',
'Silva AutoTOC']
@@ -98,7 +102,8 @@
folder_addables = IAddableContents(self.root.folder)
self.assertTrue(verifyObject(IAddableContents, folder_addables))
self.assertEqual(
- folder_addables.get_authorized_addables(), AUTHOR_ALL_ADDABLES)
+ folder_addables.get_authorized_addables(),
+ AUTHOR_ALL_ADDABLES)
self.root.set_silva_addables_allowed_in_container(
['Silva Publication', 'Silva File'])
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/tests/test_folder_quota.py
--- a/Products/Silva/tests/test_folder_quota.py Mon Nov 04 17:03:35 2013 +0100
+++ b/Products/Silva/tests/test_folder_quota.py Fri Nov 08 16:36:46 2013 +0100
@@ -346,9 +346,9 @@
with manager1.ghoster() as ghoster:
ghoster(folder2['subfolder1'])
- # And check used space
- self.assertEqual(folder1.used_space, zipfile_size)
- self.assertEqual(self.root.used_space, (2 * zipfile_size) + image_size)
+ # And check used space. Ghost Assets don't use any quota.
+ self.assertEqual(folder1.used_space, 0)
+ self.assertEqual(self.root.used_space, zipfile_size + image_size)
self.assertEqual(folder2.used_space, zipfile_size + image_size)
# Delete the ghost
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/tests/test_ghost_asset.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Products/Silva/tests/test_ghost_asset.py Fri Nov 08 16:36:46 2013 +0100
@@ -0,0 +1,123 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013 Infrae. All rights reserved.
+# See also LICENSE.txt
+
+import unittest
+
+from Acquisition import aq_chain
+from DateTime import DateTime
+
+from zope.component import getUtility
+from zope.interface.verify import verifyObject
+
+from Products.Silva.testing import FunctionalLayer, Transaction
+from silva.core.interfaces import IGhostAsset
+from silva.core.services.interfaces import IMetadataService
+
+
+class GhostAssetTestCase(unittest.TestCase):
+ layer = FunctionalLayer
+
+ def setUp(self):
+ self.root = self.layer.get_application()
+ self.layer.login('editor')
+
+ with Transaction():
+ factory = self.root.manage_addProduct['Silva']
+ with self.layer.open_fixture('silva.png') as stream:
+ factory.manage_addImage('logo', 'Silva Logo', stream)
+
+ with Transaction():
+ metadata = getUtility(IMetadataService).getMetadata(self.root.logo)
+ metadata.setValues(
+ 'silva-extra',
+ {'modificationtime': DateTime('2010-04-25T12:00:00Z')})
+
+ self.assertEqual(
+ self.root.logo.get_modification_datetime(),
+ DateTime('2010-04-25T12:00:00Z'))
+
+ def test_create_and_access(self):
+ """Create and test a Ghost Asset content type.
+ """
+ with Transaction():
+ factory = self.root.manage_addProduct['Silva']
+ factory.manage_addGhostAsset('ghost', None, haunted=self.root.logo)
+
+ self.assertIn('ghost', self.root.objectIds())
+ ghost = self.root._getOb('ghost')
+ self.assertTrue(verifyObject(IGhostAsset, ghost))
+ self.assertIn(ghost, self.root.get_non_publishables())
+ self.assertEqual(ghost.get_link_status(), None)
+ self.assertEqual(ghost.get_haunted(), self.root.logo)
+ self.assertEqual(ghost.get_filename(), 'logo.png')
+ self.assertEqual(ghost.get_mime_type(), 'image/png')
+ self.assertEqual(
+ ghost.get_file_size(),
+ self.root.logo.get_file_size())
+ self.assertEqual(
+ ghost.get_modification_datetime(),
+ self.root.logo.get_modification_datetime())
+ self.assertEqual(
+ aq_chain(ghost.get_haunted()),
+ aq_chain(self.root.logo))
+
+ def test_download(self):
+ """create and download a Ghost Asset content type.
+ """
+ with Transaction():
+ factory = self.root.manage_addProduct['Silva']
+ factory.manage_addGhostAsset('ghost', None, haunted=self.root.logo)
+
+ with self.layer.get_browser() as browser:
+ self.assertEqual(browser.open('/root/ghost'), 200)
+ self.assertEqual(
+ len(browser.contents),
+ self.root.logo.get_file_size())
+ self.assertEqual(
+ int(browser.headers['Content-Length']),
+ self.root.logo.get_file_size())
+ self.assertEqual(
+ browser.headers['Content-Type'],
+ 'image/png')
+ self.assertEqual(
+ browser.headers['Content-Disposition'],
+ 'inline;filename=logo.png')
+ self.assertEqual(
+ browser.headers['Last-Modified'],
+ 'Sun, 25 Apr 2010 12:00:00 GMT')
+ self.assertIn(
+ browser.headers['Accept-Ranges'],
+ ('none', 'bytes'))
+
+ def test_head_request(self):
+ with Transaction():
+ factory = self.root.manage_addProduct['Silva']
+ factory.manage_addGhostAsset('ghost', None, haunted=self.root.logo)
+
+ with self.layer.get_browser() as browser:
+ self.assertEqual(browser.open('/root/ghost', method='HEAD'), 200)
+ self.assertEqual(
+ len(browser.contents),
+ 0)
+ self.assertEqual(
+ int(browser.headers['Content-Length']),
+ self.root.logo.get_file_size())
+ self.assertEqual(
+ browser.headers['Content-Type'],
+ 'image/png')
+ self.assertEqual(
+ browser.headers['Content-Disposition'],
+ 'inline;filename=logo.png')
+ self.assertIn(
+ browser.headers['Accept-Ranges'],
+ ('none', 'bytes'))
+ self.assertEqual(
+ browser.headers['Last-Modified'],
+ 'Sun, 25 Apr 2010 12:00:00 GMT')
+
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(GhostAssetTestCase))
+ return suite
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/tests/test_ghost_folder.py
--- a/Products/Silva/tests/test_ghost_folder.py Mon Nov 04 17:03:35 2013 +0100
+++ b/Products/Silva/tests/test_ghost_folder.py Fri Nov 08 16:36:46 2013 +0100
@@ -9,12 +9,12 @@
from Acquisition import aq_chain
from Products.Silva.testing import FunctionalLayer
-from Products.Silva.tests.mockers import IMockupAsset
+from Products.Silva.tests.mockers import IMockupNonPublishable
from Products.SilvaMetadata.interfaces import IMetadataService, ReadOnlyError
from silva.core.interfaces import IContainerManager, IPublicationWorkflow
-from silva.core.interfaces import IGhostFolder, IGhost
-from silva.core.interfaces import IPublication, IFolder
+from silva.core.interfaces import IGhost, IGhostAsset, IGhostVersion
+from silva.core.interfaces import IPublication, IFolder, IGhostFolder
from silva.core.interfaces import errors
from silva.core.references.interfaces import IReferenceService, IReferenceValue
@@ -46,7 +46,7 @@
factory.manage_addMockupVersionedContent('document', 'Document')
factory.manage_addFolder('folder', 'Folder')
- def test_ghost_folder(self):
+ def test_get_folder(self):
"""Test a Ghost Folder haunting to a Folder.
"""
factory = self.root.target.manage_addProduct['Silva']
@@ -454,16 +454,106 @@
self.assertTrue('folder' in ghost.objectIds())
self.assertFalse('results' in ghost.folder.objectIds())
- ghost.haunt()
- self.assertTrue('testing' in ghost.objectIds())
+ self.assertTrue(ghost.haunt())
+ self.assertIn('testing', ghost.objectIds())
self.assertTrue(verifyObject(IGhost, ghost.testing))
- self.assertTrue('folder' in ghost.objectIds())
- self.assertTrue('results' in ghost.folder.objectIds())
+ self.assertTrue(verifyObject(IGhostVersion,
+ ghost.testing.get_viewable()))
+ self.assertEqual(
+ ghost.testing.get_viewable().get_haunted(),
+ self.root.folder.testing)
+ self.assertEqual(
+ aq_chain(ghost.testing.get_viewable().get_haunted()),
+ aq_chain(self.root.folder.testing))
+ self.assertIn('folder', ghost.objectIds())
+ self.assertIn('results', ghost.folder.objectIds())
self.assertTrue(verifyObject(IGhostFolder, ghost.folder.results))
+ self.assertEqual(
+ ghost.folder.results.get_haunted(),
+ self.root.folder.folder.results)
+ self.assertEqual(
+ aq_chain(ghost.folder.results.get_haunted()),
+ aq_chain(self.root.folder.folder.results))
+
+ def test_ghost_haunt_add_non_publishables(self):
+ """Test modifications: adding new non publishables in the
+ target folder copy them in the ghost folder when it is
+ haunted.
+ """
+ factory = self.root.target.manage_addProduct['Silva']
+ factory.manage_addGhostFolder('ghost', None, haunted=self.root.folder)
+
+ ghost = self.root.target.ghost
+ factory = self.root.folder.manage_addProduct['Silva']
+ factory.manage_addMockupNonPublishable('stuff', 'Configuration stuff')
+ factory = self.root.folder.publication.manage_addProduct['Silva']
+ factory.manage_addMockupNonPublishable('results', 'Results set 1')
+
+ self.assertIn('publication', ghost.objectIds())
+ self.assertTrue(verifyObject(IGhostFolder, ghost.publication))
+ self.assertNotIn('data', ghost.objectIds())
+ self.assertNotIn('results', ghost.publication.objectIds())
+
+ self.assertTrue(ghost.haunt())
+ self.assertTrue('stuff' in ghost.objectIds())
+ self.assertTrue(verifyObject(IMockupNonPublishable, ghost.stuff))
+ self.assertIn('publication', ghost.objectIds())
+ self.assertTrue(verifyObject(IGhostFolder, ghost.publication))
+ self.assertIn('results', ghost.publication.objectIds())
+ self.assertTrue(verifyObject(IMockupNonPublishable,
+ ghost.publication.results))
+
+ def test_ghost_haunt_convert_assets_to_ghost_assets(self):
+ """Test update: old ghost folders might have copys of assets
+ instead of ghost assets poiting to them. In this case, if you
+ re-haunt the folder, they should be replaced with ghost
+ assets.
+ """
+ factory = self.root.target.manage_addProduct['Silva']
+ factory.manage_addGhostFolder('ghost', None, haunted=self.root.folder)
+
+ ghost = self.root.target.ghost
+ factory = self.root.folder.manage_addProduct['Silva']
+ factory.manage_addMockupAsset('data', 'Data set 1')
+ factory = self.root.folder.publication.manage_addProduct['Silva']
+ factory.manage_addMockupAsset('results', 'Results set 1')
+ factory.manage_addMockupAsset('analyze', 'Analyze set 1')
+
+ # Add assets with the same id in the ghost folder.
+ factory = ghost.manage_addProduct['Silva']
+ factory.manage_addMockupAsset('data', 'Data set 1')
+ factory = ghost.publication.manage_addProduct['Silva']
+ factory.manage_addMockupAsset('results', 'Results set 1')
+ factory.manage_addMockupNonPublishable('analyze', 'Analyze set 1')
+
+ # Now if we rehaunt the folder, they should be replaced by ghost assets.
+ self.assertTrue(ghost.haunt())
+ self.assertIn('data', ghost.objectIds())
+ self.assertTrue(verifyObject(IGhostAsset, ghost.data))
+ self.assertEqual(
+ ghost.data.get_haunted(),
+ self.root.folder.data)
+ self.assertEqual(
+ aq_chain(ghost.data.get_haunted()),
+ aq_chain(self.root.folder.data))
+ self.assertIn('publication', ghost.objectIds())
+ self.assertTrue(verifyObject(IGhostFolder, ghost.publication))
+ self.assertIn('results', ghost.publication.objectIds())
+ self.assertTrue(verifyObject(IGhostAsset, ghost.publication.results))
+ self.assertEqual(
+ ghost.publication.results.get_haunted(),
+ self.root.folder.publication.results)
+ self.assertEqual(
+ aq_chain(ghost.publication.results.get_haunted()),
+ aq_chain(self.root.folder.publication.results))
+ # Analyze was not an asset, it didn't get replaced
+ self.assertIn('analyze', ghost.publication.objectIds())
+ self.assertTrue(verifyObject(IMockupNonPublishable,
+ ghost.publication.analyze))
def test_ghost_haunt_add_assets(self):
"""Test modifications: adding assets in the target creates new
- assets in the ghost folder when the ghost folder is haunted.
+ ghost assets in the ghost folder when it is haunted.
"""
factory = self.root.target.manage_addProduct['Silva']
factory.manage_addGhostFolder('ghost', None, haunted=self.root.folder)
@@ -474,15 +564,30 @@
factory = self.root.folder.publication.manage_addProduct['Silva']
factory.manage_addMockupAsset('results', 'Results set 1')
- self.assertFalse('data' in ghost.objectIds())
- self.assertTrue('publication' in ghost.objectIds())
- self.assertFalse('results' in ghost.publication.objectIds())
+ self.assertIn('publication', ghost.objectIds())
+ self.assertTrue(verifyObject(IGhostFolder, ghost.publication))
+ self.assertNotIn('data', ghost.objectIds())
+ self.assertNotIn('results', ghost.publication.objectIds())
- ghost.haunt()
- self.assertTrue('data' in ghost.objectIds())
- self.assertTrue(verifyObject(IMockupAsset, ghost.data))
- self.assertTrue('publication' in ghost.objectIds())
- self.assertTrue(verifyObject(IMockupAsset, ghost.publication.results))
+ self.assertTrue(ghost.haunt())
+ self.assertIn('data', ghost.objectIds())
+ self.assertTrue(verifyObject(IGhostAsset, ghost.data))
+ self.assertEqual(
+ ghost.data.get_haunted(),
+ self.root.folder.data)
+ self.assertEqual(
+ aq_chain(ghost.data.get_haunted()),
+ aq_chain(self.root.folder.data))
+ self.assertIn('publication', ghost.objectIds())
+ self.assertTrue(verifyObject(IGhostFolder, ghost.publication))
+ self.assertIn('results', ghost.publication.objectIds())
+ self.assertTrue(verifyObject(IGhostAsset, ghost.publication.results))
+ self.assertEqual(
+ ghost.publication.results.get_haunted(),
+ self.root.folder.publication.results)
+ self.assertEqual(
+ aq_chain(ghost.publication.results.get_haunted()),
+ aq_chain(self.root.folder.publication.results))
def test_ghost_haunt_remove_assets(self):
"""Test modifications: removing asssets in the target doesn't
@@ -496,18 +601,35 @@
factory.manage_addGhostFolder('ghost', None, haunted=self.root.folder)
ghost = self.root.target.ghost
+ self.assertIn('data', ghost.objectIds())
+ self.assertTrue(verifyObject(IGhostAsset, ghost.data))
+ self.assertEqual(
+ ghost.data.get_haunted(),
+ self.root.folder.data)
+ self.assertEqual(
+ aq_chain(ghost.data.get_haunted()),
+ aq_chain(self.root.folder.data))
+ self.assertIn('export', ghost.objectIds())
+ self.assertTrue(verifyObject(IGhostAsset, ghost.export))
+ self.assertEqual(
+ ghost.export.get_haunted(),
+ self.root.folder.export)
+ self.assertEqual(
+ aq_chain(ghost.export.get_haunted()),
+ aq_chain(self.root.folder.export))
+
with IContainerManager(self.root.folder).deleter() as deleter:
deleter(self.root.folder.data)
deleter(self.root.folder.export)
- self.assertTrue('data' in ghost.objectIds())
- self.assertTrue(verifyObject(IMockupAsset, ghost.data))
- self.assertTrue('export' in ghost.objectIds())
- self.assertTrue(verifyObject(IMockupAsset, ghost.export))
+ # Ghost should have been auto-deleted
+ self.assertNotIn('data', ghost.objectIds())
+ self.assertNotIn('export', ghost.objectIds())
- ghost.haunt()
- self.assertFalse('data' in ghost.objectIds())
- self.assertFalse('export' in ghost.objectIds())
+ # And they won't come back
+ self.assertTrue(ghost.haunt())
+ self.assertNotIn('data', ghost.objectIds())
+ self.assertNotIn('export', ghost.objectIds())
def test_ghost_haunt_add_ghosts_for_ghosts(self):
"""Test modification: adding ghost to the target will create
@@ -523,8 +645,8 @@
IPublicationWorkflow(self.root.folder.notes).publish()
self.assertFalse('notes' in ghost.objectIds())
- ghost.haunt()
- self.assertTrue('notes' in ghost.objectIds())
+ self.assertTrue(ghost.haunt())
+ self.assertIn('notes', ghost.objectIds())
self.assertTrue(verifyObject(IGhost, ghost.notes))
version = ghost.notes.get_editable()
self.assertIs(version, None)
@@ -567,10 +689,10 @@
# Nothing should change if the folder is ghosted (broken
# ghosts should not be created).
- ghost.haunt()
- self.assertTrue('notes' in self.root.folder.objectIds())
+ self.assertTrue(ghost.haunt())
+ self.assertIn('notes', self.root.folder.objectIds())
self.assertEqual(self.root.folder.notes.get_haunted(), None)
- self.assertFalse('notes' in ghost.objectIds())
+ self.assertNotIn('notes', ghost.objectIds())
def test_ghost_haunt_remove_ghosted_content_for_ghosts(self):
"""Test modification: remove a target of a ghost that is
@@ -609,9 +731,9 @@
self.assertEqual(version.get_haunted(), self.root.folder.document)
# Haunting should remove the ghost since it does no longer exists
- ghost.haunt()
- self.assertFalse('notes' in self.root.folder.objectIds())
- self.assertFalse('notes' in ghost.objectIds())
+ self.assertTrue(ghost.haunt())
+ self.assertNotIn('notes', self.root.folder.objectIds())
+ self.assertNotIn('notes', ghost.objectIds())
def test_ghost_haunt_add_ghost_folders_for_ghost_folders(self):
"""Test modification: add a ghost folder in the target
@@ -627,10 +749,10 @@
factory = self.root.folder.manage_addProduct['Silva']
factory.manage_addGhostFolder(
'backup', None, haunted=self.root.folder.publication)
- self.assertFalse('backup' in ghost.objectIds())
+ self.assertNotIn('backup', ghost.objectIds())
- ghost.haunt()
- self.assertTrue('backup' in ghost.objectIds())
+ self.assertTrue(ghost.haunt())
+ self.assertIn('backup', ghost.objectIds())
self.assertTrue(verifyObject(IGhostFolder, ghost.backup))
self.assertEqual(ghost.backup.get_link_status(), None)
self.assertEqual(
@@ -640,7 +762,7 @@
aq_chain(ghost.backup.get_haunted()),
aq_chain(self.root.folder.publication))
# Content in this ghost folder have been ghosted
- self.assertTrue('document' in ghost.backup.objectIds())
+ self.assertIn('document', ghost.backup.objectIds())
self.assertTrue(verifyObject(IGhost, ghost.backup.document))
def test_ghost_haunt_remove_ghost_folders_for_ghost_folders(self):
@@ -670,12 +792,12 @@
self.assertFalse('backup' in ghost.objectIds())
# Ghosting should not change anything
- ghost.haunt()
- self.assertTrue('backup' in self.root.folder.objectIds())
+ self.assertTrue(ghost.haunt())
+ self.assertIn('backup', self.root.folder.objectIds())
self.assertEqual(
self.root.folder.backup.get_link_status(),
errors.EmptyInvalidTarget())
- self.assertFalse('backup' in ghost.objectIds())
+ self.assertNotIn('backup', ghost.objectIds())
def test_suite():
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/tests/test_image.py
--- a/Products/Silva/tests/test_image.py Mon Nov 04 17:03:35 2013 +0100
+++ b/Products/Silva/tests/test_image.py Fri Nov 08 16:36:46 2013 +0100
@@ -71,11 +71,14 @@
self.assertEquals(image.get_mime_type(), 'image/tiff')
# Image methods
- self.assertTrue(image.tag() is not None)
+ self.assertTrue(image.get_html_tag() is not None)
self.assertEquals(image.get_format(), 'TIFF')
self.assertEquals(image.get_web_format(), 'JPEG')
self.assertEquals(image.get_dimensions(), (960, 1280))
self.assertEquals(str(image.get_orientation()), "portrait")
+ self.assertEquals(
+ image.get_download_url(),
+ 'http://localhost/root/test_image')
image.set_web_presentation_properties('JPEG', '100x100', '')
self.assertEquals(image.get_web_format(), 'JPEG')
@@ -87,6 +90,9 @@
data = image.get_image(hires=True, webformat=False)
self.assertHashEqual(self.image_data, data)
+ self.assertEquals(
+ image.get_download_url(hires=True),
+ 'http://localhost/root/test_image?hires')
data = io.BytesIO(image.get_image(hires=True, webformat=True))
pil_image = PILImage.open(data)
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/tests/test_xml_export.py
--- a/Products/Silva/tests/test_xml_export.py Mon Nov 04 17:03:35 2013 +0100
+++ b/Products/Silva/tests/test_xml_export.py Fri Nov 08 16:36:46 2013 +0100
@@ -7,7 +7,6 @@
import re
import io
import unittest
-import transaction
from zope.component import getAdapter, getUtility
@@ -220,7 +219,7 @@
self.layer.login('author')
exporter = self.assertExportEqual(
self.root.folder,
- 'test_export_ghostfolder.silvaxml')
+ 'test_export_ghost_folder.silvaxml')
self.assertEqual(
exporter.getZexpPaths(),
[])
@@ -250,6 +249,31 @@
self.layer.login('author')
self.assertExportFail(self.root.ghost)
+ def test_ghost_asset(self):
+ """Export a file and a ghost asset.
+ """
+ with Transaction():
+ self.layer.login('editor')
+ factory = self.root.folder.manage_addProduct['Silva']
+ with self.layer.open_fixture('silva.png') as stream:
+ factory.manage_addFile('logo', 'Silva Logo', stream)
+ factory.manage_addGhostAsset(
+ 'ghost', None, haunted=self.root.folder.logo)
+
+ self.layer.login('author')
+ exporter = self.assertExportEqual(
+ self.root.folder,
+ 'test_export_ghost_asset.silvaxml')
+ self.assertEqual(
+ exporter.getZexpPaths(),
+ [])
+ self.assertEqual(
+ exporter.getAssetPaths(),
+ [(('', 'root', 'folder', 'logo'), '1')])
+ self.assertEqual(
+ exporter.getProblems(),
+ [])
+
def test_link_relative(self):
"""Export a link with to an another Silva object.
"""
diff -r 419ecbf1c43e -r 13f06e1c78b7 Products/Silva/tests/test_xml_import.py
--- a/Products/Silva/tests/test_xml_import.py Mon Nov 04 17:03:35 2013 +0100
+++ b/Products/Silva/tests/test_xml_import.py Fri Nov 08 16:36:46 2013 +0100
@@ -700,6 +700,35 @@
self.assertEqual(folder.objectIds(), ['zope2folder'])
self.assertEqual(folder.zope2folder.meta_type, 'Folder')
+ def test_ghost_asset(self):
+ """Import a ghost asset pointing to an image.
+ """
+ importer = self.assertImportZip(
+ 'test_import_ghost_asset.zip',
+ ['/root/folder',
+ '/root/folder/torvald',
+ '/root/folder/ghost'])
+ self.assertEqual(importer.getProblems(), [])
+ self.assertItemsEqual(
+ self.root.folder.objectIds(),
+ ['torvald',
+ 'ghost'])
+
+ image = self.root.folder.torvald
+ self.assertTrue(interfaces.IImage.providedBy(image))
+ self.assertEqual(image.get_web_format(), 'GIF')
+ self.assertEqual(image.get_web_scale(), '200%')
+
+ ghost = self.root.folder.ghost
+ self.assertTrue(interfaces.IGhostAsset.providedBy(ghost))
+ self.assertEqual(ghost.get_haunted(), image)
+ self.assertEqual(aq_chain(ghost.get_haunted()), aq_chain(image))
+
+ get_metadata = self.metadata.getMetadata(image).get
+ self.assertEqual(
+ get_metadata('silva-extra', 'comment'),
+ u'Torvald public face.')
+
def test_image(self):
"""Import an image.
"""