Newer
Older
DocumentLibrary / src / DocumentLibrary / TopicIndex.py
##################################################################################
#
#   Kaivo Public Software License
#
#   Copyright (c) 2001, Kaivo, Inc.
#   All rights reserved.
#
#   Redistribution and use in source and binary forms, with or without
#   modification, are permitted provided that the following conditions are met:
#
#       o Redistributions of source code must retain the above copyright notice,
#         this list of conditions and the following disclaimer.
#
#       o 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.
#
#       o Neither Kaivo nor the names of its contributors may be used to endorse
#         or promote products derived from this software without specific prior
#         written permission.
#
#   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 KAIVO OR ITS CONTRIBUTORS 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.
#
##################################################################################

__doc__ = """Document Library Topic Index"""
__version__ = '1.0b2'

from os import path
from string import join, find, strip
import Globals
from Globals import DTMLFile
from App.ImageFile import ImageFile
from OFS.Folder import Folder
from Acquisition import aq_base
from CatalogPlus import CatalogPlus, FieldIndex, TextIndex, KeywordIndex
from IconImage import IconImage
try:
    from Products.ZCatalog.CatalogPathAwareness import CatalogAware
except ImportError:
    from Products.ZCatalog.CatalogAwareness import CatalogAware

class TopicIndexBase(Folder):
    """Document Library Index Topic Base Class"""

    all_meta_types = ({'name': 'Topic Index', 'action': 'manage_addTopicIndexForm'},)

    __ac_permissions__ = Folder.__ac_permissions__ + (
        ('Add Library Topic Indexes', ('manage_addTopicIndexForm', 'manage_addTopicIndex')),
        ('View', ('topicParents', 'topicPath', 'getTopicId')),
    )    

    isLibraryTopicIndex = 1

    def __init__(self, id, title=''):
        self.id = id
        self.title = title
        self.documents = []

    manage_addTopicIndexForm = DTMLFile('dtml/AddIndexForm', globals())

    def manage_addTopicIndex(self, id, title='', REQUEST=None):
        """Construct a Topic Index object"""

        self._setObject(id, TopicIndex(id, title))

        if REQUEST is not None:
            return self.manage_main(self, REQUEST, update_menu=1)

    def topicParents(self):
        """Return a list of topic indexes from the topic index root down to this topic"""
        
        r = [self]
        parent = self.aq_parent
        
        while hasattr(parent.aq_explicit, 'isLibraryTopicIndex'):
            r.append(parent)
            parent = parent.aq_parent

        r.reverse()
        return r

    def topicPath(self):
        """Returns the path from the index root to this topic index"""
        parent_ids = map(lambda ob: ob.getId(), self.topicParents())
        return join(parent_ids[1:],'/')

    def getTopicId(self):
        """Returns the topic id of the current index topic.
           Used to pass the id without namespace collisions"""
        if self.meta_type != TopicIndexRoot.meta_type:
            return self.id
        else:
            return None

    TopicIndexBase_icon = ImageFile('www/DefaultTopicIndex_icon.gif', globals())
    def icon(self):
        root = self.topicParents()[0]
        return join(root.getPhysicalPath()[1:],'/') + '/TopicIndexBase_icon'
        

class TopicIndex(CatalogAware, TopicIndexBase):
    """Document Library Index Topic"""

    meta_type = 'Topic Index'

    _properties = TopicIndexBase._properties + (
        { 'id': 'visible', 'type': 'boolean', 'mode': 'w' },
    )

    manage_options = TopicIndexBase.manage_options + (
       { 'label': 'Icon', 'action': 'manage_changeIconForm' },
    )

    visible = 0

    # Icon management stuff

    manage_changeIconForm = DTMLFile('dtml/IndexIconForm', globals())

    def manage_changeIcon(self, file, REQUEST=None, RESPONSE=None):
        """Change topic index icon"""
        self.topic_icon = IconImage('topic_icon', file)

        if REQUEST is not None and RESPONSE is not None:
            return self.manage_changeIconForm(REQUEST, RESPONSE)

    def icon(self):
        if hasattr(self.aq_base, 'topic_icon'):
            return self.topic_icon
        elif hasattr(self, 'default_icon'):
            return self.default_icon
        else:
            return TopicIndexBase.icon(self)

    def manage_changeProperties(self, REQUEST=None, **kw):
        """Update properties and reindex"""
        r = TopicIndexBase.manage_changeProperties(self, REQUEST, kw=kw)

        # Check for afterEditTopicIndex hook method and call it
        if hasattr(self, 'afterEditTopicIndex'):
            self.afterEditTopicIndex()

        self.reindex_object()
        return r

    def manage_editProperties(self, REQUEST):
        """Edit Properties and reindex"""
        r = TopicIndexBase.manage_editProperties(self, REQUEST)

        # Check for afterEditTopicIndex hook method and call it
        if hasattr(self, 'afterEditTopicIndex'):
            self.afterEditTopicIndex()

        self.reindex_object() 
        return r

    def _setPropValue(self, id, value):
        """Override this private method from PropertyManager to make sure
           setting the visible property propagates up though the parents"""
        
        if id == 'visible' and value:
            # Make sure the visible value is set to 1 as a value for indexing purposes
            TopicIndexBase._setPropValue(self, 'visible', 1)
            self.showTopic()
        else:
            TopicIndexBase._setPropValue(self, id, value)

    def manage_renameObject(self, id, new_id, REQUEST=None):
        """Rename object and reindex"""
        # Check for beforeDeleteTopicIndex hook method and call it
        if hasattr(self, 'beforeDeleteTopicIndex'):
            self.beforeDeleteTopicIndex()

        r = TopicIndexBase.manage_renameObject(self, id, new_id, REQUEST)

        self.reindex_object() 
        return r

    def manage_afterAdd(self, item, container):
        """Call afterAddTopicIndex hook method if any"""
        if hasattr(self, 'afterAddTopicIndex'):
            self.afterAddTopicIndex()

        self.index_object()

    def manage_beforeDelete(self, item, container):
        """Call beforeDeleteTopicIndex hook method if any"""
        if hasattr(self, 'beforeDeleteTopicIndex'):
            self.beforeDeleteTopicIndex()

        self.unindex_object()

    def manage_afterClone(self, item):
        """Call afterAddTopicIndex hook method if any"""
        if hasattr(self, 'afterAddTopicIndex'):
            self.afterAddTopicIndex()

        self.index_object()

    def showTopic(self):
        """Make this topic and parent topics visible"""
        ob = self

        while ob.meta_type == self.meta_type:
            ob.visible = 1
            ob.reindex_object()
            ob = ob.aq_parent

    def hideTopic(self):
        """Make this topic invisible"""
        self.visible = 0
        self.reindex_object()


Globals.InitializeClass(TopicIndex)

manage_addLibraryTopicIndexRootForm = DTMLFile('dtml/AddTopicIndexRootForm', globals())

def manage_addLibraryTopicIndexRoot(self, id, title='', vocab_id = None, 
                                    REQUEST=None):
    """Add a document store instance to a library"""
    ob = TopicIndexRoot(id, title, vocab_id, self)
    self._setObject(id, ob)

    if REQUEST is not None:
        REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_main')


class TopicIndexRoot(TopicIndexBase):
    """Document Library Topic Index Root"""

    meta_type = 'Topic Index Root'

    manage_options = TopicIndexBase.manage_options + (
       { 'label': 'Import Topic Index', 'action': 'manage_importIndexForm' },
       { 'label': 'Default Topic Icon', 'action': 'manage_changeIconForm' }
    )

    __ac_permissions__ = TopicIndexBase.__ac_permissions__ + (
        ('Manage properties', ('manage_changeIconForm', 'manage_changeIcon')),
        ('Add Document Library Topics', ('manage_importIndexForm', 'manage_importIndex')),
        ('Delete objects', ('manage_importIndexForm', 'manage_importIndex')),
        ('Search ZCatalog', ('query', 'getpath')),
    )

    manage_changeIconForm = DTMLFile('dtml/IndexIconForm', globals())

    def manage_changeIcon(self, file, REQUEST=None, RESPONSE=None):
        """Change topic index icon"""
        self.default_icon = IconImage('default_icon', file)

        if REQUEST is not None and RESPONSE is not None:
            return self.manage_changeIconForm(REQUEST, RESPONSE)
    
    TopicIndexRoot_icon = ImageFile('www/TopicIndexRoot_icon.gif', globals())
    def icon(self):
        """Management icon"""
        return join(self.getPhysicalPath()[1:],'/') + '/TopicIndexRoot_icon'

    def _initCatalog(self, vocab_id):
        """Setup the index root catalog"""
        self.Catalog = CatalogPlus(vocab_id)
        lexicon = self.Catalog.getLexicon()

        self.Catalog.addColumn('id')
        self.Catalog.addIndex('id', FieldIndex('id'))       
        self.Catalog.addColumn('title')
        self.Catalog.addIndex('title', TextIndex('title', lexicon=lexicon))
        self.Catalog.addIndex('visible', FieldIndex('visible'))

    def __init__(self, id, title='', vocab_id=None, container=None):
        if vocab_id is not None and container is None:
            raise AttributeError, ("You cannot specify a vocab_id without "
                                     "also specifying a container.")
        if container is not None:
            self=self.__of__(container)

        self._initCatalog(vocab_id)
        TopicIndexBase.__init__(aq_base(self), id, title)

    manage_importIndexForm = DTMLFile('dtml/ImportIndexForm', globals())

    def manage_importIndex(self, file, clear=1, delimiter=';', RESPONSE=None):
        """Create a topics from a text outline file"""

        if delimiter == '\\t': delimiter = '\t'

        if clear:
            # Clear existing topics
            self.manage_delObjects(self.objectIds(TopicIndex.meta_type))
            # We shouldn't need to reinit the catalog, but...
            self._initCatalog(self.Catalog.lexicon)

            # Execute clearIndexTopics hook method if any
            if hasattr(self, 'clearIndexTopics'):
                self.clearIndexTopics()
        
        topic_id_seq=0
        indent=0
        new_index=None
        line_num = 0
        drill = [self]
        
        for line in file.readlines():
            line_num = line_num + 1

            # figure out the indent of this line
            last_indent = indent
            indent = 0
            while line[indent] == '\t' and indent < len(line):
                indent = indent + 1

            if indent - last_indent > 1:
                raise 'IndentError', 'Indentation error at line %d of index file' % line_num

            if indent > last_indent:
                if new_index is not None:
                    drill.append(new_index)
                else:
                    raise 'IndentError', 'Indentation error at line %d of index file' % line_num
            elif indent < last_indent:
                drill = drill[:indent + 1]

            id, title = getTopicInfo(line[indent:], delimiter)

            if title:
                index = drill[-1]
                if not id: 
                    id = nextTopicId(index, topic_id_seq)
                    topic_id_seq = int(id) + 1
                new_index = TopicIndex(id, title)
                index._setObject(id, new_index)
                new_index = index._getOb(id)
                new_index.index_object()
        
        if RESPONSE is not None:
            RESPONSE.redirect('manage_main')

    #
    #   Catalog query support methods
    #

    def query(self, REQUEST=None, **kw):
        """Query the Index for matching topics"""
        return apply(self.Catalog.searchResults, (REQUEST,), kw)

    def getpath(self, rid):
        """
        Return the path to a cataloged object given a 'data_record_id_'
        Used by the Catalog brains
        """
        return self.Catalog.paths[rid]

    def getTopicIds(self, title):
        """Return a list of topic ids that match the title query string"""
        topics = self.query(title=title)
        if topics:
            return map(lambda t: t.id, topics)
        else:
            return []

Globals.InitializeClass(TopicIndexRoot)

def getTopicInfo(line, delimiter):
    """Return the title and id from the topic import file line"""
    
    d = find(line, delimiter)
    l = len(delimiter)

    if d > 0:
        title = strip(line[:d])
        id = strip(line[d + l:])
    else:
        id = ''
        title = strip(line)

    return (id, title)          
            
def nextTopicId(index, seq, pad='000000'):
    """Returns the next topic id string and increments the sequence for import"""
    
    i=index.aq_base
    id =''

    while not id or hasattr(i, id):
        id = str(seq)
        if len(id) < len(pad): id = pad[len(id):] + id
        seq = seq + 1

    return id