##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
""" Topic: Canned catalog queries

$Id: Topic.py 38001 2005-08-18 23:37:25Z jens $
"""

from AccessControl import ClassSecurityInfo
from Acquisition import aq_parent, aq_inner, aq_base
from Globals import InitializeClass

from Products.CMFDefault.SkinnedFolder import SkinnedFolder
from Products.CMFCore.utils import _getViewFor
from Products.CMFCore.utils import getToolByName

from permissions import ListFolderContents
from permissions import View
from permissions import AddTopics
from permissions import ChangeTopics


# Factory type information -- makes Topic objects play nicely
# with the Types Tool (portal_types )
factory_type_information = (
  { 'id'             : 'Topic'
  , 'icon'           : 'topic_icon.gif'
  , 'meta_type'      : 'Portal Topic'
  , 'description'    : 'Topics are canned queries for organizing content '
                       'with up to date queries into the catalog.'
  , 'product'        : 'CMFTopic'
  , 'factory'        : 'addTopic'
  , 'immediate_view' : 'topic_edit_form'
  , 'allowed_content_types': ('Topic',)
  , 'aliases'        : {'(Default)': 'topic_view',
                        'view': 'topic_view'}
  , 'actions'        : ( { 'id'            : 'view'
                         , 'name'          : 'View'
                         , 'action': 'string:${object_url}/topic_view'
                         , 'permissions'   : (View,)
                         }
                       , { 'id'            : 'edit'
                         , 'name'          : 'Edit'
                         , 'action': 'string:${object_url}/topic_edit_form'
                         , 'permissions'   : (ChangeTopics,)
                         }
                       , { 'id'            : 'criteria'
                         , 'name'          : 'Criteria'
                         , 'action': 'string:${object_url}/topic_criteria_form'
                         , 'permissions'   : (ChangeTopics,)
                         }
                       , { 'id'            : 'folderContents'
                         , 'name'          : 'Subtopics'
                         , 'action': 'string:${object_url}/folder_contents'
                         , 'permissions'   : (ListFolderContents,)
                         }
                       , { 'id'            : 'new'
                         , 'name'          : 'New...'
                         , 'action': 'string:${object_url}/folder_factories'
                         , 'permissions'   : (AddTopics,)
                         , 'visible'       : 0
                         }
                       , { 'id'            : 'rename_items'
                         , 'name'          : 'Rename items'
                         , 'action': 'string:${object_url}/folder_rename_form'
                         , 'permissions'   : (AddTopics,)
                         , 'visible'       : 0
                         }
                       )
  }
,
)

def addTopic( self, id, title='', REQUEST=None ):

    """ Create an empty topic.
    """
    topic = Topic( id )
    topic.id = id
    topic.title = title
    self._setObject( id, topic )

    if REQUEST is not None:
        REQUEST['RESPONSE'].redirect( 'manage_main' )


class Topic( SkinnedFolder ):

    """ Topics are 'canned queries'
    
    o Each topic holds a set of zero or more Criteria objects specifying
      the query.
    """

    meta_type='Portal Topic'

    security = ClassSecurityInfo()
    security.declareObjectProtected(View)

    acquireCriteria = 1
    _criteriaTypes = []

    def __call__( self ):
        """ Invoke the default action.
        """
        ti = self.getTypeInfo()
        method_id = ti and ti.queryMethodID('(Default)', context=self)
        if method_id:
            method = getattr(self, method_id)
        else:
            method = _getViewFor(self)

        if getattr(aq_base(method), 'isDocTemp', 0):
            return method(self, self.REQUEST, self.REQUEST['RESPONSE'])
        else:
            return method()  

    index_html = None  # This special value informs ZPublisher to use __call__

    security.declareProtected(View, 'view')
    def view( self ):

        """ Return the default view even if index_html is overridden.
        """
        return self()

    security.declareProtected(ChangeTopics, 'listCriteria')
    def listCriteria( self ):

        """ Return a list of our criteria objects.
        """
        return self.objectValues( self._criteria_metatype_ids() )


    security.declareProtected(ChangeTopics, 'listCriteriaTypes')
    def listCriteriaTypes( self ):

        """ List the available criteria types.
        """
        out = []

        for ct in self._criteriaTypes:
            out.append( { 'name': ct.meta_type } )

        return out

    security.declareProtected(ChangeTopics, 'listAvailableFields')
    def listAvailableFields( self ):

        """ Return a list of available fields for new criteria.
        """
        portal_catalog = getToolByName( self, 'portal_catalog' )
        currentfields = map( lambda x: x.Field(), self.listCriteria() )
        availfields = filter(
            lambda field, cf=currentfields: field not in cf,
            portal_catalog.indexes()
            )
        return availfields

    security.declareProtected(ChangeTopics, 'listSubtopics')
    def listSubtopics( self ):

        """ Return a list of our subtopics.
        """
        return self.objectValues( self.meta_type )

    security.declareProtected(ChangeTopics, 'edit')
    def edit( self, acquireCriteria, title=None, description=None ):

        """ Set the flag which indicates whether to acquire criteria.

        o If set, reuse creiteria from parent topics;
        
        o Also update metadata about the Topic.
        """
        self.acquireCriteria = acquireCriteria
        if title is not None:
            self.title = title
        self.description = description

        self.reindexObject()

    security.declareProtected(View, 'buildQuery')
    def buildQuery( self ):

        """ Construct a catalog query using our criterion objects.
        """
        result = {}

        if self.acquireCriteria:

            try:
                # Tracker 290 asks to allow combinations, like this:
                # parent = aq_parent( self )
                parent = aq_parent( aq_inner( self ) )
                result.update( parent.buildQuery() )

            except: # oh well, can't find parent, or it isn't a Topic.
                pass

        for criterion in self.listCriteria():

            for key, value in criterion.getCriteriaItems():
                result[ key ] = value

        return result

    security.declareProtected(View, 'queryCatalog')
    def queryCatalog( self, REQUEST=None, **kw ):

        """ Invoke the catalog using our criteria.
        
        o Built-in criteria update any criteria passed in 'kw'.
        """
        kw.update( self.buildQuery() )
        portal_catalog = getToolByName( self, 'portal_catalog' )
        return portal_catalog.searchResults(REQUEST, **kw)

    security.declareProtected(View, 'synContentValues')
    def synContentValues( self ):

        """ Return a limited subset of the brains for our query.
        
        o Return no more brain objects than the limit set by the
          syndication tool.
        """
        syn_tool = getToolByName( self, 'portal_syndication' )
        limit = syn_tool.getMaxItems( self )
        brains = self.queryCatalog( sort_limit=limit )[ :limit ]
        return [ brain.getObject() for brain in brains ] 

    ### Criteria adding/editing/deleting
    security.declareProtected(ChangeTopics, 'addCriterion')
    def addCriterion( self, field, criterion_type ):

        """ Add a new search criterion.
        """
        crit = None
        newid = 'crit__%s' % field

        for ct in self._criteriaTypes:

            if criterion_type == ct.meta_type:
                crit = ct( newid, field )

        if crit is None:
            # No criteria type matched passed in value
            raise NameError, 'Unknown Criterion Type: %s' % criterion_type

        self._setObject( newid, crit )

    security.declareProtected(ChangeTopics, 'deleteCriterion')
    def deleteCriterion( self, criterion_id ):

        """ Delete selected criterion.
        """
        if type( criterion_id ) is type( '' ):
            self._delObject( criterion_id )
        elif type( criterion_id ) in ( type( () ), type( [] ) ):
            for cid in criterion_id:
                self._delObject( cid )

    security.declareProtected(View, 'getCriterion')
    def getCriterion( self, criterion_id ):

        """ Get the criterion object.
        """
        try:
            return self._getOb( 'crit__%s' % criterion_id )
        except AttributeError:
            return self._getOb( criterion_id )

    security.declareProtected(AddTopics, 'addSubtopic')
    def addSubtopic( self, id ):

        """ Add a new subtopic.
        """
        ti = self.getTypeInfo()
        ti.constructInstance(self, id)
        return self._getOb( id )

    #
    #   Helper methods
    #
    security.declarePrivate( '_criteria_metatype_ids' )
    def _criteria_metatype_ids( self ):

        result = []

        for mt in self._criteriaTypes:
            result.append( mt.meta_type )

        return tuple( result )

    #
    #   Cataloging helper to make finding this item easier
    #
    security.declareProtected(View, 'SearchableText')
    def SearchableText(self):
        """
        SeachableText is used for full text seraches of a portal.  It
        should return a concatenation of all useful text.
        """
        return "%s %s" % (self.title, self.description) 

InitializeClass( Topic )
