[Zope-Checkins] CVS: Zope/lib/python/Products/DateIndexes - DateIndex.py:1.1.2.1 DateRangeIndex.py:1.1.2.1 __init__.py:1.1.2.1

Martijn Pieters mj@zope.com
Wed, 8 May 2002 16:17:18 -0400


Update of /cvs-repository/Zope/lib/python/Products/DateIndexes
In directory cvs.zope.org:/tmp/cvs-serv320

Added Files:
      Tag: mj-dateindexes_integration-branch
	DateIndex.py DateRangeIndex.py __init__.py 
Log Message:
Initial import of date and date-range index code. Tests are still somewhat
broken.


=== Added File Zope/lib/python/Products/DateIndexes/DateIndex.py ===
from DateTime.DateTime import DateTime
from Products.PluginIndexes import PluggableIndex
from Products.PluginIndexes.common.UnIndex import UnIndex
from Products.PluginIndexes.common.util import parseIndexRequest
from types import StringType, FloatType, IntType

from Globals import DTMLFile
from OFS.SimpleItem import SimpleItem
from BTrees.IOBTree import IOBTree
from BTrees.OIBTree import OIBTree
from BTrees.IIBTree import IISet, union

_marker = []


class DateIndex( UnIndex
               , PluggableIndex.PluggableIndex 
               , SimpleItem 
               ):
    """ Index for Dates """
    __implements__ = (PluggableIndex.PluggableIndexInterface,)

    meta_type = 'DateIndex'
    query_options = ['query', 'range']

    manage = manage_main = DTMLFile( 'dtml/manageDateIndex', globals() )
    manage_main._setName( 'manage_main' )
    manage_options = ( { 'label' : 'Settings'
                       , 'action' : 'manage_main'
                       },
                     )

    def clear( self ):
        """ Complete reset """
        self._index = IOBTree()
        self._unindex = OIBTree()


    def index_object( self, documentId, obj, threshold=None ):
        """index an object"""
        returnStatus = 0

        try:
            date_attr = getattr( obj, self.id )
            if callable( date_attr ):
                date_attr = date_attr()

            ConvertedDate = self._convert( value=date_attr, default=_marker )
        except AttributeError:
            ConvertedDate = _marker

        oldConvertedDate = self._unindex.get( documentId, _marker )

        if ConvertedDate != oldConvertedDate:
            if oldConvertedDate is not _marker:
                self.removeForwardIndexEntry( oldConvertedDate, documentId )

            if ConvertedDate is not _marker:
                self.insertForwardIndexEntry( ConvertedDate, documentId )
                self._unindex[documentId] = ConvertedDate

            returnStatus = 1

        return returnStatus


    def _apply_index( self, request, cid='', type=type, None=None ):
        """Apply the index to query parameters given in the argument"""
        record = parseIndexRequest( request, self.id, self.query_options )
        if record.keys == None: return None

        keys = map( self._convert, record.keys )

        index = self._index
        r = None
        opr = None
                                
        #experimental code for specifing the operator
        operator = record.get( 'operator', self.useOperator )
        if not operator in self.operators :
            raise exepctions.RuntimeError,"operator not valid: %s" % operator
                                                                          
        # depending on the operator we use intersection or union
        if operator=="or": set_func = union
        else: set_func = intersection

        # range parameter
        if record.get('range',None):
            opr = "range"
            opr_args = []
            if range.find("min")>-1:  opr_args.append("min")
            if range.find("max")>-1:  opr_args.append("max")

        if record.get('usage',None):
            # see if any usage params are sent to field
            opr = record.usage.lower().split(':')
            opr, opr_args=opr[0], opr[1:]

        if opr=="range":   # range search
            if 'min' in opr_args: lo = min(keys)
            else: lo = None

            if 'max' in opr_args: hi = max(keys)
            else: hi = None

            if hi:
                setlist = index.items(lo,hi)
            else:
                setlist = index.items(lo)

            for k, set in setlist:
                if type(set) is IntType:
                    set = IISet((set,))
                r = set_func(r, set) 

        else: # not a range search
            for key in keys:
                set=index.get(key, None)
                if set is not None:
                    if type(set) is IntType:
                        set = IISet((set,))
                    r = set_func(r, set)

        if type(r) is IntType:  r=IISet((r,))
        if r is None:
            return IISet(), (self.id,)
        else:
            return r, (self.id,)


    def numObjects( self ):
        """ How many objects are in the index? """
        return len( self._unindex )


    def _convert( self, value, default=None ):
        """Convert Date/Time value to our internal representation"""
        if isinstance( value, DateTime ):
            t_tup = value.parts()
        elif type( value ) is FloatType:
            t_tup = time.gmtime( value )
        elif type( value ) is StringType:
            t_obj = DateTime( value )
            t_tup = t_obj.parts()
        else:
            return default

        yr = t_tup[0]
        mo = t_tup[1]
        dy = t_tup[2]
        hr = t_tup[3]
        mn = t_tup[4]

        t_val = ( ( ( ( yr * 12 + mo ) * 31 + dy ) * 24 + hr ) * 60 + mn )

        return t_val


manage_addDateIndexForm = DTMLFile( 'dtml/addDateIndex', globals() )

def manage_addDateIndex( self, id, REQUEST=None, RESPONSE=None, URL3=None):
    """Add a Date index"""
    return self.manage_addIndex(id, 'DateIndex', extra=None, \
                    REQUEST=REQUEST, RESPONSE=RESPONSE, URL1=URL3)
    


=== Added File Zope/lib/python/Products/DateIndexes/DateRangeIndex.py ===
from Products.PluginIndexes import PluggableIndex
from Products.PluginIndexes.common.util import parseIndexRequest
from OFS.SimpleItem import SimpleItem

from BTrees.IOBTree import IOBTree
from BTrees.IIBTree import IISet, union, intersection

from Globals import package_home, DTMLFile, InitializeClass
from AccessControl import ClassSecurityInfo
from DateTime.DateTime import DateTime
import os

_dtmldir = os.path.join( package_home( globals() ), 'dtml' )

VIEW_PERMISSION         = 'View'
INDEX_MGMT_PERMISSION   = 'Manage ZCatalogIndex Entries'

class DateRangeIndex( PluggableIndex.PluggableIndex, SimpleItem ):
    """
        Index a date range, such as the canonical "effective-expiration"
        range in the CMF.  Any object may return None for either the
        start or the end date:  for the start date, this should be
        the logical equivalent of "since the beginning of time";  for the
        end date, "until the end of time".
        
        Therefore, divide the space of indexed objects into four containers:

        - Objects which always match ( i.e., they returned None for both );

        - Objects which match after a given time ( i.e., they returned None
          for the end date );

        - Objects which match until a given time ( i.e., they returned None
          for the start date );

        - Objects which match only during a specific interval.
    """
    __implements__ = ( PluggableIndex.PluggableIndexInterface, )

    security = ClassSecurityInfo()

    meta_type = "DateRangeIndex"

    manage_options= ( { 'label'     : 'Properties'
                      , 'action'    : 'manage_indexProperties'
                      }
                    ,
                    )

    since_field = until_field = None

    def __init__(self, id, since_field=None, until_field=None,
            caller=None, extra=None):

        if extra:
            since_field = extra.since_field
            until_field = extra.until_field

        self._setId(id)
        self._edit(since_field, until_field)
        self.clear()

    security.declareProtected( VIEW_PERMISSION
                             , 'getSinceField'
                             )
    def getSinceField( self ):
        """
        """
        return self._since_field

    security.declareProtected( VIEW_PERMISSION
                             , 'getUntilField'
                             )
    def getUntilField( self ):
        """
        """
        return self._until_field

    manage_indexProperties = DTMLFile( 'dri_properties', _dtmldir )

    security.declareProtected( INDEX_MGMT_PERMISSION
                             , 'manage_edit'
                             )
    def manage_edit( self, since_field, until_field, REQUEST ):
        """
        """
        self._edit( since_field, until_field )
        REQUEST[ 'RESPONSE' ].redirect( '%s/manage_main'
                                        '?manage_tabs_message=Updated'
                                      % REQUEST.get('URL2')
                                      )

    security.declarePrivate( '_edit' )
    def _edit( self, since_field, until_field ):
        """
            Update the fields used to compute the range.
        """
        self._since_field = since_field
        self._until_field = until_field


    security.declareProtected( INDEX_MGMT_PERMISSION
                             , 'clear'
                             )
    def clear( self ):
        """
            Start over fresh.
        """
        self._always        = IISet()   # XXX is this the right one?
        self._since_only    = IOBTree()
        self._until_only    = IOBTree()
        self._since         = IOBTree()
        self._until         = IOBTree()
        self._unindex       = IOBTree() # 'datum' will be a tuple of date ints

    #
    #   PluggableIndexInterface implementation (XXX inherit assertions?)
    #
    def getEntryForObject( self, documentId, default=None ):
        """
            Get all information contained for the specific object 
            identified by 'documentId'.  Return 'default' if not found.
        """
        return self._unindex.get( documentId, default )
       
    def index_object( self, documentId, obj, threshold=None ):
        """
            Index an object:

             - 'documentId' is the integer ID of the document

             - 'obj' is the object to be indexed

             - ignore threshold
        """
        if self._since_field is None:
            return 0

        since = getattr( obj, self._since_field, None )
        if callable( since ):
            since = since()
        since = self._convertDateTime( since )

        until = getattr( obj, self._until_field, None )
        if callable( until ):
            until = until()
        until = self._convertDateTime( until )

        datum = ( since, until )

        old_datum = self._unindex.get( documentId, None )
        if datum == old_datum: # No change?  bail out!
            return 0

        if old_datum is not None:
            old_since, old_until = old_datum
            self._removeForwardIndexEntry( old_since, old_until, documentId )

        self._insertForwardIndexEntry( since, until, documentId )
        self._unindex[ documentId ] = datum

        return 1

    def unindex_object( self, documentId ):
        """
            Remove the object corresponding to 'documentId' from the index.
        """
        datum = self._unindex.get( documentId, None )

        if datum is None:
            return

        since, until = datum

        self._removeForwardIndexEntry( since, until, documentId )
        del self._unindex[ documentId ]

    def uniqueValues( self, name=None, withLengths=0 ):
        """
            Return a list of unique values for 'name'.

            If 'withLengths' is true, return a sequence of tuples, in
            the form '( value, length )'.
        """
        if not name in ( self._since_field, self._until_field ):
            return []

        if name == self._since_field:

            t1 = self._since
            t2 = self._since_only

        else:

            t1 = self._until
            t2 = self._until_only

        result = []
        IntType = type( 0 )

        if not withValues:

            result.extend( t1.keys() )
            result.extend( t2.keys() )

        else:

            for key in t1.keys():
                set = t1[ key ]
                if type( set ) is IntType:
                    length = 1
                else:
                    length = len( set )
                result.append( ( key, length) )

            for key in t2.keys():
                set = t2[ key ]
                if type( set ) is IntType:
                    length = 1
                else:
                    length = len( set )
                result.append( ( key, length) )

        return tuple( result )

    def _apply_index( self, request, cid='' ):
        """
            Apply the index to query parameters given in 'request', which
            should be a mapping object.

            If the request does not contain the needed parametrs, then
            return None.

            If the request contains a parameter with the name of the
            column + "_usage", snif for information on how to handle
            applying the index.

            Otherwise return two objects.  The first object is a ResultSet
            containing the record numbers of the matching records.  The
            second object is a tuple containing the names of all data fields
            used.
        """
        record = parseIndexRequest( request, self.getId() )
        if record.keys is None:
            return None

        term        = self._convertDateTime( record.keys[0] )

        #
        #   Aggregate sets for each bucket separately, to avoid
        #   large-small union penalties.
        #
        until_only  = IISet()
        # XXX use multi-union
        map( until_only.update, self._until_only.values( term ) )

        since_only  = IISet()
        # XXX use multi-union
        map( since_only.update, self._since_only.values( None, term ) )

        until       = IISet()
        # XXX use multi-union
        map( until.update, self._until.values( term ) )

        since       = IISet()
        # XXX use multi-union
        map( since.update, self._since.values( None, term ) )
        
        bounded     = intersection( until, since )

        result      = union( self._always, until_only )
        result      = union( result, since_only )
        result      = union( result, bounded )

        return result, ( self._since_field, self._until_field )

    #
    #   ZCatalog needs this, although it isn't (yet) part of the interface.
    #
    security.declareProtected( VIEW_PERMISSION , 'numObjects' )
    def numObjects( self ):
        """
        """
        return len( self._unindex )

    #
    #   Helper functions.
    #
    def _insertForwardIndexEntry( self, since, until, documentId ):
        """
            Insert 'documentId' into the appropriate set based on
            'datum'.
        """
        if since is None and until is None:

            self._always.insert( documentId )

        elif since is None:

            set = self._until_only.get( until, None )
            if set is None:
                set = self._until_only[ until ] = IISet()  # XXX: Store an int?
            set.insert( documentId )

        elif until is None:

            set = self._since_only.get( since, None )
            if set is None:
                set = self._since_only[ since ] = IISet()  # XXX: Store an int?
            set.insert( documentId )

        else:

            set = self._since.get( since, None )
            if set is None:
                set = self._since[ since ] = IISet()   # XXX: Store an int?
            set.insert( documentId )

            set = self._until.get( until, None )
            if set is None:
                set = self._until[ until ] = IISet() # XXX: Store an int?
            set.insert( documentId )

    def _removeForwardIndexEntry( self, since, until, documentId ):
        """
            Remove 'documentId' from the appropriate set based on
            'datum'.
        """
        if since is None and until is None:

            self._always.remove( documentId )

        elif since is None:

            set = self._until_only.get( until, None )
            if set is not None:

                set.remove( documentId )

                if not set:
                    del self._until_only[ until ]

        elif until is None:

            set = self._since_only.get( since, None )
            if set is not None:

                set.remove( documentId )

                if not set:
                    del self._since_only[ since ]

        else:

            set = self._since.get( since, None )
            if set is not None:
                set.remove( documentId )
                
                if not set:
                    del self._since[ since ]

            set = self._until.get( until, None )
            if set is not None:
                set.remove( documentId )
                
                if not set:
                    del self._until[ until ]

    def _convertDateTime( self, value ):
        if value is None:
            return value
        if type( value ) == type( '' ):
            dt_obj = DateTime( value )
            value = dt_obj.millis() / 1000 / 60 # flatten to minutes
        if isinstance( value, DateTime ):
            value = value.millis() / 1000 / 60 # flatten to minutes
        return int( value )

InitializeClass( DateRangeIndex )

addDateRangeIndexForm = DTMLFile( 'dri_add', _dtmldir )

def addDateRangeIndex(self, id, extra=None,
        REQUEST=None, RESPONSE=None, URL3=None):
    """
        Add a date range index to the catalog, using the incredibly icky
        double-indirection-which-hides-NOTHING.
    """
    return self.manage_addIndex(id, 'DateRangeIndex', extra,
        REQUEST, RESPONSE, URL3)


=== Added File Zope/lib/python/Products/DateIndexes/__init__.py ===
"""
    Implement specialized index for date-range queries.
"""

import DateRangeIndex
import DateIndex

from DateTime.DateTime import DateTime

DRI_CTORS = ( ( 'manage_addDateRangeIndexForm'
              , DateRangeIndex.addDateRangeIndexForm
              )
            , DateRangeIndex.addDateRangeIndex
            )

def initialize( context ):

    context.registerClass( DateRangeIndex.DateRangeIndex
                         , constructors=DRI_CTORS
                         , permission='Add Pluggable Index'
                         , icon='www/index.gif'
                         , visibility=None
                         )

    context.registerClass( DateIndex.DateIndex
                         , permission='Add Pluggable Index'
                         , constructors=( DateIndex.manage_addDateIndexForm
                                        , DateIndex.manage_addDateIndex
                                        )
                         , icon='www/index.gif'
                         , visibility=None
                         )