r867 - in zope-quotafolder: branches/upstream/0.1.1 branches/upstream/current trunk

lunar at alioth.debian.org lunar at alioth.debian.org
Mon May 14 10:26:42 UTC 2007


Author: lunar
Date: 2007-05-14 10:26:40 +0000 (Mon, 14 May 2007)
New Revision: 867

Added:
   zope-quotafolder/branches/upstream/0.1.1/DESIGN
   zope-quotafolder/branches/upstream/0.1.1/KNOWNBUGS
   zope-quotafolder/branches/upstream/0.1.1/LICENSE
   zope-quotafolder/branches/upstream/0.1.1/QuotaFolder.py
   zope-quotafolder/branches/upstream/0.1.1/README.txt
   zope-quotafolder/branches/upstream/0.1.1/TODO
   zope-quotafolder/branches/upstream/0.1.1/__init__.py
   zope-quotafolder/branches/upstream/0.1.1/manage_addQuotaFolderForm.dtml
   zope-quotafolder/branches/upstream/0.1.1/manage_editQuotaForm.dtml
   zope-quotafolder/branches/upstream/0.1.1/refresh.txt
   zope-quotafolder/branches/upstream/current/DESIGN
   zope-quotafolder/branches/upstream/current/KNOWNBUGS
   zope-quotafolder/branches/upstream/current/LICENSE
   zope-quotafolder/branches/upstream/current/QuotaFolder.py
   zope-quotafolder/branches/upstream/current/README.txt
   zope-quotafolder/branches/upstream/current/TODO
   zope-quotafolder/branches/upstream/current/__init__.py
   zope-quotafolder/branches/upstream/current/manage_addQuotaFolderForm.dtml
   zope-quotafolder/branches/upstream/current/manage_editQuotaForm.dtml
   zope-quotafolder/branches/upstream/current/refresh.txt
   zope-quotafolder/trunk/DESIGN
   zope-quotafolder/trunk/KNOWNBUGS
   zope-quotafolder/trunk/LICENSE
   zope-quotafolder/trunk/QuotaFolder.py
   zope-quotafolder/trunk/README.txt
   zope-quotafolder/trunk/TODO
   zope-quotafolder/trunk/__init__.py
   zope-quotafolder/trunk/manage_addQuotaFolderForm.dtml
   zope-quotafolder/trunk/manage_editQuotaForm.dtml
   zope-quotafolder/trunk/refresh.txt
Removed:
   zope-quotafolder/branches/upstream/0.1.1/QuotaFolder-0.1.1.tar.gz
   zope-quotafolder/branches/upstream/current/QuotaFolder-0.1.1.tar.gz
   zope-quotafolder/trunk/QuotaFolder-0.1.1.tar.gz
   zope-quotafolder/trunk/QuotaFolder-0.1.1.tar.gz.cdbs-config_list
Log:
Really use upstream tarball instead of a tarball of a tarball. *sigh*


Added: zope-quotafolder/branches/upstream/0.1.1/DESIGN
===================================================================
--- zope-quotafolder/branches/upstream/0.1.1/DESIGN	                        (rev 0)
+++ zope-quotafolder/branches/upstream/0.1.1/DESIGN	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,117 @@
+Patch ObjectManager zdd:
+
+ - het get_usage() ondersteunt
+   Deze itereert over alle subobjects en roept, indien mogelijk, get_usage
+   aan.
+
+   Return: files, size
+
+ - creatie van objecten verifieert met parent object
+
+ 
+Patch File en evt. andere objecten zdd. 
+
+ - deze get_usage() ondersteunen
+ - manage_upload / whatever checken
+
+ Quota awareness
+
+Implementeer een speciale QuotaFolder welke op basis van get_usage() 
+gebruik bepaalt.
+
+Hou rekening met:
+
+- transparante migratie
+- sync optie om quota up2date te krijgen
+- Nesten van QuotaFolders
+- consistentie bij fouten/exceptions (i.e. QuotaExceeded)
+
+- hard/soft quota
+- mailnotificatie
+- security check in context van parent
+
+- wat als een folderstructuur gepaste wordt? Met een te-groot object?
+
+Is het zinnig quota etc bij te houden als er geen QuotaFolder parent is?
+Alleen als parent later QuotaFolder wordt... Kan dit? (migratie)?
+Waarom dan niet gewoon syncen?
+
+Nesten van QuotaFolders is uiteindelijk wel wenselijk, hier dient rekening
+mee gehouden te worden...
+
+- wat als folder gedelete wordt?
+- als data geupload/vergroot wordt?
+- check ftp, webdav
+- len(GET()) geeft rendered lengte?
+- len(.zexp)?
+
+Bij aanpassing van een object relatieve verschil quota_checken(), of
+recursief naar boven toe herberekenen? (geen optie bij grote site)
+
+
+-----------------------------------------------------------------------
+
+Alle objecten en in het byzonder ObjectManagers worden quota aware gemaakt.
+Dit betekent dat:
+
+Objectmanagers
+
+- add/delete van objecten bijhouden (eigen count) en aan parent doorgeven
+- size van subobjecten bijhouden (recursief)
+- notificaties krijgen van size changes van subobjecten
+
+Andere Objecten (File, Image, DTMLDocument, PythonScript, ...)
+
+- eigen size bijhouden 
+- uploads bijhouden en melden aan parent (=ObjectManager)
+- changes bijhouden en melden aan parent
+
+  (beiden kunnen groei/krimp zijn)
+
+- stort het systeem in (afgezien van quotafolders) als er geen QuotaProduct
+  meer is?
+- Ignore quota voor superuser
+- undo van (delete) transaction -> gevolgen?
+
+Toekomst:
+
+- soft/hard limit, expiry
+- mogelijkheid tot aanroepen script bij quotum exceeding (soft/hard) -> mail
+- Filteren/beperken meta types
+
+
+Specifiek testen:
+
+- exception bij overschrijding abort transaction?
+- support bij ftp, webdav (+ errorhandling)
+- photofolder
+- Btree folder
+- Transparentfolder?
+- Wat gebeurt er als je een quota aware folder importeert in een ander
+  systeem? (niks! denk ik)
+- undo quota change door manager?
+- objecten deleten, tegen quotum aanzitten, deletion undo-en
+- versions ?! Lijken transparant te werken... (alhoewel je wel veel state in versions kan bewaren)
+- metapublisher/formulator
+
+scripts als:
+
+doc = container.doc
+container.manage_delObjects(['doc'])
+doc.manage_edit('hello'*100, 'hacked')
+
+Evt. met vervangende pseudo parent
+
+BUGS
+----
+
+- zcatalog aanmaken geeft 1 object, zcatalog deleten is -2 FIXED
+- tutorial geeft niet teveel objecten/size, deleten gaat fout FIXED
+- manage import/export voegt ook objecten toe zonder setOb FIXED
+- acquisition werkt mogelijk niet meteen/direct voor imported data FIXED
+
+Probleem:
+---------
+
+Naast .zexp zijn ook pasted objecten objecten met veel subobjects (evt 
+hierarchisch)

Added: zope-quotafolder/branches/upstream/0.1.1/KNOWNBUGS
===================================================================
--- zope-quotafolder/branches/upstream/0.1.1/KNOWNBUGS	                        (rev 0)
+++ zope-quotafolder/branches/upstream/0.1.1/KNOWNBUGS	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,52 @@
+This is a list of known issues / incompatibilities with QuotaFolder
+
+Bugs in Zope:
+
+- ZopeTutorial does some inproper exception catching, causing QuotaExceeded
+  errors to be ignored (and actually the product to be imported twice).
+  The relevant code is:
+
+      try:
+        folder.manage_importObject(tutorialExamplesFile)
+    except:
+        folder._p_jar=self.Destination()._p_jar
+        folder.manage_importObject(tutorialExamplesFile)
+        
+- A similair problem probably occurs when installing userfolders - if a user is past 
+  his quota and tries to create a userfolder, the error 'this folder already contains a 
+  userfolder', and the userfolder is created nonetheless
+
+  This actually brings the quotafolder out of sync!!! (because due to the exception,
+  the quota isn't updated, but the transaction isn't aborted either)
+
+  -- RESOLVED 16Mar02 This bug has been worked around, though the error 'this object
+     already has a userfolder' will appear if the quota is exceeded.
+
+Bugs in QuotaFolder:
+
+- Maximum object size is not always enforced, esp. when importing objects
+  (or hierarchies of objects)
+
+- The following script used to give problems:
+
+    doc = container.doc
+    container.manage_delObjects(['doc'])
+    doc.manage_edit('hello'*100, 'hacked')
+
+  Hooked objects now check if they are still contained in their parent.
+  What if doc is placed in a pseudo parent folder?
+
+- Pressing 'Sync' in the Quota tab brings you back to the Quota tab, however,
+  the tab itself indicates 'Contents' is selected. -- RESOLVED 18Mar02
+
+- QuotaExceeded exceptions aren't always precise - it displays how much quota
+  you would need for the operation, but that may not be correct.
+
+- QuotaFolder seems to mess up undo capabilities (under what circumstances?)
+
+- QuotaFolder is not compatible with FLE. Creation goes fine (it seems), but
+  newly created objects / modified objects are not added. -- RESOLVED 16Mar02
+
+- It is still possible to store large amounts of data in properties. A solution
+  would be to test if an object is an instance of PropertyManager, and account
+  the size of the properties appropriately.

Added: zope-quotafolder/branches/upstream/0.1.1/LICENSE
===================================================================
--- zope-quotafolder/branches/upstream/0.1.1/LICENSE	                        (rev 0)
+++ zope-quotafolder/branches/upstream/0.1.1/LICENSE	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,27 @@
+Copyright (c) 2002 Ivo van der Wijk, Amaze Internet Services (ivo at amaze.nl)
+
+All rights reserved. 
+
+Redistribution and use in source and binary forms, with or without 
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+3. The name of the author may not be used to endorse or promote products
+   derived from this software without specific prior written permission. 
+   
+   
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Deleted: zope-quotafolder/branches/upstream/0.1.1/QuotaFolder-0.1.1.tar.gz
===================================================================
(Binary files differ)

Added: zope-quotafolder/branches/upstream/0.1.1/QuotaFolder.py
===================================================================
--- zope-quotafolder/branches/upstream/0.1.1/QuotaFolder.py	                        (rev 0)
+++ zope-quotafolder/branches/upstream/0.1.1/QuotaFolder.py	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,253 @@
+##
+##  Copyright (c) 2002 Ivo van der Wijk, Amaze Internet Services (ivo at amaze.nl)
+## 
+##  All rights reserved. 
+## 
+##  Redistribution and use in source and binary forms, with or without 
+##  modification, are permitted provided that the following conditions
+##  are met:
+## 
+## 1. Redistributions of source code must retain the above copyright
+##    notice, this list of conditions and the following disclaimer.
+## 2. Redistributions in binary form must reproduce the above copyright
+##    notice, this list of conditions and the following disclaimer in the
+##    documentation and/or other materials provided with the distribution.
+## 3. The name of the author may not be used to endorse or promote products
+##    derived from this software without specific prior written permission. 
+##   
+##   
+## THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+## IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+## OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+## IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+## INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+## NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+## THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+##
+
+import App, Globals, OFS
+import string
+
+from Persistence import Persistent
+
+from Globals import DTMLFile
+from OFS.Folder import Folder
+from Acquisition import aq_base, aq_parent, Acquired, Implicit
+from AccessControl import getSecurityManager, SpecialUsers
+
+from zLOG import LOG, WARNING
+
+def manage_addQuotaFolder(self, id, title, quota_bytes=0, quota_objects=0, 
+                          quota_maxsize=0, REQUEST=None):
+    """ Add a new QuotaFolder """
+
+    o = QuotaFolder(id, title, quota_bytes, quota_maxsize, quota_objects, REQUEST)
+    self._setObject(id, o)
+    o = self._getOb(id)
+
+    if REQUEST:
+        return self.manage_main(self, REQUEST, update_menu=1)
+    
+def _replaceFolder(parent, id, quota_bytes=0, quota_objects=0, quota_maxsize=0):
+    # Replaces an OFS.Folder with a QuotaFolder. Borrowed from BTreeFolder
+    ob = QuotaFolder(id, "", quota_bytes, quota_maxsize, quota_objects)
+    f = parent._getOb(id)
+    # Copy the contents of the folder to the new QuotaFolder
+
+    ids = f.objectIds()
+    for key, value in f.__dict__.items():
+        if key not in ids:
+            # Not an ObjectManager item.
+            ob.__dict__[key] = value
+    for key in ids:
+        subob = f._getOb(key)
+        subob = getattr(subob, 'aq_base', subob)
+        ob._setOb(key, subob)
+    parent._setOb(id, ob)
+
+
+manage_addQuotaFolderForm=DTMLFile('manage_addQuotaFolderForm', globals())
+
+QuotaExceededException='Quota Exceeded'
+
+class QuotaFolder(Folder, Persistent, Implicit):
+    """ A QuotaFolder """
+
+    meta_type='QuotaFolder'
+
+    manage_options=(
+        Folder.manage_options+
+        (
+        {'label': 'Quota', 'action': 'manage_editQuotaForm'},
+        )
+        )
+
+    __ac_permissions__= \
+        Folder.__ac_permissions__ +  \
+        (
+        ('View current quota', ('currentCount','currentSize')),
+        )
+
+    manage_editQuotaForm=DTMLFile('manage_editQuotaForm', globals())    
+
+    def __init__(self, id, title="", quota=0, maxFileSize=0, maxNumberOfFiles=0,
+                 manager_role=1, REQUEST=None):
+        self.id=id
+        self.title=title
+        self._quota_bytes=int(quota)
+        self._quota_objects=int(maxNumberOfFiles)
+        self._quota_maxsize=int(maxFileSize)
+
+        ##
+        ## current accounting
+        self._quota_filecount = 0
+        self._quota_size = 0
+
+        self._manager_role = 0
+        if manager_role:
+            self._manager_role = 1
+        self._debug = 0
+        self._allow_nested = 0
+
+    def _check_quota(self, filechange, sizechange=0, objsize=0):
+        """ Check if the change in usage violates the quota settings """
+        #
+        # print "I'm a quota folder: ", filechange, sizechange, objsize
+        # print "Current quota: ", self._quota_filecount, self._quota_size 
+
+        #
+        # The emergency user can create *some* objects (i.e. userfolders).
+        # make sure this never gives a QuotaExceeded exception
+
+        emergency = 0
+        user=getSecurityManager().getUser()
+        if (SpecialUsers.emergency_user and
+            aq_base(user) is SpecialUsers.emergency_user):
+            LOG('QuotaFolder', INFO, 
+                "Emergency user detected - not enforcing quota")
+            emergency = 1
+
+        if not emergency and \
+           filechange > 0 and \
+           self._quota_filecount + filechange > self._quota_objects \
+                                              > 0:
+            raise QuotaExceededException, \
+                  "Too many files, this operation would require a quotum " + \
+                  "of %d files, your current quotum is %d files" % \
+                  (self._quota_filecount + filechange, self._quota_objects)
+        if not emergency and \
+           sizechange > 0 and \
+           self._quota_size + sizechange > self._quota_bytes > 0:
+            raise QuotaExceededException, \
+                 "Too much space, this operation would require a " + \
+                 "quotum of %d bytes, your current quotum is %d bytes" % \
+                 (self._quota_size + sizechange, self._quota_bytes)
+        if not emergency and objsize > self._quota_maxsize > 0:
+            raise QuotaExceededException, \
+                  "The object is too large. It requires %d bytes, " + \
+                  "your maximum object size limit is %d bytes" % \
+                  (objsize, self._quota_maxsize)
+
+        self._quota_filecount = self._quota_filecount + filechange
+        self._quota_size = self._quota_size + sizechange
+
+        ##
+        ## This should not happen... 
+        path = string.join(self.getPhysicalPath(), "/")
+        if self._quota_filecount < 0:
+            LOG('QuotaFolder', WARNING, "Negative quota filecount in %s" % path)
+            if not self._debug:
+                self._quota_filecount = 0
+        if self._quota_size < 0:
+            LOG('QuotaFolder', WARNING, "Negative quota size in %s" % path)
+            if not self._debug:
+                self._quota_size = 0
+
+        # print "New quota: ", self._quota_filecount, self._quota_size 
+
+    def current_count(self):
+        return self._quota_filecount
+
+    def current_size(self):
+        return self._quota_size
+
+    def quota_bytes(self):
+        return self._quota_bytes
+
+    def quota_objects(self):
+        return self._quota_objects
+
+    def quota_maxsize(self):
+        return self._quota_maxsize
+
+    def authorized_to_edit(self):
+        """ 
+            return true/false if user is authorized to change in current context
+        """
+
+        security=getSecurityManager()
+        security.addContext(self)
+        user = security.getUser()
+        if not self._manager_role:
+            ## XXX use permission?
+            return user.allowed(self, ['Manager'])
+            
+        ## use aq_base? 
+        return user.allowed(aq_parent(self), ['Manager'])
+        
+    def manager_role(self):
+        return self._manager_role
+
+    def manage_editQuota(self, quota=-1, maxFileSize=-1, maxNumberOfFiles=-1,
+                         REQUEST=None):
+        """ Update the current Quota """
+        ##
+        ## Perhaps always sync after edit?
+
+        if REQUEST.has_key('Sync'):
+            self._qa_sync()
+            ## XXX report difference to parent?
+            if REQUEST:
+                ## A redirect selects the proper tab
+                message = "Quota synchronized"
+                REQUEST.RESPONSE.redirect(
+                      'manage_editQuotaForm?manage_tabs_message=%s' % message)
+                return self.manage_editQuotaForm(self, REQUEST,
+                                                 manage_tabs_message=message)
+            return ''
+
+        if not self.authorized_to_edit():
+           raise "Unauthorized", \
+                 "You are not authorized to edit this quota folder"
+
+        if quota != -1:
+            self._quota_bytes=int(quota)
+        if maxFileSize != -1:
+            self._quota_maxsize=int(maxFileSize)
+        if maxNumberOfFiles != -1:
+            self._quota_objects=int(maxNumberOfFiles)
+        self._manager_role = REQUEST.get("manager_role", 0)
+        if REQUEST:
+            message = "Changes saved"
+            ## A redirect selects the proper tab
+            REQUEST.RESPONSE.redirect(
+                     'manage_editQuotaForm?manage_tabs_message=%s' % message)
+            return self.manage_editQuotaForm(self, REQUEST,
+                                                 manage_tabs_message=message)
+        return ''
+
+    def manage_afterAdd(self, item, container):
+        # print "afterAdd %s %s"%(str(item), str(container))
+        if not self._allow_nested:
+            for parent in self.aq_chain[1:]:
+                if hasattr(parent, "meta_type") and \
+                   parent.meta_type == "QuotaFolder":
+                    raise QuotaExceededException, \
+                          "Cannot add a QuotaFolder inside another QuotaFolder"
+
+        Folder.manage_afterAdd(self, item, container)
+
+Globals.default__class_init__(QuotaFolder)

Added: zope-quotafolder/branches/upstream/0.1.1/README.txt
===================================================================
--- zope-quotafolder/branches/upstream/0.1.1/README.txt	                        (rev 0)
+++ zope-quotafolder/branches/upstream/0.1.1/README.txt	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,187 @@
+QuotaFolder - Quota support for Zope
+
+What is it?
+-----------
+
+QuotaFolder is a folder-ish object that restricts the total number of objects,
+their total size and their individual maximum size. QuotaFolder takes
+subfolders (recursively) into account, so it should not be possible to escape
+the quota restrictions.
+
+The basic goal of QuotaFolder is not to put an absolute limit on ZODB usage
+- it is impossible to determine this. For example, each object may have
+several revisions in the ZODB, and some of the internal state of the object
+is stored in the ZODB that's not returned by get_size(). The goal is to 
+limit the use of objects in general, to prevent people from offering large 
+files or using up resources with enormous amounts of objects.
+
+How does it work?
+-----------------
+
+QuotaFolder 'MonkeyPatches (tm)' some of the internal Zope components such
+as ObjectManager, File, Image, DTMLMethod, DTMLDocument and more. The patching
+constist of making the components 'quota aware'. This basically means the 
+objects will track changes and report them to parent folders. If one if the
+parent folders is a QuotaFolder, and the QuotaFolder detects that the change
+will exceed the quota, a QuotaExceededException is raised, and the transaction
+is rolled back.
+
+At this moment, if an object supports the get_size() method, it's used. The
+value reported by this method will not take all usage (i.e. properties) into
+account, so this may be changed in the future.
+
+Supported products
+------------------
+
+The QuotaFolder product knows how to account File, Image, DTMLMethod,
+DTMLDocument and PythonScript objects. It also understands ObjectManager
+based products such as Folder and BTreeFolder (TransparentFolder is not
+thoroughly tested, but it seems to work well)
+
+Most products (i.e. FLE, Squishdot, PhotoFolder, ZWiki) exist of ObjectManagers 
+with subobjects, and QuotaFolder knows how to handle these quite well. 
+Unknown objects who's size (and changes in size) cannot be determined are 
+accounted as 1 object with size 0. If this is not satisfactory (for example, 
+it's not at this moment for TinyTablePlus), extra support can be built in for 
+these objects (as has been done with, for example, ZPT).
+
+QuotaFolder also supports .zexp imports and copy/paste operations.
+
+QuotaFolder has been tested with Zope 2.4.3, 2.4.4b1 and 2.5.0. It has not
+been tested in ZEO setups.
+
+Installing
+----------
+
+**WARNING**
+
+This product patches classes in your Zope server. Please test the code on
+a test-server or shadow server first! This code has been known to work 
+succesfully with Zope 2.4.x and Zope 2.5. Use this product at your own
+risk, and backup your Data.fs first!
+
+Install the QuotaFolder product by simply unpacking into your Products
+directory (either in your SOFTWARE_HOME or your INSTANCE_HOME) and restart
+your Zope server. The QuotaFolder product should appear in 
+/Control_Panel/Products, and it should also be available in the products
+dropdown.
+
+When creating (or editting) a QuotaFolder, you will be presented with the
+following fields:
+
+id
+title               
+Quota size in bytes          - this is the maximum total size of all objects 
+                               that's allowed to be created
+Maximum object size in bytes - The maximum allowed total size for single
+                               objects
+Maximum number of objects    - this is the maximum number of objects that's
+                               allowed to be created
+Require manager role in parent context?
+                             - If this setting is enabled, a user must be 
+                               manager in the folder context *above* the 
+                               quotafolder itself to be able to edit the
+                               quota. If you want to limit your users in their
+                               usage, you don't want them to be able to edit
+                               their quota themselves, do you?
+
+After creation, you will get the same contents view as with a standard folder,
+but with an extra tab to the right where you can view and (optionally edit)
+the quota.
+
+When visiting the quota tab, you will see an extra button 'Sync'. Pressing
+this button will cause the QuotaFolder to recalculate all usage. Usually,
+the folder shouldn't be out of sync. If you manage to get a QuotaFolder
+out of sync, please contact me.
+
+Migrating
+---------
+
+There are two ways to migrate a standard folder to a QuotaFolder:
+
+- Create a new QuotaFolder, copy all objects from the old folder, paste
+  them into the QuotaFolder and rename the folders. This will probably not
+  work if you have versions (cut/paste may work with versions)
+
+- Use the builtin _replaceFolder method. I.e. create the following external
+  method:
+
+  from Products.QuotaFolder.QuotaFolder import _replaceFolder
+
+  def replace(self, name, quota_bytes, quota_objects, quota_maxsize, REQUEST):
+    _replaceFolder(self, name, quota_bytes, quota_objects, quota_maxsize)
+    return "%s converted" % name
+
+  Create an appropriate external method object in your zope server and invoke
+  it with an appropriately formatted url or create a dtml form.
+
+Hacking contest!
+----------------
+
+QuotaFolder has been thoroughly tested, and seems very stable and compatible.
+However, every now and then, new situations seem to appear where it's possible
+to use more objects or space than the QuotaFolder should enforce. This may
+be through unsupported objects (though these should generaly not make it
+possible to create more free space for other objects), or using trickery
+with PythonScripts, etc.
+
+If anyone finds issues like this, please contact me (info below), so we can
+make this product even more stable and robust :)
+
+Please check the file KNOWNBUGS before reporting issues.
+
+Release info
+------------
+
+0.1     Initial version, basic support for quota
+
+Future plans
+------------
+
+- Support soft and hardlimit quota, with configurable timeleft
+- Restrict installable metatypes
+- Limit number of certain metatypes
+
+Contact/License
+---------------
+
+QuotaFolder is partially based on ideas by Andrew Kenneth, though most of his
+old QuotaFolder has disappeared.
+
+QuotaFolder is written by Ivo van der Wijk as part of Amaze Internet Service's
+FreeZope.org Free Zope hosting environment.
+
+I can be contacted through ivo at amaze.nl or on IRC as VladDrac / VladDrak @ OPN
+
+Recent versions of QuotaFolder and other Zope products can be found at:
+
+http://www.zope.org/Members/ivo
+http://vanderijk.info/
+
+QuotaFolder is (c) 2002 Ivo van der Wijk / Amaze Internet Services
+
+All rights reserved. 
+
+Redistribution and use in source and binary forms, with or without 
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+3. The name of the author may not be used to endorse or promote products
+   derived from this software without specific prior written permission. 
+   
+   
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Added: zope-quotafolder/branches/upstream/0.1.1/TODO
===================================================================
--- zope-quotafolder/branches/upstream/0.1.1/TODO	                        (rev 0)
+++ zope-quotafolder/branches/upstream/0.1.1/TODO	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,23 @@
+First release:
+
+- check required attributes on objects (i.e. data on File/Image), report
+  warning if missing
+- Provide a conversion script (external method) to convert standard folders
+  to QuotaFolders                                       DONE
+- Prevent 0-changes from propagating
+- properly determine size for TinyTablePlus objects
+- better QuotaExceeded exception strings                DONE
+- ignore superuser, etc.                                DONE
+- add proper permissions                                DONE?
+- Do not raise exceptions on negative changes           DONE
+- LICENSE
+
+Later release
+- support TinyTablePlus
+- soft/hard quota
+- Limit/restrict installable meta types
+- configurable size for objects without explicit size? (seems pointless)
+- configure max object size per meta type (+ limit # of instances)
+- optionally prevent quota updates from propagating beyond a folder (checkbox)
+ -> this doesn't have to break nested quotafolders! (though it may give 
+ problems)

Added: zope-quotafolder/branches/upstream/0.1.1/__init__.py
===================================================================
--- zope-quotafolder/branches/upstream/0.1.1/__init__.py	                        (rev 0)
+++ zope-quotafolder/branches/upstream/0.1.1/__init__.py	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,395 @@
+##
+##  Copyright (c) 2002 Ivo van der Wijk, Amaze Internet Services (ivo at amaze.nl)
+## 
+##  All rights reserved. 
+## 
+##  Redistribution and use in source and binary forms, with or without 
+##  modification, are permitted provided that the following conditions
+##  are met:
+## 
+## 1. Redistributions of source code must retain the above copyright
+##    notice, this list of conditions and the following disclaimer.
+## 2. Redistributions in binary form must reproduce the above copyright
+##    notice, this list of conditions and the following disclaimer in the
+##    documentation and/or other materials provided with the distribution.
+## 3. The name of the author may not be used to endorse or promote products
+##    derived from this software without specific prior written permission. 
+##   
+##   
+## THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+## IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+## OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+## IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+## INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+## NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+## THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+##
+
+from zLOG import LOG, WARNING, BLATHER, INFO
+
+from OFS.ObjectManager import ObjectManager
+from Acquisition import aq_parent, aq_chain, aq_inner
+import QuotaFolder
+
+##
+## NOTE TO DEVELOPERS:
+##
+## Refreshing this product may not always work. If you do not get the expected
+## behaviour after refreshing, try restarting your zopeserver!
+##
+
+##
+## Idea: use *args / **args for wrapper methods and apply parameters to methods
+##
+## QA stands for Quota Aware, QU for Quota Unaware
+
+##
+## Invoke chain of _qf_check's, so all parents can update? (for delete of
+## folders etc)
+
+def QA_setOb(self, id, object):
+    ##
+    ## Is it smarter to patch setObject instead? Is setOb ever invoked directly?
+
+    ##
+    ## We rely on the transaction being rolled back if an exception occurs.
+    ## This means that QuotaExceeded exceptions shouldn't occur. Unfortunately,
+    ## sometimes they are caught.
+    if isinstance(object, ObjectManager):
+        make_quota_aware(object)
+
+    if hasattr(object, "_qa_sync"):
+       object._qa_sync()
+    _files, _size = QA_determine_usage(object)
+    try:
+        self._qf_check(_files, _size)
+    except QuotaFolder.QuotaExceededException:
+        ##
+        ## This horrible fix is because manage_addUserFolder ignores all
+        ## exceptions, and assumes there already is a UserFolder, while
+        ## _setObject happily changes _objects as well while ignoring 
+        ## anything that may go wrong
+        # print "O", self._objects
+        o = []
+        ##
+        ## Could this go wrong if there already is an object with id? 
+        ## (replacable?) (check using getattr?)
+        if not hasattr(self, id):
+            for i in self._objects:
+                if i['id'] != id:
+                    o.append(i)
+            self._objects = tuple(o)
+        raise
+    result = self._qu_setOb(id, object)
+    return result
+
+def QA_delOb(self, id):
+    # we can't use _getOb because it doesn't allow _attrs
+    ##
+    ## this works with BtreeFolders as well (well, with non-basicBtreeFolders)
+    obj = getattr(self, id)
+    try:
+        _files,_size = QA_determine_usage(obj)
+    except:
+        LOG('QuotaProduct', BLATHER, "Can't get size for %s" % id)
+        _files, _size = 1,0
+
+    self._qf_check(-_files, -_size) 
+
+    return self._qu_delOb(id)
+
+def getid(id):
+    """ sometimes id is a string, sometimes a method """
+    if callable(id): return id()
+    return id
+
+def findparent(object):
+    """ find the direct parent """
+
+    for parent in aq_chain(object):
+        if parent is object:
+            continue
+        if parent == object:
+            continue
+        if isinstance(parent, ObjectManager):
+            return parent
+    return None    
+    
+def QA_check_quota(self, filechange, sizechange=0, objsize=0):
+    """ 
+        Notify a change in usage:
+
+        filechange      number of added/removed files
+        sizechange      change in size of usage
+        objsize         total size of object
+
+        changes can be negative as well
+    """
+    
+    if hasattr(self, "_check_quota"):
+        ## check hook for QuotaFolder
+        self._check_quota(filechange, sizechange, objsize)
+    else: 
+        ## do own administration
+        make_quota_aware(self)
+        self._quota_filecount = self._quota_filecount + filechange
+        self._quota_size = self._quota_size + sizechange
+
+    ##
+    ## Acquisition is scary. The chain looks different when traversing
+    ## through ftp/webdav. basically, 'self' may appear in the chain multiple
+    ## times (with A NullResource in between), and may not always be equal
+    ## through 'is' (so try == as well)
+    ##
+    ## Tip: use aq_base(parent) XXX
+
+    for parent in aq_chain(self):
+        if parent is self:
+            continue
+        if parent == self:
+            continue
+        if hasattr(parent, "_qf_check"):
+            if hasattr(parent, getid(self.id)):
+                parent._qf_check(filechange,sizechange,objsize)
+            else:
+                for name,ob in parent.objectItems():
+                    if ob.id == getid(self.id):
+                        parent._qf_check(filechange,sizechange,objsize)
+                        return # !!
+            break
+    
+def QA_manage_importObject(self, file, REQUEST=None, set_owner=1):
+    """ currently a useless hook. To be removed if not used """
+    result = apply(self._qu_manage_importObject, (file, REQUEST, set_owner))
+    return result
+    
+def QA_sync(self):
+    """ synchronize, i.e. recursively recalculate sizes """
+    self._quota_filecount = 0 # not 1, get_usage adds 1
+    self._quota_size = 0
+
+    for child in self.objectValues():
+        if hasattr(child, "_qa_sync"):
+            child._qa_sync()
+
+        _files, _size = QA_determine_usage(child)
+        self._quota_filecount = self._quota_filecount + _files
+        self._quota_size = self._quota_size + _size
+
+def make_quota_aware(object):
+    if not hasattr(object, "_quota_filecount"):
+        object._quota_filecount = 0
+    if not hasattr(object, "_quota_size"):
+        object._quota_size = 0
+
+def QA_get_usage(self):
+    """
+        Return the size of an ObjectManager object. This is 1 + the number
+        of subobjects. The size of an empty ObjectManager is considered to
+        be 0
+    """
+    return self._quota_filecount+1, self._quota_size
+
+def QA_determine_usage(object):
+    """ 
+        attempt to determine number of size and number of subobjects of object 
+    """
+    ##
+    ## ZPT doesn't return the size of the unrendered content it seems, 
+    ## which leads to
+    ## very inconsistent sizes. Let's hope this fixes it...
+    if zpt_installed and isinstance(object, PageTemplate):
+        # workaround for ZPT
+        if hasattr(object, "_text"):
+            return 1, len(object._text)
+        return 1, 0
+    if hasattr(object, "_get_usage"):
+        return object._get_usage()
+    elif hasattr(object, "get_size"):
+        ##
+        ## Some products (such as PhotoFolders Photo) raise an exception
+        ## when requested their size at init time
+        try:
+            _size = object.get_size()
+        except: # can be any kind of exception
+            LOG('QuotaProduct', BLATHER, 'Failed to get size for %s/%s' % \
+                 (getid(object.id), object.meta_type))
+            _size = 0
+        return 1, _size
+    elif hasattr(object, "getSize"):
+        ## getSize is deprecated, but if it's all there is...
+        return 1, object.getSize()
+    else:
+        LOG('QuotaProduct', BLATHER,  "Can't get size for %s/%s" % \
+            (getid(object.id), object.meta_type))
+        return 1, 0
+
+##
+## Patch the PythonScript object
+
+from Products.PythonScripts.PythonScript import PythonScript
+
+def QA_ps_write(self, text):
+    """ invoked at creation, edit and upload - what else do you want? """
+    oldsize = self.get_size()
+    result = self._qu_write(text)
+    newsize = self.get_size()
+
+    parent = findparent(self)
+
+    if parent:
+        if hasattr(parent, getid(self.id)):
+            if hasattr(parent, "_qf_check"):
+                parent._qf_check(0, newsize-oldsize, newsize)
+
+    return result
+
+##
+## Patch DT_String, which effectively patches DTMLDocument and DTMLMethod
+
+from DocumentTemplate.DT_String import String
+
+def QA_dt_munge(self, source_string=None, mapping=None, **vars):
+    oldsize = self.get_size()
+    result = apply(self._qu_dt_munge, (source_string, mapping), vars)
+    newsize = self.get_size()
+
+    parent = findparent(self)
+
+    if parent:
+        if hasattr(parent, getid(self.id)):
+            if hasattr(parent, "_qf_check"):
+                parent._qf_check(0, newsize-oldsize, newsize)
+
+    return result
+
+##
+## Patch File and Image
+
+from OFS.Image import File, Image
+
+def QA_file_update_data(self, data, content_type=None, size=None):
+    ##
+    ## This method may be invoked when self.data hasn't been initialized
+    if not hasattr(self, "data"):
+        oldsize = 0
+    else:
+        oldsize = self.get_size()
+    result = apply(self._qu_update_data, (data, content_type, size))
+    newsize = self.get_size()
+
+    parent = findparent(self)
+
+    if parent:
+        if hasattr(parent, getid(self.id)):
+            if hasattr(parent, "_qf_check"):
+                parent._qf_check(0, newsize-oldsize, newsize)
+    
+    return result
+
+## 
+## Patch BTreeFolder, if available
+
+btree_installed = 0
+
+try:
+    from Products.BTreeFolder.BTreeFolder import BTreeFolder
+    LOG('QuotaProduct', INFO, "Patching BTreeFolder")
+    btree_installed = 1
+
+except ImportError:
+    pass
+    
+##
+## Patch ZopePageTemplate, if available
+
+zpt_installed = 0
+
+try:
+    from Products.PageTemplates.ZopePageTemplate import ZopePageTemplate, PageTemplate
+    LOG('QuotaProduct', INFO, "Patching ZPT")
+    zpt_installed = 1
+except ImportError:
+    zpt_installed = 0
+
+##
+## ZPT write hook
+def QA_zpt_write(self, text):
+    oldsize = len(self._text)
+    result = self._qu_write(text)
+    newsize = len(self._text)
+    
+    parent = findparent(self)
+
+    if parent:
+        if hasattr(parent, getid(self.id)):
+            if hasattr(parent, "_qf_check"):
+                parent._qf_check(0, newsize-oldsize, newsize)
+    
+    return result
+    
+##
+## Install all hooks
+##
+## Attempt to detect when we're refreshing and when we're restarting
+if not hasattr(ObjectManager, "_quota_aware"):
+    ##
+    ## save 'old' methods
+    LOG('QuotaProduct', INFO, 'Detected server restart')
+    ObjectManager._qu_setOb = ObjectManager._setOb
+    ObjectManager._qu_delOb = ObjectManager._delOb
+    ObjectManager._quota_aware = 1
+    ObjectManager._qu_manage_importObject = ObjectManager.manage_importObject
+    PythonScript._qu_write = PythonScript.write
+    String._qu_dt_munge = String.munge
+    File._qu_update_data = File.update_data
+    Image._qu_update_data = Image.update_data
+
+    if btree_installed:
+        BTreeFolder._qu_setOb = BTreeFolder._setOb
+        BTreeFolder._qu_delOb = BTreeFolder._delOb
+    if zpt_installed:
+        ZopePageTemplate._qu_write = ZopePageTemplate.write
+else:
+    LOG('QuotaProduct', INFO, 'Detected product refresh')
+
+##
+## Install the new methods. It won't hurt if this happens during a refresh
+ObjectManager._setOb = QA_setOb
+ObjectManager._delOb = QA_delOb
+ObjectManager.manage_importObject = QA_manage_importObject
+ObjectManager._qf_check = QA_check_quota
+ObjectManager._get_usage = QA_get_usage
+ObjectManager._qa_sync = QA_sync
+PythonScript.write = QA_ps_write
+String.munge = QA_dt_munge
+File.update_data = QA_file_update_data
+Image.update_data = QA_file_update_data
+
+if btree_installed:
+    BTreeFolder._setOb = QA_setOb
+    BTreeFolder._delOb = QA_delOb
+if zpt_installed:
+    ZopePageTemplate.write = QA_zpt_write
+
+##
+## finally, install the QuotaFolder Product
+
+import QuotaFolder
+
+def initialize(context):
+    try:
+        context.registerClass(
+            QuotaFolder.QuotaFolder,
+            meta_type='QuotaFolder',
+            permission='Add QuotaFolder',
+            constructors=(QuotaFolder.manage_addQuotaFolderForm,
+                          QuotaFolder.manage_addQuotaFolder),
+            )
+
+    except:
+        import traceback
+        traceback.print_exc()

Added: zope-quotafolder/branches/upstream/0.1.1/manage_addQuotaFolderForm.dtml
===================================================================
--- zope-quotafolder/branches/upstream/0.1.1/manage_addQuotaFolderForm.dtml	                        (rev 0)
+++ zope-quotafolder/branches/upstream/0.1.1/manage_addQuotaFolderForm.dtml	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,86 @@
+<dtml-var manage_page_header>
+<dtml-var "manage_form_title(this(), _,
+           form_title='Add Quota Folder'
+           )">
+
+<FORM ACTION="manage_addQuotaFolder" METHOD="POST">
+
+<table cellspacing="0" cellpadding="2" border="0">
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Id
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="id" size="40" />
+    </td>
+  </tr>
+
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-optional">
+    Title
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="title" size="40" />
+    </td>
+  </tr>
+	
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+	Quota Size in Bytes (0 for unlimited)
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="quota" size="40" value="0"/>
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+	Maximum Object Size in Bytes (0 for no limit)
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="filesize" size="40" value="0" />
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+	Maximum number of Objects (0 for unlimited)
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="numberFiles" size="40" value="0" />
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+	Require Manager role in parent context to edit
+    </div>
+    </td>
+    <td align="left" valign="top">
+        <input type="checkbox" name="manager_role" value="yes" CHECKED>
+    </td>
+  </tr>
+
+  <tr>
+    <td align="left" valign="top">
+    </td>
+    <td align="left" valign="top">
+    <div class="form-element">
+    <input class="form-element" type="submit" name="submit" 
+     value="Add" /> 
+    </div>
+    </td>
+  </tr>
+</table>
+</form>
+
+
+<dtml-var manage_page_footer>

Added: zope-quotafolder/branches/upstream/0.1.1/manage_editQuotaForm.dtml
===================================================================
--- zope-quotafolder/branches/upstream/0.1.1/manage_editQuotaForm.dtml	                        (rev 0)
+++ zope-quotafolder/branches/upstream/0.1.1/manage_editQuotaForm.dtml	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,114 @@
+<dtml-var manage_page_header>
+<dtml-var manage_tabs>
+
+<dtml-var "manage_form_title(this(), _,
+           form_title='Edit Quota Folder'
+           )">
+
+<FORM ACTION="manage_editQuota" METHOD="POST">
+<table cellspacing="0" cellpadding="2" border="0">
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Current quota usage
+    </div>
+    </td>
+    <td align="left" valign="top">
+	<dtml-var current_size> bytes
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Number of objects
+    </div>
+    </td>
+    <td align="left" valign="top">
+	<dtml-var current_count> Objects
+    </td>
+  </tr>	
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+	Quota Size in Bytes (0 for no limit)
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <dtml-if authorized_to_edit>
+        <input type="text" name="quota" size="40" value="<dtml-var quota_bytes>"/>
+    <dtml-else>
+        <dtml-var quota_bytes>
+    </dtml-if>
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+	Maximum number of Objects (0 for unlimited)
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <dtml-if authorized_to_edit>
+        <input type="text" name="maxNumberOfFiles" size="40" value="<dtml-var quota_objects>"/>
+    <dtml-else>
+        <dtml-var quota_objects>
+    </dtml-if>
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+	Maximum Object Size in Bytes (0 for no limit)
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <dtml-if authorized_to_edit>
+        <input type="text" name="maxFileSize" size="40" value="<dtml-var quota_maxsize>" />
+    <dtml-else>
+        <dtml-var quota_maxsize>
+    </dtml-if>
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+	Require Manager role in parent context to edit
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <dtml-if authorized_to_edit>
+        <input type="checkbox" name="manager_role" value="yes"
+        <dtml-if manager_role>
+            CHECKED
+        </dtml-if>
+        >
+        (Warning: checking this box may revoke your rights to edit quota!)
+    <dtml-else>
+        <dtml-if manager_role>
+            YES
+        <dtml-else>
+            NO
+        </dtml-if>
+        (You are not authorized to edit this quota folder)
+    </dtml-if>
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <input class="form-element" type="submit" name="Sync" value="Sync" /> 
+    </td>
+    <dtml-if authorized_to_edit>
+    <td align="left" valign="top">
+    <div class="form-element">
+    <input class="form-element" type="submit" name="submit" value="Edit" /> 
+    </div>
+    </td>
+    <dtml-else>
+    <td></td>
+    </dtml-if>
+  </tr>
+</table>
+</form>
+
+
+<dtml-var manage_page_footer>

Added: zope-quotafolder/branches/upstream/0.1.1/refresh.txt
===================================================================
--- zope-quotafolder/branches/upstream/0.1.1/refresh.txt	                        (rev 0)
+++ zope-quotafolder/branches/upstream/0.1.1/refresh.txt	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,6 @@
+
+NOTE TO DEVELOPERS:
+
+Refreshing this product may not always work. If you do not get the expected
+behaviour after refreshing, try restarting your zopeserver!
+

Added: zope-quotafolder/branches/upstream/current/DESIGN
===================================================================
--- zope-quotafolder/branches/upstream/current/DESIGN	                        (rev 0)
+++ zope-quotafolder/branches/upstream/current/DESIGN	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,117 @@
+Patch ObjectManager zdd:
+
+ - het get_usage() ondersteunt
+   Deze itereert over alle subobjects en roept, indien mogelijk, get_usage
+   aan.
+
+   Return: files, size
+
+ - creatie van objecten verifieert met parent object
+
+ 
+Patch File en evt. andere objecten zdd. 
+
+ - deze get_usage() ondersteunen
+ - manage_upload / whatever checken
+
+ Quota awareness
+
+Implementeer een speciale QuotaFolder welke op basis van get_usage() 
+gebruik bepaalt.
+
+Hou rekening met:
+
+- transparante migratie
+- sync optie om quota up2date te krijgen
+- Nesten van QuotaFolders
+- consistentie bij fouten/exceptions (i.e. QuotaExceeded)
+
+- hard/soft quota
+- mailnotificatie
+- security check in context van parent
+
+- wat als een folderstructuur gepaste wordt? Met een te-groot object?
+
+Is het zinnig quota etc bij te houden als er geen QuotaFolder parent is?
+Alleen als parent later QuotaFolder wordt... Kan dit? (migratie)?
+Waarom dan niet gewoon syncen?
+
+Nesten van QuotaFolders is uiteindelijk wel wenselijk, hier dient rekening
+mee gehouden te worden...
+
+- wat als folder gedelete wordt?
+- als data geupload/vergroot wordt?
+- check ftp, webdav
+- len(GET()) geeft rendered lengte?
+- len(.zexp)?
+
+Bij aanpassing van een object relatieve verschil quota_checken(), of
+recursief naar boven toe herberekenen? (geen optie bij grote site)
+
+
+-----------------------------------------------------------------------
+
+Alle objecten en in het byzonder ObjectManagers worden quota aware gemaakt.
+Dit betekent dat:
+
+Objectmanagers
+
+- add/delete van objecten bijhouden (eigen count) en aan parent doorgeven
+- size van subobjecten bijhouden (recursief)
+- notificaties krijgen van size changes van subobjecten
+
+Andere Objecten (File, Image, DTMLDocument, PythonScript, ...)
+
+- eigen size bijhouden 
+- uploads bijhouden en melden aan parent (=ObjectManager)
+- changes bijhouden en melden aan parent
+
+  (beiden kunnen groei/krimp zijn)
+
+- stort het systeem in (afgezien van quotafolders) als er geen QuotaProduct
+  meer is?
+- Ignore quota voor superuser
+- undo van (delete) transaction -> gevolgen?
+
+Toekomst:
+
+- soft/hard limit, expiry
+- mogelijkheid tot aanroepen script bij quotum exceeding (soft/hard) -> mail
+- Filteren/beperken meta types
+
+
+Specifiek testen:
+
+- exception bij overschrijding abort transaction?
+- support bij ftp, webdav (+ errorhandling)
+- photofolder
+- Btree folder
+- Transparentfolder?
+- Wat gebeurt er als je een quota aware folder importeert in een ander
+  systeem? (niks! denk ik)
+- undo quota change door manager?
+- objecten deleten, tegen quotum aanzitten, deletion undo-en
+- versions ?! Lijken transparant te werken... (alhoewel je wel veel state in versions kan bewaren)
+- metapublisher/formulator
+
+scripts als:
+
+doc = container.doc
+container.manage_delObjects(['doc'])
+doc.manage_edit('hello'*100, 'hacked')
+
+Evt. met vervangende pseudo parent
+
+BUGS
+----
+
+- zcatalog aanmaken geeft 1 object, zcatalog deleten is -2 FIXED
+- tutorial geeft niet teveel objecten/size, deleten gaat fout FIXED
+- manage import/export voegt ook objecten toe zonder setOb FIXED
+- acquisition werkt mogelijk niet meteen/direct voor imported data FIXED
+
+Probleem:
+---------
+
+Naast .zexp zijn ook pasted objecten objecten met veel subobjects (evt 
+hierarchisch)

Added: zope-quotafolder/branches/upstream/current/KNOWNBUGS
===================================================================
--- zope-quotafolder/branches/upstream/current/KNOWNBUGS	                        (rev 0)
+++ zope-quotafolder/branches/upstream/current/KNOWNBUGS	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,52 @@
+This is a list of known issues / incompatibilities with QuotaFolder
+
+Bugs in Zope:
+
+- ZopeTutorial does some inproper exception catching, causing QuotaExceeded
+  errors to be ignored (and actually the product to be imported twice).
+  The relevant code is:
+
+      try:
+        folder.manage_importObject(tutorialExamplesFile)
+    except:
+        folder._p_jar=self.Destination()._p_jar
+        folder.manage_importObject(tutorialExamplesFile)
+        
+- A similair problem probably occurs when installing userfolders - if a user is past 
+  his quota and tries to create a userfolder, the error 'this folder already contains a 
+  userfolder', and the userfolder is created nonetheless
+
+  This actually brings the quotafolder out of sync!!! (because due to the exception,
+  the quota isn't updated, but the transaction isn't aborted either)
+
+  -- RESOLVED 16Mar02 This bug has been worked around, though the error 'this object
+     already has a userfolder' will appear if the quota is exceeded.
+
+Bugs in QuotaFolder:
+
+- Maximum object size is not always enforced, esp. when importing objects
+  (or hierarchies of objects)
+
+- The following script used to give problems:
+
+    doc = container.doc
+    container.manage_delObjects(['doc'])
+    doc.manage_edit('hello'*100, 'hacked')
+
+  Hooked objects now check if they are still contained in their parent.
+  What if doc is placed in a pseudo parent folder?
+
+- Pressing 'Sync' in the Quota tab brings you back to the Quota tab, however,
+  the tab itself indicates 'Contents' is selected. -- RESOLVED 18Mar02
+
+- QuotaExceeded exceptions aren't always precise - it displays how much quota
+  you would need for the operation, but that may not be correct.
+
+- QuotaFolder seems to mess up undo capabilities (under what circumstances?)
+
+- QuotaFolder is not compatible with FLE. Creation goes fine (it seems), but
+  newly created objects / modified objects are not added. -- RESOLVED 16Mar02
+
+- It is still possible to store large amounts of data in properties. A solution
+  would be to test if an object is an instance of PropertyManager, and account
+  the size of the properties appropriately.

Added: zope-quotafolder/branches/upstream/current/LICENSE
===================================================================
--- zope-quotafolder/branches/upstream/current/LICENSE	                        (rev 0)
+++ zope-quotafolder/branches/upstream/current/LICENSE	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,27 @@
+Copyright (c) 2002 Ivo van der Wijk, Amaze Internet Services (ivo at amaze.nl)
+
+All rights reserved. 
+
+Redistribution and use in source and binary forms, with or without 
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+3. The name of the author may not be used to endorse or promote products
+   derived from this software without specific prior written permission. 
+   
+   
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Deleted: zope-quotafolder/branches/upstream/current/QuotaFolder-0.1.1.tar.gz
===================================================================
(Binary files differ)

Added: zope-quotafolder/branches/upstream/current/QuotaFolder.py
===================================================================
--- zope-quotafolder/branches/upstream/current/QuotaFolder.py	                        (rev 0)
+++ zope-quotafolder/branches/upstream/current/QuotaFolder.py	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,253 @@
+##
+##  Copyright (c) 2002 Ivo van der Wijk, Amaze Internet Services (ivo at amaze.nl)
+## 
+##  All rights reserved. 
+## 
+##  Redistribution and use in source and binary forms, with or without 
+##  modification, are permitted provided that the following conditions
+##  are met:
+## 
+## 1. Redistributions of source code must retain the above copyright
+##    notice, this list of conditions and the following disclaimer.
+## 2. Redistributions in binary form must reproduce the above copyright
+##    notice, this list of conditions and the following disclaimer in the
+##    documentation and/or other materials provided with the distribution.
+## 3. The name of the author may not be used to endorse or promote products
+##    derived from this software without specific prior written permission. 
+##   
+##   
+## THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+## IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+## OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+## IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+## INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+## NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+## THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+##
+
+import App, Globals, OFS
+import string
+
+from Persistence import Persistent
+
+from Globals import DTMLFile
+from OFS.Folder import Folder
+from Acquisition import aq_base, aq_parent, Acquired, Implicit
+from AccessControl import getSecurityManager, SpecialUsers
+
+from zLOG import LOG, WARNING
+
+def manage_addQuotaFolder(self, id, title, quota_bytes=0, quota_objects=0, 
+                          quota_maxsize=0, REQUEST=None):
+    """ Add a new QuotaFolder """
+
+    o = QuotaFolder(id, title, quota_bytes, quota_maxsize, quota_objects, REQUEST)
+    self._setObject(id, o)
+    o = self._getOb(id)
+
+    if REQUEST:
+        return self.manage_main(self, REQUEST, update_menu=1)
+    
+def _replaceFolder(parent, id, quota_bytes=0, quota_objects=0, quota_maxsize=0):
+    # Replaces an OFS.Folder with a QuotaFolder. Borrowed from BTreeFolder
+    ob = QuotaFolder(id, "", quota_bytes, quota_maxsize, quota_objects)
+    f = parent._getOb(id)
+    # Copy the contents of the folder to the new QuotaFolder
+
+    ids = f.objectIds()
+    for key, value in f.__dict__.items():
+        if key not in ids:
+            # Not an ObjectManager item.
+            ob.__dict__[key] = value
+    for key in ids:
+        subob = f._getOb(key)
+        subob = getattr(subob, 'aq_base', subob)
+        ob._setOb(key, subob)
+    parent._setOb(id, ob)
+
+
+manage_addQuotaFolderForm=DTMLFile('manage_addQuotaFolderForm', globals())
+
+QuotaExceededException='Quota Exceeded'
+
+class QuotaFolder(Folder, Persistent, Implicit):
+    """ A QuotaFolder """
+
+    meta_type='QuotaFolder'
+
+    manage_options=(
+        Folder.manage_options+
+        (
+        {'label': 'Quota', 'action': 'manage_editQuotaForm'},
+        )
+        )
+
+    __ac_permissions__= \
+        Folder.__ac_permissions__ +  \
+        (
+        ('View current quota', ('currentCount','currentSize')),
+        )
+
+    manage_editQuotaForm=DTMLFile('manage_editQuotaForm', globals())    
+
+    def __init__(self, id, title="", quota=0, maxFileSize=0, maxNumberOfFiles=0,
+                 manager_role=1, REQUEST=None):
+        self.id=id
+        self.title=title
+        self._quota_bytes=int(quota)
+        self._quota_objects=int(maxNumberOfFiles)
+        self._quota_maxsize=int(maxFileSize)
+
+        ##
+        ## current accounting
+        self._quota_filecount = 0
+        self._quota_size = 0
+
+        self._manager_role = 0
+        if manager_role:
+            self._manager_role = 1
+        self._debug = 0
+        self._allow_nested = 0
+
+    def _check_quota(self, filechange, sizechange=0, objsize=0):
+        """ Check if the change in usage violates the quota settings """
+        #
+        # print "I'm a quota folder: ", filechange, sizechange, objsize
+        # print "Current quota: ", self._quota_filecount, self._quota_size 
+
+        #
+        # The emergency user can create *some* objects (i.e. userfolders).
+        # make sure this never gives a QuotaExceeded exception
+
+        emergency = 0
+        user=getSecurityManager().getUser()
+        if (SpecialUsers.emergency_user and
+            aq_base(user) is SpecialUsers.emergency_user):
+            LOG('QuotaFolder', INFO, 
+                "Emergency user detected - not enforcing quota")
+            emergency = 1
+
+        if not emergency and \
+           filechange > 0 and \
+           self._quota_filecount + filechange > self._quota_objects \
+                                              > 0:
+            raise QuotaExceededException, \
+                  "Too many files, this operation would require a quotum " + \
+                  "of %d files, your current quotum is %d files" % \
+                  (self._quota_filecount + filechange, self._quota_objects)
+        if not emergency and \
+           sizechange > 0 and \
+           self._quota_size + sizechange > self._quota_bytes > 0:
+            raise QuotaExceededException, \
+                 "Too much space, this operation would require a " + \
+                 "quotum of %d bytes, your current quotum is %d bytes" % \
+                 (self._quota_size + sizechange, self._quota_bytes)
+        if not emergency and objsize > self._quota_maxsize > 0:
+            raise QuotaExceededException, \
+                  "The object is too large. It requires %d bytes, " + \
+                  "your maximum object size limit is %d bytes" % \
+                  (objsize, self._quota_maxsize)
+
+        self._quota_filecount = self._quota_filecount + filechange
+        self._quota_size = self._quota_size + sizechange
+
+        ##
+        ## This should not happen... 
+        path = string.join(self.getPhysicalPath(), "/")
+        if self._quota_filecount < 0:
+            LOG('QuotaFolder', WARNING, "Negative quota filecount in %s" % path)
+            if not self._debug:
+                self._quota_filecount = 0
+        if self._quota_size < 0:
+            LOG('QuotaFolder', WARNING, "Negative quota size in %s" % path)
+            if not self._debug:
+                self._quota_size = 0
+
+        # print "New quota: ", self._quota_filecount, self._quota_size 
+
+    def current_count(self):
+        return self._quota_filecount
+
+    def current_size(self):
+        return self._quota_size
+
+    def quota_bytes(self):
+        return self._quota_bytes
+
+    def quota_objects(self):
+        return self._quota_objects
+
+    def quota_maxsize(self):
+        return self._quota_maxsize
+
+    def authorized_to_edit(self):
+        """ 
+            return true/false if user is authorized to change in current context
+        """
+
+        security=getSecurityManager()
+        security.addContext(self)
+        user = security.getUser()
+        if not self._manager_role:
+            ## XXX use permission?
+            return user.allowed(self, ['Manager'])
+            
+        ## use aq_base? 
+        return user.allowed(aq_parent(self), ['Manager'])
+        
+    def manager_role(self):
+        return self._manager_role
+
+    def manage_editQuota(self, quota=-1, maxFileSize=-1, maxNumberOfFiles=-1,
+                         REQUEST=None):
+        """ Update the current Quota """
+        ##
+        ## Perhaps always sync after edit?
+
+        if REQUEST.has_key('Sync'):
+            self._qa_sync()
+            ## XXX report difference to parent?
+            if REQUEST:
+                ## A redirect selects the proper tab
+                message = "Quota synchronized"
+                REQUEST.RESPONSE.redirect(
+                      'manage_editQuotaForm?manage_tabs_message=%s' % message)
+                return self.manage_editQuotaForm(self, REQUEST,
+                                                 manage_tabs_message=message)
+            return ''
+
+        if not self.authorized_to_edit():
+           raise "Unauthorized", \
+                 "You are not authorized to edit this quota folder"
+
+        if quota != -1:
+            self._quota_bytes=int(quota)
+        if maxFileSize != -1:
+            self._quota_maxsize=int(maxFileSize)
+        if maxNumberOfFiles != -1:
+            self._quota_objects=int(maxNumberOfFiles)
+        self._manager_role = REQUEST.get("manager_role", 0)
+        if REQUEST:
+            message = "Changes saved"
+            ## A redirect selects the proper tab
+            REQUEST.RESPONSE.redirect(
+                     'manage_editQuotaForm?manage_tabs_message=%s' % message)
+            return self.manage_editQuotaForm(self, REQUEST,
+                                                 manage_tabs_message=message)
+        return ''
+
+    def manage_afterAdd(self, item, container):
+        # print "afterAdd %s %s"%(str(item), str(container))
+        if not self._allow_nested:
+            for parent in self.aq_chain[1:]:
+                if hasattr(parent, "meta_type") and \
+                   parent.meta_type == "QuotaFolder":
+                    raise QuotaExceededException, \
+                          "Cannot add a QuotaFolder inside another QuotaFolder"
+
+        Folder.manage_afterAdd(self, item, container)
+
+Globals.default__class_init__(QuotaFolder)

Added: zope-quotafolder/branches/upstream/current/README.txt
===================================================================
--- zope-quotafolder/branches/upstream/current/README.txt	                        (rev 0)
+++ zope-quotafolder/branches/upstream/current/README.txt	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,187 @@
+QuotaFolder - Quota support for Zope
+
+What is it?
+-----------
+
+QuotaFolder is a folder-ish object that restricts the total number of objects,
+their total size and their individual maximum size. QuotaFolder takes
+subfolders (recursively) into account, so it should not be possible to escape
+the quota restrictions.
+
+The basic goal of QuotaFolder is not to put an absolute limit on ZODB usage
+- it is impossible to determine this. For example, each object may have
+several revisions in the ZODB, and some of the internal state of the object
+is stored in the ZODB that's not returned by get_size(). The goal is to 
+limit the use of objects in general, to prevent people from offering large 
+files or using up resources with enormous amounts of objects.
+
+How does it work?
+-----------------
+
+QuotaFolder 'MonkeyPatches (tm)' some of the internal Zope components such
+as ObjectManager, File, Image, DTMLMethod, DTMLDocument and more. The patching
+constist of making the components 'quota aware'. This basically means the 
+objects will track changes and report them to parent folders. If one if the
+parent folders is a QuotaFolder, and the QuotaFolder detects that the change
+will exceed the quota, a QuotaExceededException is raised, and the transaction
+is rolled back.
+
+At this moment, if an object supports the get_size() method, it's used. The
+value reported by this method will not take all usage (i.e. properties) into
+account, so this may be changed in the future.
+
+Supported products
+------------------
+
+The QuotaFolder product knows how to account File, Image, DTMLMethod,
+DTMLDocument and PythonScript objects. It also understands ObjectManager
+based products such as Folder and BTreeFolder (TransparentFolder is not
+thoroughly tested, but it seems to work well)
+
+Most products (i.e. FLE, Squishdot, PhotoFolder, ZWiki) exist of ObjectManagers 
+with subobjects, and QuotaFolder knows how to handle these quite well. 
+Unknown objects who's size (and changes in size) cannot be determined are 
+accounted as 1 object with size 0. If this is not satisfactory (for example, 
+it's not at this moment for TinyTablePlus), extra support can be built in for 
+these objects (as has been done with, for example, ZPT).
+
+QuotaFolder also supports .zexp imports and copy/paste operations.
+
+QuotaFolder has been tested with Zope 2.4.3, 2.4.4b1 and 2.5.0. It has not
+been tested in ZEO setups.
+
+Installing
+----------
+
+**WARNING**
+
+This product patches classes in your Zope server. Please test the code on
+a test-server or shadow server first! This code has been known to work 
+succesfully with Zope 2.4.x and Zope 2.5. Use this product at your own
+risk, and backup your Data.fs first!
+
+Install the QuotaFolder product by simply unpacking into your Products
+directory (either in your SOFTWARE_HOME or your INSTANCE_HOME) and restart
+your Zope server. The QuotaFolder product should appear in 
+/Control_Panel/Products, and it should also be available in the products
+dropdown.
+
+When creating (or editting) a QuotaFolder, you will be presented with the
+following fields:
+
+id
+title               
+Quota size in bytes          - this is the maximum total size of all objects 
+                               that's allowed to be created
+Maximum object size in bytes - The maximum allowed total size for single
+                               objects
+Maximum number of objects    - this is the maximum number of objects that's
+                               allowed to be created
+Require manager role in parent context?
+                             - If this setting is enabled, a user must be 
+                               manager in the folder context *above* the 
+                               quotafolder itself to be able to edit the
+                               quota. If you want to limit your users in their
+                               usage, you don't want them to be able to edit
+                               their quota themselves, do you?
+
+After creation, you will get the same contents view as with a standard folder,
+but with an extra tab to the right where you can view and (optionally edit)
+the quota.
+
+When visiting the quota tab, you will see an extra button 'Sync'. Pressing
+this button will cause the QuotaFolder to recalculate all usage. Usually,
+the folder shouldn't be out of sync. If you manage to get a QuotaFolder
+out of sync, please contact me.
+
+Migrating
+---------
+
+There are two ways to migrate a standard folder to a QuotaFolder:
+
+- Create a new QuotaFolder, copy all objects from the old folder, paste
+  them into the QuotaFolder and rename the folders. This will probably not
+  work if you have versions (cut/paste may work with versions)
+
+- Use the builtin _replaceFolder method. I.e. create the following external
+  method:
+
+  from Products.QuotaFolder.QuotaFolder import _replaceFolder
+
+  def replace(self, name, quota_bytes, quota_objects, quota_maxsize, REQUEST):
+    _replaceFolder(self, name, quota_bytes, quota_objects, quota_maxsize)
+    return "%s converted" % name
+
+  Create an appropriate external method object in your zope server and invoke
+  it with an appropriately formatted url or create a dtml form.
+
+Hacking contest!
+----------------
+
+QuotaFolder has been thoroughly tested, and seems very stable and compatible.
+However, every now and then, new situations seem to appear where it's possible
+to use more objects or space than the QuotaFolder should enforce. This may
+be through unsupported objects (though these should generaly not make it
+possible to create more free space for other objects), or using trickery
+with PythonScripts, etc.
+
+If anyone finds issues like this, please contact me (info below), so we can
+make this product even more stable and robust :)
+
+Please check the file KNOWNBUGS before reporting issues.
+
+Release info
+------------
+
+0.1     Initial version, basic support for quota
+
+Future plans
+------------
+
+- Support soft and hardlimit quota, with configurable timeleft
+- Restrict installable metatypes
+- Limit number of certain metatypes
+
+Contact/License
+---------------
+
+QuotaFolder is partially based on ideas by Andrew Kenneth, though most of his
+old QuotaFolder has disappeared.
+
+QuotaFolder is written by Ivo van der Wijk as part of Amaze Internet Service's
+FreeZope.org Free Zope hosting environment.
+
+I can be contacted through ivo at amaze.nl or on IRC as VladDrac / VladDrak @ OPN
+
+Recent versions of QuotaFolder and other Zope products can be found at:
+
+http://www.zope.org/Members/ivo
+http://vanderijk.info/
+
+QuotaFolder is (c) 2002 Ivo van der Wijk / Amaze Internet Services
+
+All rights reserved. 
+
+Redistribution and use in source and binary forms, with or without 
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+3. The name of the author may not be used to endorse or promote products
+   derived from this software without specific prior written permission. 
+   
+   
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Added: zope-quotafolder/branches/upstream/current/TODO
===================================================================
--- zope-quotafolder/branches/upstream/current/TODO	                        (rev 0)
+++ zope-quotafolder/branches/upstream/current/TODO	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,23 @@
+First release:
+
+- check required attributes on objects (i.e. data on File/Image), report
+  warning if missing
+- Provide a conversion script (external method) to convert standard folders
+  to QuotaFolders                                       DONE
+- Prevent 0-changes from propagating
+- properly determine size for TinyTablePlus objects
+- better QuotaExceeded exception strings                DONE
+- ignore superuser, etc.                                DONE
+- add proper permissions                                DONE?
+- Do not raise exceptions on negative changes           DONE
+- LICENSE
+
+Later release
+- support TinyTablePlus
+- soft/hard quota
+- Limit/restrict installable meta types
+- configurable size for objects without explicit size? (seems pointless)
+- configure max object size per meta type (+ limit # of instances)
+- optionally prevent quota updates from propagating beyond a folder (checkbox)
+ -> this doesn't have to break nested quotafolders! (though it may give 
+ problems)

Added: zope-quotafolder/branches/upstream/current/__init__.py
===================================================================
--- zope-quotafolder/branches/upstream/current/__init__.py	                        (rev 0)
+++ zope-quotafolder/branches/upstream/current/__init__.py	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,395 @@
+##
+##  Copyright (c) 2002 Ivo van der Wijk, Amaze Internet Services (ivo at amaze.nl)
+## 
+##  All rights reserved. 
+## 
+##  Redistribution and use in source and binary forms, with or without 
+##  modification, are permitted provided that the following conditions
+##  are met:
+## 
+## 1. Redistributions of source code must retain the above copyright
+##    notice, this list of conditions and the following disclaimer.
+## 2. Redistributions in binary form must reproduce the above copyright
+##    notice, this list of conditions and the following disclaimer in the
+##    documentation and/or other materials provided with the distribution.
+## 3. The name of the author may not be used to endorse or promote products
+##    derived from this software without specific prior written permission. 
+##   
+##   
+## THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+## IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+## OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+## IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+## INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+## NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+## THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+##
+
+from zLOG import LOG, WARNING, BLATHER, INFO
+
+from OFS.ObjectManager import ObjectManager
+from Acquisition import aq_parent, aq_chain, aq_inner
+import QuotaFolder
+
+##
+## NOTE TO DEVELOPERS:
+##
+## Refreshing this product may not always work. If you do not get the expected
+## behaviour after refreshing, try restarting your zopeserver!
+##
+
+##
+## Idea: use *args / **args for wrapper methods and apply parameters to methods
+##
+## QA stands for Quota Aware, QU for Quota Unaware
+
+##
+## Invoke chain of _qf_check's, so all parents can update? (for delete of
+## folders etc)
+
+def QA_setOb(self, id, object):
+    ##
+    ## Is it smarter to patch setObject instead? Is setOb ever invoked directly?
+
+    ##
+    ## We rely on the transaction being rolled back if an exception occurs.
+    ## This means that QuotaExceeded exceptions shouldn't occur. Unfortunately,
+    ## sometimes they are caught.
+    if isinstance(object, ObjectManager):
+        make_quota_aware(object)
+
+    if hasattr(object, "_qa_sync"):
+       object._qa_sync()
+    _files, _size = QA_determine_usage(object)
+    try:
+        self._qf_check(_files, _size)
+    except QuotaFolder.QuotaExceededException:
+        ##
+        ## This horrible fix is because manage_addUserFolder ignores all
+        ## exceptions, and assumes there already is a UserFolder, while
+        ## _setObject happily changes _objects as well while ignoring 
+        ## anything that may go wrong
+        # print "O", self._objects
+        o = []
+        ##
+        ## Could this go wrong if there already is an object with id? 
+        ## (replacable?) (check using getattr?)
+        if not hasattr(self, id):
+            for i in self._objects:
+                if i['id'] != id:
+                    o.append(i)
+            self._objects = tuple(o)
+        raise
+    result = self._qu_setOb(id, object)
+    return result
+
+def QA_delOb(self, id):
+    # we can't use _getOb because it doesn't allow _attrs
+    ##
+    ## this works with BtreeFolders as well (well, with non-basicBtreeFolders)
+    obj = getattr(self, id)
+    try:
+        _files,_size = QA_determine_usage(obj)
+    except:
+        LOG('QuotaProduct', BLATHER, "Can't get size for %s" % id)
+        _files, _size = 1,0
+
+    self._qf_check(-_files, -_size) 
+
+    return self._qu_delOb(id)
+
+def getid(id):
+    """ sometimes id is a string, sometimes a method """
+    if callable(id): return id()
+    return id
+
+def findparent(object):
+    """ find the direct parent """
+
+    for parent in aq_chain(object):
+        if parent is object:
+            continue
+        if parent == object:
+            continue
+        if isinstance(parent, ObjectManager):
+            return parent
+    return None    
+    
+def QA_check_quota(self, filechange, sizechange=0, objsize=0):
+    """ 
+        Notify a change in usage:
+
+        filechange      number of added/removed files
+        sizechange      change in size of usage
+        objsize         total size of object
+
+        changes can be negative as well
+    """
+    
+    if hasattr(self, "_check_quota"):
+        ## check hook for QuotaFolder
+        self._check_quota(filechange, sizechange, objsize)
+    else: 
+        ## do own administration
+        make_quota_aware(self)
+        self._quota_filecount = self._quota_filecount + filechange
+        self._quota_size = self._quota_size + sizechange
+
+    ##
+    ## Acquisition is scary. The chain looks different when traversing
+    ## through ftp/webdav. basically, 'self' may appear in the chain multiple
+    ## times (with A NullResource in between), and may not always be equal
+    ## through 'is' (so try == as well)
+    ##
+    ## Tip: use aq_base(parent) XXX
+
+    for parent in aq_chain(self):
+        if parent is self:
+            continue
+        if parent == self:
+            continue
+        if hasattr(parent, "_qf_check"):
+            if hasattr(parent, getid(self.id)):
+                parent._qf_check(filechange,sizechange,objsize)
+            else:
+                for name,ob in parent.objectItems():
+                    if ob.id == getid(self.id):
+                        parent._qf_check(filechange,sizechange,objsize)
+                        return # !!
+            break
+    
+def QA_manage_importObject(self, file, REQUEST=None, set_owner=1):
+    """ currently a useless hook. To be removed if not used """
+    result = apply(self._qu_manage_importObject, (file, REQUEST, set_owner))
+    return result
+    
+def QA_sync(self):
+    """ synchronize, i.e. recursively recalculate sizes """
+    self._quota_filecount = 0 # not 1, get_usage adds 1
+    self._quota_size = 0
+
+    for child in self.objectValues():
+        if hasattr(child, "_qa_sync"):
+            child._qa_sync()
+
+        _files, _size = QA_determine_usage(child)
+        self._quota_filecount = self._quota_filecount + _files
+        self._quota_size = self._quota_size + _size
+
+def make_quota_aware(object):
+    if not hasattr(object, "_quota_filecount"):
+        object._quota_filecount = 0
+    if not hasattr(object, "_quota_size"):
+        object._quota_size = 0
+
+def QA_get_usage(self):
+    """
+        Return the size of an ObjectManager object. This is 1 + the number
+        of subobjects. The size of an empty ObjectManager is considered to
+        be 0
+    """
+    return self._quota_filecount+1, self._quota_size
+
+def QA_determine_usage(object):
+    """ 
+        attempt to determine number of size and number of subobjects of object 
+    """
+    ##
+    ## ZPT doesn't return the size of the unrendered content it seems, 
+    ## which leads to
+    ## very inconsistent sizes. Let's hope this fixes it...
+    if zpt_installed and isinstance(object, PageTemplate):
+        # workaround for ZPT
+        if hasattr(object, "_text"):
+            return 1, len(object._text)
+        return 1, 0
+    if hasattr(object, "_get_usage"):
+        return object._get_usage()
+    elif hasattr(object, "get_size"):
+        ##
+        ## Some products (such as PhotoFolders Photo) raise an exception
+        ## when requested their size at init time
+        try:
+            _size = object.get_size()
+        except: # can be any kind of exception
+            LOG('QuotaProduct', BLATHER, 'Failed to get size for %s/%s' % \
+                 (getid(object.id), object.meta_type))
+            _size = 0
+        return 1, _size
+    elif hasattr(object, "getSize"):
+        ## getSize is deprecated, but if it's all there is...
+        return 1, object.getSize()
+    else:
+        LOG('QuotaProduct', BLATHER,  "Can't get size for %s/%s" % \
+            (getid(object.id), object.meta_type))
+        return 1, 0
+
+##
+## Patch the PythonScript object
+
+from Products.PythonScripts.PythonScript import PythonScript
+
+def QA_ps_write(self, text):
+    """ invoked at creation, edit and upload - what else do you want? """
+    oldsize = self.get_size()
+    result = self._qu_write(text)
+    newsize = self.get_size()
+
+    parent = findparent(self)
+
+    if parent:
+        if hasattr(parent, getid(self.id)):
+            if hasattr(parent, "_qf_check"):
+                parent._qf_check(0, newsize-oldsize, newsize)
+
+    return result
+
+##
+## Patch DT_String, which effectively patches DTMLDocument and DTMLMethod
+
+from DocumentTemplate.DT_String import String
+
+def QA_dt_munge(self, source_string=None, mapping=None, **vars):
+    oldsize = self.get_size()
+    result = apply(self._qu_dt_munge, (source_string, mapping), vars)
+    newsize = self.get_size()
+
+    parent = findparent(self)
+
+    if parent:
+        if hasattr(parent, getid(self.id)):
+            if hasattr(parent, "_qf_check"):
+                parent._qf_check(0, newsize-oldsize, newsize)
+
+    return result
+
+##
+## Patch File and Image
+
+from OFS.Image import File, Image
+
+def QA_file_update_data(self, data, content_type=None, size=None):
+    ##
+    ## This method may be invoked when self.data hasn't been initialized
+    if not hasattr(self, "data"):
+        oldsize = 0
+    else:
+        oldsize = self.get_size()
+    result = apply(self._qu_update_data, (data, content_type, size))
+    newsize = self.get_size()
+
+    parent = findparent(self)
+
+    if parent:
+        if hasattr(parent, getid(self.id)):
+            if hasattr(parent, "_qf_check"):
+                parent._qf_check(0, newsize-oldsize, newsize)
+    
+    return result
+
+## 
+## Patch BTreeFolder, if available
+
+btree_installed = 0
+
+try:
+    from Products.BTreeFolder.BTreeFolder import BTreeFolder
+    LOG('QuotaProduct', INFO, "Patching BTreeFolder")
+    btree_installed = 1
+
+except ImportError:
+    pass
+    
+##
+## Patch ZopePageTemplate, if available
+
+zpt_installed = 0
+
+try:
+    from Products.PageTemplates.ZopePageTemplate import ZopePageTemplate, PageTemplate
+    LOG('QuotaProduct', INFO, "Patching ZPT")
+    zpt_installed = 1
+except ImportError:
+    zpt_installed = 0
+
+##
+## ZPT write hook
+def QA_zpt_write(self, text):
+    oldsize = len(self._text)
+    result = self._qu_write(text)
+    newsize = len(self._text)
+    
+    parent = findparent(self)
+
+    if parent:
+        if hasattr(parent, getid(self.id)):
+            if hasattr(parent, "_qf_check"):
+                parent._qf_check(0, newsize-oldsize, newsize)
+    
+    return result
+    
+##
+## Install all hooks
+##
+## Attempt to detect when we're refreshing and when we're restarting
+if not hasattr(ObjectManager, "_quota_aware"):
+    ##
+    ## save 'old' methods
+    LOG('QuotaProduct', INFO, 'Detected server restart')
+    ObjectManager._qu_setOb = ObjectManager._setOb
+    ObjectManager._qu_delOb = ObjectManager._delOb
+    ObjectManager._quota_aware = 1
+    ObjectManager._qu_manage_importObject = ObjectManager.manage_importObject
+    PythonScript._qu_write = PythonScript.write
+    String._qu_dt_munge = String.munge
+    File._qu_update_data = File.update_data
+    Image._qu_update_data = Image.update_data
+
+    if btree_installed:
+        BTreeFolder._qu_setOb = BTreeFolder._setOb
+        BTreeFolder._qu_delOb = BTreeFolder._delOb
+    if zpt_installed:
+        ZopePageTemplate._qu_write = ZopePageTemplate.write
+else:
+    LOG('QuotaProduct', INFO, 'Detected product refresh')
+
+##
+## Install the new methods. It won't hurt if this happens during a refresh
+ObjectManager._setOb = QA_setOb
+ObjectManager._delOb = QA_delOb
+ObjectManager.manage_importObject = QA_manage_importObject
+ObjectManager._qf_check = QA_check_quota
+ObjectManager._get_usage = QA_get_usage
+ObjectManager._qa_sync = QA_sync
+PythonScript.write = QA_ps_write
+String.munge = QA_dt_munge
+File.update_data = QA_file_update_data
+Image.update_data = QA_file_update_data
+
+if btree_installed:
+    BTreeFolder._setOb = QA_setOb
+    BTreeFolder._delOb = QA_delOb
+if zpt_installed:
+    ZopePageTemplate.write = QA_zpt_write
+
+##
+## finally, install the QuotaFolder Product
+
+import QuotaFolder
+
+def initialize(context):
+    try:
+        context.registerClass(
+            QuotaFolder.QuotaFolder,
+            meta_type='QuotaFolder',
+            permission='Add QuotaFolder',
+            constructors=(QuotaFolder.manage_addQuotaFolderForm,
+                          QuotaFolder.manage_addQuotaFolder),
+            )
+
+    except:
+        import traceback
+        traceback.print_exc()

Added: zope-quotafolder/branches/upstream/current/manage_addQuotaFolderForm.dtml
===================================================================
--- zope-quotafolder/branches/upstream/current/manage_addQuotaFolderForm.dtml	                        (rev 0)
+++ zope-quotafolder/branches/upstream/current/manage_addQuotaFolderForm.dtml	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,86 @@
+<dtml-var manage_page_header>
+<dtml-var "manage_form_title(this(), _,
+           form_title='Add Quota Folder'
+           )">
+
+<FORM ACTION="manage_addQuotaFolder" METHOD="POST">
+
+<table cellspacing="0" cellpadding="2" border="0">
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Id
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="id" size="40" />
+    </td>
+  </tr>
+
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-optional">
+    Title
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="title" size="40" />
+    </td>
+  </tr>
+	
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+	Quota Size in Bytes (0 for unlimited)
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="quota" size="40" value="0"/>
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+	Maximum Object Size in Bytes (0 for no limit)
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="filesize" size="40" value="0" />
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+	Maximum number of Objects (0 for unlimited)
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="numberFiles" size="40" value="0" />
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+	Require Manager role in parent context to edit
+    </div>
+    </td>
+    <td align="left" valign="top">
+        <input type="checkbox" name="manager_role" value="yes" CHECKED>
+    </td>
+  </tr>
+
+  <tr>
+    <td align="left" valign="top">
+    </td>
+    <td align="left" valign="top">
+    <div class="form-element">
+    <input class="form-element" type="submit" name="submit" 
+     value="Add" /> 
+    </div>
+    </td>
+  </tr>
+</table>
+</form>
+
+
+<dtml-var manage_page_footer>

Added: zope-quotafolder/branches/upstream/current/manage_editQuotaForm.dtml
===================================================================
--- zope-quotafolder/branches/upstream/current/manage_editQuotaForm.dtml	                        (rev 0)
+++ zope-quotafolder/branches/upstream/current/manage_editQuotaForm.dtml	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,114 @@
+<dtml-var manage_page_header>
+<dtml-var manage_tabs>
+
+<dtml-var "manage_form_title(this(), _,
+           form_title='Edit Quota Folder'
+           )">
+
+<FORM ACTION="manage_editQuota" METHOD="POST">
+<table cellspacing="0" cellpadding="2" border="0">
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Current quota usage
+    </div>
+    </td>
+    <td align="left" valign="top">
+	<dtml-var current_size> bytes
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Number of objects
+    </div>
+    </td>
+    <td align="left" valign="top">
+	<dtml-var current_count> Objects
+    </td>
+  </tr>	
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+	Quota Size in Bytes (0 for no limit)
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <dtml-if authorized_to_edit>
+        <input type="text" name="quota" size="40" value="<dtml-var quota_bytes>"/>
+    <dtml-else>
+        <dtml-var quota_bytes>
+    </dtml-if>
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+	Maximum number of Objects (0 for unlimited)
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <dtml-if authorized_to_edit>
+        <input type="text" name="maxNumberOfFiles" size="40" value="<dtml-var quota_objects>"/>
+    <dtml-else>
+        <dtml-var quota_objects>
+    </dtml-if>
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+	Maximum Object Size in Bytes (0 for no limit)
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <dtml-if authorized_to_edit>
+        <input type="text" name="maxFileSize" size="40" value="<dtml-var quota_maxsize>" />
+    <dtml-else>
+        <dtml-var quota_maxsize>
+    </dtml-if>
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+	Require Manager role in parent context to edit
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <dtml-if authorized_to_edit>
+        <input type="checkbox" name="manager_role" value="yes"
+        <dtml-if manager_role>
+            CHECKED
+        </dtml-if>
+        >
+        (Warning: checking this box may revoke your rights to edit quota!)
+    <dtml-else>
+        <dtml-if manager_role>
+            YES
+        <dtml-else>
+            NO
+        </dtml-if>
+        (You are not authorized to edit this quota folder)
+    </dtml-if>
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <input class="form-element" type="submit" name="Sync" value="Sync" /> 
+    </td>
+    <dtml-if authorized_to_edit>
+    <td align="left" valign="top">
+    <div class="form-element">
+    <input class="form-element" type="submit" name="submit" value="Edit" /> 
+    </div>
+    </td>
+    <dtml-else>
+    <td></td>
+    </dtml-if>
+  </tr>
+</table>
+</form>
+
+
+<dtml-var manage_page_footer>

Added: zope-quotafolder/branches/upstream/current/refresh.txt
===================================================================
--- zope-quotafolder/branches/upstream/current/refresh.txt	                        (rev 0)
+++ zope-quotafolder/branches/upstream/current/refresh.txt	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,6 @@
+
+NOTE TO DEVELOPERS:
+
+Refreshing this product may not always work. If you do not get the expected
+behaviour after refreshing, try restarting your zopeserver!
+

Added: zope-quotafolder/trunk/DESIGN
===================================================================
--- zope-quotafolder/trunk/DESIGN	                        (rev 0)
+++ zope-quotafolder/trunk/DESIGN	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,117 @@
+Patch ObjectManager zdd:
+
+ - het get_usage() ondersteunt
+   Deze itereert over alle subobjects en roept, indien mogelijk, get_usage
+   aan.
+
+   Return: files, size
+
+ - creatie van objecten verifieert met parent object
+
+ 
+Patch File en evt. andere objecten zdd. 
+
+ - deze get_usage() ondersteunen
+ - manage_upload / whatever checken
+
+ Quota awareness
+
+Implementeer een speciale QuotaFolder welke op basis van get_usage() 
+gebruik bepaalt.
+
+Hou rekening met:
+
+- transparante migratie
+- sync optie om quota up2date te krijgen
+- Nesten van QuotaFolders
+- consistentie bij fouten/exceptions (i.e. QuotaExceeded)
+
+- hard/soft quota
+- mailnotificatie
+- security check in context van parent
+
+- wat als een folderstructuur gepaste wordt? Met een te-groot object?
+
+Is het zinnig quota etc bij te houden als er geen QuotaFolder parent is?
+Alleen als parent later QuotaFolder wordt... Kan dit? (migratie)?
+Waarom dan niet gewoon syncen?
+
+Nesten van QuotaFolders is uiteindelijk wel wenselijk, hier dient rekening
+mee gehouden te worden...
+
+- wat als folder gedelete wordt?
+- als data geupload/vergroot wordt?
+- check ftp, webdav
+- len(GET()) geeft rendered lengte?
+- len(.zexp)?
+
+Bij aanpassing van een object relatieve verschil quota_checken(), of
+recursief naar boven toe herberekenen? (geen optie bij grote site)
+
+
+-----------------------------------------------------------------------
+
+Alle objecten en in het byzonder ObjectManagers worden quota aware gemaakt.
+Dit betekent dat:
+
+Objectmanagers
+
+- add/delete van objecten bijhouden (eigen count) en aan parent doorgeven
+- size van subobjecten bijhouden (recursief)
+- notificaties krijgen van size changes van subobjecten
+
+Andere Objecten (File, Image, DTMLDocument, PythonScript, ...)
+
+- eigen size bijhouden 
+- uploads bijhouden en melden aan parent (=ObjectManager)
+- changes bijhouden en melden aan parent
+
+  (beiden kunnen groei/krimp zijn)
+
+- stort het systeem in (afgezien van quotafolders) als er geen QuotaProduct
+  meer is?
+- Ignore quota voor superuser
+- undo van (delete) transaction -> gevolgen?
+
+Toekomst:
+
+- soft/hard limit, expiry
+- mogelijkheid tot aanroepen script bij quotum exceeding (soft/hard) -> mail
+- Filteren/beperken meta types
+
+
+Specifiek testen:
+
+- exception bij overschrijding abort transaction?
+- support bij ftp, webdav (+ errorhandling)
+- photofolder
+- Btree folder
+- Transparentfolder?
+- Wat gebeurt er als je een quota aware folder importeert in een ander
+  systeem? (niks! denk ik)
+- undo quota change door manager?
+- objecten deleten, tegen quotum aanzitten, deletion undo-en
+- versions ?! Lijken transparant te werken... (alhoewel je wel veel state in versions kan bewaren)
+- metapublisher/formulator
+
+scripts als:
+
+doc = container.doc
+container.manage_delObjects(['doc'])
+doc.manage_edit('hello'*100, 'hacked')
+
+Evt. met vervangende pseudo parent
+
+BUGS
+----
+
+- zcatalog aanmaken geeft 1 object, zcatalog deleten is -2 FIXED
+- tutorial geeft niet teveel objecten/size, deleten gaat fout FIXED
+- manage import/export voegt ook objecten toe zonder setOb FIXED
+- acquisition werkt mogelijk niet meteen/direct voor imported data FIXED
+
+Probleem:
+---------
+
+Naast .zexp zijn ook pasted objecten objecten met veel subobjects (evt 
+hierarchisch)

Added: zope-quotafolder/trunk/KNOWNBUGS
===================================================================
--- zope-quotafolder/trunk/KNOWNBUGS	                        (rev 0)
+++ zope-quotafolder/trunk/KNOWNBUGS	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,52 @@
+This is a list of known issues / incompatibilities with QuotaFolder
+
+Bugs in Zope:
+
+- ZopeTutorial does some inproper exception catching, causing QuotaExceeded
+  errors to be ignored (and actually the product to be imported twice).
+  The relevant code is:
+
+      try:
+        folder.manage_importObject(tutorialExamplesFile)
+    except:
+        folder._p_jar=self.Destination()._p_jar
+        folder.manage_importObject(tutorialExamplesFile)
+        
+- A similair problem probably occurs when installing userfolders - if a user is past 
+  his quota and tries to create a userfolder, the error 'this folder already contains a 
+  userfolder', and the userfolder is created nonetheless
+
+  This actually brings the quotafolder out of sync!!! (because due to the exception,
+  the quota isn't updated, but the transaction isn't aborted either)
+
+  -- RESOLVED 16Mar02 This bug has been worked around, though the error 'this object
+     already has a userfolder' will appear if the quota is exceeded.
+
+Bugs in QuotaFolder:
+
+- Maximum object size is not always enforced, esp. when importing objects
+  (or hierarchies of objects)
+
+- The following script used to give problems:
+
+    doc = container.doc
+    container.manage_delObjects(['doc'])
+    doc.manage_edit('hello'*100, 'hacked')
+
+  Hooked objects now check if they are still contained in their parent.
+  What if doc is placed in a pseudo parent folder?
+
+- Pressing 'Sync' in the Quota tab brings you back to the Quota tab, however,
+  the tab itself indicates 'Contents' is selected. -- RESOLVED 18Mar02
+
+- QuotaExceeded exceptions aren't always precise - it displays how much quota
+  you would need for the operation, but that may not be correct.
+
+- QuotaFolder seems to mess up undo capabilities (under what circumstances?)
+
+- QuotaFolder is not compatible with FLE. Creation goes fine (it seems), but
+  newly created objects / modified objects are not added. -- RESOLVED 16Mar02
+
+- It is still possible to store large amounts of data in properties. A solution
+  would be to test if an object is an instance of PropertyManager, and account
+  the size of the properties appropriately.

Added: zope-quotafolder/trunk/LICENSE
===================================================================
--- zope-quotafolder/trunk/LICENSE	                        (rev 0)
+++ zope-quotafolder/trunk/LICENSE	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,27 @@
+Copyright (c) 2002 Ivo van der Wijk, Amaze Internet Services (ivo at amaze.nl)
+
+All rights reserved. 
+
+Redistribution and use in source and binary forms, with or without 
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+3. The name of the author may not be used to endorse or promote products
+   derived from this software without specific prior written permission. 
+   
+   
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Deleted: zope-quotafolder/trunk/QuotaFolder-0.1.1.tar.gz
===================================================================
(Binary files differ)

Deleted: zope-quotafolder/trunk/QuotaFolder-0.1.1.tar.gz.cdbs-config_list
===================================================================

Added: zope-quotafolder/trunk/QuotaFolder.py
===================================================================
--- zope-quotafolder/trunk/QuotaFolder.py	                        (rev 0)
+++ zope-quotafolder/trunk/QuotaFolder.py	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,253 @@
+##
+##  Copyright (c) 2002 Ivo van der Wijk, Amaze Internet Services (ivo at amaze.nl)
+## 
+##  All rights reserved. 
+## 
+##  Redistribution and use in source and binary forms, with or without 
+##  modification, are permitted provided that the following conditions
+##  are met:
+## 
+## 1. Redistributions of source code must retain the above copyright
+##    notice, this list of conditions and the following disclaimer.
+## 2. Redistributions in binary form must reproduce the above copyright
+##    notice, this list of conditions and the following disclaimer in the
+##    documentation and/or other materials provided with the distribution.
+## 3. The name of the author may not be used to endorse or promote products
+##    derived from this software without specific prior written permission. 
+##   
+##   
+## THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+## IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+## OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+## IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+## INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+## NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+## THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+##
+
+import App, Globals, OFS
+import string
+
+from Persistence import Persistent
+
+from Globals import DTMLFile
+from OFS.Folder import Folder
+from Acquisition import aq_base, aq_parent, Acquired, Implicit
+from AccessControl import getSecurityManager, SpecialUsers
+
+from zLOG import LOG, WARNING
+
+def manage_addQuotaFolder(self, id, title, quota_bytes=0, quota_objects=0, 
+                          quota_maxsize=0, REQUEST=None):
+    """ Add a new QuotaFolder """
+
+    o = QuotaFolder(id, title, quota_bytes, quota_maxsize, quota_objects, REQUEST)
+    self._setObject(id, o)
+    o = self._getOb(id)
+
+    if REQUEST:
+        return self.manage_main(self, REQUEST, update_menu=1)
+    
+def _replaceFolder(parent, id, quota_bytes=0, quota_objects=0, quota_maxsize=0):
+    # Replaces an OFS.Folder with a QuotaFolder. Borrowed from BTreeFolder
+    ob = QuotaFolder(id, "", quota_bytes, quota_maxsize, quota_objects)
+    f = parent._getOb(id)
+    # Copy the contents of the folder to the new QuotaFolder
+
+    ids = f.objectIds()
+    for key, value in f.__dict__.items():
+        if key not in ids:
+            # Not an ObjectManager item.
+            ob.__dict__[key] = value
+    for key in ids:
+        subob = f._getOb(key)
+        subob = getattr(subob, 'aq_base', subob)
+        ob._setOb(key, subob)
+    parent._setOb(id, ob)
+
+
+manage_addQuotaFolderForm=DTMLFile('manage_addQuotaFolderForm', globals())
+
+QuotaExceededException='Quota Exceeded'
+
+class QuotaFolder(Folder, Persistent, Implicit):
+    """ A QuotaFolder """
+
+    meta_type='QuotaFolder'
+
+    manage_options=(
+        Folder.manage_options+
+        (
+        {'label': 'Quota', 'action': 'manage_editQuotaForm'},
+        )
+        )
+
+    __ac_permissions__= \
+        Folder.__ac_permissions__ +  \
+        (
+        ('View current quota', ('currentCount','currentSize')),
+        )
+
+    manage_editQuotaForm=DTMLFile('manage_editQuotaForm', globals())    
+
+    def __init__(self, id, title="", quota=0, maxFileSize=0, maxNumberOfFiles=0,
+                 manager_role=1, REQUEST=None):
+        self.id=id
+        self.title=title
+        self._quota_bytes=int(quota)
+        self._quota_objects=int(maxNumberOfFiles)
+        self._quota_maxsize=int(maxFileSize)
+
+        ##
+        ## current accounting
+        self._quota_filecount = 0
+        self._quota_size = 0
+
+        self._manager_role = 0
+        if manager_role:
+            self._manager_role = 1
+        self._debug = 0
+        self._allow_nested = 0
+
+    def _check_quota(self, filechange, sizechange=0, objsize=0):
+        """ Check if the change in usage violates the quota settings """
+        #
+        # print "I'm a quota folder: ", filechange, sizechange, objsize
+        # print "Current quota: ", self._quota_filecount, self._quota_size 
+
+        #
+        # The emergency user can create *some* objects (i.e. userfolders).
+        # make sure this never gives a QuotaExceeded exception
+
+        emergency = 0
+        user=getSecurityManager().getUser()
+        if (SpecialUsers.emergency_user and
+            aq_base(user) is SpecialUsers.emergency_user):
+            LOG('QuotaFolder', INFO, 
+                "Emergency user detected - not enforcing quota")
+            emergency = 1
+
+        if not emergency and \
+           filechange > 0 and \
+           self._quota_filecount + filechange > self._quota_objects \
+                                              > 0:
+            raise QuotaExceededException, \
+                  "Too many files, this operation would require a quotum " + \
+                  "of %d files, your current quotum is %d files" % \
+                  (self._quota_filecount + filechange, self._quota_objects)
+        if not emergency and \
+           sizechange > 0 and \
+           self._quota_size + sizechange > self._quota_bytes > 0:
+            raise QuotaExceededException, \
+                 "Too much space, this operation would require a " + \
+                 "quotum of %d bytes, your current quotum is %d bytes" % \
+                 (self._quota_size + sizechange, self._quota_bytes)
+        if not emergency and objsize > self._quota_maxsize > 0:
+            raise QuotaExceededException, \
+                  "The object is too large. It requires %d bytes, " + \
+                  "your maximum object size limit is %d bytes" % \
+                  (objsize, self._quota_maxsize)
+
+        self._quota_filecount = self._quota_filecount + filechange
+        self._quota_size = self._quota_size + sizechange
+
+        ##
+        ## This should not happen... 
+        path = string.join(self.getPhysicalPath(), "/")
+        if self._quota_filecount < 0:
+            LOG('QuotaFolder', WARNING, "Negative quota filecount in %s" % path)
+            if not self._debug:
+                self._quota_filecount = 0
+        if self._quota_size < 0:
+            LOG('QuotaFolder', WARNING, "Negative quota size in %s" % path)
+            if not self._debug:
+                self._quota_size = 0
+
+        # print "New quota: ", self._quota_filecount, self._quota_size 
+
+    def current_count(self):
+        return self._quota_filecount
+
+    def current_size(self):
+        return self._quota_size
+
+    def quota_bytes(self):
+        return self._quota_bytes
+
+    def quota_objects(self):
+        return self._quota_objects
+
+    def quota_maxsize(self):
+        return self._quota_maxsize
+
+    def authorized_to_edit(self):
+        """ 
+            return true/false if user is authorized to change in current context
+        """
+
+        security=getSecurityManager()
+        security.addContext(self)
+        user = security.getUser()
+        if not self._manager_role:
+            ## XXX use permission?
+            return user.allowed(self, ['Manager'])
+            
+        ## use aq_base? 
+        return user.allowed(aq_parent(self), ['Manager'])
+        
+    def manager_role(self):
+        return self._manager_role
+
+    def manage_editQuota(self, quota=-1, maxFileSize=-1, maxNumberOfFiles=-1,
+                         REQUEST=None):
+        """ Update the current Quota """
+        ##
+        ## Perhaps always sync after edit?
+
+        if REQUEST.has_key('Sync'):
+            self._qa_sync()
+            ## XXX report difference to parent?
+            if REQUEST:
+                ## A redirect selects the proper tab
+                message = "Quota synchronized"
+                REQUEST.RESPONSE.redirect(
+                      'manage_editQuotaForm?manage_tabs_message=%s' % message)
+                return self.manage_editQuotaForm(self, REQUEST,
+                                                 manage_tabs_message=message)
+            return ''
+
+        if not self.authorized_to_edit():
+           raise "Unauthorized", \
+                 "You are not authorized to edit this quota folder"
+
+        if quota != -1:
+            self._quota_bytes=int(quota)
+        if maxFileSize != -1:
+            self._quota_maxsize=int(maxFileSize)
+        if maxNumberOfFiles != -1:
+            self._quota_objects=int(maxNumberOfFiles)
+        self._manager_role = REQUEST.get("manager_role", 0)
+        if REQUEST:
+            message = "Changes saved"
+            ## A redirect selects the proper tab
+            REQUEST.RESPONSE.redirect(
+                     'manage_editQuotaForm?manage_tabs_message=%s' % message)
+            return self.manage_editQuotaForm(self, REQUEST,
+                                                 manage_tabs_message=message)
+        return ''
+
+    def manage_afterAdd(self, item, container):
+        # print "afterAdd %s %s"%(str(item), str(container))
+        if not self._allow_nested:
+            for parent in self.aq_chain[1:]:
+                if hasattr(parent, "meta_type") and \
+                   parent.meta_type == "QuotaFolder":
+                    raise QuotaExceededException, \
+                          "Cannot add a QuotaFolder inside another QuotaFolder"
+
+        Folder.manage_afterAdd(self, item, container)
+
+Globals.default__class_init__(QuotaFolder)

Added: zope-quotafolder/trunk/README.txt
===================================================================
--- zope-quotafolder/trunk/README.txt	                        (rev 0)
+++ zope-quotafolder/trunk/README.txt	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,187 @@
+QuotaFolder - Quota support for Zope
+
+What is it?
+-----------
+
+QuotaFolder is a folder-ish object that restricts the total number of objects,
+their total size and their individual maximum size. QuotaFolder takes
+subfolders (recursively) into account, so it should not be possible to escape
+the quota restrictions.
+
+The basic goal of QuotaFolder is not to put an absolute limit on ZODB usage
+- it is impossible to determine this. For example, each object may have
+several revisions in the ZODB, and some of the internal state of the object
+is stored in the ZODB that's not returned by get_size(). The goal is to 
+limit the use of objects in general, to prevent people from offering large 
+files or using up resources with enormous amounts of objects.
+
+How does it work?
+-----------------
+
+QuotaFolder 'MonkeyPatches (tm)' some of the internal Zope components such
+as ObjectManager, File, Image, DTMLMethod, DTMLDocument and more. The patching
+constist of making the components 'quota aware'. This basically means the 
+objects will track changes and report them to parent folders. If one if the
+parent folders is a QuotaFolder, and the QuotaFolder detects that the change
+will exceed the quota, a QuotaExceededException is raised, and the transaction
+is rolled back.
+
+At this moment, if an object supports the get_size() method, it's used. The
+value reported by this method will not take all usage (i.e. properties) into
+account, so this may be changed in the future.
+
+Supported products
+------------------
+
+The QuotaFolder product knows how to account File, Image, DTMLMethod,
+DTMLDocument and PythonScript objects. It also understands ObjectManager
+based products such as Folder and BTreeFolder (TransparentFolder is not
+thoroughly tested, but it seems to work well)
+
+Most products (i.e. FLE, Squishdot, PhotoFolder, ZWiki) exist of ObjectManagers 
+with subobjects, and QuotaFolder knows how to handle these quite well. 
+Unknown objects who's size (and changes in size) cannot be determined are 
+accounted as 1 object with size 0. If this is not satisfactory (for example, 
+it's not at this moment for TinyTablePlus), extra support can be built in for 
+these objects (as has been done with, for example, ZPT).
+
+QuotaFolder also supports .zexp imports and copy/paste operations.
+
+QuotaFolder has been tested with Zope 2.4.3, 2.4.4b1 and 2.5.0. It has not
+been tested in ZEO setups.
+
+Installing
+----------
+
+**WARNING**
+
+This product patches classes in your Zope server. Please test the code on
+a test-server or shadow server first! This code has been known to work 
+succesfully with Zope 2.4.x and Zope 2.5. Use this product at your own
+risk, and backup your Data.fs first!
+
+Install the QuotaFolder product by simply unpacking into your Products
+directory (either in your SOFTWARE_HOME or your INSTANCE_HOME) and restart
+your Zope server. The QuotaFolder product should appear in 
+/Control_Panel/Products, and it should also be available in the products
+dropdown.
+
+When creating (or editting) a QuotaFolder, you will be presented with the
+following fields:
+
+id
+title               
+Quota size in bytes          - this is the maximum total size of all objects 
+                               that's allowed to be created
+Maximum object size in bytes - The maximum allowed total size for single
+                               objects
+Maximum number of objects    - this is the maximum number of objects that's
+                               allowed to be created
+Require manager role in parent context?
+                             - If this setting is enabled, a user must be 
+                               manager in the folder context *above* the 
+                               quotafolder itself to be able to edit the
+                               quota. If you want to limit your users in their
+                               usage, you don't want them to be able to edit
+                               their quota themselves, do you?
+
+After creation, you will get the same contents view as with a standard folder,
+but with an extra tab to the right where you can view and (optionally edit)
+the quota.
+
+When visiting the quota tab, you will see an extra button 'Sync'. Pressing
+this button will cause the QuotaFolder to recalculate all usage. Usually,
+the folder shouldn't be out of sync. If you manage to get a QuotaFolder
+out of sync, please contact me.
+
+Migrating
+---------
+
+There are two ways to migrate a standard folder to a QuotaFolder:
+
+- Create a new QuotaFolder, copy all objects from the old folder, paste
+  them into the QuotaFolder and rename the folders. This will probably not
+  work if you have versions (cut/paste may work with versions)
+
+- Use the builtin _replaceFolder method. I.e. create the following external
+  method:
+
+  from Products.QuotaFolder.QuotaFolder import _replaceFolder
+
+  def replace(self, name, quota_bytes, quota_objects, quota_maxsize, REQUEST):
+    _replaceFolder(self, name, quota_bytes, quota_objects, quota_maxsize)
+    return "%s converted" % name
+
+  Create an appropriate external method object in your zope server and invoke
+  it with an appropriately formatted url or create a dtml form.
+
+Hacking contest!
+----------------
+
+QuotaFolder has been thoroughly tested, and seems very stable and compatible.
+However, every now and then, new situations seem to appear where it's possible
+to use more objects or space than the QuotaFolder should enforce. This may
+be through unsupported objects (though these should generaly not make it
+possible to create more free space for other objects), or using trickery
+with PythonScripts, etc.
+
+If anyone finds issues like this, please contact me (info below), so we can
+make this product even more stable and robust :)
+
+Please check the file KNOWNBUGS before reporting issues.
+
+Release info
+------------
+
+0.1     Initial version, basic support for quota
+
+Future plans
+------------
+
+- Support soft and hardlimit quota, with configurable timeleft
+- Restrict installable metatypes
+- Limit number of certain metatypes
+
+Contact/License
+---------------
+
+QuotaFolder is partially based on ideas by Andrew Kenneth, though most of his
+old QuotaFolder has disappeared.
+
+QuotaFolder is written by Ivo van der Wijk as part of Amaze Internet Service's
+FreeZope.org Free Zope hosting environment.
+
+I can be contacted through ivo at amaze.nl or on IRC as VladDrac / VladDrak @ OPN
+
+Recent versions of QuotaFolder and other Zope products can be found at:
+
+http://www.zope.org/Members/ivo
+http://vanderijk.info/
+
+QuotaFolder is (c) 2002 Ivo van der Wijk / Amaze Internet Services
+
+All rights reserved. 
+
+Redistribution and use in source and binary forms, with or without 
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+3. The name of the author may not be used to endorse or promote products
+   derived from this software without specific prior written permission. 
+   
+   
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Added: zope-quotafolder/trunk/TODO
===================================================================
--- zope-quotafolder/trunk/TODO	                        (rev 0)
+++ zope-quotafolder/trunk/TODO	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,23 @@
+First release:
+
+- check required attributes on objects (i.e. data on File/Image), report
+  warning if missing
+- Provide a conversion script (external method) to convert standard folders
+  to QuotaFolders                                       DONE
+- Prevent 0-changes from propagating
+- properly determine size for TinyTablePlus objects
+- better QuotaExceeded exception strings                DONE
+- ignore superuser, etc.                                DONE
+- add proper permissions                                DONE?
+- Do not raise exceptions on negative changes           DONE
+- LICENSE
+
+Later release
+- support TinyTablePlus
+- soft/hard quota
+- Limit/restrict installable meta types
+- configurable size for objects without explicit size? (seems pointless)
+- configure max object size per meta type (+ limit # of instances)
+- optionally prevent quota updates from propagating beyond a folder (checkbox)
+ -> this doesn't have to break nested quotafolders! (though it may give 
+ problems)

Added: zope-quotafolder/trunk/__init__.py
===================================================================
--- zope-quotafolder/trunk/__init__.py	                        (rev 0)
+++ zope-quotafolder/trunk/__init__.py	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,395 @@
+##
+##  Copyright (c) 2002 Ivo van der Wijk, Amaze Internet Services (ivo at amaze.nl)
+## 
+##  All rights reserved. 
+## 
+##  Redistribution and use in source and binary forms, with or without 
+##  modification, are permitted provided that the following conditions
+##  are met:
+## 
+## 1. Redistributions of source code must retain the above copyright
+##    notice, this list of conditions and the following disclaimer.
+## 2. Redistributions in binary form must reproduce the above copyright
+##    notice, this list of conditions and the following disclaimer in the
+##    documentation and/or other materials provided with the distribution.
+## 3. The name of the author may not be used to endorse or promote products
+##    derived from this software without specific prior written permission. 
+##   
+##   
+## THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+## IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+## OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+## IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+## INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+## NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+## THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+##
+
+from zLOG import LOG, WARNING, BLATHER, INFO
+
+from OFS.ObjectManager import ObjectManager
+from Acquisition import aq_parent, aq_chain, aq_inner
+import QuotaFolder
+
+##
+## NOTE TO DEVELOPERS:
+##
+## Refreshing this product may not always work. If you do not get the expected
+## behaviour after refreshing, try restarting your zopeserver!
+##
+
+##
+## Idea: use *args / **args for wrapper methods and apply parameters to methods
+##
+## QA stands for Quota Aware, QU for Quota Unaware
+
+##
+## Invoke chain of _qf_check's, so all parents can update? (for delete of
+## folders etc)
+
+def QA_setOb(self, id, object):
+    ##
+    ## Is it smarter to patch setObject instead? Is setOb ever invoked directly?
+
+    ##
+    ## We rely on the transaction being rolled back if an exception occurs.
+    ## This means that QuotaExceeded exceptions shouldn't occur. Unfortunately,
+    ## sometimes they are caught.
+    if isinstance(object, ObjectManager):
+        make_quota_aware(object)
+
+    if hasattr(object, "_qa_sync"):
+       object._qa_sync()
+    _files, _size = QA_determine_usage(object)
+    try:
+        self._qf_check(_files, _size)
+    except QuotaFolder.QuotaExceededException:
+        ##
+        ## This horrible fix is because manage_addUserFolder ignores all
+        ## exceptions, and assumes there already is a UserFolder, while
+        ## _setObject happily changes _objects as well while ignoring 
+        ## anything that may go wrong
+        # print "O", self._objects
+        o = []
+        ##
+        ## Could this go wrong if there already is an object with id? 
+        ## (replacable?) (check using getattr?)
+        if not hasattr(self, id):
+            for i in self._objects:
+                if i['id'] != id:
+                    o.append(i)
+            self._objects = tuple(o)
+        raise
+    result = self._qu_setOb(id, object)
+    return result
+
+def QA_delOb(self, id):
+    # we can't use _getOb because it doesn't allow _attrs
+    ##
+    ## this works with BtreeFolders as well (well, with non-basicBtreeFolders)
+    obj = getattr(self, id)
+    try:
+        _files,_size = QA_determine_usage(obj)
+    except:
+        LOG('QuotaProduct', BLATHER, "Can't get size for %s" % id)
+        _files, _size = 1,0
+
+    self._qf_check(-_files, -_size) 
+
+    return self._qu_delOb(id)
+
+def getid(id):
+    """ sometimes id is a string, sometimes a method """
+    if callable(id): return id()
+    return id
+
+def findparent(object):
+    """ find the direct parent """
+
+    for parent in aq_chain(object):
+        if parent is object:
+            continue
+        if parent == object:
+            continue
+        if isinstance(parent, ObjectManager):
+            return parent
+    return None    
+    
+def QA_check_quota(self, filechange, sizechange=0, objsize=0):
+    """ 
+        Notify a change in usage:
+
+        filechange      number of added/removed files
+        sizechange      change in size of usage
+        objsize         total size of object
+
+        changes can be negative as well
+    """
+    
+    if hasattr(self, "_check_quota"):
+        ## check hook for QuotaFolder
+        self._check_quota(filechange, sizechange, objsize)
+    else: 
+        ## do own administration
+        make_quota_aware(self)
+        self._quota_filecount = self._quota_filecount + filechange
+        self._quota_size = self._quota_size + sizechange
+
+    ##
+    ## Acquisition is scary. The chain looks different when traversing
+    ## through ftp/webdav. basically, 'self' may appear in the chain multiple
+    ## times (with A NullResource in between), and may not always be equal
+    ## through 'is' (so try == as well)
+    ##
+    ## Tip: use aq_base(parent) XXX
+
+    for parent in aq_chain(self):
+        if parent is self:
+            continue
+        if parent == self:
+            continue
+        if hasattr(parent, "_qf_check"):
+            if hasattr(parent, getid(self.id)):
+                parent._qf_check(filechange,sizechange,objsize)
+            else:
+                for name,ob in parent.objectItems():
+                    if ob.id == getid(self.id):
+                        parent._qf_check(filechange,sizechange,objsize)
+                        return # !!
+            break
+    
+def QA_manage_importObject(self, file, REQUEST=None, set_owner=1):
+    """ currently a useless hook. To be removed if not used """
+    result = apply(self._qu_manage_importObject, (file, REQUEST, set_owner))
+    return result
+    
+def QA_sync(self):
+    """ synchronize, i.e. recursively recalculate sizes """
+    self._quota_filecount = 0 # not 1, get_usage adds 1
+    self._quota_size = 0
+
+    for child in self.objectValues():
+        if hasattr(child, "_qa_sync"):
+            child._qa_sync()
+
+        _files, _size = QA_determine_usage(child)
+        self._quota_filecount = self._quota_filecount + _files
+        self._quota_size = self._quota_size + _size
+
+def make_quota_aware(object):
+    if not hasattr(object, "_quota_filecount"):
+        object._quota_filecount = 0
+    if not hasattr(object, "_quota_size"):
+        object._quota_size = 0
+
+def QA_get_usage(self):
+    """
+        Return the size of an ObjectManager object. This is 1 + the number
+        of subobjects. The size of an empty ObjectManager is considered to
+        be 0
+    """
+    return self._quota_filecount+1, self._quota_size
+
+def QA_determine_usage(object):
+    """ 
+        attempt to determine number of size and number of subobjects of object 
+    """
+    ##
+    ## ZPT doesn't return the size of the unrendered content it seems, 
+    ## which leads to
+    ## very inconsistent sizes. Let's hope this fixes it...
+    if zpt_installed and isinstance(object, PageTemplate):
+        # workaround for ZPT
+        if hasattr(object, "_text"):
+            return 1, len(object._text)
+        return 1, 0
+    if hasattr(object, "_get_usage"):
+        return object._get_usage()
+    elif hasattr(object, "get_size"):
+        ##
+        ## Some products (such as PhotoFolders Photo) raise an exception
+        ## when requested their size at init time
+        try:
+            _size = object.get_size()
+        except: # can be any kind of exception
+            LOG('QuotaProduct', BLATHER, 'Failed to get size for %s/%s' % \
+                 (getid(object.id), object.meta_type))
+            _size = 0
+        return 1, _size
+    elif hasattr(object, "getSize"):
+        ## getSize is deprecated, but if it's all there is...
+        return 1, object.getSize()
+    else:
+        LOG('QuotaProduct', BLATHER,  "Can't get size for %s/%s" % \
+            (getid(object.id), object.meta_type))
+        return 1, 0
+
+##
+## Patch the PythonScript object
+
+from Products.PythonScripts.PythonScript import PythonScript
+
+def QA_ps_write(self, text):
+    """ invoked at creation, edit and upload - what else do you want? """
+    oldsize = self.get_size()
+    result = self._qu_write(text)
+    newsize = self.get_size()
+
+    parent = findparent(self)
+
+    if parent:
+        if hasattr(parent, getid(self.id)):
+            if hasattr(parent, "_qf_check"):
+                parent._qf_check(0, newsize-oldsize, newsize)
+
+    return result
+
+##
+## Patch DT_String, which effectively patches DTMLDocument and DTMLMethod
+
+from DocumentTemplate.DT_String import String
+
+def QA_dt_munge(self, source_string=None, mapping=None, **vars):
+    oldsize = self.get_size()
+    result = apply(self._qu_dt_munge, (source_string, mapping), vars)
+    newsize = self.get_size()
+
+    parent = findparent(self)
+
+    if parent:
+        if hasattr(parent, getid(self.id)):
+            if hasattr(parent, "_qf_check"):
+                parent._qf_check(0, newsize-oldsize, newsize)
+
+    return result
+
+##
+## Patch File and Image
+
+from OFS.Image import File, Image
+
+def QA_file_update_data(self, data, content_type=None, size=None):
+    ##
+    ## This method may be invoked when self.data hasn't been initialized
+    if not hasattr(self, "data"):
+        oldsize = 0
+    else:
+        oldsize = self.get_size()
+    result = apply(self._qu_update_data, (data, content_type, size))
+    newsize = self.get_size()
+
+    parent = findparent(self)
+
+    if parent:
+        if hasattr(parent, getid(self.id)):
+            if hasattr(parent, "_qf_check"):
+                parent._qf_check(0, newsize-oldsize, newsize)
+    
+    return result
+
+## 
+## Patch BTreeFolder, if available
+
+btree_installed = 0
+
+try:
+    from Products.BTreeFolder.BTreeFolder import BTreeFolder
+    LOG('QuotaProduct', INFO, "Patching BTreeFolder")
+    btree_installed = 1
+
+except ImportError:
+    pass
+    
+##
+## Patch ZopePageTemplate, if available
+
+zpt_installed = 0
+
+try:
+    from Products.PageTemplates.ZopePageTemplate import ZopePageTemplate, PageTemplate
+    LOG('QuotaProduct', INFO, "Patching ZPT")
+    zpt_installed = 1
+except ImportError:
+    zpt_installed = 0
+
+##
+## ZPT write hook
+def QA_zpt_write(self, text):
+    oldsize = len(self._text)
+    result = self._qu_write(text)
+    newsize = len(self._text)
+    
+    parent = findparent(self)
+
+    if parent:
+        if hasattr(parent, getid(self.id)):
+            if hasattr(parent, "_qf_check"):
+                parent._qf_check(0, newsize-oldsize, newsize)
+    
+    return result
+    
+##
+## Install all hooks
+##
+## Attempt to detect when we're refreshing and when we're restarting
+if not hasattr(ObjectManager, "_quota_aware"):
+    ##
+    ## save 'old' methods
+    LOG('QuotaProduct', INFO, 'Detected server restart')
+    ObjectManager._qu_setOb = ObjectManager._setOb
+    ObjectManager._qu_delOb = ObjectManager._delOb
+    ObjectManager._quota_aware = 1
+    ObjectManager._qu_manage_importObject = ObjectManager.manage_importObject
+    PythonScript._qu_write = PythonScript.write
+    String._qu_dt_munge = String.munge
+    File._qu_update_data = File.update_data
+    Image._qu_update_data = Image.update_data
+
+    if btree_installed:
+        BTreeFolder._qu_setOb = BTreeFolder._setOb
+        BTreeFolder._qu_delOb = BTreeFolder._delOb
+    if zpt_installed:
+        ZopePageTemplate._qu_write = ZopePageTemplate.write
+else:
+    LOG('QuotaProduct', INFO, 'Detected product refresh')
+
+##
+## Install the new methods. It won't hurt if this happens during a refresh
+ObjectManager._setOb = QA_setOb
+ObjectManager._delOb = QA_delOb
+ObjectManager.manage_importObject = QA_manage_importObject
+ObjectManager._qf_check = QA_check_quota
+ObjectManager._get_usage = QA_get_usage
+ObjectManager._qa_sync = QA_sync
+PythonScript.write = QA_ps_write
+String.munge = QA_dt_munge
+File.update_data = QA_file_update_data
+Image.update_data = QA_file_update_data
+
+if btree_installed:
+    BTreeFolder._setOb = QA_setOb
+    BTreeFolder._delOb = QA_delOb
+if zpt_installed:
+    ZopePageTemplate.write = QA_zpt_write
+
+##
+## finally, install the QuotaFolder Product
+
+import QuotaFolder
+
+def initialize(context):
+    try:
+        context.registerClass(
+            QuotaFolder.QuotaFolder,
+            meta_type='QuotaFolder',
+            permission='Add QuotaFolder',
+            constructors=(QuotaFolder.manage_addQuotaFolderForm,
+                          QuotaFolder.manage_addQuotaFolder),
+            )
+
+    except:
+        import traceback
+        traceback.print_exc()

Added: zope-quotafolder/trunk/manage_addQuotaFolderForm.dtml
===================================================================
--- zope-quotafolder/trunk/manage_addQuotaFolderForm.dtml	                        (rev 0)
+++ zope-quotafolder/trunk/manage_addQuotaFolderForm.dtml	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,86 @@
+<dtml-var manage_page_header>
+<dtml-var "manage_form_title(this(), _,
+           form_title='Add Quota Folder'
+           )">
+
+<FORM ACTION="manage_addQuotaFolder" METHOD="POST">
+
+<table cellspacing="0" cellpadding="2" border="0">
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Id
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="id" size="40" />
+    </td>
+  </tr>
+
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-optional">
+    Title
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="title" size="40" />
+    </td>
+  </tr>
+	
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+	Quota Size in Bytes (0 for unlimited)
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="quota" size="40" value="0"/>
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+	Maximum Object Size in Bytes (0 for no limit)
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="filesize" size="40" value="0" />
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+	Maximum number of Objects (0 for unlimited)
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="numberFiles" size="40" value="0" />
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+	Require Manager role in parent context to edit
+    </div>
+    </td>
+    <td align="left" valign="top">
+        <input type="checkbox" name="manager_role" value="yes" CHECKED>
+    </td>
+  </tr>
+
+  <tr>
+    <td align="left" valign="top">
+    </td>
+    <td align="left" valign="top">
+    <div class="form-element">
+    <input class="form-element" type="submit" name="submit" 
+     value="Add" /> 
+    </div>
+    </td>
+  </tr>
+</table>
+</form>
+
+
+<dtml-var manage_page_footer>

Added: zope-quotafolder/trunk/manage_editQuotaForm.dtml
===================================================================
--- zope-quotafolder/trunk/manage_editQuotaForm.dtml	                        (rev 0)
+++ zope-quotafolder/trunk/manage_editQuotaForm.dtml	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,114 @@
+<dtml-var manage_page_header>
+<dtml-var manage_tabs>
+
+<dtml-var "manage_form_title(this(), _,
+           form_title='Edit Quota Folder'
+           )">
+
+<FORM ACTION="manage_editQuota" METHOD="POST">
+<table cellspacing="0" cellpadding="2" border="0">
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Current quota usage
+    </div>
+    </td>
+    <td align="left" valign="top">
+	<dtml-var current_size> bytes
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Number of objects
+    </div>
+    </td>
+    <td align="left" valign="top">
+	<dtml-var current_count> Objects
+    </td>
+  </tr>	
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+	Quota Size in Bytes (0 for no limit)
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <dtml-if authorized_to_edit>
+        <input type="text" name="quota" size="40" value="<dtml-var quota_bytes>"/>
+    <dtml-else>
+        <dtml-var quota_bytes>
+    </dtml-if>
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+	Maximum number of Objects (0 for unlimited)
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <dtml-if authorized_to_edit>
+        <input type="text" name="maxNumberOfFiles" size="40" value="<dtml-var quota_objects>"/>
+    <dtml-else>
+        <dtml-var quota_objects>
+    </dtml-if>
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+	Maximum Object Size in Bytes (0 for no limit)
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <dtml-if authorized_to_edit>
+        <input type="text" name="maxFileSize" size="40" value="<dtml-var quota_maxsize>" />
+    <dtml-else>
+        <dtml-var quota_maxsize>
+    </dtml-if>
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+	Require Manager role in parent context to edit
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <dtml-if authorized_to_edit>
+        <input type="checkbox" name="manager_role" value="yes"
+        <dtml-if manager_role>
+            CHECKED
+        </dtml-if>
+        >
+        (Warning: checking this box may revoke your rights to edit quota!)
+    <dtml-else>
+        <dtml-if manager_role>
+            YES
+        <dtml-else>
+            NO
+        </dtml-if>
+        (You are not authorized to edit this quota folder)
+    </dtml-if>
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <input class="form-element" type="submit" name="Sync" value="Sync" /> 
+    </td>
+    <dtml-if authorized_to_edit>
+    <td align="left" valign="top">
+    <div class="form-element">
+    <input class="form-element" type="submit" name="submit" value="Edit" /> 
+    </div>
+    </td>
+    <dtml-else>
+    <td></td>
+    </dtml-if>
+  </tr>
+</table>
+</form>
+
+
+<dtml-var manage_page_footer>

Added: zope-quotafolder/trunk/refresh.txt
===================================================================
--- zope-quotafolder/trunk/refresh.txt	                        (rev 0)
+++ zope-quotafolder/trunk/refresh.txt	2007-05-14 10:26:40 UTC (rev 867)
@@ -0,0 +1,6 @@
+
+NOTE TO DEVELOPERS:
+
+Refreshing this product may not always work. If you do not get the expected
+behaviour after refreshing, try restarting your zopeserver!
+




More information about the pkg-zope-commits mailing list