[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.