[Products.Silva][Sylvain Viollon] Implement a site-level quota s...

[email protected] Mon, 14 Oct 2013 15:30:47 +0200
Newsgroups gmane.comp.web.zope.silva.cvs
Message-ID <[email protected]>
author:    Sylvain Viollon
date:      Mon Oct 14 15:30:42 2013 +0200
revision:  11866:f959b428859d in Products.Silva
branch:    2.4
details:   https://hg.infrae.com/Products.Silva?cmd=changeset;node=f959b428859d
modified:  Products/Silva/ExtensionService.py Products/Silva/ExtensionService_templates/manageextensions.pt Products/Silva/Publication/content.py Products/Silva/Root.py Products/Silva/Root_templates/managesiteform.pt Products/Silva/Root_templates/zopewelcomepage.pt Products/Silva/static/welcome.css
added:     Products/Silva/Root_templates/managesiteform.pt
removed:   
log:       Implement a site-level quota system.


diffstat:

 Products/Silva/ExtensionService.py                            |   17 +-
 Products/Silva/ExtensionService_templates/manageextensions.pt |   40 +-
 Products/Silva/Publication/content.py                         |   27 +-
 Products/Silva/Root.py                                        |  153 +++++++++-
 Products/Silva/Root_templates/managesiteform.pt               |   58 +++
 Products/Silva/Root_templates/zopewelcomepage.pt              |   19 +-
 Products/Silva/static/welcome.css                             |    4 +
 7 files changed, 273 insertions(+), 45 deletions(-)

diffs (544 lines):

diff -r ffe5308f7cea -r f959b428859d Products/Silva/ExtensionService.py
--- a/Products/Silva/ExtensionService.py	Wed Oct 09 17:51:49 2013 +0200
+++ b/Products/Silva/ExtensionService.py	Mon Oct 14 15:30:42 2013 +0200
@@ -118,6 +118,7 @@
         {'label': 'Logs', 'action':'manage_main'},
         ) + SilvaService.manage_options
 
+    _site_quota = 0
     _quota_enabled = False
     _quota_verify = False
 
@@ -179,7 +180,11 @@
     def disable_quota_subsystem(self):
         """Disable quota sub-system.
         """
-        assert (self._quota_enabled)
+        if not self._quota_enabled:
+            return False
+        if self._site_quota:
+            # You cannot disable the quota system if there is a site quota.
+            return False
         root = self.get_root()
 
         # Disable metadata for quota
@@ -192,13 +197,15 @@
 
         self._quota_enabled = False
         self._quota_verify = False
+        return True
 
     security.declareProtected(
         'View management screens', 'enable_quota_subsystem')
     def enable_quota_subsystem(self):
         """Enable quota sub-system.
         """
-        assert (not self._quota_enabled)
+        if self._quota_enabled:
+            return False
         root = self.get_root()
 
         # Setup metadata for quota
@@ -220,6 +227,7 @@
         root.used_space = compute_used_space(root)
         self._quota_enabled = True
         self._quota_verify = True
+        return True
 
     security.declareProtected(
         'View management screens', 'upgrade_content')
@@ -247,6 +255,11 @@
         return self._quota_verify
 
     security.declareProtected(
+        'Access contents information', 'get_site_quota')
+    def get_site_quota(self):
+        return self._site_quota
+
+    security.declareProtected(
         'Access contents information', 'is_installed')
     def is_installed(self, name):
         """Is extension installed?
diff -r ffe5308f7cea -r f959b428859d Products/Silva/ExtensionService_templates/manageextensions.pt
--- a/Products/Silva/ExtensionService_templates/manageextensions.pt	Wed Oct 09 17:51:49 2013 +0200
+++ b/Products/Silva/ExtensionService_templates/manageextensions.pt	Mon Oct 14 15:30:42 2013 +0200
@@ -27,22 +27,26 @@
         <input type="submit"
                name="install_documentation"
                value="install documentation"
-               tal:condition="python:context.is_installed('silva.app.document') and not hasattr(context, 'docs')"
+               tal:condition="python:context.is_installed('silva.app.document') and root._getOb('docs', None) is None"
                i18n:attributes="value" />
         <input type="submit"
                name="refresh_catalog"
                value="rebuild catalog"
                i18n:attributes="value" />
-        <input type="submit"
-               name="disable_quota_subsystem"
-               value="disable quota subsystem"
-               tal:condition="view/quota_enabled"
-               i18n:attributes="value" />
-        <input type="submit"
-               name="enable_quota_subsystem"
-               value="enable quota subsystem"
-               tal:condition="not:view/quota_enabled"
-               i18n:attributes="value" />
+        <tal:quota tal:condition="not: context/get_site_quota">
+          <!-- Site quota is not enabled, so you can enable or not the
+               feature. -->
+          <input type="submit"
+                 name="disable_quota_subsystem"
+                 value="disable quota subsystem"
+                 tal:condition="view/quota_enabled"
+                 i18n:attributes="value" />
+          <input type="submit"
+                 name="enable_quota_subsystem"
+                 value="enable quota subsystem"
+                 tal:condition="not:view/quota_enabled"
+                 i18n:attributes="value" />
+        </tal:quota>
         <input type="submit"
                name="purge_old_versions"
                value="purge old versions"
@@ -50,12 +54,18 @@
         <input type="submit" name="upgrade_all" value="upgrade content"
                onclick="if (!confirm('This will upgrade all Silva content, and may take a long time on a large site.\nThis is, however, necessary to make old Silva content work with new versions.\nAre you sure you want to continue?')) return false;"
                i18n:attributes="value" />
-        <br />
-        <br />
-        <span class="std-text">
+        <p class="std-text">
           (Software - <span tal:replace="root/get_silva_software_version">version</span> /
           Content - <span tal:replace="root/get_silva_content_version">version</span>)
-        </span>
+        </p>
+        <p class="std-text"
+           tal:condition="context/get_site_quota">
+          This site has a global quota of <span tal:replace="context/get_site_quota" /> MB.
+        </p>
+        <p class="std-text"
+           tal:condition="not: context/get_site_quota">
+          This site has no global quota restrictions.
+        </p>
       </form>
 
       <h3 i18n:translate="">Available extensions</h3>
diff -r ffe5308f7cea -r f959b428859d Products/Silva/Publication/content.py
--- a/Products/Silva/Publication/content.py	Wed Oct 09 17:51:49 2013 +0200
+++ b/Products/Silva/Publication/content.py	Mon Oct 14 15:30:42 2013 +0200
@@ -11,15 +11,16 @@
 from App.special_dtml import DTMLFile
 from App.class_init import InitializeClass
 from zExceptions import BadRequest
+from zope.component import getUtility
 
 # Silva
 from Products.Silva.Folder import Folder, FolderAddForm
 from Products.Silva import SilvaPermissions, helpers
 
 from silva.core import conf as silvaconf
-from silva.core.interfaces import (
-    IPublication, IRoot, ISiteManager, IInvisibleService)
+from silva.core.interfaces import IPublication, ISiteManager, IInvisibleService
 from silva.core.interfaces import ContentError
+from silva.core.services.interfaces import IMetadataService
 from silva.translations import translate as _
 
 
@@ -102,8 +103,8 @@
         if value < 0:
             # Quota can't be negative.
             return False
-        if (not value) or IRoot.providedBy(self):
-            # 0 means no quota, Root don't have any parents.
+        if not value:
+            # 0 or means no quota.
             return True
         parent = aq_parent(self).get_publication()
         quota = parent.get_current_quota()
@@ -124,16 +125,16 @@
     def get_current_quota(self):
         """Return the current quota value on the publication.
         """
-        service_metadata = self.service_metadata
-        binding = service_metadata.getMetadata(self)
+        binding = getUtility(IMetadataService).getMetadata(self)
         try:
-            return int(binding.get('silva-quota', element_id='quota') or 0)
-        except KeyError:        # This publication object doesn't have
-                                # this metadata set
-            parent = aq_parent(self)
-            if not IRoot.providedBy(parent):
-                return parent.get_current_quota()
-            return 0
+            local = int(binding.get('silva-quota', element_id='quota') or 0)
+            if local > 0:
+                return local
+        except KeyError:
+            pass
+        # Test parent publication/root.
+        parent = aq_parent(self)
+        return parent.get_current_quota()
 
     security.declareProtected(
         SilvaPermissions.AccessContentsInformation, 'get_publication')
diff -r ffe5308f7cea -r f959b428859d Products/Silva/Root.py
--- a/Products/Silva/Root.py	Wed Oct 09 17:51:49 2013 +0200
+++ b/Products/Silva/Root.py	Mon Oct 14 15:30:42 2013 +0200
@@ -5,8 +5,9 @@
 # Zope 3
 from five import grok
 from five.localsitemanager import make_site
-from zope import schema, interface, component
+from zope import schema, interface
 from zope.event import notify
+from zope.component import getUtility
 from zope.lifecycleevent import ObjectCreatedEvent
 from zope.site.hooks import setSite, setHooks
 from zope.traversing.browser import absoluteURL
@@ -16,7 +17,7 @@
 from App.class_init import InitializeClass
 from Products.PageTemplates.PageTemplateFile import PageTemplateFile
 from OFS.Application import Application
-from zExceptions import Unauthorized
+from zExceptions import Unauthorized, Redirect
 import Globals
 import transaction
 
@@ -33,10 +34,30 @@
 from silva.core.interfaces.events import InstallRootEvent
 from silva.core.interfaces.events import InstallRootServicesEvent
 from silva.core.messages.interfaces import IMessageService
+from silva.core.services.interfaces import IExtensionService, IMetadataService
 from silva.translations import translate as _
 from zeam.form import silva as silvaforms
 
 
+class IQuotaRootManager(interface.Interface):
+    """Describe a quota for a Silva root.
+    """
+    identifier = schema.TextLine(
+        title=_(u"Site"),
+        readonly=True,
+        required=True)
+    quota = schema.Int(
+        title=_(u"Site quota"),
+        min=-1,
+        default=-1,
+        required=True)
+    used = schema.Float(
+        title=_(u'Used space'),
+        min=-1.0,
+        readonly=True,
+        required=True)
+
+
 class ISilvaRootAddFields(interface.Interface):
     """Describe an add form for a new Silva root.
     """
@@ -59,6 +80,7 @@
 class ZopeWelcomePage(silvaforms.ZMIForm):
     grok.context(Application)
     grok.name('index.html')
+    grok.require('zope2.Public')
 
     fields = silvaforms.Fields(ISilvaRootAddFields)
 
@@ -67,21 +89,23 @@
         self.is_dev = Globals.DevelopmentMode
         self.version = extensionRegistry.get_extension('Silva').version
 
-    def is_allowed_to_add_root(self):
+    def is_manager(self):
         return getSecurityManager().checkPermission(
             'View Management Screens', self.context)
 
     @silvaforms.action(
         _(u"Authenticate first to add a new site"),
-        available=lambda form:not form.is_allowed_to_add_root())
+        identifier='login',
+        available=lambda form:not form.is_manager())
     def login(self):
-        if not self.is_allowed_to_add_root():
+        if not self.is_manager():
             raise Unauthorized("You must authenticate to add a new Silva Site")
 
     @silvaforms.action(
         _(u"Add a new site"),
-        available=lambda form:form.is_allowed_to_add_root())
-    def new_root(self):
+        identifier='add_site',
+        available=lambda form:form.is_manager())
+    def add_site(self):
         data, errors = self.extractData()
         if errors:
             return silvaforms.FAILURE
@@ -89,8 +113,7 @@
             data['identifier'],
             data.getDefault('title'))
         root = self.context._getOb(data['identifier'])
-        service = component.getUtility(IMessageService)
-        service.send(
+        getUtility(IMessageService).send(
             _(u"New Silva site ${identifier} added.",
               mapping={'identifier': data['identifier']}),
             self.request)
@@ -110,6 +133,76 @@
         return silvaforms.SUCCESS
 
 
+class ManageSiteForm(silvaforms.ZMIComposedForm):
+    grok.context(Application)
+    grok.name('manage.html')
+
+
+class QuotaRootFactory(object):
+
+    def __init__(self, site):
+        self._site = site
+        self.identifier = site.getId()
+        self.quota = -1
+        self.used = -1
+        quota = self._site.service_extensions.get_site_quota()
+        if quota:
+            self.quota = quota
+        used = self._site.used_space
+        if used:
+            self.used = used / (1024*1014)
+
+    def update(self, value):
+        """Update the quota.
+        """
+        service = self._site.service_extensions
+        if value > 0:
+            service._site_quota = value
+            if not service.get_quota_subsystem_status():
+                try:
+                    setSite(self._site)
+                    setHooks()
+                    service.enable_quota_subsystem()
+                finally:
+                    setSite(None)
+                    setHooks()
+        else:
+            service._site_quota = None
+
+
+class SetQuotaAction(silvaforms.Action):
+    title = _(u'Set Quota')
+
+    def __call__(self, form, quota, line):
+        data, errors = line.extractData(form.tableFields)
+        if errors:
+            raise silvaforms.ActionError('Invalid quota settings.')
+        quota.update(data['quota'])
+        return silvaforms.SUCCESS
+
+
+class ManageQuotaRootForm(silvaforms.ZMISubTableForm):
+    grok.context(Application)
+    grok.view(ManageSiteForm)
+
+    label = _(u'Manage sites')
+    description = _(u'Modify the space (in MB) allocated to a complete Silva '
+                    u'site. This setting is not modifiable from within the site.')
+    ignoreContent = False
+    batchSize = 25
+    batchItemFactory = QuotaRootFactory
+    tableFields = silvaforms.Fields(IQuotaRootManager)
+    tableFields['identifier'].mode = silvaforms.DISPLAY
+    tableFields['used'].mode = silvaforms.DISPLAY
+    tableActions = silvaforms.TableActions(SetQuotaAction())
+
+    def getItems(self):
+        return self.context.objectValues('Silva Root')
+
+    def getItemIdentifier(self, item, position):
+        return item.identifier
+
+
 class Root(Publication):
     """Root of Silva site.
     """
@@ -138,6 +231,24 @@
 
     # MANIPULATORS
 
+    security.declareProtected(
+        SilvaPermissions.AccessContentsInformation, 'validate_wanted_quota')
+    def validate_wanted_quota(self, value, REQUEST=None):
+        """Validate the wanted quota is correct the current
+        publication.
+        """
+        if value < 0:
+            # Quota can't be negative.
+            return False
+        if not value:
+            # 0 or means no quota.
+            return True
+        # Quota can't be be bigger than the site quota.
+        if self.service_extensions._site_quota:
+            if self.service_extensions._site_quota < value:
+                return False
+        return True
+
     security.declareProtected(SilvaPermissions.ApproveSilvaContent,
                               'to_folder')
     def to_folder(self):
@@ -186,9 +297,27 @@
         site.
         """
         if not hasattr(self.aq_base, '_addables_forbidden'):
-            return 0
-        else:
-            return self._addables_forbidden.has_key(meta_type)
+            return False
+        return meta_type in self._addables_forbidden
+
+    security.declareProtected(
+        SilvaPermissions.AccessContentsInformation, 'get_current_quota')
+    def get_current_quota(self):
+        """Return the current quota value on the publication.
+        """
+        site = getUtility(IExtensionService).get_site_quota()
+        binding = getUtility(IMetadataService).getMetadata(self)
+        try:
+            local = int(binding.get('silva-quota', element_id='quota') or 0)
+            if local < 0:
+                local = 0
+        except KeyError:
+            local = 0
+        if site and local:
+            return min(site, local)
+        if site:
+            return site
+        return local
 
     security.declarePublic('get_silva_software_version')
     def get_silva_software_version(self):
diff -r ffe5308f7cea -r f959b428859d Products/Silva/Root_templates/managesiteform.pt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Products/Silva/Root_templates/managesiteform.pt	Mon Oct 14 15:30:42 2013 +0200
@@ -0,0 +1,58 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html
+   i18n:domain="silva">
+  <head>
+    <title>Zope / Silva</title>
+    <link rel="stylesheet" href="#" type="text/css"
+          tal:attributes="href view/static/welcome.css" />
+  </head>
+  <body>
+    <div class="welcome">
+      <div class="title">
+        <a href="#" title="Site overview"
+           i18n:attributes="title"
+           tal:attributes="href context/@@absolute_url">
+          <img src="#" alt="Silva logo"
+               tal:attributes="src view/static/silva_logo.png" />
+        </a>
+        <h1 i18n:translate="">
+          Manage Silva sites
+        </h1>
+      </div>
+
+      <h2 tal:condition="view/label"
+          tal:content="view/label">Label</h2>
+
+      <p tal:condition="view/description"
+         tal:content="view/description">Description</p>
+
+      <p class="form-error"
+         tal:condition="view/errors">
+        There were errors:
+        <tal:error tal:repeat="error view/formErrors">
+          <br />
+          <span tal:replace="error/title" />
+        </tal:error>
+      </p>
+
+      <div class="subforms">
+        <div class="subform"
+             tal:repeat="subform view/subforms"
+             tal:content="structure subform/render">
+          Subform
+        </div>
+      </div>
+
+      <div class="actions"
+           tal:condition="view/actionWidgets">
+        <div class="action" tal:repeat="widget view/actionWidgets">
+          <tal:widget tal:content="structure widget/render" />
+        </div>
+      </div>
+
+      <div class="zope"
+           tal:content="structure context/ZopeAttributionButton">
+      </div>
+    </div>
+  </body>
+</html>
diff -r ffe5308f7cea -r f959b428859d Products/Silva/Root_templates/zopewelcomepage.pt
--- a/Products/Silva/Root_templates/zopewelcomepage.pt	Wed Oct 09 17:51:49 2013 +0200
+++ b/Products/Silva/Root_templates/zopewelcomepage.pt	Mon Oct 14 15:30:42 2013 +0200
@@ -9,8 +9,12 @@
   <body>
     <div class="welcome">
       <div class="title">
-        <img src="#" alt="Silva logo"
-             tal:attributes="src view/static/silva_logo.png" />
+        <a href="#" title="Site overview"
+           i18n:attributes="title"
+           tal:attributes="href context/@@absolute_url">
+          <img src="#" alt="Silva logo"
+               tal:attributes="src view/static/silva_logo.png" />
+        </a>
         <h1 i18n:translate="">
           Welcome to Silva
           <span i18n:name="version" tal:content="view/version" />
@@ -38,6 +42,14 @@
             </tal:site>
           </li>
         </ul>
+        <p tal:condition="view/is_manager">
+          <i><a href="#" title="Manage quota"
+             tal:attributes="href string:${context/@@absolute_url}/manage.html"
+             i18n:attributes="title"
+             i18n:translate="">
+            Advanced management tasks...
+          </a></i>
+        </p>
       </div>
       <div class="new-site">
         <form action="." tal:attributes="action request/URL" method="post"
@@ -54,7 +66,7 @@
           </p>
 
           <div class="fields"
-               tal:condition="view/fieldWidgets">
+               tal:condition="view/is_manager">
             <div class="field" tal:repeat="widget view/fieldWidgets">
               <label class="field-label" for="#"
                      tal:attributes="for widget/htmlId"
@@ -82,6 +94,7 @@
               <tal:widget tal:content="structure widget/render" />
             </div>
           </div>
+          <br />
         </form>
       </div>
       <div class="info">
diff -r ffe5308f7cea -r f959b428859d Products/Silva/static/welcome.css
--- a/Products/Silva/static/welcome.css	Wed Oct 09 17:51:49 2013 +0200
+++ b/Products/Silva/static/welcome.css	Mon Oct 14 15:30:42 2013 +0200
@@ -33,6 +33,10 @@
   text-align: right;
 }
 
+.action {
+  float: left;
+}
+
 .form-error, .field-error {
   color: red;
   font-weight: bold;