[Products.Silva][Sylvain Viollon] Refactor quota system without ...
[email protected] Tue, 22 Oct 2013 11:43:23 +0200
| Newsgroups | gmane.comp.web.zope.silva.cvs |
|---|---|
| Message-ID | <[email protected]> |
author: Sylvain Viollon
date: Tue Oct 22 11:43:03 2013 +0200
revision: 11869:77bcc4d9b129 in Products.Silva
branch: 2.4
details: https://hg.infrae.com/Products.Silva?cmd=changeset;node=77bcc4d9b129
modified: Products/Silva/Asset.py Products/Silva/File/content.py Products/Silva/Folder/content.py Products/Silva/SilvaObject.py Products/Silva/silvaxml/xmlimport.py Products/Silva/tests/test_folder_quota.py
added:
removed:
log: Refactor quota system without breaking it (just move stuff around).
diffstat:
Products/Silva/Asset.py | 83 +++-----------------
Products/Silva/File/content.py | 22 ++--
Products/Silva/Folder/content.py | 32 +------
Products/Silva/SilvaObject.py | 117 +++++++++++++++++++++++++++++-
Products/Silva/silvaxml/xmlimport.py | 7 +
Products/Silva/tests/test_folder_quota.py | 9 +-
6 files changed, 160 insertions(+), 110 deletions(-)
diffs (482 lines):
diff -r f16b33bcedf6 -r 77bcc4d9b129 Products/Silva/Asset.py
--- a/Products/Silva/Asset.py Fri Oct 18 15:58:44 2013 +0200
+++ b/Products/Silva/Asset.py Tue Oct 22 11:43:03 2013 +0200
@@ -5,21 +5,17 @@
import logging
from five import grok
-from zope.component import queryUtility
from AccessControl import ClassSecurityInfo
-from Acquisition import aq_parent
from App.class_init import InitializeClass
from OFS import SimpleItem
-import OFS.interfaces
from Products.Silva import SilvaPermissions
-from Products.Silva.SilvaObject import ViewableObject
+from Products.Silva.SilvaObject import ViewableObject, QuotaObject
from Products.Silva.Publishable import NonPublishable
from Products.Silva.mangle import Bytes
-from silva.core.interfaces import IAsset, IImage
-from silva.core.services.interfaces import IExtensionService
+from silva.core.interfaces import IAsset
from silva.core.smi.content import IEditScreen
from silva.core.smi.content.metadata import ContentReferencedBy
from silva.core.views import views as silvaviews
@@ -29,40 +25,13 @@
logger = logging.getLogger('silva.core')
-class Asset(NonPublishable, ViewableObject, SimpleItem.SimpleItem):
+class Asset(NonPublishable, ViewableObject, QuotaObject,
+ SimpleItem.SimpleItem):
grok.baseclass()
grok.implements(IAsset)
security = ClassSecurityInfo()
- _old_size = 0 # Old size of the object.
-
- # MANIPULATORS
-
- security.declareProtected(
- SilvaPermissions.ChangeSilvaContent, 'update_quota')
- def update_quota(self):
- service = queryUtility(IExtensionService)
- if service is None:
- return
- verify = service.get_quota_subsystem_status()
- if verify is None:
- return
-
- parent = aq_parent(self)
- if not IImage.providedBy(parent):
- new_size = self.get_file_size()
- delta = new_size - self._old_size
- if delta:
- parent.update_quota(delta, verify)
- self._old_size = new_size
-
- security.declareProtected(
- SilvaPermissions.ChangeSilvaContent, 'reset_quota')
- def reset_quota(self):
- self._old_size = self.get_file_size()
- return self._old_size
-
# ACCESSORS
security.declareProtected(
@@ -87,44 +56,20 @@
"""
return None
+ def get_quota_usage(self):
+ try:
+ return self.get_file_size()
+ 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))
+ return -1
+
InitializeClass(Asset)
[email protected](IAsset, OFS.interfaces.IObjectWillBeMovedEvent)
-def asset_moved_update_quota(asset, event):
- """Event called on Asset when they are moved to update quota on
- parents folders.
- """
- if asset != event.object or event.newParent is event.oldParent:
- return
-
- service = queryUtility(IExtensionService)
- if service is None:
- return
- verify = service.get_quota_subsystem_status()
- if verify is None:
- # Quota accouting is disabled
- return
-
- try:
- size = asset.get_file_size()
- except (AttributeError, NotImplementedError):
- # Well, not all asset respect its interface.
- path = '/'.join(asset.getPhysicalPath())
- klass = str(asset.__class__)
- logger.error('bad asset object %s - %s' % (path, klass))
- return
-
- if not size:
- return
-
- if event.oldParent:
- event.oldParent.update_quota(-size, verify)
- if event.newParent:
- event.newParent.update_quota(size, verify)
-
-
class AssetEditTab(silvaforms.SMIComposedForm):
""" Edit tab
"""
diff -r f16b33bcedf6 -r 77bcc4d9b129 Products/Silva/File/content.py
--- a/Products/Silva/File/content.py Fri Oct 18 15:58:44 2013 +0200
+++ b/Products/Silva/File/content.py Tue Oct 22 11:43:03 2013 +0200
@@ -14,9 +14,10 @@
from five import grok
from zope.component import getUtility
from zope.event import notify
-from zope.lifecycleevent import ObjectCreatedEvent
-from zope.lifecycleevent import ObjectModifiedEvent
-import zope.lifecycleevent.interfaces
+from zope.lifecycleevent import ObjectCreatedEvent, ObjectModifiedEvent
+from zope.lifecycleevent.interfaces import IObjectMovedEvent
+from zope.lifecycleevent.interfaces import IObjectCreatedEvent
+from zope.lifecycleevent.interfaces import IObjectModifiedEvent
# Zope 2
from AccessControl import ClassSecurityInfo
@@ -299,7 +300,7 @@
self._file.content_type = 'text/plain; charset=utf-8'
self._content_encoding = content_encoding
if not interfaces.IImage.providedBy(aq_parent(self)):
- # If we are not a storage of an image, trigger an event.
+ # # If we are not a storage of an image, trigger an event.
notify(ObjectModifiedEvent(self))
security.declareProtected(
@@ -365,7 +366,7 @@
if self._content_type == 'text/plain':
self._content_type = 'text/plain; charset=utf-8'
if not interfaces.IImage.providedBy(aq_parent(self)):
- # If we are not a storage of an image, trigger an event.
+ # # If we are not a storage of an image, trigger an event.
notify(ObjectModifiedEvent(self))
security.declareProtected(
@@ -374,7 +375,7 @@
with self._file.open('w') as descriptor:
descriptor.write(text)
if not interfaces.IImage.providedBy(aq_parent(self)):
- # If we are not a storage of an image, trigger an event.
+ # # If we are not a storage of an image, trigger an event.
notify(ObjectModifiedEvent(self))
security.declareProtected(
@@ -423,16 +424,15 @@
return self.context.get_file()
[email protected](
- interfaces.IFile, zope.lifecycleevent.interfaces.IObjectModifiedEvent)
[email protected](interfaces.IFile, IObjectModifiedEvent)
[email protected](interfaces.IFile, IObjectCreatedEvent)
def file_modified(content, event):
getUtility(IMimeTypeClassifier).guess_filename(content, content.getId())
- content.update_quota()
[email protected](
- interfaces.IFile, zope.lifecycleevent.interfaces.IObjectMovedEvent)
[email protected](interfaces.IFile, IObjectMovedEvent)
def file_added(content, event):
if content is not event.object or event.newName is None:
return
getUtility(IMimeTypeClassifier).guess_filename(content, event.newName)
+
diff -r f16b33bcedf6 -r 77bcc4d9b129 Products/Silva/Folder/content.py
--- a/Products/Silva/Folder/content.py Fri Oct 18 15:58:44 2013 +0200
+++ b/Products/Silva/Folder/content.py Tue Oct 22 11:43:03 2013 +0200
@@ -13,7 +13,6 @@
from zope.traversing.browser import absoluteURL
# Zope
-from Acquisition import aq_inner, aq_parent
from AccessControl import ClassSecurityInfo
from App.class_init import InitializeClass
from OFS.Folder import Folder as BaseFolder
@@ -22,13 +21,14 @@
import OFS.interfaces
# Silva
+from Products.Silva.SilvaObject import QuotaContainer
from Products.Silva.ExtensionRegistry import meta_types_for_interface
from Products.Silva.Publishable import Publishable
from Products.Silva import SilvaPermissions
from Products.Silva import helpers
from silva.core.interfaces import (
- IContentImporter, INonPublishable, IPublishable, IOrderManager,
+ INonPublishable, IPublishable, IOrderManager,
IVersionedContent, IFolder, IRoot, IContent)
from silva.core import conf as silvaconf
from silva.core.interfaces import ContentError
@@ -39,7 +39,7 @@
_marker = object()
-class Folder(Publishable, BaseFolder):
+class Folder(Publishable, QuotaContainer, BaseFolder):
__doc__ = _("""The presentation of the information within a
publication is structured with folders. They determine the visual
hierarchy that a Visitor sees. Folders on the top level
@@ -65,7 +65,6 @@
BaseFolder.manage_options[1:]
_allow_feeds = False
- used_space = 0
def __init__(self, id):
super(Folder, self).__init__(id)
@@ -175,25 +174,6 @@
raise ContentError(
_(u"You cannot convert a folder into a folder."), self)
- def _verify_quota(self):
- # Hook to check quota. Do nothing by default.
- pass
-
- security.declareProtected(
- SilvaPermissions.ChangeSilvaContent, 'update_quota')
- def update_quota(self, delta, verify=True):
- if IContentImporter.providedBy(aq_parent(self)):
- aq_inner(self).update_quota(delta, verify)
- return
-
- self.used_space += delta
- # If we add stuff, check we're not over quota.
- if verify and delta > 0:
- self._verify_quota()
-
- if not IRoot.providedBy(self):
- aq_parent(self).update_quota(delta, verify)
-
# Silva addables
security.declareProtected(SilvaPermissions.ApproveSilvaContent,
@@ -324,7 +304,7 @@
@silvaconf.subscribe(IFolder, OFS.interfaces.IObjectWillBeMovedEvent)
-def folder_moved_update_quota(content, event):
+def folder_moved_update_used_quota(content, event):
"""Event called on folder, when they are moved, we want to update
the quota on parents folders.
"""
@@ -348,8 +328,8 @@
if not size:
return
if event.oldParent:
- event.oldParent.update_quota(-size, verify)
+ event.oldParent.update_used_quota(-size, verify)
if event.newParent:
- event.newParent.update_quota(size, verify)
+ event.newParent.update_used_quota(size, verify)
diff -r f16b33bcedf6 -r 77bcc4d9b129 Products/Silva/SilvaObject.py
--- a/Products/Silva/SilvaObject.py Fri Oct 18 15:58:44 2013 +0200
+++ b/Products/Silva/SilvaObject.py Tue Oct 22 11:43:03 2013 +0200
@@ -4,7 +4,7 @@
# Zope 3
from five import grok
-from zope.component import getUtility
+from zope.component import getUtility, queryUtility
from zope.container.interfaces import IContainerModifiedEvent
from zope.lifecycleevent.interfaces import IObjectAddedEvent
from zope.lifecycleevent.interfaces import IObjectCopiedEvent
@@ -15,6 +15,7 @@
# Zope 2
from AccessControl import ClassSecurityInfo
+from Acquisition import aq_inner, aq_parent
from App.class_init import InitializeClass
from OFS.interfaces import IObjectClonedEvent
from OFS.interfaces import IObjectWillBeAddedEvent
@@ -25,9 +26,12 @@
from Products.Silva.Security import Security, ChangesTask
# Silva adapters
-from silva.core.interfaces import ISilvaObject, IVersionedContent
+from silva.core.interfaces import ISilvaObject, IVersionedContent, IRoot
+from silva.core.interfaces import IQuotaContainer, IQuotaObject
+from silva.core.interfaces import IContentImporter
from silva.core.services.interfaces import ICataloging
from silva.core.services.interfaces import IMetadataService
+from silva.core.services.interfaces import IExtensionService
class TitledObject(object):
@@ -180,6 +184,105 @@
InitializeClass(ViewableObject)
+class QuotaObject(object):
+ """A content that uses some of the site quota
+ """
+ security = ClassSecurityInfo()
+ _old_size = 0 # Old size of the object.
+
+ security.declareProtected(
+ SilvaPermissions.AccessContentsInformation, 'get_quota_usage')
+ def get_quota_usage(self):
+ return -1
+
+ security.declareProtected(
+ SilvaPermissions.ChangeSilvaContent, 'update_quota')
+ def update_quota(self):
+ parent = aq_parent(self)
+ if IQuotaContainer.providedBy(parent):
+ service = queryUtility(IExtensionService)
+ if service is None:
+ return
+ verify = service.get_quota_subsystem_status()
+ if verify is None:
+ return
+
+ # Every content must be inside a container (unless they
+ # are inside an image ...).
+ new_size = self.get_quota_usage()
+ if new_size < 0:
+ # Broken quota usage
+ return
+ delta = new_size - self._old_size
+ if delta:
+ parent.update_used_quota(delta, verify)
+ self._old_size = new_size
+
+ security.declareProtected(
+ SilvaPermissions.ChangeSilvaContent, 'reset_quota')
+ def reset_quota(self):
+ self._old_size = max(0, self.get_quota_usage())
+ return self._old_size
+
+InitializeClass(QuotaObject)
+
+
+class QuotaContainer(object):
+ """A container that aggregate multiple quota objects
+ """
+ security = ClassSecurityInfo()
+ used_space = 0
+
+ def _verify_quota(self):
+ # Hook to check quota. Do nothing by default.
+ pass
+
+ security.declarePrivate('update_used_quota')
+ def update_used_quota(self, delta, verify=True):
+ if IContentImporter.providedBy(aq_parent(self)):
+ aq_inner(self).update_used_quota(delta, verify)
+ return
+
+ self.used_space += delta
+ # If we add stuff, check we're not over quota.
+ if verify and delta > 0:
+ self._verify_quota()
+
+ if not IRoot.providedBy(self):
+ container = aq_parent(self)
+ if container is not None:
+ container.update_used_quota(delta, verify)
+
+InitializeClass(QuotaContainer)
+
+
[email protected](IQuotaObject, IObjectWillBeMovedEvent)
+def update_moved_content_quota(content, event):
+ """Event called on a quotable when they are moved to update quota
+ on parents folders.
+ """
+ if content != event.object or event.newParent is event.oldParent:
+ return
+
+ service = queryUtility(IExtensionService)
+ if service is None:
+ return
+
+ verify = service.get_quota_subsystem_status()
+ if verify is None:
+ # Quota accouting is disabled
+ return
+
+ size = content.get_quota_usage()
+ if not size or size < 0:
+ return
+
+ if event.oldParent and IQuotaContainer.providedBy(event.oldParent):
+ event.oldParent.update_used_quota(-size, verify)
+ if event.newParent and IQuotaContainer.providedBy(event.newParent):
+ event.newParent.update_used_quota(size, verify)
+
+
@grok.subscribe(ISilvaObject, IObjectCreatedEvent)
@grok.subscribe(ISilvaObject, IObjectClonedEvent)
def content_created(content, event):
@@ -208,6 +311,16 @@
ICataloging(content).reindex()
[email protected](IQuotaObject, IObjectCreatedEvent)
+def update_quota_created(content, event):
+ content.update_quota()
+
+
[email protected](IQuotaObject, IObjectModifiedEvent)
+def update_quota_modified(content, event):
+ content.update_quota()
+
+
@grok.subscribe(ISilvaObject, IObjectMovedEvent)
def index_moved_content(content, event):
"""We index all added content (due to a move).
diff -r f16b33bcedf6 -r 77bcc4d9b129 Products/Silva/silvaxml/xmlimport.py
--- a/Products/Silva/silvaxml/xmlimport.py Fri Oct 18 15:58:44 2013 +0200
+++ b/Products/Silva/silvaxml/xmlimport.py Tue Oct 22 11:43:03 2013 +0200
@@ -22,6 +22,13 @@
ICataloging(content).index()
[email protected](interfaces.IQuotaObject, IContentImported)
+def update_quota_import_content(content, event):
+ """Update the quota of imported content (to be sure).
+ """
+ content.update_quota()
+
+
class FolderHandler(handlers.SilvaContainerHandler):
grok.name('folder')
diff -r f16b33bcedf6 -r 77bcc4d9b129 Products/Silva/tests/test_folder_quota.py
--- a/Products/Silva/tests/test_folder_quota.py Fri Oct 18 15:58:44 2013 +0200
+++ b/Products/Silva/tests/test_folder_quota.py Tue Oct 22 11:43:03 2013 +0200
@@ -8,8 +8,9 @@
from zope.interface.verify import verifyObject
from zope.component import getUtility
+from Products.Silva import File
from Products.Silva.Publication import OverQuotaException
-from Products.Silva.testing import FunctionalLayer, TestRequest
+from Products.Silva.testing import FunctionalLayer, TestRequest, Transaction
from silva.core.xml import ZipImporter
from silva.core.interfaces import IArchiveFileImporter, IAsset, IPublication
from silva.core.interfaces import IContainerManager
@@ -146,8 +147,12 @@
def setUp(self):
self.root = self.layer.get_application()
+ with Transaction():
+ self.root = self.layer.get_application()
+ self.root.service_files.storage = File.BlobFile
+ self.root.service_extensions.enable_quota_subsystem()
+
self.layer.login('editor')
- self.root.service_extensions.enable_quota_subsystem()
def test_validate_wanted_quota_on_publication(self):
"""Test validate wanted quota on a publication.