vendor/CMF/1.5.5-beta/CMFTopic

changeset 0:4341d8feb2f9 CMFTopic tip

Vendor import of CMF 1.5.5-beta
author fguillaume
date Mon, 07 Nov 2005 22:03:41 +0000
parents
children
files AbstractCriterion.py DEPENDENCIES.txt DateCriteria.py Extensions/Update.py ListCriterion.py README.txt SimpleIntCriterion.py SimpleStringCriterion.py SortCriterion.py Topic.py TopicPermissions.py __init__.py configure.zcml help/Overview.stx help/Topics.stx interfaces/Criterion.py interfaces/__init__.py permissions.py skins/topic/friendlydatec_editform.dtml skins/topic/listc_edit.dtml skins/topic/sic_edit.dtml skins/topic/sort_edit.dtml skins/topic/ssc_edit.dtml skins/topic/topic_addCriterion.py skins/topic/topic_addSubtopic.py skins/topic/topic_criteria_form.dtml skins/topic/topic_deleteCriteria.py skins/topic/topic_editCriteria.py skins/topic/topic_editTopic.py skins/topic/topic_edit_form.dtml skins/topic/topic_icon.gif skins/topic/topic_subtopics_form.dtml skins/topic/topic_view.dtml skins/zpt_topic/friendlydatec_editform.pt skins/zpt_topic/listc_edit.pt skins/zpt_topic/sic_edit.pt skins/zpt_topic/sort_edit.pt skins/zpt_topic/ssc_edit.pt skins/zpt_topic/topic_addCriterion.py skins/zpt_topic/topic_criteria_form.pt skins/zpt_topic/topic_deleteCriteria.py skins/zpt_topic/topic_editCriteria.py skins/zpt_topic/topic_editTopic.py skins/zpt_topic/topic_edit_form.pt skins/zpt_topic/topic_icon.gif skins/zpt_topic/topic_subtopics_form.pt skins/zpt_topic/topic_view.pt tests/__init__.py tests/common.py tests/test_DateC.py tests/test_ListC.py tests/test_SIC.py tests/test_SSC.py tests/test_SortC.py tests/test_Topic.py tests/test_all.py version.txt
diffstat 57 files changed, 3660 insertions(+), 0 deletions(-) [+]
line diff
     1.1 new file mode 100644
     1.2 --- /dev/null
     1.3 +++ b/AbstractCriterion.py
     1.4 @@ -0,0 +1,85 @@
     1.5 +##############################################################################
     1.6 +#
     1.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
     1.8 +#
     1.9 +# This software is subject to the provisions of the Zope Public License,
    1.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    1.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    1.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    1.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    1.14 +# FOR A PARTICULAR PURPOSE.
    1.15 +#
    1.16 +##############################################################################
    1.17 +""" Home of the abstract Criterion base class.
    1.18 +
    1.19 +$Id: AbstractCriterion.py 36457 2004-08-12 15:07:44Z jens $
    1.20 +"""
    1.21 +from Acquisition import Implicit
    1.22 +from AccessControl import ClassSecurityInfo
    1.23 +from Persistence import Persistent
    1.24 +from Globals import InitializeClass
    1.25 +from OFS.SimpleItem import Item
    1.26 +
    1.27 +from permissions import AccessContentsInformation
    1.28 +from permissions import ChangeTopics
    1.29 +
    1.30 +
    1.31 +class AbstractCriterion( Persistent, Item, Implicit ):
    1.32 +    """
    1.33 +        Abstract base class for Criterion objects.
    1.34 +    """
    1.35 +
    1.36 +    security = ClassSecurityInfo()
    1.37 +
    1.38 +    security.declareProtected(ChangeTopics, 'apply')
    1.39 +    def apply( self, command ):
    1.40 +        """
    1.41 +            Apply 'command', which is expected to be a dictionary,
    1.42 +            to 'self.edit' (makes using Python Scripts easier).
    1.43 +        """
    1.44 +        self.edit(**command)
    1.45 +
    1.46 +    security.declareProtected( ChangeTopics, 'editableAttributes' )
    1.47 +    def editableAttributes( self ):
    1.48 +        """
    1.49 +            Return a list of editable attributes, used by topics
    1.50 +            to build commands to send to the 'edit' command of each
    1.51 +            criterion, which may vary.
    1.52 +
    1.53 +            Requires concrete subclasses to implement the attribute
    1.54 +            '_editableAttributes' which is a tuple of attributes
    1.55 +            that can be edited, for example:
    1.56 +
    1.57 +            _editableAttributes = ( 'value', 'direction' )
    1.58 +        """
    1.59 +        return self._editableAttributes
    1.60 +
    1.61 +    security.declareProtected( AccessContentsInformation, 'Type' )
    1.62 +    def Type( self ):
    1.63 +        """
    1.64 +            Return the Type of Criterion this object is.  This
    1.65 +            method can be overriden in subclasses, or those
    1.66 +            concrete subclasses must define the 'meta_type'
    1.67 +            attribute.
    1.68 +        """
    1.69 +        return self.meta_type
    1.70 +
    1.71 +    security.declareProtected( AccessContentsInformation, 'Field' )
    1.72 +    def Field( self ):
    1.73 +        """
    1.74 +            Return the field that this criterion searches on.  The
    1.75 +            concrete subclasses can override this method, or have
    1.76 +            the 'field' attribute.
    1.77 +        """
    1.78 +        return self.field
    1.79 +
    1.80 +    security.declareProtected( AccessContentsInformation, 'Description' )
    1.81 +    def Description( self ):
    1.82 +        """
    1.83 +            Return a brief but helpful description of the Criterion type,
    1.84 +            preferably based on the classes __doc__ string.
    1.85 +        """
    1.86 +        lines = [ line.strip() for line in self.__doc__.splitlines() ]
    1.87 +        return ' '.join( [ line for line in lines if line ] )
    1.88 +
    1.89 +InitializeClass( AbstractCriterion )
     2.1 new file mode 100644
     2.2 --- /dev/null
     2.3 +++ b/DEPENDENCIES.txt
     2.4 @@ -0,0 +1,3 @@
     2.5 +Zope >= 2.7.0
     2.6 +CMFCore
     2.7 +CMFDefault
     3.1 new file mode 100644
     3.2 --- /dev/null
     3.3 +++ b/DateCriteria.py
     3.4 @@ -0,0 +1,171 @@
     3.5 +##############################################################################
     3.6 +#
     3.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
     3.8 +#
     3.9 +# This software is subject to the provisions of the Zope Public License,
    3.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    3.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    3.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    3.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    3.14 +# FOR A PARTICULAR PURPOSE.
    3.15 +#
    3.16 +##############################################################################
    3.17 +""" Various date criteria
    3.18 +
    3.19 +$Id: DateCriteria.py 37998 2005-08-18 22:15:42Z jens $
    3.20 +"""
    3.21 +
    3.22 +from AccessControl import ClassSecurityInfo
    3.23 +from DateTime.DateTime import DateTime
    3.24 +from Globals import InitializeClass
    3.25 +
    3.26 +from permissions import View
    3.27 +from permissions import ChangeTopics
    3.28 +from AbstractCriterion import AbstractCriterion
    3.29 +from interfaces import Criterion
    3.30 +from Topic import Topic
    3.31 +
    3.32 +
    3.33 +class FriendlyDateCriterion( AbstractCriterion ):
    3.34 +    """
    3.35 +        Put a friendly interface on date range searches, like
    3.36 +        'where effective date is less than 5 days old'.
    3.37 +    """
    3.38 +    __implements__ = ( Criterion, )
    3.39 +
    3.40 +    meta_type = 'Friendly Date Criterion'
    3.41 +
    3.42 +    security = ClassSecurityInfo()
    3.43 +
    3.44 +    _editableAttributes = ( 'value', 'operation', 'daterange' )
    3.45 +
    3.46 +    _defaultDateOptions = ( (     0, 'Now'      )
    3.47 +                          , (     1, '1 Day'    )
    3.48 +                          , (     2, '2 Days'   )
    3.49 +                          , (     5, '5 Days'   )
    3.50 +                          , (     7, '1 Week'   )
    3.51 +                          , (    14, '2 Weeks'  )
    3.52 +                          , (    31, '1 Month'  )
    3.53 +                          , (  31*3, '3 Months' )
    3.54 +                          , (  31*6, '6 Months' )
    3.55 +                          , (   365, '1 Year'   )
    3.56 +                          , ( 365*2, '2 years'  )
    3.57 +                          )
    3.58 +
    3.59 +    def __init__( self, id, field ):
    3.60 +
    3.61 +        self.id = id
    3.62 +        self.field = field
    3.63 +        self.value = None
    3.64 +        self.operation = 'min'
    3.65 +        self.daterange = 'old'
    3.66 +
    3.67 +    security.declarePublic( 'defaultDateOptions' )
    3.68 +    def defaultDateOptions( self ):
    3.69 +        """
    3.70 +            Return a list of default values and labels for date options.
    3.71 +        """
    3.72 +        return self._defaultDateOptions
    3.73 +
    3.74 +    security.declareProtected( ChangeTopics, 'getEditForm' )
    3.75 +    def getEditForm( self ):
    3.76 +        """
    3.77 +            Return the name of the skin method used by Topic to edit
    3.78 +            criteria of this type.
    3.79 +        """
    3.80 +        return 'friendlydatec_editform'
    3.81 +
    3.82 +    security.declareProtected( ChangeTopics, 'edit' )
    3.83 +    def edit( self
    3.84 +            , value=None
    3.85 +            , operation='min'
    3.86 +            , daterange='old'
    3.87 +            ):
    3.88 +        """
    3.89 +            Update the values to match against.
    3.90 +        """
    3.91 +        if value in ( None, '' ):
    3.92 +            self.value = None
    3.93 +        else:
    3.94 +            try:
    3.95 +                self.value = int( value )
    3.96 +            except:
    3.97 +                raise ValueError, 'Supplied value should be an int'
    3.98 +
    3.99 +        if operation in ( 'min', 'max', 'within_day' ):
   3.100 +            self.operation = operation
   3.101 +        else:
   3.102 +            raise ValueError, 'Operation type not in set {min,max,within_day}'
   3.103 +
   3.104 +        if daterange in ( 'old', 'ahead' ):
   3.105 +            self.daterange = daterange
   3.106 +        else:
   3.107 +            raise ValueError, 'Date range not in set {old,ahead}'
   3.108 +
   3.109 +    security.declareProtected(View, 'getCriteriaItems')
   3.110 +    def getCriteriaItems( self ):
   3.111 +        """
   3.112 +            Return a sequence of items to be used to build the catalog query.
   3.113 +        """
   3.114 +        if self.value is not None:
   3.115 +            field = self.Field()
   3.116 +            value = self.value
   3.117 +            operation = self.operation
   3.118 +
   3.119 +            # Negate the value for 'old' days
   3.120 +            if self.daterange == 'old' and value != 0:
   3.121 +                value = -value
   3.122 +
   3.123 +                # Also reverse the operator to match what a user would expect.
   3.124 +                # Queries such as "More than 2 days ago" should match dates
   3.125 +                # *earlier* than "today minus 2", and "Less than 2 days ago"
   3.126 +                # would be expected to return dates *later* then "today minus
   3.127 +                # two".
   3.128 +                if operation == 'max':
   3.129 +                    operation = 'min'
   3.130 +                elif operation == 'min':
   3.131 +                    operation = 'max'
   3.132 +
   3.133 +            date = DateTime() + value
   3.134 +
   3.135 +            if operation == 'within_day':
   3.136 +                # When items within a day are requested, the range is between
   3.137 +                # the earliest and latest time of that particular day
   3.138 +                range = ( date.earliestTime(), date.latestTime() )
   3.139 +                return ( ( field, {'query': range, 'range': 'min:max'} ), )
   3.140 +
   3.141 +            elif operation == 'min':
   3.142 +                if value != 0:
   3.143 +                    if self.daterange == 'old':
   3.144 +                        date_range = (date, DateTime())
   3.145 +                        return ( ( field, { 'query': date_range
   3.146 +                                          , 'range': 'min:max'
   3.147 +                                          } ), )
   3.148 +                    else:
   3.149 +                        return ( ( field, { 'query': date.earliestTime()
   3.150 +                                          , 'range': operation 
   3.151 +                                          } ), )
   3.152 +                else:
   3.153 +                    # Value 0 means "Now", so get everything from now on
   3.154 +                    return ( ( field, {'query': date,'range': operation } ), )
   3.155 +
   3.156 +            elif operation == 'max':
   3.157 +                if value != 0:
   3.158 +                    if self.daterange == 'old':
   3.159 +                        return ((field, {'query': date, 'range': operation}),)
   3.160 +                    else:
   3.161 +                        date_range = (DateTime(), date.latestTime())
   3.162 +                        return ( ( field, { 'query': date_range
   3.163 +                                          , 'range': 'min:max'
   3.164 +                                          } ), )
   3.165 +                else:
   3.166 +                    # Value is 0, meaning "Now", get everything before "Now"
   3.167 +                    return ( ( field, {'query': date, 'range': operation} ), )
   3.168 +        else:
   3.169 +            return ()
   3.170 +
   3.171 +InitializeClass(FriendlyDateCriterion)
   3.172 +
   3.173 +
   3.174 +# Register as a criteria type with the Topic class
   3.175 +Topic._criteriaTypes.append( FriendlyDateCriterion )
     4.1 new file mode 100644
     4.2 --- /dev/null
     4.3 +++ b/Extensions/Update.py
     4.4 @@ -0,0 +1,71 @@
     4.5 +##############################################################################
     4.6 +#
     4.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
     4.8 +# 
     4.9 +# This software is subject to the provisions of the Zope Public License,
    4.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    4.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    4.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    4.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    4.14 +# FOR A PARTICULAR PURPOSE.
    4.15 +# 
    4.16 +##############################################################################
    4.17 +
    4.18 +from Products.CMFCore.utils import getToolByName
    4.19 +from cStringIO import StringIO
    4.20 +import sys
    4.21 +
    4.22 +def update(self):
    4.23 +    """\
    4.24 +    Calls into UpdateTopic to perform updates on CMF Topic installations.
    4.25 +    """
    4.26 +    out = StringIO()
    4.27 +    Updater = UpdateTopic(out)
    4.28 +    out.write('Updating types tool configuration 1.0 to 1.1...\n')
    4.29 +    Updater.update_TypesToolConfiguration_10_11(target=self)
    4.30 +
    4.31 +    return out.getvalue()
    4.32 +
    4.33 +
    4.34 +class UpdateTopic:
    4.35 +    """\
    4.36 +    A suite of methods for applying updates to CMF Topic installations,
    4.37 +    used by external method(s) or other upgrade scenarios.
    4.38 +    """
    4.39 +
    4.40 +    def __init__(self, stream):
    4.41 +        """\
    4.42 +        stream is expected to be some writable file object,
    4.43 +        like a StringIO, that output will be sent to.
    4.44 +        """
    4.45 +        self.stream = stream
    4.46 +
    4.47 +    def update_TypesToolConfiguration_10_11(self, target):
    4.48 +        """\
    4.49 +        This updates the types tool configuration to reflect the name
    4.50 +        changes from 'topic_edit' to 'topic_edit_form' (etc), and sets
    4.51 +        the immediate_view to 'topic_edit_form'.
    4.52 +        """
    4.53 +        typestool = getToolByName(target, 'portal_types')
    4.54 +        write = self.stream.write
    4.55 +
    4.56 +        for ti in typestool.listTypeInfo():
    4.57 +            if ti.content_meta_type != 'Portal Topic':
    4.58 +                continue
    4.59 +            acts = list(ti.getActions())
    4.60 +            for action in acts:
    4.61 +                ta = action['action']
    4.62 +                if ta in ('topic_edit', 'topic_criteria', 'topic_subtopics',):
    4.63 +                    write(" Changed '%s' in %s to '%s_form'\n" % (ta,
    4.64 +                                                                  ti.id,
    4.65 +                                                                  ta,
    4.66 +                                                                   )
    4.67 +                          )
    4.68 +                    ta = '%s_form' % ta
    4.69 +                action['action'] = ta
    4.70 +            ti._actions = tuple(acts)
    4.71 +            initial = getattr(ti, 'immediate_view', None)
    4.72 +            if initial == 'topic_edit':
    4.73 +                s="Changed the immediate view in %s to topic_edit_form" % ti.id
    4.74 +                write("  %s\n" % s)
    4.75 +                ti.immediate_view = 'topic_edit_form'
     5.1 new file mode 100644
     5.2 --- /dev/null
     5.3 +++ b/ListCriterion.py
     5.4 @@ -0,0 +1,105 @@
     5.5 +##############################################################################
     5.6 +#
     5.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
     5.8 +#
     5.9 +# This software is subject to the provisions of the Zope Public License,
    5.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    5.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    5.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    5.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    5.14 +# FOR A PARTICULAR PURPOSE.
    5.15 +#
    5.16 +##############################################################################
    5.17 +""" List Criterion: A criterion that is a list
    5.18 +
    5.19 +$Id: ListCriterion.py 36457 2004-08-12 15:07:44Z jens $
    5.20 +"""
    5.21 +from Globals import InitializeClass
    5.22 +from AccessControl import ClassSecurityInfo
    5.23 +
    5.24 +
    5.25 +from permissions import View
    5.26 +from permissions import ChangeTopics
    5.27 +from AbstractCriterion import AbstractCriterion
    5.28 +from interfaces import Criterion
    5.29 +from Topic import Topic
    5.30 +
    5.31 +
    5.32 +class ListCriterion( AbstractCriterion ):
    5.33 +    """
    5.34 +        Represent a criterion which is a list of values (for an
    5.35 +        'OR' search).
    5.36 +    """
    5.37 +    __implements__ = ( Criterion, )
    5.38 +
    5.39 +    meta_type = 'List Criterion'
    5.40 +    operator = None
    5.41 +    value = ( '', )
    5.42 +
    5.43 +    security = ClassSecurityInfo()
    5.44 +
    5.45 +    _editableAttributes = ( 'value', 'operator' )
    5.46 +
    5.47 +    def __init__( self, id, field ):
    5.48 +        self.id = id
    5.49 +        self.field = field
    5.50 +        self._clear()
    5.51 +
    5.52 +    security.declarePrivate( '_clear' )
    5.53 +    def _clear( self ):
    5.54 +        """
    5.55 +            Restore to original value.
    5.56 +        """
    5.57 +        self.value = ( '', )    # *Not* '()', which won't do at all!
    5.58 +        self.operator = None
    5.59 +
    5.60 +    security.declareProtected( ChangeTopics, 'getEditForm' )
    5.61 +    def getEditForm( self ):
    5.62 +        """
    5.63 +            Return the name of skin method which renders the form
    5.64 +            used to edit this kind of criterion.
    5.65 +        """
    5.66 +        return "listc_edit"
    5.67 +
    5.68 +    security.declareProtected( ChangeTopics, 'edit' )
    5.69 +    def edit( self, value=None, operator=None ):
    5.70 +        """
    5.71 +            Update the value we match against.
    5.72 +        """
    5.73 +        if value is None:
    5.74 +            self._clear()
    5.75 +        else:
    5.76 +            if type( value ) == type( '' ):
    5.77 +                value = value.split('\n')
    5.78 +            self.value = tuple( value )
    5.79 +
    5.80 +        if not operator:
    5.81 +            operator = None
    5.82 +
    5.83 +        self.operator = operator
    5.84 +
    5.85 +    security.declareProtected(View, 'getCriteriaItems')
    5.86 +    def getCriteriaItems( self ):
    5.87 +        """
    5.88 +            Return a tuple of query elements to be passed to the catalog
    5.89 +            (used by 'Topic.buildQuery()').
    5.90 +        """
    5.91 +        # filter out empty strings
    5.92 +        result = []
    5.93 +
    5.94 +        value = tuple( filter( None, self.value ) )
    5.95 +        if not value:
    5.96 +            return ()
    5.97 +        result.append( ( self.field, self.value ), )
    5.98 +
    5.99 +        if self.operator is not None:
   5.100 +            result.append( ( '%s_operator' % self.field, self.operator ) )
   5.101 +
   5.102 +        return tuple( result )
   5.103 +
   5.104 +
   5.105 +
   5.106 +InitializeClass( ListCriterion )
   5.107 +
   5.108 +# Register as a criteria type with the Topic class
   5.109 +Topic._criteriaTypes.append( ListCriterion )
     6.1 new file mode 100644
     6.2 --- /dev/null
     6.3 +++ b/README.txt
     6.4 @@ -0,0 +1,21 @@
     6.5 +Updating CMF Topic in a CMF Site
     6.6 +
     6.7 +  Since default settings may change from time to time in CMF Topic,
     6.8 +  you may need to update your Topic types tool (and other) settings.
     6.9 +  This is done similarly to installing by adding an External Method
    6.10 +  to your CMF Site instance with the following configuration::
    6.11 +
    6.12 +    **id** -- 'update_topic'
    6.13 +    **title** -- *Update Topic*
    6.14 +    **module name** -- 'CMFTopic.Update'
    6.15 +    **function name** -- 'update'
    6.16 +
    6.17 +  Go to the management screen for the newly added external method and
    6.18 +  click the 'Try it' tab.  The update function will execute and give
    6.19 +  information about the steps it took to register and update CMF Topic 
    6.20 +  site information.
    6.21 +
    6.22 +  *Note: This update script should **only** change values that are
    6.23 +  still at their default, such as changing an action from 'topic_edit' 
    6.24 +  to 'topic_edit_form'.  If you changed that action to 'mytopic_edit', 
    6.25 +  the script should pass that by and not change your settings.*
     7.1 new file mode 100644
     7.2 --- /dev/null
     7.3 +++ b/SimpleIntCriterion.py
     7.4 @@ -0,0 +1,128 @@
     7.5 +##############################################################################
     7.6 +#
     7.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
     7.8 +#
     7.9 +# This software is subject to the provisions of the Zope Public License,
    7.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    7.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    7.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    7.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    7.14 +# FOR A PARTICULAR PURPOSE.
    7.15 +#
    7.16 +##############################################################################
    7.17 +""" Simple int-matching criterion
    7.18 +
    7.19 +$Id: SimpleIntCriterion.py 36457 2004-08-12 15:07:44Z jens $
    7.20 +"""
    7.21 +
    7.22 +from AccessControl import ClassSecurityInfo
    7.23 +from Globals import InitializeClass
    7.24 +
    7.25 +from permissions import View
    7.26 +from permissions import ChangeTopics
    7.27 +from AbstractCriterion import AbstractCriterion
    7.28 +from interfaces import Criterion
    7.29 +from Topic import Topic
    7.30 +
    7.31 +
    7.32 +class SimpleIntCriterion( AbstractCriterion ):
    7.33 +    """
    7.34 +        Represent a simple field-match for an integer value, including
    7.35 +        catalog range searches.
    7.36 +    """
    7.37 +    __implements__ = ( Criterion, )
    7.38 +
    7.39 +    meta_type = 'Integer Criterion'
    7.40 +
    7.41 +    security = ClassSecurityInfo()
    7.42 +    _editableAttributes = ( 'value', 'direction' )
    7.43 +
    7.44 +    MINIMUM = 'min'
    7.45 +    MAXIMUM = 'max'
    7.46 +    MINMAX = 'min:max'
    7.47 +
    7.48 +    def __init__(self, id, field):
    7.49 +        self.id = id
    7.50 +        self.field = field
    7.51 +        self.value = self.direction = None
    7.52 +
    7.53 +    security.declareProtected( ChangeTopics, 'getEditForm' )
    7.54 +    def getEditForm( self ):
    7.55 +        """
    7.56 +            Return the name of skin method which renders the form
    7.57 +            used to edit this kind of criterion.
    7.58 +        """
    7.59 +        return 'sic_edit'
    7.60 +
    7.61 +    security.declareProtected( ChangeTopics, 'getValueString' )
    7.62 +    def getValueString( self ):
    7.63 +        """
    7.64 +            Return a string representation of the value for which this
    7.65 +            criterion filters.
    7.66 +        """
    7.67 +        if self.value is None:
    7.68 +            return ''
    7.69 +
    7.70 +        if self.direction == self.MINMAX:
    7.71 +
    7.72 +            value = self.value
    7.73 +
    7.74 +            if type( value ) is not type( () ):
    7.75 +                value = ( value, value )
    7.76 +
    7.77 +            return '%s %s' % value
    7.78 +
    7.79 +        return str( self.value )
    7.80 +
    7.81 +    security.declareProtected( ChangeTopics, 'edit' )
    7.82 +    def edit( self, value, direction=None ):
    7.83 +        """
    7.84 +            Update the value to be filtered, and the "direction" qualifier.
    7.85 +        """
    7.86 +
    7.87 +        if type( value ) == type( '' ):
    7.88 +           value = value.strip()
    7.89 +
    7.90 +        if not value:
    7.91 +            # An empty string was passed in, which evals to None
    7.92 +            self.value = self.direction = None
    7.93 +
    7.94 +        elif direction:
    7.95 +
    7.96 +            if direction == self.MINMAX:
    7.97 +
    7.98 +                if type( value ) == type( '' ):
    7.99 +                    minimum, maximum = value.split(' ')
   7.100 +                else:
   7.101 +                    minimum, maximum = value
   7.102 +
   7.103 +                self.value = ( int( minimum ), int( maximum ) )
   7.104 +
   7.105 +            else:
   7.106 +                self.value = int( value )
   7.107 +
   7.108 +            self.direction = direction
   7.109 +
   7.110 +        else:
   7.111 +            self.value = int( value )
   7.112 +            self.direction = None
   7.113 +
   7.114 +    security.declareProtected(View, 'getCriteriaItems')
   7.115 +    def getCriteriaItems( self ):
   7.116 +        """
   7.117 +            Return a tuple of query elements to be passed to the catalog
   7.118 +            (used by 'Topic.buildQuery()').
   7.119 +        """
   7.120 +        if self.value is None:
   7.121 +            return ()
   7.122 +        elif self.direction is None:
   7.123 +            return ( ( self.Field(), self.value ), )
   7.124 +        else:
   7.125 +            return ( ( self.Field(), {'query': self.value,
   7.126 +                                      'range': self.direction} ), )
   7.127 +
   7.128 +InitializeClass( SimpleIntCriterion )
   7.129 +
   7.130 +
   7.131 +# Register as a criteria type with the Topic class
   7.132 +Topic._criteriaTypes.append( SimpleIntCriterion )
     8.1 new file mode 100644
     8.2 --- /dev/null
     8.3 +++ b/SimpleStringCriterion.py
     8.4 @@ -0,0 +1,73 @@
     8.5 +##############################################################################
     8.6 +#
     8.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
     8.8 +# 
     8.9 +# This software is subject to the provisions of the Zope Public License,
    8.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    8.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    8.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    8.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    8.14 +# FOR A PARTICULAR PURPOSE.
    8.15 +# 
    8.16 +##############################################################################
    8.17 +""" Simple string-matching criterion class
    8.18 +
    8.19 +$Id: SimpleStringCriterion.py 36457 2004-08-12 15:07:44Z jens $
    8.20 +"""
    8.21 +from Globals import InitializeClass
    8.22 +from AccessControl import ClassSecurityInfo
    8.23 +
    8.24 +from permissions import View
    8.25 +from permissions import ChangeTopics
    8.26 +from AbstractCriterion import AbstractCriterion
    8.27 +from Topic import Topic
    8.28 +from interfaces import Criterion
    8.29 +
    8.30 +class SimpleStringCriterion( AbstractCriterion ):
    8.31 +    """
    8.32 +        Represent a simple field-match for a string value.
    8.33 +    """
    8.34 +    __implements__ = ( Criterion, )
    8.35 +
    8.36 +    meta_type = 'String Criterion'
    8.37 +
    8.38 +    security = ClassSecurityInfo()
    8.39 +
    8.40 +    _editableAttributes = ( 'value', )
    8.41 +
    8.42 +    def __init__(self, id, field):
    8.43 +        self.id = id
    8.44 +        self.field = field
    8.45 +        self.value = ''
    8.46 +        
    8.47 +    security.declareProtected( ChangeTopics, 'getEditForm' )
    8.48 +    def getEditForm( self ):
    8.49 +        """
    8.50 +            Return the skinned name of the edit form.
    8.51 +        """
    8.52 +        return 'ssc_edit'
    8.53 +    
    8.54 +    security.declareProtected( ChangeTopics, 'edit' )
    8.55 +    def edit( self, value ):
    8.56 +        """
    8.57 +            Update the value we are to match up against.
    8.58 +        """
    8.59 +        self.value = str( value )
    8.60 +    
    8.61 +    security.declareProtected(View, 'getCriteriaItems')
    8.62 +    def getCriteriaItems( self ):
    8.63 +        """
    8.64 +            Return a sequence of criteria items, used by Topic.buildQuery.
    8.65 +        """
    8.66 +        result = []
    8.67 +
    8.68 +        if self.value is not '':
    8.69 +            result.append( ( self.field, self.value ) )
    8.70 +
    8.71 +        return tuple( result )
    8.72 +
    8.73 +
    8.74 +InitializeClass( SimpleStringCriterion )
    8.75 +
    8.76 +# Register as a criteria type with the Topic class
    8.77 +Topic._criteriaTypes.append( SimpleStringCriterion )
     9.1 new file mode 100644
     9.2 --- /dev/null
     9.3 +++ b/SortCriterion.py
     9.4 @@ -0,0 +1,84 @@
     9.5 +##############################################################################
     9.6 +#
     9.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
     9.8 +# 
     9.9 +# This software is subject to the provisions of the Zope Public License,
    9.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    9.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    9.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    9.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    9.14 +# FOR A PARTICULAR PURPOSE.
    9.15 +# 
    9.16 +##############################################################################
    9.17 +""" Allow topic to specify sorting.
    9.18 +
    9.19 +$Id: SortCriterion.py 36911 2005-04-07 16:38:47Z yuppie $
    9.20 +"""
    9.21 +from Globals import InitializeClass
    9.22 +from AccessControl import ClassSecurityInfo
    9.23 +
    9.24 +from permissions import View
    9.25 +from permissions import ChangeTopics
    9.26 +from AbstractCriterion import AbstractCriterion
    9.27 +from Topic import Topic
    9.28 +from interfaces import Criterion
    9.29 +
    9.30 +class SortCriterion( AbstractCriterion ):
    9.31 +    """
    9.32 +        Represent a mock criterion, to allow spelling the sort order
    9.33 +        and reversal items in a catalog query.
    9.34 +    """
    9.35 +    __implements__ = ( Criterion, )
    9.36 +
    9.37 +    meta_type = 'Sort Criterion'
    9.38 +
    9.39 +    security = ClassSecurityInfo()
    9.40 +
    9.41 +    field = None # Don't prevent use of field in other criteria
    9.42 +
    9.43 +    _editableAttributes = ( 'reversed', )
    9.44 +
    9.45 +    def __init__( self, id, index ):
    9.46 +        self.id = id
    9.47 +        self.index = index
    9.48 +        self.reversed = 0
    9.49 +        
    9.50 +    # inherit permissions
    9.51 +    def Field( self ):
    9.52 +        """
    9.53 +            Map the stock Criterion interface.
    9.54 +        """
    9.55 +        return self.index
    9.56 +
    9.57 +    security.declareProtected( ChangeTopics, 'getEditForm' )
    9.58 +    def getEditForm( self ):
    9.59 +        """
    9.60 +            Return the name of skin method which renders the form
    9.61 +            used to edit this kind of criterion.
    9.62 +        """
    9.63 +        return 'sort_edit'
    9.64 +    
    9.65 +    security.declareProtected( ChangeTopics, 'edit' )
    9.66 +    def edit( self, reversed ):
    9.67 +        """
    9.68 +            Update the value we are to match up against.
    9.69 +        """
    9.70 +        self.reversed = bool(reversed)
    9.71 +    
    9.72 +    security.declareProtected(View, 'getCriteriaItems')
    9.73 +    def getCriteriaItems( self ):
    9.74 +        """
    9.75 +            Return a tuple of query elements to be passed to the catalog
    9.76 +            (used by 'Topic.buildQuery()').
    9.77 +        """
    9.78 +        result = [ ( 'sort_on', self.index ) ]
    9.79 +
    9.80 +        if self.reversed:
    9.81 +            result.append( ( 'sort_order', 'reverse' ) )
    9.82 +
    9.83 +        return tuple( result )
    9.84 +
    9.85 +InitializeClass( SortCriterion )
    9.86 +
    9.87 +# Register as a criteria type with the Topic class
    9.88 +Topic._criteriaTypes.append( SortCriterion )
    10.1 new file mode 100644
    10.2 --- /dev/null
    10.3 +++ b/Topic.py
    10.4 @@ -0,0 +1,315 @@
    10.5 +##############################################################################
    10.6 +#
    10.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    10.8 +#
    10.9 +# This software is subject to the provisions of the Zope Public License,
   10.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   10.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   10.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   10.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   10.14 +# FOR A PARTICULAR PURPOSE.
   10.15 +#
   10.16 +##############################################################################
   10.17 +""" Topic: Canned catalog queries
   10.18 +
   10.19 +$Id: Topic.py 38001 2005-08-18 23:37:25Z jens $
   10.20 +"""
   10.21 +
   10.22 +from AccessControl import ClassSecurityInfo
   10.23 +from Acquisition import aq_parent, aq_inner, aq_base
   10.24 +from Globals import InitializeClass
   10.25 +
   10.26 +from Products.CMFDefault.SkinnedFolder import SkinnedFolder
   10.27 +from Products.CMFCore.utils import _getViewFor
   10.28 +from Products.CMFCore.utils import getToolByName
   10.29 +
   10.30 +from permissions import ListFolderContents
   10.31 +from permissions import View
   10.32 +from permissions import AddTopics
   10.33 +from permissions import ChangeTopics
   10.34 +
   10.35 +
   10.36 +# Factory type information -- makes Topic objects play nicely
   10.37 +# with the Types Tool (portal_types )
   10.38 +factory_type_information = (
   10.39 +  { 'id'             : 'Topic'
   10.40 +  , 'icon'           : 'topic_icon.gif'
   10.41 +  , 'meta_type'      : 'Portal Topic'
   10.42 +  , 'description'    : 'Topics are canned queries for organizing content '
   10.43 +                       'with up to date queries into the catalog.'
   10.44 +  , 'product'        : 'CMFTopic'
   10.45 +  , 'factory'        : 'addTopic'
   10.46 +  , 'immediate_view' : 'topic_edit_form'
   10.47 +  , 'allowed_content_types': ('Topic',)
   10.48 +  , 'aliases'        : {'(Default)': 'topic_view',
   10.49 +                        'view': 'topic_view'}
   10.50 +  , 'actions'        : ( { 'id'            : 'view'
   10.51 +                         , 'name'          : 'View'
   10.52 +                         , 'action': 'string:${object_url}/topic_view'
   10.53 +                         , 'permissions'   : (View,)
   10.54 +                         }
   10.55 +                       , { 'id'            : 'edit'
   10.56 +                         , 'name'          : 'Edit'
   10.57 +                         , 'action': 'string:${object_url}/topic_edit_form'
   10.58 +                         , 'permissions'   : (ChangeTopics,)
   10.59 +                         }
   10.60 +                       , { 'id'            : 'criteria'
   10.61 +                         , 'name'          : 'Criteria'
   10.62 +                         , 'action': 'string:${object_url}/topic_criteria_form'
   10.63 +                         , 'permissions'   : (ChangeTopics,)
   10.64 +                         }
   10.65 +                       , { 'id'            : 'folderContents'
   10.66 +                         , 'name'          : 'Subtopics'
   10.67 +                         , 'action': 'string:${object_url}/folder_contents'
   10.68 +                         , 'permissions'   : (ListFolderContents,)
   10.69 +                         }
   10.70 +                       , { 'id'            : 'new'
   10.71 +                         , 'name'          : 'New...'
   10.72 +                         , 'action': 'string:${object_url}/folder_factories'
   10.73 +                         , 'permissions'   : (AddTopics,)
   10.74 +                         , 'visible'       : 0
   10.75 +                         }
   10.76 +                       , { 'id'            : 'rename_items'
   10.77 +                         , 'name'          : 'Rename items'
   10.78 +                         , 'action': 'string:${object_url}/folder_rename_form'
   10.79 +                         , 'permissions'   : (AddTopics,)
   10.80 +                         , 'visible'       : 0
   10.81 +                         }
   10.82 +                       )
   10.83 +  }
   10.84 +,
   10.85 +)
   10.86 +
   10.87 +def addTopic( self, id, title='', REQUEST=None ):
   10.88 +
   10.89 +    """ Create an empty topic.
   10.90 +    """
   10.91 +    topic = Topic( id )
   10.92 +    topic.id = id
   10.93 +    topic.title = title
   10.94 +    self._setObject( id, topic )
   10.95 +
   10.96 +    if REQUEST is not None:
   10.97 +        REQUEST['RESPONSE'].redirect( 'manage_main' )
   10.98 +
   10.99 +
  10.100 +class Topic( SkinnedFolder ):
  10.101 +
  10.102 +    """ Topics are 'canned queries'
  10.103 +    
  10.104 +    o Each topic holds a set of zero or more Criteria objects specifying
  10.105 +      the query.
  10.106 +    """
  10.107 +
  10.108 +    meta_type='Portal Topic'
  10.109 +
  10.110 +    security = ClassSecurityInfo()
  10.111 +    security.declareObjectProtected(View)
  10.112 +
  10.113 +    acquireCriteria = 1
  10.114 +    _criteriaTypes = []
  10.115 +
  10.116 +    def __call__( self ):
  10.117 +        """ Invoke the default action.
  10.118 +        """
  10.119 +        ti = self.getTypeInfo()
  10.120 +        method_id = ti and ti.queryMethodID('(Default)', context=self)
  10.121 +        if method_id:
  10.122 +            method = getattr(self, method_id)
  10.123 +        else:
  10.124 +            method = _getViewFor(self)
  10.125 +
  10.126 +        if getattr(aq_base(method), 'isDocTemp', 0):
  10.127 +            return method(self, self.REQUEST, self.REQUEST['RESPONSE'])
  10.128 +        else:
  10.129 +            return method()  
  10.130 +
  10.131 +    index_html = None  # This special value informs ZPublisher to use __call__
  10.132 +
  10.133 +    security.declareProtected(View, 'view')
  10.134 +    def view( self ):
  10.135 +
  10.136 +        """ Return the default view even if index_html is overridden.
  10.137 +        """
  10.138 +        return self()
  10.139 +
  10.140 +    security.declareProtected(ChangeTopics, 'listCriteria')
  10.141 +    def listCriteria( self ):
  10.142 +
  10.143 +        """ Return a list of our criteria objects.
  10.144 +        """
  10.145 +        return self.objectValues( self._criteria_metatype_ids() )
  10.146 +
  10.147 +
  10.148 +    security.declareProtected(ChangeTopics, 'listCriteriaTypes')
  10.149 +    def listCriteriaTypes( self ):
  10.150 +
  10.151 +        """ List the available criteria types.
  10.152 +        """
  10.153 +        out = []
  10.154 +
  10.155 +        for ct in self._criteriaTypes:
  10.156 +            out.append( { 'name': ct.meta_type } )
  10.157 +
  10.158 +        return out
  10.159 +
  10.160 +    security.declareProtected(ChangeTopics, 'listAvailableFields')
  10.161 +    def listAvailableFields( self ):
  10.162 +
  10.163 +        """ Return a list of available fields for new criteria.
  10.164 +        """
  10.165 +        portal_catalog = getToolByName( self, 'portal_catalog' )
  10.166 +        currentfields = map( lambda x: x.Field(), self.listCriteria() )
  10.167 +        availfields = filter(
  10.168 +            lambda field, cf=currentfields: field not in cf,
  10.169 +            portal_catalog.indexes()
  10.170 +            )
  10.171 +        return availfields
  10.172 +
  10.173 +    security.declareProtected(ChangeTopics, 'listSubtopics')
  10.174 +    def listSubtopics( self ):
  10.175 +
  10.176 +        """ Return a list of our subtopics.
  10.177 +        """
  10.178 +        return self.objectValues( self.meta_type )
  10.179 +
  10.180 +    security.declareProtected(ChangeTopics, 'edit')
  10.181 +    def edit( self, acquireCriteria, title=None, description=None ):
  10.182 +
  10.183 +        """ Set the flag which indicates whether to acquire criteria.
  10.184 +
  10.185 +        o If set, reuse creiteria from parent topics;
  10.186 +        
  10.187 +        o Also update metadata about the Topic.
  10.188 +        """
  10.189 +        self.acquireCriteria = acquireCriteria
  10.190 +        if title is not None:
  10.191 +            self.title = title
  10.192 +        self.description = description
  10.193 +
  10.194 +        self.reindexObject()
  10.195 +
  10.196 +    security.declareProtected(View, 'buildQuery')
  10.197 +    def buildQuery( self ):
  10.198 +
  10.199 +        """ Construct a catalog query using our criterion objects.
  10.200 +        """
  10.201 +        result = {}
  10.202 +
  10.203 +        if self.acquireCriteria:
  10.204 +
  10.205 +            try:
  10.206 +                # Tracker 290 asks to allow combinations, like this:
  10.207 +                # parent = aq_parent( self )
  10.208 +                parent = aq_parent( aq_inner( self ) )
  10.209 +                result.update( parent.buildQuery() )
  10.210 +
  10.211 +            except: # oh well, can't find parent, or it isn't a Topic.
  10.212 +                pass
  10.213 +
  10.214 +        for criterion in self.listCriteria():
  10.215 +
  10.216 +            for key, value in criterion.getCriteriaItems():
  10.217 +                result[ key ] = value
  10.218 +
  10.219 +        return result
  10.220 +
  10.221 +    security.declareProtected(View, 'queryCatalog')
  10.222 +    def queryCatalog( self, REQUEST=None, **kw ):
  10.223 +
  10.224 +        """ Invoke the catalog using our criteria.
  10.225 +        
  10.226 +        o Built-in criteria update any criteria passed in 'kw'.
  10.227 +        """
  10.228 +        kw.update( self.buildQuery() )
  10.229 +        portal_catalog = getToolByName( self, 'portal_catalog' )
  10.230 +        return portal_catalog.searchResults(REQUEST, **kw)
  10.231 +
  10.232 +    security.declareProtected(View, 'synContentValues')
  10.233 +    def synContentValues( self ):
  10.234 +
  10.235 +        """ Return a limited subset of the brains for our query.
  10.236 +        
  10.237 +        o Return no more brain objects than the limit set by the
  10.238 +          syndication tool.
  10.239 +        """
  10.240 +        syn_tool = getToolByName( self, 'portal_syndication' )
  10.241 +        limit = syn_tool.getMaxItems( self )
  10.242 +        brains = self.queryCatalog( sort_limit=limit )[ :limit ]
  10.243 +        return [ brain.getObject() for brain in brains ] 
  10.244 +
  10.245 +    ### Criteria adding/editing/deleting
  10.246 +    security.declareProtected(ChangeTopics, 'addCriterion')
  10.247 +    def addCriterion( self, field, criterion_type ):
  10.248 +
  10.249 +        """ Add a new search criterion.
  10.250 +        """
  10.251 +        crit = None
  10.252 +        newid = 'crit__%s' % field
  10.253 +
  10.254 +        for ct in self._criteriaTypes:
  10.255 +
  10.256 +            if criterion_type == ct.meta_type:
  10.257 +                crit = ct( newid, field )
  10.258 +
  10.259 +        if crit is None:
  10.260 +            # No criteria type matched passed in value
  10.261 +            raise NameError, 'Unknown Criterion Type: %s' % criterion_type
  10.262 +
  10.263 +        self._setObject( newid, crit )
  10.264 +
  10.265 +    security.declareProtected(ChangeTopics, 'deleteCriterion')
  10.266 +    def deleteCriterion( self, criterion_id ):
  10.267 +
  10.268 +        """ Delete selected criterion.
  10.269 +        """
  10.270 +        if type( criterion_id ) is type( '' ):
  10.271 +            self._delObject( criterion_id )
  10.272 +        elif type( criterion_id ) in ( type( () ), type( [] ) ):
  10.273 +            for cid in criterion_id:
  10.274 +                self._delObject( cid )
  10.275 +
  10.276 +    security.declareProtected(View, 'getCriterion')
  10.277 +    def getCriterion( self, criterion_id ):
  10.278 +
  10.279 +        """ Get the criterion object.
  10.280 +        """
  10.281 +        try:
  10.282 +            return self._getOb( 'crit__%s' % criterion_id )
  10.283 +        except AttributeError:
  10.284 +            return self._getOb( criterion_id )
  10.285 +
  10.286 +    security.declareProtected(AddTopics, 'addSubtopic')
  10.287 +    def addSubtopic( self, id ):
  10.288 +
  10.289 +        """ Add a new subtopic.
  10.290 +        """
  10.291 +        ti = self.getTypeInfo()
  10.292 +        ti.constructInstance(self, id)
  10.293 +        return self._getOb( id )
  10.294 +
  10.295 +    #
  10.296 +    #   Helper methods
  10.297 +    #
  10.298 +    security.declarePrivate( '_criteria_metatype_ids' )
  10.299 +    def _criteria_metatype_ids( self ):
  10.300 +
  10.301 +        result = []
  10.302 +
  10.303 +        for mt in self._criteriaTypes:
  10.304 +            result.append( mt.meta_type )
  10.305 +
  10.306 +        return tuple( result )
  10.307 +
  10.308 +    #
  10.309 +    #   Cataloging helper to make finding this item easier
  10.310 +    #
  10.311 +    security.declareProtected(View, 'SearchableText')
  10.312 +    def SearchableText(self):
  10.313 +        """
  10.314 +        SeachableText is used for full text seraches of a portal.  It
  10.315 +        should return a concatenation of all useful text.
  10.316 +        """
  10.317 +        return "%s %s" % (self.title, self.description) 
  10.318 +
  10.319 +InitializeClass( Topic )
    11.1 new file mode 100644
    11.2 --- /dev/null
    11.3 +++ b/TopicPermissions.py
    11.4 @@ -0,0 +1,24 @@
    11.5 +##############################################################################
    11.6 +#
    11.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    11.8 +#
    11.9 +# This software is subject to the provisions of the Zope Public License,
   11.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   11.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   11.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   11.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   11.14 +# FOR A PARTICULAR PURPOSE.
   11.15 +#
   11.16 +##############################################################################
   11.17 +""" Backward-compatibility module for CMFTopic product permissions.
   11.18 +
   11.19 +$Id: TopicPermissions.py 36457 2004-08-12 15:07:44Z jens $
   11.20 +"""
   11.21 +
   11.22 +from permissions import *
   11.23 +
   11.24 +from warnings import warn
   11.25 +
   11.26 +warn( "The module, 'Products.CMFTopic.TopicPermissions' is a deprecated "
   11.27 +      "compatiblity alias for 'Products.CMFTopic.permissions';  please use "
   11.28 +      "the new module instead.", DeprecationWarning)
    12.1 new file mode 100644
    12.2 --- /dev/null
    12.3 +++ b/__init__.py
    12.4 @@ -0,0 +1,59 @@
    12.5 +##############################################################################
    12.6 +#
    12.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    12.8 +#
    12.9 +# This software is subject to the provisions of the Zope Public License,
   12.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   12.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   12.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   12.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   12.14 +# FOR A PARTICULAR PURPOSE.
   12.15 +#
   12.16 +##############################################################################
   12.17 +""" Topic: Canned catalog queries
   12.18 +
   12.19 +$Id: __init__.py 36457 2004-08-12 15:07:44Z jens $
   12.20 +"""
   12.21 +
   12.22 +import sys
   12.23 +
   12.24 +from ZClasses import createZClassForBase
   12.25 +
   12.26 +from Products.CMFCore.utils import ContentInit
   12.27 +from Products.CMFCore.DirectoryView import registerDirectory
   12.28 +
   12.29 +import Topic
   12.30 +import SimpleStringCriterion
   12.31 +import SimpleIntCriterion
   12.32 +import ListCriterion
   12.33 +import DateCriteria
   12.34 +import SortCriterion
   12.35 +from permissions import AddTopics
   12.36 +
   12.37 +
   12.38 +bases = ( Topic.Topic, )
   12.39 +
   12.40 +this_module = sys.modules[ __name__ ]
   12.41 +
   12.42 +for base in bases:
   12.43 +    createZClassForBase( base, this_module )
   12.44 +
   12.45 +# This is used by a script (external method) that can be run
   12.46 +# to set up Topics in an existing CMF Site instance.
   12.47 +topic_globals = globals()
   12.48 +
   12.49 +# Make the skins available as DirectoryViews
   12.50 +registerDirectory( 'skins', globals() )
   12.51 +
   12.52 +def initialize( context ):
   12.53 +
   12.54 +    context.registerHelpTitle( 'CMF Topic Help' )
   12.55 +    context.registerHelp( directory='help' )
   12.56 +
   12.57 +    # CMF Initializers
   12.58 +    ContentInit( 'CMF Topic Objects'
   12.59 +               , content_types = (Topic.Topic,)
   12.60 +               , permission = AddTopics
   12.61 +               , extra_constructors = (Topic.addTopic,)
   12.62 +               , fti = Topic.factory_type_information
   12.63 +               ).initialize( context )
    13.1 new file mode 100644
    13.2 --- /dev/null
    13.3 +++ b/configure.zcml
    13.4 @@ -0,0 +1,36 @@
    13.5 +<configure
    13.6 +    xmlns:five="http://namespaces.zope.org/five"
    13.7 +    >
    13.8 +
    13.9 +  <five:bridge
   13.10 +      zope2=".interfaces.Criterion.Criterion"
   13.11 +      package=".interfaces"
   13.12 +      name="ICriterion"
   13.13 +      />
   13.14 +
   13.15 +  <five:implements
   13.16 +      class=".DateCriteria.FriendlyDateCriterion"
   13.17 +      interface=".interfaces.ICriterion"
   13.18 +      />
   13.19 +
   13.20 +  <five:implements
   13.21 +      class=".ListCriterion.ListCriterion"
   13.22 +      interface=".interfaces.ICriterion"
   13.23 +      />
   13.24 +
   13.25 +  <five:implements
   13.26 +      class=".SimpleIntCriterion.SimpleIntCriterion"
   13.27 +      interface=".interfaces.ICriterion"
   13.28 +      />
   13.29 +
   13.30 +  <five:implements
   13.31 +      class=".SimpleStringCriterion.SimpleStringCriterion"
   13.32 +      interface=".interfaces.ICriterion"
   13.33 +      />
   13.34 +
   13.35 +  <five:implements
   13.36 +      class=".SortCriterion.SortCriterion"
   13.37 +      interface=".interfaces.ICriterion"
   13.38 +      />
   13.39 +
   13.40 +</configure>
    14.1 new file mode 100644
    14.2 --- /dev/null
    14.3 +++ b/help/Overview.stx
    14.4 @@ -0,0 +1,92 @@
    14.5 +PortalTopic Overview
    14.6 +
    14.7 +  *Note: this help file is old and being updated (ney, replaced) as
    14.8 +  **CMF Topic Overview**.*
    14.9 +
   14.10 +  PortalTopics present collections of portal items according to catalog
   14.11 +  searches formulated by the PortalTopic creator/configurer.
   14.12 +
   14.13 +  Visitors to a portal topic see a brief description of the topic, its
   14.14 +  criteria, available subtopics, and the batch-browsable results.
   14.15 +  (Eventually the visitor will be able to sort and filter the results to
   14.16 +  their liking.  Initially, we will be providing rudimentary batched
   14.17 +  browsing.)  Visitors will also see links to subtopics which refine the
   14.18 +
   14.19 +  PortalTopic configurers (the creator and anyone generally enabled to
   14.20 +  configure a portal topic, according to site policy) can toggle the
   14.21 +  browsing view to adjust the topic query criteria, adding, deleting,
   14.22 +  and modifying textual, numeric, and list criteria against the site's
   14.23 +  standard content metadata fields.
   14.24 +
   14.25 +  PortalTopic configurers will also be able to add new topic objects to
   14.26 +  the topic that will act as subtopics, with their queries refining the
   14.27 +  results of the containing topic query.  Subtopic nesting, and the
   14.28 +  cumulative refinement, is unlimited.
   14.29 +
   14.30 +  Use Cases
   14.31 +
   14.32 +    **Topic Visitor** browses topic on PortalTopic page --
   14.33 +      PortalTopics visitors see a (possibly empty) description of the
   14.34 +      topic, its (possibly) empty collectino of subtopics, and
   14.35 +      batch-browsable links to the topic contents.
   14.36 +
   14.37 +      Visitor visits topic --
   14.38 +        ... sees topic description, subtopics, and first batch of topic
   14.39 +        contents links.
   14.40 +
   14.41 +        Visitor browses topic collection --
   14.42 +          The visitor can follow a result link to the target contents,
   14.43 +          advance bakwards and forward in the results batch (if it's
   14.44 +          more than a single screenful),
   14.45 +
   14.46 +        *(Not in v1.0.) Visitor twiddles filtering and sorting parameters* --
   14.47 +          *to adjust their view of the results.*
   14.48 +
   14.49 +        Visitor navigates to subtopic --
   14.50 +          ... by following subtopic link.
   14.51 +
   14.52 +        Visitor gets help about PortalTopics purpose and navigation --
   14.53 +        ... by hitting help button.  *(Just this design doc, in v1.0.)*
   14.54 +
   14.55 +        * (Not in v1.0.)  Eventually, when returning to a topic, the
   14.56 +        visitor's view resumes with batch, sort, and filter state as
   14.57 +        they last left it.  For now, they return to start.*
   14.58 +
   14.59 +    **Topic configurer** configures topic
   14.60 +      Configurers can toggle the view of the topic to reveal controls
   14.61 +      for adjusting the topic description, subtopics, and topic query
   14.62 +      criteria.
   14.63 +
   14.64 +      Topic configurer adjusts topic criteria --
   14.65 +        Configurer hits a button that opens the configuration form,
   14.66 +        showing a view of the same topic, with:
   14.67 +
   14.68 +        - A text area for the filling in the topic description
   14.69 +
   14.70 +        - An add/delete/rename list of subtopics, for managing
   14.71 +          their containment.
   14.72 +
   14.73 +        - A section for changing the topic criteria.
   14.74 +
   14.75 +          The top of the section is a table with columns for string,
   14.76 +          integer, and list criteria entries.  The bottom is a row of
   14.77 +          buttons: "Submit Changes", "Delete Checked", and "Reset"
   14.78 +
   14.79 +          Table entries for already set criteria will consist of a
   14.80 +          checkbox, the criterion field name, and an input box for the
   14.81 +          value.  The checkbox indicates entries to be deleted.
   14.82 +
   14.83 +          The bottom of each column will have a "blank" entry, for
   14.84 +          adding a new criterion.  It will be like the existing
   14.85 +          entries but it will not have the checkbox, and its initial
   14.86 +          value will be empty.
   14.87 +
   14.88 +        - *(Not in v1.0.) A control for designating whether or not to
   14.89 +          apply the topic query.  (The topic may only be for
   14.90 +          collecting and specifying the common aspects of a query for
   14.91 +          it's subtopics).*
   14.92 +
   14.93 +      The qeury results will display as they would for a regular visitor.
   14.94 +
   14.95 +      Topic configurer gets help about configuring PortalTopics --
   14.96 +        ... by hitting help button.  *(Just this design doc, in v1.0.)*
    15.1 new file mode 100644
    15.2 --- /dev/null
    15.3 +++ b/help/Topics.stx
    15.4 @@ -0,0 +1,18 @@
    15.5 +CMF Topic Overview
    15.6 +
    15.7 + CMF Topics present a way of defining a *canned catalog query*.  They
    15.8 + help organize a site into dynamically executed searches according to
    15.9 + a set of static criteria defined by the person who created or
   15.10 + configured the Topic.
   15.11 +
   15.12 + Visitors to a particular Topic will see its results, and also links
   15.13 + to any subtopics, which may use their parent topics criteria to
   15.14 + further refine a search.  Clicking on any particular result will lead 
   15.15 + to the item.  Of course, being skinnable, this behavior may be
   15.16 + altered by the site designer as needed.
   15.17 +
   15.18 + The configurer (the creator and anyone generally enabled to configure 
   15.19 + CMF Topics according to site policy) configures the Topic by adding
   15.20 + criterion and setting the values.  The current set of criteria
   15.21 + include simple String, Integer, and List, as well as some Date
   15.22 + criterion.  Subtopics may be nested for cumulative refinement.
   15.23 \ No newline at end of file
    16.1 new file mode 100644
    16.2 --- /dev/null
    16.3 +++ b/interfaces/Criterion.py
    16.4 @@ -0,0 +1,83 @@
    16.5 +##############################################################################
    16.6 +#
    16.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    16.8 +#
    16.9 +# This software is subject to the provisions of the Zope Public License,
   16.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   16.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   16.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   16.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   16.14 +# FOR A PARTICULAR PURPOSE.
   16.15 +#
   16.16 +##############################################################################
   16.17 +""" Declare interface for search criterion classes, as used by Topic instances
   16.18 +to build their queries.
   16.19 +
   16.20 +$Id: Criterion.py 37039 2005-06-13 17:32:07Z yuppie $
   16.21 +"""
   16.22 +
   16.23 +from Interface import Interface
   16.24 +
   16.25 +
   16.26 +class Criterion(Interface):
   16.27 +    """\
   16.28 +    A Topic is composed of Criterion objects which specify the query
   16.29 +    used for the Topic.  By supplying some basic information, the
   16.30 +    Criterion objects can be plugged into Topics without the Topic
   16.31 +    having to be too aware of the Criteria types.
   16.32 +    """
   16.33 +
   16.34 +    def Type():
   16.35 +        """\
   16.36 +        Return the type of criterion object this is (ie - 'List Criterion')
   16.37 +        """
   16.38 +
   16.39 +    def Field():
   16.40 +        """\
   16.41 +        Return the field this criterion object searches on.
   16.42 +        """
   16.43 +
   16.44 +    def Description():
   16.45 +        """\
   16.46 +        Return a brief description of the criteria type.
   16.47 +        """
   16.48 +
   16.49 +    def editableAttributes():
   16.50 +        """\
   16.51 +        Returns a tuble of editable attributes.  The values of this
   16.52 +        are used by the topic to build commands to send to the
   16.53 +        'edit' method based on each criterion's setup.
   16.54 +        """
   16.55 +
   16.56 +    def getEditForm():
   16.57 +        """\
   16.58 +        Return the name of a DTML component used to edit criterion.
   16.59 +        Editforms should be specific to their type of criteria.
   16.60 +        """
   16.61 +
   16.62 +    def apply(command):
   16.63 +        """\
   16.64 +        To make it easier to apply values from the rather dynamic
   16.65 +        Criterion edit form using Python Scripts, apply takes a
   16.66 +        mapping object as a default and applies itself to self.edit.
   16.67 +
   16.68 +        It's basically a nice and protected wrapper around
   16.69 +        self.edit(**command).
   16.70 +        """
   16.71 +
   16.72 +# XXX: Interfaces have to specify the signature.
   16.73 +##    def edit(**kw):
   16.74 +##        """\
   16.75 +##        The signature of this method should be specific to the
   16.76 +##        criterion.  Using the values in the attribute
   16.77 +##        '_editableAttributes', the Topic can apply the right
   16.78 +##        commands to each criteria object as its being edited without
   16.79 +##        having to know too much about the structure.
   16.80 +##        """
   16.81 +
   16.82 +    def getCriteriaItems():
   16.83 +        """\
   16.84 +        Return a sequence of key-value tuples, each representing
   16.85 +        a value to be injected into the query dictionary (and,
   16.86 +        therefore, tailored to work with the catalog).
   16.87 +        """
    17.1 new file mode 100644
    17.2 --- /dev/null
    17.3 +++ b/interfaces/__init__.py
    17.4 @@ -0,0 +1,17 @@
    17.5 +##############################################################################
    17.6 +#
    17.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    17.8 +# 
    17.9 +# This software is subject to the provisions of the Zope Public License,
   17.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   17.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   17.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   17.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   17.14 +# FOR A PARTICULAR PURPOSE.
   17.15 +# 
   17.16 +##############################################################################
   17.17 +"""\
   17.18 +Loads interface names into the package.
   17.19 +"""
   17.20 +
   17.21 +from Criterion import Criterion
    18.1 new file mode 100644
    18.2 --- /dev/null
    18.3 +++ b/permissions.py
    18.4 @@ -0,0 +1,38 @@
    18.5 +##############################################################################
    18.6 +#
    18.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    18.8 +#
    18.9 +# This software is subject to the provisions of the Zope Public License,
   18.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   18.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   18.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   18.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   18.14 +# FOR A PARTICULAR PURPOSE.
   18.15 +#
   18.16 +##############################################################################
   18.17 +""" Permissions used throughout CMFTopic.
   18.18 +
   18.19 +$Id: permissions.py 36457 2004-08-12 15:07:44Z jens $
   18.20 +"""
   18.21 +from AccessControl import ModuleSecurityInfo
   18.22 +
   18.23 +security = ModuleSecurityInfo('Products.CMFTopic.permissions')
   18.24 +
   18.25 +from Products.CMFCore.permissions import setDefaultRoles
   18.26 +
   18.27 +security.declarePublic('AddTopics')
   18.28 +AddTopics = 'Add portal topics'
   18.29 +setDefaultRoles(AddTopics, ('Manager',))
   18.30 +
   18.31 +security.declarePublic('ChangeTopics')
   18.32 +ChangeTopics = 'Change portal topics'
   18.33 +setDefaultRoles(ChangeTopics, ('Manager', 'Owner',))
   18.34 +
   18.35 +security.declarePublic('AccessContentsInformation')
   18.36 +from Products.CMFCore.permissions import AccessContentsInformation
   18.37 +
   18.38 +security.declarePublic('ListFolderContents')
   18.39 +from Products.CMFCore.permissions import ListFolderContents
   18.40 +
   18.41 +security.declarePublic('View')
   18.42 +from Products.CMFCore.permissions import View
    19.1 new file mode 100644
    19.2 --- /dev/null
    19.3 +++ b/skins/topic/friendlydatec_editform.dtml
    19.4 @@ -0,0 +1,43 @@
    19.5 +<table border="0" cellpadding="0" cellspacing="2" class="FormLayout">
    19.6 + <tr>
    19.7 +  <td width="20">
    19.8 +   <input type="checkbox" value="&dtml-getId;"
    19.9 +    name="criterion_ids:list" id="cb_&dtml-getId;">
   19.10 +  </td>
   19.11 +  <td align="left">
   19.12 +   <label for="cb_&dtml-getId;">
   19.13 +    <strong title="&dtml-Description;">"Friendly" Date:</strong>
   19.14 +    <tt>&dtml-Field;</tt>
   19.15 +   </label>
   19.16 +   <input type="hidden" name="criteria.id:records" value="&dtml-getId;">
   19.17 +  </td>
   19.18 + </tr>
   19.19 + <tr valign="top">
   19.20 +  <td width="20">&nbsp;</td>
   19.21 +  <td>
   19.22 +   <dtml-let minselected="operation == 'min' and 'selected' or ''"
   19.23 +             maxselected="operation == 'max' and 'selected' or ''">
   19.24 +    <select name="criteria.operation:records">
   19.25 +     <option value="min" &dtml-minselected;>At the least:</option>
   19.26 +     <option value="max" &dtml-maxselected;>At the most:</option>
   19.27 +    </select>
   19.28 +   </dtml-let>
   19.29 +
   19.30 +   <select name="criteria.value:records">
   19.31 +    <dtml-in name="defaultDateOptions">
   19.32 +     <option value="&dtml-sequence-key;"
   19.33 +      <dtml-if expr="value == _['sequence-key']">selected</dtml-if>
   19.34 +     >&dtml-sequence-item;</option>
   19.35 +    </dtml-in>
   19.36 +   </select>
   19.37 +
   19.38 +   <dtml-let oldselected="daterange == 'old' and 'selected' or ''"
   19.39 +             aheadselected="daterange == 'ahead' and 'selected' or ''">
   19.40 +    <select name="criteria.daterange:records">
   19.41 +     <option value="old" &dtml-oldselected;>old</option>
   19.42 +     <option value="ahead" &dtml-aheadselected;>ahead</option>
   19.43 +    </select>
   19.44 +   </dtml-let>
   19.45 +  </td>
   19.46 + </tr>
   19.47 +</table>
    20.1 new file mode 100644
    20.2 --- /dev/null
    20.3 +++ b/skins/topic/listc_edit.dtml
    20.4 @@ -0,0 +1,33 @@
    20.5 +<table border="0" cellpadding="0" cellspacing="2" class="FormLayout">
    20.6 + <tr>
    20.7 +  <td width="20">
    20.8 +   <input type="checkbox" value="&dtml-getId;"
    20.9 +    name="criterion_ids:list" id="cb_&dtml-getId;">
   20.10 +  </td>
   20.11 +  <td align="left">
   20.12 +   <label for="cb_&dtml-getId;">
   20.13 +    <strong title="&dtml-Description;">List:</strong>
   20.14 +    <tt>&dtml-Field;</tt>
   20.15 +   </label>
   20.16 +   <input type="hidden" name="criteria.id:records" value="&dtml-getId;">
   20.17 +  </td>
   20.18 + </tr>
   20.19 + <tr valign="top">
   20.20 +  <td width="20">&nbsp;</td>
   20.21 +  <td>
   20.22 +   <strong>Value:</strong><br>
   20.23 +   <textarea name="criteria.value:lines:records" rows="5" cols="40"
   20.24 +   ><dtml-var expr="_.string.join(value, '\n')"></textarea><br>
   20.25 +   <strong>Operator:</strong><br>
   20.26 +   <dtml-let noneSel="not operator and 'selected' or ''"
   20.27 +             orSel="operator == 'or' and 'selected' or ''"
   20.28 +             andSel="operator == 'and' and 'selected' or ''">
   20.29 +   <select name="criteria.operator:records">
   20.30 +    <option value="" &dtml-noneSel;>-- none --</option>
   20.31 +    <option value="or" &dtml-orSel;>or</option>
   20.32 +    <option value="and" &dtml-andSel;>and</option>
   20.33 +   </select>
   20.34 +   </dtml-let>
   20.35 +  </td>
   20.36 + </tr>
   20.37 +</table>
    21.1 new file mode 100644
    21.2 --- /dev/null
    21.3 +++ b/skins/topic/sic_edit.dtml
    21.4 @@ -0,0 +1,43 @@
    21.5 +<table border="0" cellpadding="0" cellspacing="2" class="FormLayout">
    21.6 + <tr>
    21.7 +  <td width="20"> 
    21.8 +   <input type="checkbox" value="&dtml-getId;" 
    21.9 +    name="criterion_ids:list" id="cb_&dtml-getId;">
   21.10 +  </td>
   21.11 +  <td align="left">
   21.12 +   <label for="cb_&dtml-getId;">
   21.13 +    <strong title="&dtml-Description;">Integer:</strong> 
   21.14 +    <tt>&dtml-Field;</tt>
   21.15 +   </label>
   21.16 +   <input type="hidden" name="criteria.id:records" value="&dtml-getId;">
   21.17 +  </td>
   21.18 + </tr>
   21.19 + <tr>
   21.20 +  <td width="20">&nbsp;</td>
   21.21 +  <td>
   21.22 +   <strong>Value:</strong> 
   21.23 +   <input type="text" name="criteria.value:records"
   21.24 +          value="&dtml-getValueString;" size="40">
   21.25 +  </td>
   21.26 + </tr>
   21.27 + <tr>
   21.28 +  <td width="20">&nbsp;</td>
   21.29 +  <td>
   21.30 +   <strong>Direction:</strong>
   21.31 +   <dtml-let minChecked="direction=='min' and 'checked' or ''"
   21.32 +             maxChecked="direction=='max' and 'checked' or ''"
   21.33 +             minmaxChecked="direction=='min:max' and 'checked' or ''">
   21.34 +   <input type="radio" name="criteria.direction__&dtml-getId;:records"
   21.35 +    value="min" &dtml-minChecked; id="&dtml-getId;_min" /> 
   21.36 +      <label for="&dtml-getId;_min">Minimum</label>
   21.37 +
   21.38 +   <input type="radio" name="criteria.direction__&dtml-getId;:records"
   21.39 +    value="max" &dtml-maxChecked; id="&dtml-getId;_max" />
   21.40 +      <label for="&dtml-getId;_max">Maximum</label>
   21.41 +
   21.42 +   <input type="radio" name="criteria.direction__&dtml-getId;:records" 
   21.43 +    value="min:max" &dtml-minmaxChecked; id="&dtml-getId;_minmax">
   21.44 +      <label for="&dtml-getId;_minmax">Min/Max</label>
   21.45 +   </dtml-let>
   21.46 +  </td>
   21.47 +</table>
    22.1 new file mode 100644
    22.2 --- /dev/null
    22.3 +++ b/skins/topic/sort_edit.dtml
    22.4 @@ -0,0 +1,24 @@
    22.5 +<table border="0" cellpadding="0" cellspacing="2" class="FormLayout">
    22.6 + <tr>
    22.7 +  <td width="20">
    22.8 +   <input type="checkbox" value="&dtml-getId;"
    22.9 +    name="criterion_ids:list" id="cb_&dtml-getId;" />
   22.10 +  </td>
   22.11 +  <td align="left">
   22.12 +   <label for="cb_&dtml-getId;">
   22.13 +    <strong title="&dtml-Description;">Sort:</strong>
   22.14 +    <tt>&dtml-Field;</tt>
   22.15 +   </label>
   22.16 +   <input type="hidden" name="criteria.id:records" value="&dtml-getId;" />
   22.17 +  </td>
   22.18 + </tr>
   22.19 + <tr>
   22.20 +  <td width="20">&nbsp;</td>
   22.21 +  <td><strong>Reversed?</strong>
   22.22 +  <dtml-let chk="reversed and 'checked' or ''">
   22.23 +  <input type="hidden" name="criteria.reversed::int:default:records" value="0">
   22.24 +  <input type="checkbox" name="criteria.reversed:records"
   22.25 +         value="1" &dtml-chk; /></td>
   22.26 +  </dtml-let>
   22.27 + </tr>
   22.28 +</table>
    23.1 new file mode 100644
    23.2 --- /dev/null
    23.3 +++ b/skins/topic/ssc_edit.dtml
    23.4 @@ -0,0 +1,21 @@
    23.5 +<table border="0" cellpadding="0" cellspacing="2" class="FormLayout">
    23.6 + <tr>
    23.7 +  <td width="20">
    23.8 +   <input type="checkbox" value="&dtml-getId;"
    23.9 +    name="criterion_ids:list" id="cb_&dtml-getId;" />
   23.10 +  </td>
   23.11 +  <td align="left">
   23.12 +   <label for="cb_&dtml-getId;">
   23.13 +    <strong title="&dtml-Description;">String:</strong>
   23.14 +    <tt>&dtml-Field;</tt>
   23.15 +   </label>
   23.16 +   <input type="hidden" name="criteria.id:records" value="&dtml-getId;" />
   23.17 +  </td>
   23.18 + </tr>
   23.19 + <tr>
   23.20 +  <td width="20">&nbsp;</td>
   23.21 +  <td><strong>Value:</strong>
   23.22 +  <input type="text" name="criteria.value:records"
   23.23 +   value="&dtml-value;" size="40" /></td>
   23.24 + </tr>
   23.25 +</table>
    24.1 new file mode 100644
    24.2 --- /dev/null
    24.3 +++ b/skins/topic/topic_addCriterion.py
    24.4 @@ -0,0 +1,13 @@
    24.5 +## Script (Python) "topic_addCriterion"
    24.6 +##bind container=container
    24.7 +##bind context=context
    24.8 +##bind namespace=
    24.9 +##bind script=script
   24.10 +##bind subpath=traverse_subpath
   24.11 +##parameters=REQUEST, RESPONSE, field, criterion_type
   24.12 +##title=
   24.13 +##
   24.14 +
   24.15 +context.addCriterion(field=field, criterion_type=criterion_type)
   24.16 +
   24.17 +RESPONSE.redirect('%s/topic_criteria_form' % context.absolute_url())
    25.1 new file mode 100644
    25.2 --- /dev/null
    25.3 +++ b/skins/topic/topic_addSubtopic.py
    25.4 @@ -0,0 +1,20 @@
    25.5 +## Script (Python) "topic_addSubtopic"
    25.6 +##bind container=container
    25.7 +##bind context=context
    25.8 +##bind namespace=
    25.9 +##bind script=script
   25.10 +##bind subpath=traverse_subpath
   25.11 +##parameters=REQUEST, RESPONSE, new_id
   25.12 +##title=
   25.13 +##
   25.14 +
   25.15 +topictype = context.getTypeInfo()
   25.16 +context.addSubtopic(new_id)
   25.17 +
   25.18 +action = topictype.getActionById('subtopics')
   25.19 +url = '%s/%s?portal_status_message=%s' % (
   25.20 +    context.absolute_url(),
   25.21 +    action,
   25.22 +    "Subtopic+'%s'+added" % new_id
   25.23 +    )
   25.24 +RESPONSE.redirect(url)
    26.1 new file mode 100644
    26.2 --- /dev/null
    26.3 +++ b/skins/topic/topic_criteria_form.dtml
    26.4 @@ -0,0 +1,52 @@
    26.5 +<dtml-var standard_html_header>
    26.6 +
    26.7 +<div class="Desktop">
    26.8 +
    26.9 +<div class="Topic">
   26.10 +
   26.11 +<h2> Topic Criteria: &dtml-getId; </h2>
   26.12 +
   26.13 +<form action="&dtml-absolute_url;" method="post">
   26.14 +
   26.15 +<dtml-in listCriteria>
   26.16 + <dtml-var expr="_[getEditForm()]">
   26.17 +</dtml-in>
   26.18 +<input type="submit" name="topic_editCriteria:action"
   26.19 +       value=" Save changes "> &nbsp;&nbsp;
   26.20 +<input type="submit" name="topic_deleteCriteria:action"
   26.21 +       value=" Delete selected ">
   26.22 +<input type="hidden" name=":default_action" value="editCriteria">
   26.23 +</form>
   26.24 +
   26.25 +
   26.26 +<form action="&dtml-absolute_url;/topic_addCriterion" method="post">
   26.27 +<h2> Add New Topic Criteria: </h2>
   26.28 +<table class="FormLayout">
   26.29 + <tr valign="top">
   26.30 +  <th align="right"> Field id: </th>
   26.31 +  <td><select name="field">
   26.32 +    <dtml-in name="listAvailableFields">
   26.33 +    <option value="&dtml-sequence-item;">&dtml-sequence-item;</option>
   26.34 +    </dtml-in>
   26.35 +  </select></td>
   26.36 + </tr>
   26.37 + <tr valign="top">
   26.38 +  <th align="left"> Criterion type: </th>
   26.39 +  <td><select name="criterion_type">
   26.40 +    <dtml-in name="listCriteriaTypes" mapping>
   26.41 +    <option value="&dtml-name;">&dtml-name;</option>
   26.42 +    </dtml-in>
   26.43 +  </select></td>
   26.44 + </tr>
   26.45 + <tr>
   26.46 +  <td>&nbsp;</td>
   26.47 +  <td><input type="submit" value=" Add "></td>
   26.48 + </tr>
   26.49 +</table>
   26.50 +</form>
   26.51 +
   26.52 +</div>
   26.53 +
   26.54 +</div>
   26.55 +
   26.56 +<dtml-var standard_html_footer>
    27.1 new file mode 100644
    27.2 --- /dev/null
    27.3 +++ b/skins/topic/topic_deleteCriteria.py
    27.4 @@ -0,0 +1,17 @@
    27.5 +## Script (Python) "topic_deleteCriteria"
    27.6 +##bind container=container
    27.7 +##bind context=context
    27.8 +##bind namespace=
    27.9 +##bind script=script
   27.10 +##bind subpath=traverse_subpath
   27.11 +##parameters=REQUEST, RESPONSE, criterion_ids
   27.12 +##title=
   27.13 +##
   27.14 +
   27.15 +for cid in criterion_ids:
   27.16 +    context.deleteCriterion(cid)
   27.17 +
   27.18 +message = 'Criteria+deleted.'
   27.19 +RESPONSE.redirect('%s/topic_criteria_form?portal_status_message=%s' % (
   27.20 +    context.absolute_url(), message)
   27.21 +                  )
    28.1 new file mode 100644
    28.2 --- /dev/null
    28.3 +++ b/skins/topic/topic_editCriteria.py
    28.4 @@ -0,0 +1,35 @@
    28.5 +## Script (Python) "topic_editCriteria"
    28.6 +##bind container=container
    28.7 +##bind context=context
    28.8 +##bind namespace=
    28.9 +##bind script=script
   28.10 +##bind subpath=traverse_subpath
   28.11 +##parameters=REQUEST, RESPONSE, criteria
   28.12 +##title=
   28.13 +##
   28.14 +"""\
   28.15 +Save changes to the list of criteria.  This is done by going over
   28.16 +the submitted criteria records and comparing them against the
   28.17 +criteria object's editable attributes.  A 'command' object is
   28.18 +built to send to the Criterion objects 'apply' method, which in turn
   28.19 +applies the command to the Criterion objects 'edit' method.
   28.20 +"""
   28.21 +
   28.22 +for rec in criteria:
   28.23 +    crit = context.getCriterion(rec.id)
   28.24 +    command = {}
   28.25 +    for attr in crit.editableAttributes():
   28.26 +        tmp = getattr(rec, attr, None)
   28.27 +        # Due to having multiple radio buttons on the same page
   28.28 +        # with the same name but belonging to different records,
   28.29 +        # they needed to be associated with different records with ids
   28.30 +        if tmp is None:
   28.31 +            tmp = getattr(rec, '%s__%s' % (attr, rec.id), None)
   28.32 +        command[attr] = tmp
   28.33 +    crit.apply(command)
   28.34 +
   28.35 +message='Changes+saved.'
   28.36 +RESPONSE.redirect('%s/topic_criteria_form?portal_status_message=%s' % (
   28.37 +    context.absolute_url(), message)
   28.38 +                  )
   28.39 +
    29.1 new file mode 100644
    29.2 --- /dev/null
    29.3 +++ b/skins/topic/topic_editTopic.py
    29.4 @@ -0,0 +1,15 @@
    29.5 +## Script (Python) "topic_editTopic"
    29.6 +##bind container=container
    29.7 +##bind context=context
    29.8 +##bind namespace=
    29.9 +##bind script=script
   29.10 +##bind subpath=traverse_subpath
   29.11 +##parameters=REQUEST, RESPONSE, acquireCriteria, title=None, description=None
   29.12 +##title=
   29.13 +##
   29.14 +
   29.15 +context.edit(acquireCriteria=acquireCriteria,
   29.16 +             title=title,
   29.17 +             description=description)
   29.18 +
   29.19 +RESPONSE.redirect('%s/topic_view' % context.absolute_url())
    30.1 new file mode 100644
    30.2 --- /dev/null
    30.3 +++ b/skins/topic/topic_edit_form.dtml
    30.4 @@ -0,0 +1,49 @@
    30.5 +<dtml-var standard_html_header>
    30.6 +
    30.7 +<div class="Desktop">
    30.8 +
    30.9 +<div class="Topic">
   30.10 +
   30.11 +<h2> Edit Topic: <dtml-var id> </h2>
   30.12 +
   30.13 +<form action="&dtml-absolute_url;" method="post">
   30.14 +<table class="FormLayout">
   30.15 +
   30.16 + <tr valign="top">
   30.17 +  <th align="right"> Title: </th>
   30.18 +  <td><input type="text" name="title" value="&dtml-title;" size="30" /></td>
   30.19 + </tr>
   30.20 + <tr valign="top">
   30.21 +  <th align="right"> Description: </th>
   30.22 +  <td><textarea name="description:test" rows="5" cols="65"
   30.23 +       >&dtml-Description;</textarea></td>
   30.24 + </tr>
   30.25 + <tr valign="top">
   30.26 +  <th align="right"> Acquire Criteria<br>from Parent: </th>
   30.27 +  <td>
   30.28 +   <dtml-let acqChecked="acquireCriteria and 'checked' or ''">
   30.29 +   <input type="checkbox"
   30.30 +          name="acquireCriteria"
   30.31 +          value="1" &dtml-acqChecked;>
   30.32 +   </dtml-let>
   30.33 +   <input type="hidden"
   30.34 +          name="acquireCriteria:default"
   30.35 +          value="">
   30.36 +  </td>
   30.37 + </tr>
   30.38 +
   30.39 + <tr valign="top">
   30.40 +  <td> <br> </td>
   30.41 +  <td>
   30.42 +   <input type="submit" name="topic_editTopic:action" value="Change">
   30.43 +  </td>
   30.44 + </tr>
   30.45 +
   30.46 +</table>
   30.47 +</form>
   30.48 +
   30.49 +</div>
   30.50 +
   30.51 +</div>
   30.52 +
   30.53 +<dtml-var standard_html_footer>
    31.1 new file mode 100644
    31.2 index 0000000000000000000000000000000000000000..20938928380bb9eb976b1fd5ccaaa9da03100e56
    31.3 GIT binary patch
    31.4 literal 77
    31.5 zc${<hbhEHb6krfwSjfQe|3Ab2{Xh}~6o0ZXaxpM5=r8~QNS=X7Zc6{k(`*8_D)qN%
    31.6 b88lz^dhR*vMD1dUMfZ12S$*1wmBAVSC1o7f
    31.7 
    32.1 new file mode 100644
    32.2 --- /dev/null
    32.3 +++ b/skins/topic/topic_subtopics_form.dtml
    32.4 @@ -0,0 +1,56 @@
    32.5 +<dtml-var standard_html_header>
    32.6 +
    32.7 +<div class="Desktop">
    32.8 +
    32.9 +<div class="Topic">
   32.10 +
   32.11 +<h2> Topic Subtopics: <dtml-var getId> </h2>
   32.12 +
   32.13 +<form action="&dtml-absolute_url;" method="post">
   32.14 +<table class="FormLayout">
   32.15 +
   32.16 +<dtml-in listSubtopics>
   32.17 +
   32.18 + <tr valign="top">
   32.19 +  <td> <input type="checkbox" name="ids:list" value="&dtml-getId;"> </td>
   32.20 +  <td> 
   32.21 +   <a href="&dtml-getId;">&dtml-getId;</a>
   32.22 +   <dtml-with buildQuery>
   32.23 +    <dtml-in items>
   32.24 +     <dtml-if sequence-start>(</dtml-if>
   32.25 +     <dtml-var sequence-key> : <dtml-var sequence-item>
   32.26 +     <dtml-unless sequence-end>,</dtml-unless>
   32.27 +     <dtml-if sequence-end>)</dtml-if>
   32.28 +    </dtml-in>
   32.29 +   </dtml-with>
   32.30 +  </td>
   32.31 + </tr>
   32.32 +
   32.33 +</dtml-in>
   32.34 +
   32.35 + <tr valign="top">
   32.36 +  <td> <br> </td>
   32.37 +  <td>
   32.38 +   <input type="submit" name="folder_rename_form:action" value="Rename">
   32.39 +   <input type="submit" name="folder_cut:action" value="Cut">
   32.40 +   <input type="submit" name="folder_copy:action" value="Copy">
   32.41 +   <input type="submit" name="folder_paste:action" value="Paste">
   32.42 +   <input type="submit" name="folder_delete:action" value="Delete">
   32.43 +  </td>
   32.44 + </tr>
   32.45 +
   32.46 +</table>
   32.47 +</form>
   32.48 +
   32.49 +<form action="topic_addSubtopic" method="get">
   32.50 +<h2> Add subtopic: </h2>
   32.51 +<p><strong>Id: </strong>
   32.52 +<input type="text" name="new_id" size="30" /><br />
   32.53 +<input type="submit" value=" Add " /></p>
   32.54 +</form>
   32.55 +
   32.56 +</div>
   32.57 +
   32.58 +</div>
   32.59 +
   32.60 +<dtml-var standard_html_footer>
    33.1 new file mode 100644
    33.2 --- /dev/null
    33.3 +++ b/skins/topic/topic_view.dtml
    33.4 @@ -0,0 +1,67 @@
    33.5 +<dtml-var standard_html_header>
    33.6 +
    33.7 +<div class="Desktop">
    33.8 +
    33.9 +<div class="Topic">
   33.10 +
   33.11 +<h2> Topic: <dtml-var title> </h2>
   33.12 +
   33.13 +<dtml-in expr="contentValues(['Portal Topic'])">
   33.14 + <dtml-if sequence-start>
   33.15 +  <h4>Subtopics: </h4>
   33.16 +  <div>
   33.17 + </dtml-if>
   33.18 +  <a href="&dtml-absolute_url;/topic_view"
   33.19 +   ><dtml-if Title>&dtml-Title;<dtml-else>&dtml-getId;</dtml-if></a>
   33.20 +  <dtml-unless sequence-end>, </dtml-unless>
   33.21 + <dtml-if sequence-end>
   33.22 + </div>
   33.23 + </dtml-if>
   33.24 +</dtml-in>
   33.25 +
   33.26 +<dtml-let results=queryCatalog>
   33.27 +<h4>Topic matches: </h4>
   33.28 +<dtml-in results size="20" start=qs>
   33.29 +<dtml-let objURL="getURL() + '/view'">
   33.30 +<dtml-if next-sequence>
   33.31 + <dtml-call "REQUEST.set('next-sequence',
   33.32 +                         _['next-sequence-start-number'])">
   33.33 +</dtml-if>
   33.34 +<dtml-if previous-sequence>
   33.35 + <dtml-call "REQUEST.set('previous-sequence',
   33.36 +                         _['previous-sequence-start-number'])">
   33.37 +</dtml-if>
   33.38 +<dtml-if sequence-start>
   33.39 + <ul>
   33.40 +</dtml-if>
   33.41 +  <li> <a href="&dtml-objURL;"><dtml-var Title></a> </li>
   33.42 +<dtml-if sequence-end>
   33.43 + </ul>
   33.44 +</dtml-if>
   33.45 +</dtml-let>
   33.46 +</dtml-in>
   33.47 +
   33.48 +<dtml-if previous-sequence>
   33.49 + <a href="&dtml-URL;?qs=&dtml-previous-sequence;">Previous items</a>
   33.50 +</dtml-if>
   33.51 +<dtml-if next-sequence>
   33.52 + <a href="&dtml-URL;?qs=&dtml-next-sequence;">Next items</a>
   33.53 +</dtml-if>
   33.54 +
   33.55 +</dtml-let>
   33.56 +
   33.57 +<h3> Query Parameters </h3>
   33.58 +
   33.59 +<ul>
   33.60 +<dtml-with buildQuery>
   33.61 +<dtml-in items>
   33.62 + <li> <dtml-var sequence-key> : <dtml-var sequence-item> </li>
   33.63 +</dtml-in>
   33.64 +</dtml-with>
   33.65 +</ul>
   33.66 +
   33.67 +</div>
   33.68 +
   33.69 +</div>
   33.70 +
   33.71 +<dtml-var standard_html_footer>
    34.1 new file mode 100644
    34.2 --- /dev/null
    34.3 +++ b/skins/zpt_topic/friendlydatec_editform.pt
    34.4 @@ -0,0 +1,54 @@
    34.5 +<table border="0" cellpadding="0" cellspacing="2" class="FormLayout">
    34.6 + <tr>
    34.7 +  <td width="20">
    34.8 +   <input type="checkbox" value="" name="criterion_ids:list" id=""
    34.9 +        tal:attributes="value here/getId; id string:cb_${here/getId}">
   34.10 +  </td>
   34.11 +  <td align="left">
   34.12 +   <label for="" tal:attributes="for string:cb_${here/getId}">
   34.13 +    <strong title="" tal:attributes="title here/Description"
   34.14 +    >"Friendly" Date:</strong>
   34.15 +    <tt tal:content="here/Field">Field</tt>
   34.16 +   </label>
   34.17 +   <input type="hidden" name="criteria.id:records" value=""
   34.18 +          tal:attributes="value here/getId" />
   34.19 +  </td>
   34.20 + </tr>
   34.21 + <tr valign="top">
   34.22 +  <td width="20">&nbsp;</td>
   34.23 +  <td>
   34.24 +    <select name="criteria.operation:records">
   34.25 +     <option value="min"
   34.26 +             tal:attributes="selected python:here.operation=='min'"
   34.27 +     >More than</option>
   34.28 +     <option value="max"
   34.29 +             tal:attributes="selected python:here.operation=='max'"
   34.30 +     >Less than</option>
   34.31 +     <option value="within_day"
   34.32 +             tal:attributes="selected python:here.operation=='within_day'"
   34.33 +     >On the day</option>
   34.34 +    </select>
   34.35 +
   34.36 +   <select name="criteria.value:records"
   34.37 +           tal:define="dateoptions here/defaultDateOptions">
   34.38 +     <option value=""
   34.39 +        tal:repeat="doption dateoptions"
   34.40 +        tal:attributes="value python:doption[0];
   34.41 +                        selected python:here.value == doption[0];
   34.42 +                       "
   34.43 +        tal:content="python:doption[1]"
   34.44 +     >Date Option</option>
   34.45 +   </select>
   34.46 +
   34.47 +    <select name="criteria.daterange:records">
   34.48 +     <option value="old"
   34.49 +             tal:attributes="selected python:here.daterange == 'old'"
   34.50 +     >ago</option>
   34.51 +     <option value="ahead"
   34.52 +             tal:attributes="selected python:here.daterange == 'ahead'"
   34.53 +     >ahead</option>
   34.54 +    </select>
   34.55 +  </td>
   34.56 + </tr>
   34.57 +</table>
   34.58 +
    35.1 new file mode 100644
    35.2 --- /dev/null
    35.3 +++ b/skins/zpt_topic/listc_edit.pt
    35.4 @@ -0,0 +1,35 @@
    35.5 +<table border="0" cellpadding="0" cellspacing="2" class="FormLayout">
    35.6 + <tr>
    35.7 +  <td width="20">
    35.8 +   <input type="checkbox" value="" name="criterion_ids:list" id=""
    35.9 +          tal:attributes="value here/getId; id string:cb_${here/getId}">
   35.10 +  </td>
   35.11 +  <td align="left">
   35.12 +   <label for="" tal:attributes="for string:cb_${here/getId}">
   35.13 +    <strong title="" tal:attributes="title here/Description">List:</strong>
   35.14 +    <tt tal:content="here/Field">Field</tt>
   35.15 +   </label>
   35.16 +   <input type="hidden" name="criteria.id:records" value=""
   35.17 +          tal:attributes="value here/getId">
   35.18 +  </td>
   35.19 + </tr>
   35.20 + <tr valign="top">
   35.21 +  <td width="20">&nbsp;</td>
   35.22 +  <td valign="top">
   35.23 +   <strong>Value:</strong><br>
   35.24 +   <textarea name="criteria.value:lines:records" rows="5" cols="40"
   35.25 +             tal:content="python:modules['string'].join(here.value, '\n')"
   35.26 +   ></textarea><br>
   35.27 +   <strong>Operator:</strong><br>
   35.28 +   <select name="criteria.operator:records">
   35.29 +    <option value=""
   35.30 +            tal:attributes="selected not: here/operator">-- none --</option>
   35.31 +    <option value="or"
   35.32 +            tal:attributes="selected python:here.operator == 'or'">or</option>
   35.33 +    <option value="and" 
   35.34 +            tal:attributes="selected python:here.operator == 'and'">and</option>
   35.35 +   </select>
   35.36 +  </td>
   35.37 + </tr>
   35.38 +</table>
   35.39 +
    36.1 new file mode 100644
    36.2 --- /dev/null
    36.3 +++ b/skins/zpt_topic/sic_edit.pt
    36.4 @@ -0,0 +1,58 @@
    36.5 +<table border="0" cellpadding="0" cellspacing="2" class="FormLayout">
    36.6 + <tr>
    36.7 +  <td width="20">
    36.8 +   <input type="checkbox" value="" name="criterion_ids:list" id=""
    36.9 +          tal:attributes="value here/getId;
   36.10 +                          id string:cb_${here/getId};
   36.11 +                         " />
   36.12 +  </td>
   36.13 +  <td align="left">
   36.14 +   <label for=""
   36.15 +            tal:attributes="for string:cb_${here/getId}">
   36.16 +            <strong title=""
   36.17 +                    tal:attributes="title here/Description">Integer:</strong>
   36.18 +            <tt tal:content="here/Field">Field</tt>
   36.19 +   </label>
   36.20 +   <input type="hidden" name="criteria.id:records" value=""
   36.21 +          tal:attributes="value here/getId" />
   36.22 +  </td>
   36.23 + </tr>
   36.24 + <tr>
   36.25 +  <td width="20">&nbsp;</td>
   36.26 +  <td>
   36.27 +   <strong>Value:</strong>
   36.28 +   <input type="text" name="criteria.value:records" value="" size="40"
   36.29 +            tal:attributes="value here/getValueString" />
   36.30 +  </td>
   36.31 + </tr>
   36.32 + <tr>
   36.33 +  <td width="20">&nbsp;</td>
   36.34 +  <td>
   36.35 +   <strong>Direction:</strong>
   36.36 +   <input type="radio" name="" value="min" id=""
   36.37 +          tal:attributes="checked python:here.direction=='min';
   36.38 +                          id string:${here/getId}_min;
   36.39 +                          name string:criteria.direction__${here/getId}:records;
   36.40 +                         " />
   36.41 +   <label for=""
   36.42 +          tal:attributes="for string:${here/getId}_min">Minimum</label>
   36.43 +
   36.44 +   <input type="radio" name="" value="max" id=""
   36.45 +          tal:attributes="checked python:here.direction=='max';
   36.46 +                          id string:${here/getId}_max;
   36.47 +                          name string:criteria.direction__${here/getId}:records;
   36.48 +                         " />
   36.49 +   <label for=""
   36.50 +          tal:attributes="for string:${here/getId}_max">Maximum</label>
   36.51 +
   36.52 +   <input type="radio" name="" value="min:max" id=""
   36.53 +          tal:attributes="checked python:here.direction=='min:max';
   36.54 +                          id string:${here/getId}_minmax;
   36.55 +                          name string:criteria.direction__${here/getId}:records;
   36.56 +                         " />
   36.57 +   <label for=""
   36.58 +          tal:attributes="for string:${here/getId}_minmax">Min/Max</label>
   36.59 +  </td>
   36.60 + </tr>
   36.61 +</table>
   36.62 +
    37.1 new file mode 100644
    37.2 --- /dev/null
    37.3 +++ b/skins/zpt_topic/sort_edit.pt
    37.4 @@ -0,0 +1,24 @@
    37.5 +<table border="0" cellpadding="0" cellspacing="2" class="FormLayout">
    37.6 + <tr>
    37.7 +  <td width="20">
    37.8 +   <input type="checkbox" value="" name="criterion_ids:list" id=""
    37.9 +          tal:attributes="value here/getId; id string:cb_${here/getId}">
   37.10 +  </td>
   37.11 +  <td align="left">
   37.12 +   <label for=""
   37.13 +          tal:attributes="for string:cb_${here/getId}">
   37.14 +    <strong title="" tal:attributes="title here/Description;">Sort:</strong>
   37.15 +     <tt tal:content="here/Field">Field</tt>
   37.16 +   </label>
   37.17 +   <input type="hidden" name="criteria.id:records" value="" tal:attributes="value here/getId" />
   37.18 +  </td>
   37.19 + </tr>
   37.20 + <tr>
   37.21 +  <td width="20">&nbsp;</td>
   37.22 +  <td><strong>Reversed?</strong>
   37.23 +  <input type="hidden" name="criteria.reversed::int:default:records" value="0">
   37.24 +  <input type="checkbox" name="criteria.reversed:records" value="1"
   37.25 +         tal:attributes="checked here/reversed"/>
   37.26 +  </td>
   37.27 + </tr>
   37.28 +</table>
    38.1 new file mode 100644
    38.2 --- /dev/null
    38.3 +++ b/skins/zpt_topic/ssc_edit.pt
    38.4 @@ -0,0 +1,33 @@
    38.5 +<table border="0" cellpadding="0" cellspacing="2" class="FormLayout">
    38.6 +<tr>
    38.7 +    <td width="20">
    38.8 +        <input type="checkbox" value="" name="criterion_ids:list" id=""
    38.9 +            tal:attributes="value here/getId; id string:cb_${here/getId}"/>
   38.10 +    </td>
   38.11 +    <td align="left">
   38.12 +    <label for=""
   38.13 +        tal:attributes="for string:cb_${here/getId}">
   38.14 +        <strong title=""
   38.15 +            tal:attributes="title here/Description">
   38.16 +                String:
   38.17 +        </strong>
   38.18 +        <tt
   38.19 +            tal:content="here/Field">
   38.20 +                Field
   38.21 +        </tt>
   38.22 +    </label>
   38.23 +    <input type="hidden" name="criteria.id:records" value=""
   38.24 +        tal:attributes="value here/getId"/>
   38.25 +    </td>
   38.26 +</tr>
   38.27 +<tr>
   38.28 +    <td width="20">&nbsp;</td>
   38.29 +    <td>
   38.30 +        <strong>Value:</strong>
   38.31 +        <input type="text" name="criteria.value:records"
   38.32 +            value="" size="40"
   38.33 +            tal:attributes="value here/value | nothing" />
   38.34 +    </td>
   38.35 +</tr>
   38.36 +</table>
   38.37 +
    39.1 new file mode 100644
    39.2 --- /dev/null
    39.3 +++ b/skins/zpt_topic/topic_addCriterion.py
    39.4 @@ -0,0 +1,13 @@
    39.5 +## Script (Python) "topic_addCriterion"
    39.6 +##bind container=container
    39.7 +##bind context=context
    39.8 +##bind namespace=
    39.9 +##bind script=script
   39.10 +##bind subpath=traverse_subpath
   39.11 +##parameters=REQUEST, RESPONSE, field, criterion_type
   39.12 +##title=
   39.13 +##
   39.14 +
   39.15 +context.addCriterion(field=field, criterion_type=criterion_type)
   39.16 +
   39.17 +RESPONSE.redirect('%s/topic_criteria_form' % context.absolute_url())
    40.1 new file mode 100644
    40.2 --- /dev/null
    40.3 +++ b/skins/zpt_topic/topic_criteria_form.pt
    40.4 @@ -0,0 +1,60 @@
    40.5 +<html xmlns:tal="http://xml.zope.org/namespaces/tal"
    40.6 +    xmlns:metal="http://xml.zope.org/namespaces/metal"
    40.7 +    metal:use-macro="container/main_template/macros/master">
    40.8 +<body>
    40.9 +<div metal:fill-slot="main">
   40.10 +
   40.11 +<div class="Desktop">
   40.12 +
   40.13 +<div class="Topic">
   40.14 +
   40.15 +<h2> Topic Criteria: <span tal:replace="here/getId" /> </h2>
   40.16 +
   40.17 +<form action="" method="post" tal:attributes="action here/absolute_url">
   40.18 +<span tal:define="criteria here/listCriteria">
   40.19 +    <span tal:repeat="criterion criteria">
   40.20 +        <span tal:define="editform string:criterion/${criterion/getEditForm}"
   40.21 +            tal:replace="structure python:path(editform)" />
   40.22 +    </span>
   40.23 +</span>
   40.24 +<input type="submit" name="topic_editCriteria:action"
   40.25 +       value=" Save changes "> &nbsp;&nbsp;
   40.26 +<input type="submit" name="topic_deleteCriteria:action"
   40.27 +       value=" Delete selected ">
   40.28 +<input type="hidden" name=":default_action" value="editCriteria">
   40.29 +</form>
   40.30 +
   40.31 +
   40.32 +<form action="" method="post" tal:attributes="action string:${here/absolute_url}/topic_addCriterion">
   40.33 +<h2> Add New Topic Criteria: </h2>
   40.34 +<table class="FormLayout">
   40.35 +<tr valign="top">
   40.36 +    <th align="right"> Field id: </th>
   40.37 +    <td>
   40.38 +        <select name="field" tal:define="fields here/listAvailableFields">
   40.39 +            <option value="" tal:repeat="field fields" tal:attributes="value field" tal:content="field">Field</option>
   40.40 +        </select>
   40.41 +    </td>
   40.42 +</tr>
   40.43 +<tr valign="top">
   40.44 +    <th align="left"> Criteria type: </th>
   40.45 +    <td>
   40.46 +        <select name="criterion_type" tal:define="types here/listCriteriaTypes">
   40.47 +            <option value="" tal:repeat="type types" tal:attributes="value type/name" tal:content="type/name">Type</option>
   40.48 +        </select>
   40.49 +    </td>
   40.50 +</tr>
   40.51 +<tr>
   40.52 +    <td>&nbsp;</td>
   40.53 +    <td><input type="submit" value=" Add "></td>
   40.54 +</tr>
   40.55 +</table>
   40.56 +</form>
   40.57 +
   40.58 +</div>
   40.59 +
   40.60 +</div>
   40.61 +
   40.62 +</div>
   40.63 +</body>
   40.64 +</html>
    41.1 new file mode 100644
    41.2 --- /dev/null
    41.3 +++ b/skins/zpt_topic/topic_deleteCriteria.py
    41.4 @@ -0,0 +1,17 @@
    41.5 +## Script (Python) "topic_deleteCriteria"
    41.6 +##bind container=container
    41.7 +##bind context=context
    41.8 +##bind namespace=
    41.9 +##bind script=script
   41.10 +##bind subpath=traverse_subpath
   41.11 +##parameters=REQUEST, RESPONSE, criterion_ids
   41.12 +##title=
   41.13 +##
   41.14 +
   41.15 +for cid in criterion_ids:
   41.16 +    context.deleteCriterion(cid)
   41.17 +
   41.18 +message = 'Criteria+deleted.'
   41.19 +RESPONSE.redirect('%s/topic_criteria_form?portal_status_message=%s' % (
   41.20 +    context.absolute_url(), message)
   41.21 +                  )
    42.1 new file mode 100644
    42.2 --- /dev/null
    42.3 +++ b/skins/zpt_topic/topic_editCriteria.py
    42.4 @@ -0,0 +1,35 @@
    42.5 +## Script (Python) "topic_editCriteria"
    42.6 +##bind container=container
    42.7 +##bind context=context
    42.8 +##bind namespace=
    42.9 +##bind script=script
   42.10 +##bind subpath=traverse_subpath
   42.11 +##parameters=REQUEST, RESPONSE, criteria
   42.12 +##title=
   42.13 +##
   42.14 +"""\
   42.15 +Save changes to the list of criteria.  This is done by going over
   42.16 +the submitted criteria records and comparing them against the
   42.17 +criteria object's editable attributes.  A 'command' object is
   42.18 +built to send to the Criterion objects 'apply' method, which in turn
   42.19 +applies the command to the Criterion objects 'edit' method.
   42.20 +"""
   42.21 +
   42.22 +for rec in criteria:
   42.23 +    crit = context.getCriterion(rec.id)
   42.24 +    command = {}
   42.25 +    for attr in crit.editableAttributes():
   42.26 +        tmp = getattr(rec, attr, None)
   42.27 +        # Due to having multiple radio buttons on the same page
   42.28 +        # with the same name but belonging to different records,
   42.29 +        # they needed to be associated with different records with ids
   42.30 +        if tmp is None:
   42.31 +            tmp = getattr(rec, '%s__%s' % (attr, rec.id), None)
   42.32 +        command[attr] = tmp
   42.33 +    crit.apply(command)
   42.34 +
   42.35 +message='Changes+saved.'
   42.36 +RESPONSE.redirect('%s/topic_criteria_form?portal_status_message=%s' % (
   42.37 +    context.absolute_url(), message)
   42.38 +                  )
   42.39 +
    43.1 new file mode 100644
    43.2 --- /dev/null
    43.3 +++ b/skins/zpt_topic/topic_editTopic.py
    43.4 @@ -0,0 +1,15 @@
    43.5 +## Script (Python) "topic_editTopic"
    43.6 +##bind container=container
    43.7 +##bind context=context
    43.8 +##bind namespace=
    43.9 +##bind script=script
   43.10 +##bind subpath=traverse_subpath
   43.11 +##parameters=REQUEST, RESPONSE, acquireCriteria, title=None, description=None
   43.12 +##title=
   43.13 +##
   43.14 +
   43.15 +context.edit(acquireCriteria=acquireCriteria,
   43.16 +             title=title,
   43.17 +             description=description)
   43.18 +
   43.19 +RESPONSE.redirect('%s/topic_view' % context.absolute_url())
    44.1 new file mode 100644
    44.2 --- /dev/null
    44.3 +++ b/skins/zpt_topic/topic_edit_form.pt
    44.4 @@ -0,0 +1,53 @@
    44.5 +<html xmlns:tal="http://xml.zope.org/namespaces/tal"
    44.6 +    xmlns:metal="http://xml.zope.org/namespaces/metal"
    44.7 +    metal:use-macro="container/main_template/macros/master">
    44.8 +<body>
    44.9 +<div metal:fill-slot="main">
   44.10 +
   44.11 +<div class="Desktop">
   44.12 +
   44.13 +<div class="Topic">
   44.14 +
   44.15 +<h2> Edit Topic: <span tal:replace="here/id" /> </h2>
   44.16 +
   44.17 +<form action="" method="post" tal:attributes="action here/absolute_url">
   44.18 +<table class="FormLayout">
   44.19 +
   44.20 + <tr valign="top">
   44.21 +  <th align="right"> Title: </th>
   44.22 +  <td><input type="text" name="title" value="Title" size="30" tal:attributes="value here/title" /></td>
   44.23 + </tr>
   44.24 + <tr valign="top">
   44.25 +  <th align="right"> Description: </th>
   44.26 +  <td><textarea name="description:text" rows="5" cols="65"
   44.27 +        tal:content="here/description"
   44.28 +       >Description</textarea></td>
   44.29 + </tr>
   44.30 + <tr valign="top">
   44.31 +  <th align="right"> Acquire Criteria<br>from Parent: </th>
   44.32 +  <td>
   44.33 +   <input type="checkbox"
   44.34 +          name="acquireCriteria"
   44.35 +          value="1" tal:attributes="checked python:here.acquireCriteria and 'checked' or ''">
   44.36 +   <input type="hidden"
   44.37 +          name="acquireCriteria:default"
   44.38 +          value="">
   44.39 +  </td>
   44.40 + </tr>
   44.41 +
   44.42 + <tr valign="top">
   44.43 +  <td> <br> </td>
   44.44 +  <td>
   44.45 +   <input type="submit" name="topic_editTopic:action" value="Change">
   44.46 +  </td>
   44.47 + </tr>
   44.48 +
   44.49 +</table>
   44.50 +</form>
   44.51 +
   44.52 +</div>
   44.53 +
   44.54 +</div>
   44.55 +</div>
   44.56 +</body>
   44.57 +</html>
    45.1 new file mode 100644
    45.2 index 0000000000000000000000000000000000000000..20938928380bb9eb976b1fd5ccaaa9da03100e56
    45.3 GIT binary patch
    45.4 literal 77
    45.5 zc${<hbhEHb6krfwSjfQe|3Ab2{Xh}~6o0ZXaxpM5=r8~QNS=X7Zc6{k(`*8_D)qN%
    45.6 b88lz^dhR*vMD1dUMfZ12S$*1wmBAVSC1o7f
    45.7 
    46.1 new file mode 100644
    46.2 --- /dev/null
    46.3 +++ b/skins/zpt_topic/topic_subtopics_form.pt
    46.4 @@ -0,0 +1,59 @@
    46.5 +<!--this template is deprecated; please use folder_contents-->
    46.6 +
    46.7 +<html xmlns:tal="http://xml.zope.org/namespaces/tal"
    46.8 +    xmlns:metal="http://xml.zope.org/namespaces/metal"
    46.9 +    metal:use-macro="container/main_template/macros/master">
   46.10 +<body>
   46.11 +<div metal:fill-slot="main">
   46.12 +
   46.13 +<div class="Desktop">
   46.14 +
   46.15 +<div class="Topic">
   46.16 +
   46.17 +<h2> Topic Subtopics: <span tal:replace="here/getId">Id</span> </h2>
   46.18 +
   46.19 +<form action="" method="post" tal:attributes="action here/absolute_url">
   46.20 +<table class="FormLayout" tal:define="subtopics here/listSubtopics">
   46.21 +
   46.22 +<tr valign="top" tal:repeat="topic subtopics">
   46.23 +    <td>
   46.24 +        <input type="checkbox" name="ids:list" value="" tal:attributes="value topic/getId">
   46.25 +    </td>
   46.26 +    <td tal:define="queries topic/buildQuery; items python:queries.items()">
   46.27 +        <a href="" tal:attributes="href topic/getId" tal:content="topic/getId">Id</a>
   46.28 +        (<span tal:repeat="item items" tal:condition="items">
   46.29 +            <span tal:define="n repeat/item/number; key python:item[0]; value python:item[1]">
   46.30 +            <span tal:replace="string:${key} : ${value}"/>
   46.31 +            <span tal:replace="string:," tal:condition="python:n < len(items)" />
   46.32 +            </span>
   46.33 +        </span>)
   46.34 +    </td>
   46.35 +</tr>
   46.36 +
   46.37 +<tr valign="top">
   46.38 +    <td> <br> </td>
   46.39 +    <td>
   46.40 +        <input type="submit" name="folder_rename_form:action" value="Rename">
   46.41 +        <input type="submit" name="folder_cut:action" value="Cut">
   46.42 +        <input type="submit" name="folder_copy:action" value="Copy">
   46.43 +        <input type="submit" name="folder_paste:action" value="Paste">
   46.44 +        <input type="submit" name="folder_delete:action" value="Delete">
   46.45 +    </td>
   46.46 +</tr>
   46.47 +
   46.48 +</table>
   46.49 +</form>
   46.50 +
   46.51 +<form action="topic_addSubtopic" method="get">
   46.52 +<h2> Add subtopic: </h2>
   46.53 +<p><strong>Id: </strong>
   46.54 +<input type="text" name="new_id" size="30" /><br />
   46.55 +<input type="submit" value=" Add " /></p>
   46.56 +</form>
   46.57 +
   46.58 +</div>
   46.59 +
   46.60 +</div>
   46.61 +</div>
   46.62 +</body>
   46.63 +</html>
    47.1 new file mode 100644
    47.2 --- /dev/null
    47.3 +++ b/skins/zpt_topic/topic_view.pt
    47.4 @@ -0,0 +1,76 @@
    47.5 +<html xmlns:tal="http://xml.zope.org/namespaces/tal"
    47.6 +      xmlns:metal="http://xml.zope.org/namespaces/metal"
    47.7 +      metal:use-macro="here/main_template/macros/master">
    47.8 +<head>
    47.9 + <metal:block fill-slot="base"
   47.10 + ><tal:span tal:replace="structure here/getBaseTag"
   47.11 +/></metal:block>
   47.12 +</head>
   47.13 +<body>
   47.14 +
   47.15 +<div metal:fill-slot="main">
   47.16 +
   47.17 +<div class="Desktop">
   47.18 +
   47.19 +<div class="Topic">
   47.20 +
   47.21 +<h2><span tal:replace="here/title">title</span> </h2>
   47.22 +
   47.23 +<span tal:define="topics python:here.objectValues( [ 'Portal Topic' ] )"
   47.24 +      tal:condition="topics">
   47.25 +    <h4>Subtopics: </h4>
   47.26 +    <div tal:repeat="topic topics">
   47.27 +        <a href=""
   47.28 +            tal:define="topictitle python:topic.Title() or topic.getId()"
   47.29 +            tal:attributes="href topic/absolute_url"
   47.30 +            tal:content="topictitle">
   47.31 +                Topic Title
   47.32 +            </a>
   47.33 +    </div>
   47.34 +</span>
   47.35 +
   47.36 +<span tal:define="
   47.37 +            b_start string:0;b_start request/b_start | b_start;
   47.38 +            results here/queryCatalog;
   47.39 +            Batch python:modules['ZTUtils'].Batch;
   47.40 +            global batch python:Batch(results, 20, int(b_start), orphan=1)">
   47.41 +    <h4>Topic Matches: </h4>
   47.42 +    <div tal:repeat="match batch" tal:condition="batch">
   47.43 +        <a href=""
   47.44 +            tal:attributes="href string:${match/getURL}/view"
   47.45 +        ><tal:span tal:content="match/getId">ID</tal:span>
   47.46 +        <tal:case tal:condition="match/Title"
   47.47 +           tal:content="string:(${match/Title})">(Title)</tal:case
   47.48 +      ></a>
   47.49 +    </div>
   47.50 +
   47.51 +
   47.52 +    <span tal:define="p batch/previous" tal:condition="p">
   47.53 +    <a href=""
   47.54 +        tal:attributes="href string:?b_start=${p/first}">Previous <span
   47.55 +        tal:replace="p/length">n</span> items</a>
   47.56 +    </span>&nbsp;&nbsp;
   47.57 +    <span tal:define="n batch/next" tal:condition="n">
   47.58 +    <a href=""
   47.59 +        tal:attributes="href string:?b_start=${batch/end}">Next <span
   47.60 +        tal:replace="n/length">n</span> items</a>
   47.61 +    </span>
   47.62 +
   47.63 +    <h3> Query Parameters </h3>
   47.64 +
   47.65 +    <ul tal:define="queries here/buildQuery; items python:queries.items()"
   47.66 +        tal:condition="queries">
   47.67 +    <span tal:repeat="item items">
   47.68 +    <li tal:define="key python:item[0]; value python:item[1]"
   47.69 +        tal:content="string:${key} : ${value}">item</li>
   47.70 +    </span>
   47.71 +    </ul>
   47.72 +</span>
   47.73 +
   47.74 +</div>
   47.75 +
   47.76 +</div>
   47.77 +</div>
   47.78 +
   47.79 +</body>
   47.80 +</html>
    48.1 new file mode 100644
    48.2 --- /dev/null
    48.3 +++ b/tests/__init__.py
    48.4 @@ -0,0 +1,3 @@
    48.5 +"""
    48.6 +This package contains the unit tests for Topic and its criteria objects.
    48.7 +"""
    49.1 new file mode 100644
    49.2 --- /dev/null
    49.3 +++ b/tests/common.py
    49.4 @@ -0,0 +1,40 @@
    49.5 +##############################################################################
    49.6 +#
    49.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    49.8 +#
    49.9 +# This software is subject to the provisions of the Zope Public License,
   49.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   49.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   49.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   49.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   49.14 +# FOR A PARTICULAR PURPOSE.
   49.15 +#
   49.16 +##############################################################################
   49.17 +""" CMFTopic product:  unit test utilities.
   49.18 +
   49.19 +$Id: common.py 38418 2005-09-09 08:40:13Z yuppie $
   49.20 +"""
   49.21 +
   49.22 +from unittest import TestCase
   49.23 +
   49.24 +
   49.25 +class CriterionTestCase(TestCase):
   49.26 +
   49.27 +    def _makeOne(self, id, *args, **kw):
   49.28 +        return self._getTargetClass()(id, *args, **kw)
   49.29 +
   49.30 +    def test_z2interfaces(self):
   49.31 +        from Interface.Verify import verifyClass
   49.32 +        from Products.CMFTopic.interfaces import Criterion as ICriterion
   49.33 +
   49.34 +        verifyClass( ICriterion, self._getTargetClass() )
   49.35 +
   49.36 +    def test_z3interfaces(self):
   49.37 +        try:
   49.38 +            from zope.interface.verify import verifyClass
   49.39 +            from Products.CMFTopic.interfaces import ICriterion
   49.40 +        except ImportError:
   49.41 +            # BBB: for Zope 2.7
   49.42 +            return
   49.43 +
   49.44 +        verifyClass( ICriterion, self._getTargetClass() )
    50.1 new file mode 100644
    50.2 --- /dev/null
    50.3 +++ b/tests/test_DateC.py
    50.4 @@ -0,0 +1,382 @@
    50.5 +##############################################################################
    50.6 +#
    50.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    50.8 +#
    50.9 +# This software is subject to the provisions of the Zope Public License,
   50.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   50.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   50.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   50.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   50.14 +# FOR A PARTICULAR PURPOSE.
   50.15 +#
   50.16 +##############################################################################
   50.17 +""" Unit tests for DateCriteria module.
   50.18 +
   50.19 +$Id: test_DateC.py 37996 2005-08-18 22:05:03Z jens $
   50.20 +"""
   50.21 +
   50.22 +from unittest import TestSuite, makeSuite, main
   50.23 +import Testing
   50.24 +try:
   50.25 +    import Zope2
   50.26 +except ImportError: # BBB: for Zope 2.7
   50.27 +    import Zope as Zope2
   50.28 +Zope2.startup()
   50.29 +
   50.30 +from DateTime.DateTime import DateTime
   50.31 +
   50.32 +from Products.CMFCore.tests.base.testcase import RequestTest
   50.33 +from Products.CMFCore.tests.base.dummy import DummyContent
   50.34 +from Products.CMFTopic.Topic import Topic
   50.35 +from common import CriterionTestCase
   50.36 +
   50.37 +
   50.38 +class FriendlyDateCriterionTests(CriterionTestCase):
   50.39 +
   50.40 +    lessThanFiveDaysOld = { 'value': 5
   50.41 +                          , 'operation': 'max'
   50.42 +                          , 'daterange': 'old'
   50.43 +                          }
   50.44 +
   50.45 +    lessThanOneMonthAhead = { 'value': 31
   50.46 +                            , 'operation': 'max'
   50.47 +                            , 'daterange': 'ahead'
   50.48 +                            }
   50.49 +    today = { 'value': 0
   50.50 +            , 'operation': 'within_day'
   50.51 +            , 'daterange': 'ahead'
   50.52 +            }
   50.53 +
   50.54 +    def _getTargetClass(self):
   50.55 +        from Products.CMFTopic.DateCriteria import FriendlyDateCriterion
   50.56 +
   50.57 +        return FriendlyDateCriterion
   50.58 +
   50.59 +    def test_Empty( self ):
   50.60 +        friendly = self._makeOne('foo', 'foofield')
   50.61 +
   50.62 +        self.assertEqual( friendly.getId(), 'foo' )
   50.63 +        self.assertEqual( friendly.field, 'foofield' )
   50.64 +        self.assertEqual( friendly.value, None )
   50.65 +        self.assertEqual( friendly.operation, 'min' )
   50.66 +        self.assertEqual( friendly.daterange, 'old' )
   50.67 +        self.assertEqual( len( friendly.getCriteriaItems() ), 0 )
   50.68 +
   50.69 +    def test_ListOfDefaultDates( self ):
   50.70 +        friendly = self._makeOne('foo', 'foofield')
   50.71 +
   50.72 +        d = friendly.defaultDateOptions()
   50.73 +        self.assertEqual( d[0][0], 0 )
   50.74 +        self.assertEqual( d[1][0], 1 )
   50.75 +        self.assertEqual( d[2][0], 2 )
   50.76 +
   50.77 +    def test_Clear( self ):
   50.78 +        friendly = self._makeOne('foo', 'foofield')
   50.79 +
   50.80 +        friendly.edit( value=None )
   50.81 +        self.assertEqual( friendly.value, None )
   50.82 +        self.assertEqual( friendly.operation, 'min' )
   50.83 +        self.assertEqual( friendly.daterange, 'old' )
   50.84 +
   50.85 +    def test_Basic( self ):
   50.86 +        friendly = self._makeOne('foo', 'foofield')
   50.87 +
   50.88 +        friendly.apply( self.lessThanFiveDaysOld )
   50.89 +        self.assertEqual( friendly.value, 5 )
   50.90 +        self.assertEqual( friendly.operation, 'max' )
   50.91 +        self.assertEqual( friendly.daterange, 'old' )
   50.92 +
   50.93 +    def test_BadInput( self ):
   50.94 +        friendly = self._makeOne('foo', 'foofield')
   50.95 +
   50.96 +        # Bogus value
   50.97 +        self.assertRaises( ValueError, friendly.edit, 'blah' )
   50.98 +
   50.99 +        # Bogus operation
  50.100 +        self.assertRaises( ValueError, friendly.edit, 4, 'min:max', 'old' )
  50.101 +
  50.102 +        # Bogus daterange
  50.103 +        self.assertRaises( ValueError, friendly.edit, 4, 'max', 'new' )
  50.104 +
  50.105 +    def test_StringAsValue( self ):
  50.106 +        friendly = self._makeOne('foo', 'foofield')
  50.107 +
  50.108 +        friendly.edit( '4' )
  50.109 +        self.assertEqual( friendly.value, 4 )
  50.110 +
  50.111 +        friendly.edit( '-4' )
  50.112 +        self.assertEqual( friendly.value, -4 )
  50.113 +
  50.114 +        friendly.edit( '' )
  50.115 +        self.assertEqual( friendly.value, None )
  50.116 +
  50.117 +    def test_Today( self ):
  50.118 +        friendly = self._makeOne('foo', 'foofield')
  50.119 +
  50.120 +        friendly.apply( self.today )
  50.121 +        self.assertEqual( friendly.daterange, 'ahead' )
  50.122 +
  50.123 +        now = DateTime()
  50.124 +
  50.125 +        result = friendly.getCriteriaItems()
  50.126 +        self.assertEqual( len(result), 1 )
  50.127 +        self.assertEqual( result[0][0], 'foofield' )
  50.128 +        self.assertEqual( result[0][1]['query'],
  50.129 +                          ( now.earliestTime(), now.latestTime() ) )
  50.130 +        self.assertEqual( result[0][1]['range'], 'min:max' )
  50.131 +
  50.132 +    def test_FiveDaysOld( self ):
  50.133 +        # This should create a query 
  50.134 +        friendly = self._makeOne('foo', 'foofield')
  50.135 +
  50.136 +        friendly.apply( self.lessThanFiveDaysOld )
  50.137 +        self.assertEqual( friendly.daterange, 'old' )
  50.138 +
  50.139 +        result = friendly.getCriteriaItems()
  50.140 +        self.assertEqual( len(result), 1 )
  50.141 +        self.assertEqual( result[0][0], 'foofield' )
  50.142 +        expect_earliest, expect_now = result[0][1]['query']
  50.143 +        self.assertEqual( expect_earliest.Date(),
  50.144 +                          ( DateTime() - 5 ).Date() )
  50.145 +        self.assertEqual( result[0][1]['range'], 'min:max' )
  50.146 +
  50.147 +    def test_OneMonthAhead( self ):
  50.148 +        friendly = self._makeOne('foo', 'foofield')
  50.149 +
  50.150 +        friendly.apply( self.lessThanOneMonthAhead )
  50.151 +        self.assertEqual( friendly.daterange, 'ahead' )
  50.152 +
  50.153 +        result = friendly.getCriteriaItems()
  50.154 +        expect_now, expect_latest = result[0][1]['query']
  50.155 +        self.assertEqual( expect_latest.Date(), ( DateTime() + 31 ).Date() )
  50.156 +        self.assertEqual( expect_now.Date(), DateTime().Date() )
  50.157 +        self.assertEqual( result[0][1]['range'], 'min:max' )
  50.158 +
  50.159 +class FriendlyDateCriterionFunctionalTests(RequestTest):
  50.160 +    # Test the date criterion using a "real CMF" with catalog etc.
  50.161 +    selectable_diffs = [0, 1, 2, 5, 7, 14, 31, 93, 186, 365, 730]
  50.162 +    nonzero_diffs = [1, 2, 5, 7, 14, 31, 93, 186, 365, 730]
  50.163 +    day_diffs = [-730, -365, -186, -93, -31, -14, -7, -5, -2, -1]
  50.164 +    day_diffs.extend(selectable_diffs)
  50.165 +
  50.166 +    def setUp(self):
  50.167 +        RequestTest.setUp(self)
  50.168 +        self.root.manage_addProduct[ 'CMFDefault' ].manage_addCMFSite( 'site' )
  50.169 +        self.site = self.root.site
  50.170 +        self.site._setObject( 'topic', Topic('topic') )
  50.171 +        self.topic = self.site.topic
  50.172 +        self.topic.addCriterion('modified', 'Friendly Date Criterion')
  50.173 +        self.topic.addCriterion('portal_type', 'String Criterion')
  50.174 +        type_crit = self.topic.getCriterion('portal_type')
  50.175 +        type_crit.edit(value='Dummy Content')
  50.176 +        self.criterion = self.topic.getCriterion('modified')
  50.177 +        self.now = DateTime()
  50.178 +
  50.179 +        for i in self.day_diffs:
  50.180 +            dummy_id = 'dummy%i' % i
  50.181 +            self.site._setObject( dummy_id, DummyContent( id=dummy_id
  50.182 +                                                        , catalog=1
  50.183 +                                                        ) )
  50.184 +            dummy_ob = getattr(self.site, dummy_id)
  50.185 +            dummy_ob.modified_date = self.now + i
  50.186 +            dummy_ob.reindexObject()
  50.187 +
  50.188 +
  50.189 +    def test_Harness(self):
  50.190 +        # Make sure the test harness is set up OK
  50.191 +        ob_values = self.site.objectValues(['Dummy'])
  50.192 +        self.assertEqual(len(ob_values), len(self.day_diffs))
  50.193 +
  50.194 +        catalog_results = self.site.portal_catalog(portal_type='Dummy Content')
  50.195 +        self.assertEqual(len(catalog_results), len(self.day_diffs))
  50.196 +
  50.197 +    def test_WithinDayAgo(self):
  50.198 +        # What items were modified "On the day X days ago"
  50.199 +        for diff in self.selectable_diffs:
  50.200 +            self.criterion.edit( value=abs(diff)
  50.201 +                               , operation='within_day'
  50.202 +                               , daterange='old'
  50.203 +                               )
  50.204 +            results = self.topic.queryCatalog()
  50.205 +
  50.206 +            # There is only one item with an modified date for this day
  50.207 +            self.assertEquals(len(results), 1)
  50.208 +            self.assertEquals( results[0].modified.Date()
  50.209 +                             , (self.now-diff).Date()
  50.210 +                             )
  50.211 +
  50.212 +    def test_WithinDayAhead(self):
  50.213 +        # What items were modified "On the day X days ahead"
  50.214 +        for diff in self.selectable_diffs:
  50.215 +            self.criterion.edit( value=abs(diff)
  50.216 +                               , operation='within_day'
  50.217 +                               , daterange='ahead'
  50.218 +                               )
  50.219 +            results = self.topic.queryCatalog()
  50.220 +
  50.221 +            # There is only one item with an modified date for this day
  50.222 +            self.assertEquals(len(results), 1)
  50.223 +            self.assertEquals( results[0].modified.Date()
  50.224 +                             , (self.now+diff).Date()
  50.225 +                             )
  50.226 +
  50.227 +    def test_MoreThanDaysAgo(self):
  50.228 +        # What items are modified "More than X days ago"
  50.229 +        resultset_size = len(self.nonzero_diffs)
  50.230 +
  50.231 +        for diff in self.nonzero_diffs:
  50.232 +            self.criterion.edit( value=diff
  50.233 +                               , operation='min'
  50.234 +                               , daterange='old'
  50.235 +                               )
  50.236 +            results = self.topic.queryCatalog()
  50.237 +            
  50.238 +            # As we move up in our date difference range, we must find as 
  50.239 +            # many items as we have "modified" values <= the current value 
  50.240 +            # in our sequence of user-selectable time differences. As we 
  50.241 +            # increase the "value", we actually move backwards in time, so 
  50.242 +            # the expected count of results *decreases*
  50.243 +            self.assertEquals(len(results), resultset_size)
  50.244 +            for brain in results:
  50.245 +                self.failUnless(brain.modified <= self.now-diff)
  50.246 +
  50.247 +            resultset_size -= 1
  50.248 +
  50.249 +    def test_MoreThanZeroDaysAgo(self):
  50.250 +        # What items are modified "More than 0 days ago"?
  50.251 +        # This represents a special case. The "special munging"
  50.252 +        # that corrects the query terms to what a human would expect
  50.253 +        # are not applied and the search is a simple 
  50.254 +        # "everything in the future" search.
  50.255 +        resultset_size = len(self.selectable_diffs)
  50.256 +        self.criterion.edit( value=0
  50.257 +                           , operation='min'
  50.258 +                           , daterange='old'
  50.259 +                           )
  50.260 +        results = self.topic.queryCatalog()
  50.261 +        self.assertEquals(len(results), resultset_size)
  50.262 +        for brain in results:
  50.263 +            self.failUnless(brain.modified >= self.now)
  50.264 + 
  50.265 +
  50.266 +    def test_MoreThanDaysAhead(self):
  50.267 +        # What items are modified "More than X days ahead"
  50.268 +        resultset_size = len(self.nonzero_diffs)
  50.269 +
  50.270 +        for diff in self.nonzero_diffs:
  50.271 +            self.criterion.edit( value=diff
  50.272 +                               , operation='min'
  50.273 +                               , daterange='ahead'
  50.274 +                               )
  50.275 +            results = self.topic.queryCatalog()
  50.276 +            
  50.277 +            # As we move up in our date difference range, we must find as 
  50.278 +            # many items as we have "modified" values >= the current value 
  50.279 +            # in our sequence of user-selectable time differences. As we 
  50.280 +            # increase the "value", we actually move formward in time, so 
  50.281 +            # the expected count of results *decreases*
  50.282 +            self.assertEquals(len(results), resultset_size)
  50.283 +            for brain in results:
  50.284 +                self.failUnless(brain.modified >= self.now+diff)
  50.285 +
  50.286 +            resultset_size -= 1
  50.287 +
  50.288 +    def test_MoreThanZeroDaysAhead(self):
  50.289 +        # What items are modified "More than 0 days ahead"?
  50.290 +        # This represents a special case. The "special munging"
  50.291 +        # that corrects the query terms to what a human would expect
  50.292 +        # are not applied and the search is a simple 
  50.293 +        # "everything in the future" search.
  50.294 +        resultset_size = len(self.selectable_diffs)
  50.295 +        self.criterion.edit( value=0
  50.296 +                           , operation='min'
  50.297 +                           , daterange='ahead'
  50.298 +                           )
  50.299 +        results = self.topic.queryCatalog()
  50.300 +        self.assertEquals(len(results), resultset_size)
  50.301 +        for brain in results:
  50.302 +            self.failUnless(brain.modified >= self.now)
  50.303 +
  50.304 +    def test_LessThanDaysAgo(self):
  50.305 +        # What items are modified "Less than X days ago"
  50.306 +        resultset_size = 2
  50.307 +
  50.308 +        for diff in self.nonzero_diffs:
  50.309 +            self.criterion.edit( value=diff
  50.310 +                               , operation='max'
  50.311 +                               , daterange='old'
  50.312 +                               )
  50.313 +            results = self.topic.queryCatalog()
  50.314 +            
  50.315 +            # With this query we are looking for items modified "less than
  50.316 +            # X days ago", meaning between the given time and now. As we move
  50.317 +            # through the selectable day values we increase the range to
  50.318 +            # search through and thus increase the resultset size.
  50.319 +            self.assertEquals(len(results), resultset_size)
  50.320 +            for brain in results:
  50.321 +                self.failUnless(self.now-diff <= brain.modified <= self.now)
  50.322 +
  50.323 +            resultset_size += 1
  50.324 +
  50.325 +    def test_LessThanZeroDaysAgo(self):
  50.326 +        # What items are modified "Less than 0 days ago"?
  50.327 +        # This represents a special case. The "special munging"
  50.328 +        # that corrects the query terms to what a human would expect
  50.329 +        # are not applied and the search is a simple 
  50.330 +        # "everything in the past" search.
  50.331 +        resultset_size = len(self.selectable_diffs)
  50.332 +        self.criterion.edit( value=0
  50.333 +                           , operation='max'
  50.334 +                           , daterange='old'
  50.335 +                           )
  50.336 +        results = self.topic.queryCatalog()
  50.337 +        self.assertEquals(len(results), resultset_size)
  50.338 +        for brain in results:
  50.339 +            self.failUnless(brain.modified <= self.now)
  50.340 +            
  50.341 +    def test_LessThanDaysAhead(self):
  50.342 +        # What items are modified "Less than X days ahead"
  50.343 +        resultset_size = 2
  50.344 +
  50.345 +        for diff in self.nonzero_diffs:
  50.346 +            self.criterion.edit( value=diff
  50.347 +                               , operation='max'
  50.348 +                               , daterange='ahead'
  50.349 +                               )
  50.350 +            results = self.topic.queryCatalog()
  50.351 +            
  50.352 +            # With this query we are looking for items modified "less than
  50.353 +            # X days ahead", meaning between now and the given time. As we move
  50.354 +            # through the selectable day values we increase the range to
  50.355 +            # search through and thus increase the resultset size.
  50.356 +            self.assertEquals(len(results), resultset_size)
  50.357 +            for brain in results:
  50.358 +                self.failUnless(self.now+diff >= brain.modified >= self.now)
  50.359 +
  50.360 +            resultset_size += 1
  50.361 +
  50.362 +    def test_LessThanZeroDaysAhead(self):
  50.363 +        # What items are modified "Less than 0 days ahead"?
  50.364 +        # This represents a special case. The "special munging"
  50.365 +        # that corrects the query terms to what a human would expect
  50.366 +        # are not applied and the search is a simple 
  50.367 +        # "everything in the past" search.
  50.368 +        resultset_size = len(self.selectable_diffs)
  50.369 +        self.criterion.edit( value=0
  50.370 +                           , operation='max'
  50.371 +                           , daterange='ahead'
  50.372 +                           )
  50.373 +        results = self.topic.queryCatalog()
  50.374 +        self.assertEquals(len(results), resultset_size)
  50.375 +        for brain in results:
  50.376 +            self.failUnless(brain.modified <= self.now)
  50.377 +
  50.378 +
  50.379 +def test_suite():
  50.380 +    return TestSuite((
  50.381 +        makeSuite(FriendlyDateCriterionTests),
  50.382 +        makeSuite(FriendlyDateCriterionFunctionalTests),
  50.383 +        ))
  50.384 +
  50.385 +if __name__ == '__main__':
  50.386 +    main(defaultTest='test_suite')
    51.1 new file mode 100644
    51.2 --- /dev/null
    51.3 +++ b/tests/test_ListC.py
    51.4 @@ -0,0 +1,94 @@
    51.5 +##############################################################################
    51.6 +#
    51.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    51.8 +#
    51.9 +# This software is subject to the provisions of the Zope Public License,
   51.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   51.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   51.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   51.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   51.14 +# FOR A PARTICULAR PURPOSE.
   51.15 +#
   51.16 +##############################################################################
   51.17 +""" Unit tests for ListCriterion module.
   51.18 +
   51.19 +$Id: test_ListC.py 37135 2005-07-08 13:24:33Z tseaver $
   51.20 +"""
   51.21 +
   51.22 +from unittest import TestSuite, makeSuite, main
   51.23 +import Testing
   51.24 +try:
   51.25 +    import Zope2
   51.26 +except ImportError: # BBB: for Zope 2.7
   51.27 +    import Zope as Zope2
   51.28 +Zope2.startup()
   51.29 +
   51.30 +from common import CriterionTestCase
   51.31 +
   51.32 +
   51.33 +class ListCriterionTests(CriterionTestCase):
   51.34 +
   51.35 +    def _getTargetClass(self):
   51.36 +        from Products.CMFTopic.ListCriterion import ListCriterion
   51.37 +
   51.38 +        return ListCriterion
   51.39 +
   51.40 +    def test_Empty( self ):
   51.41 +        listc = self._makeOne('foo', 'foofield')
   51.42 +
   51.43 +        self.assertEqual( listc.getId(), 'foo' )
   51.44 +        self.assertEqual( listc.field, 'foofield' )
   51.45 +        self.assertEqual( listc.value, ('',) )
   51.46 +        self.assertEqual( len(listc.getCriteriaItems()), 0 )
   51.47 +
   51.48 +    def test_Edit_withString( self ):
   51.49 +        listc = self._makeOne('foo', 'foofield')
   51.50 +
   51.51 +        listc.edit('bar\nbaz')
   51.52 +        self.assertEqual( listc.getId(), 'foo' )
   51.53 +        self.assertEqual( listc.field, 'foofield' )
   51.54 +        self.assertEqual( listc.value, ( 'bar', 'baz' ) )
   51.55 +
   51.56 +        items = listc.getCriteriaItems()
   51.57 +        self.assertEqual( len( items ), 1 )
   51.58 +        self.assertEqual( len( items[0] ), 2 )
   51.59 +        self.assertEqual( items[0][0], 'foofield' )
   51.60 +        self.assertEqual( items[0][1], ( 'bar', 'baz' ) )
   51.61 +
   51.62 +    def test_Edit_withList( self ):
   51.63 +        listc = self._makeOne('foo', 'foofield')
   51.64 +
   51.65 +        abc = [ 'a', 'b', 'c' ]
   51.66 +        listc.edit( abc )
   51.67 +
   51.68 +        items = listc.getCriteriaItems()
   51.69 +        self.failUnless( 'foofield' in map( lambda x: x[0], items ) )
   51.70 +        self.failUnless( tuple( abc ) in map( lambda x: x[1], items ) )
   51.71 +
   51.72 +    def test_operator( self ):
   51.73 +        listc = self._makeOne('foo', 'foofield')
   51.74 +
   51.75 +        abc = [ 'a', 'b', 'c' ]
   51.76 +
   51.77 +        listc.edit( abc )
   51.78 +        items = listc.getCriteriaItems()
   51.79 +        self.assertEqual( len( items ), 1 )
   51.80 +
   51.81 +        listc.edit( abc, 'or' )
   51.82 +        items = listc.getCriteriaItems()
   51.83 +        self.assertEqual( len( items ), 2 )
   51.84 +        self.failUnless( ( 'foofield_operator', 'or' ) in items )
   51.85 +
   51.86 +        listc.edit( abc, 'and' )
   51.87 +        items = listc.getCriteriaItems()
   51.88 +        self.assertEqual( len( items ), 2 )
   51.89 +        self.failUnless( ( 'foofield_operator', 'and' ) in items )
   51.90 +
   51.91 +
   51.92 +def test_suite():
   51.93 +    return TestSuite((
   51.94 +        makeSuite(ListCriterionTests),
   51.95 +        ))
   51.96 +
   51.97 +if __name__ == '__main__':
   51.98 +    main(defaultTest='test_suite')
    52.1 new file mode 100644
    52.2 --- /dev/null
    52.3 +++ b/tests/test_SIC.py
    52.4 @@ -0,0 +1,158 @@
    52.5 +##############################################################################
    52.6 +#
    52.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    52.8 +#
    52.9 +# This software is subject to the provisions of the Zope Public License,
   52.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   52.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   52.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   52.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   52.14 +# FOR A PARTICULAR PURPOSE.
   52.15 +#
   52.16 +##############################################################################
   52.17 +""" Unit tests for SimpleIntCriterion module.
   52.18 +
   52.19 +$Id: test_SIC.py 37135 2005-07-08 13:24:33Z tseaver $
   52.20 +"""
   52.21 +
   52.22 +from unittest import TestSuite, makeSuite, main
   52.23 +import Testing
   52.24 +try:
   52.25 +    import Zope2
   52.26 +except ImportError: # BBB: for Zope 2.7
   52.27 +    import Zope as Zope2
   52.28 +Zope2.startup()
   52.29 +
   52.30 +from common import CriterionTestCase
   52.31 +
   52.32 +
   52.33 +class SimpleIntCriterionTests(CriterionTestCase):
   52.34 +
   52.35 +    def _getTargetClass(self):
   52.36 +        from Products.CMFTopic.SimpleIntCriterion import SimpleIntCriterion
   52.37 +
   52.38 +        return SimpleIntCriterion
   52.39 +
   52.40 +    def test_Empty( self ):
   52.41 +        sic = self._makeOne('foo', 'foofield')
   52.42 +        self.assertEqual( sic.getId(), 'foo' )
   52.43 +        self.assertEqual( sic.field, 'foofield' )
   52.44 +        self.assertEqual( sic.value, None )
   52.45 +        self.assertEqual( sic.getValueString(), '' )
   52.46 +        self.assertEqual( len(sic.getCriteriaItems() ), 0 )
   52.47 +
   52.48 +    def test_EditWithString( self ):
   52.49 +        sic = self._makeOne('foo', 'foofield')
   52.50 +        sic.edit('0')
   52.51 +        self.assertEqual( sic.value, 0 )
   52.52 +        self.assertEqual( sic.getValueString(), '0' )
   52.53 +
   52.54 +        items = sic.getCriteriaItems()
   52.55 +        self.assertEqual( len( items ), 1 )
   52.56 +        self.assertEqual( len( items[0] ), 2 )
   52.57 +        self.assertEqual( items[0][0], 'foofield' )
   52.58 +        self.assertEqual( items[0][1], 0 )
   52.59 +
   52.60 +    def test_EditWithInt( self ):
   52.61 +        sic = self._makeOne('foo', 'foofield')
   52.62 +        sic.edit( 32 )
   52.63 +        self.assertEqual( sic.value, 32 )
   52.64 +        self.assertEqual( sic.getValueString(), '32' )
   52.65 +
   52.66 +        items = sic.getCriteriaItems()
   52.67 +        self.assertEqual( len(items), 1 )
   52.68 +        self.assertEqual( len(items[0]), 2 )
   52.69 +        self.assertEqual( items[0][1], 32 )
   52.70 +
   52.71 +    def test_RangeMin( self ):
   52.72 +        sic = self._makeOne('foo', 'foofield')
   52.73 +        sic.edit( '32', self._getTargetClass().MINIMUM )
   52.74 +
   52.75 +        self.assertEqual( sic.value, 32 )
   52.76 +        self.assertEqual( sic.getValueString(), '32' )
   52.77 +
   52.78 +        items = sic.getCriteriaItems()
   52.79 +        self.assertEqual( len(items), 1 )
   52.80 +        self.assertEqual( len( items[0] ), 2 )
   52.81 +        self.assertEqual( items[0][0], 'foofield' )
   52.82 +        self.assertEqual( items[0][1]['query'], 32 )
   52.83 +        self.assertEqual( items[0][1]['range'], 'min' )
   52.84 +
   52.85 +    def test_RangeMin_withInt( self ):
   52.86 +        sic = self._makeOne('foo', 'foofield')
   52.87 +        sic.edit( 32, self._getTargetClass().MINIMUM )
   52.88 +
   52.89 +        self.assertEqual( sic.value, 32 )
   52.90 +        self.assertEqual( sic.getValueString(), '32' )
   52.91 +
   52.92 +        items = sic.getCriteriaItems()
   52.93 +        self.assertEqual( len(items), 1 )
   52.94 +        self.assertEqual( len( items[0] ), 2 )
   52.95 +        self.assertEqual( items[0][0], 'foofield' )
   52.96 +        self.assertEqual( items[0][1]['query'], 32 )
   52.97 +        self.assertEqual( items[0][1]['range'], 'min' )
   52.98 +
   52.99 +    def test_RangeMax( self ):
  52.100 +        sic = self._makeOne('foo', 'foofield')
  52.101 +        sic.edit( '32', self._getTargetClass().MAXIMUM )
  52.102 +
  52.103 +        self.assertEqual( sic.value, 32 )
  52.104 +        self.assertEqual( sic.getValueString(), '32' )
  52.105 +
  52.106 +        items = sic.getCriteriaItems()
  52.107 +        self.assertEqual( len(items), 1 )
  52.108 +        self.assertEqual( len( items[0] ), 2 )
  52.109 +        self.assertEqual( items[0][0], 'foofield' )
  52.110 +        self.assertEqual( items[0][1]['query'], 32 )
  52.111 +        self.assertEqual( items[0][1]['range'], 'max' )
  52.112 +
  52.113 +    def test_RangeMax_withInt( self ):
  52.114 +        sic = self._makeOne('foo', 'foofield')
  52.115 +        sic.edit( 32, self._getTargetClass().MAXIMUM )
  52.116 +
  52.117 +        self.assertEqual( sic.value, 32 )
  52.118 +        self.assertEqual( sic.getValueString(), '32' )
  52.119 +
  52.120 +        items = sic.getCriteriaItems()
  52.121 +        self.assertEqual( len(items), 1 )
  52.122 +        self.assertEqual( len( items[0] ), 2 )
  52.123 +        self.assertEqual( items[0][0], 'foofield' )
  52.124 +        self.assertEqual( items[0][1]['query'], 32 )
  52.125 +        self.assertEqual( items[0][1]['range'], 'max' )
  52.126 +
  52.127 +    def test_RangeMinMax( self ):
  52.128 +        sic = self._makeOne('foo', 'foofield')
  52.129 +        sic.edit( '32 34', self._getTargetClass().MINMAX )
  52.130 +
  52.131 +        self.assertEqual( sic.value, ( 32, 34 ) )
  52.132 +        self.assertEqual( sic.getValueString(), '32 34' )
  52.133 +
  52.134 +        items = sic.getCriteriaItems()
  52.135 +        self.assertEqual( len(items), 1 )
  52.136 +        self.assertEqual( len( items[0] ), 2 )
  52.137 +        self.assertEqual( items[0][0], 'foofield' )
  52.138 +        self.assertEqual( items[0][1]['query'], ( 32, 34 ) )
  52.139 +        self.assertEqual( items[0][1]['range'], 'min:max' )
  52.140 +
  52.141 +    def test_RangeMinMax_withTuple( self ):
  52.142 +        sic = self._makeOne('foo', 'foofield')
  52.143 +        sic.edit( ( 32, 34 ), self._getTargetClass().MINMAX )
  52.144 +
  52.145 +        self.assertEqual( sic.value, ( 32, 34 ) )
  52.146 +        self.assertEqual( sic.getValueString(), '32 34' )
  52.147 +
  52.148 +        items = sic.getCriteriaItems()
  52.149 +        self.assertEqual( len(items), 1 )
  52.150 +        self.assertEqual( len( items[0] ), 2 )
  52.151 +        self.assertEqual( items[0][0], 'foofield' )
  52.152 +        self.assertEqual( items[0][1]['query'], ( 32, 34 ) )
  52.153 +        self.assertEqual( items[0][1]['range'], 'min:max' )
  52.154 +
  52.155 +
  52.156 +def test_suite():
  52.157 +    return TestSuite((
  52.158 +        makeSuite(SimpleIntCriterionTests),
  52.159 +        ))
  52.160 +
  52.161 +if __name__ == '__main__':
  52.162 +    main(defaultTest='test_suite')
    53.1 new file mode 100644
    53.2 --- /dev/null
    53.3 +++ b/tests/test_SSC.py
    53.4 @@ -0,0 +1,67 @@
    53.5 +##############################################################################
    53.6 +#
    53.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    53.8 +#
    53.9 +# This software is subject to the provisions of the Zope Public License,
   53.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   53.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   53.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   53.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   53.14 +# FOR A PARTICULAR PURPOSE.
   53.15 +#
   53.16 +##############################################################################
   53.17 +""" Unit tests for SimpleStringCriterion module.
   53.18 +
   53.19 +$Id: test_SSC.py 37135 2005-07-08 13:24:33Z tseaver $
   53.20 +"""
   53.21 +
   53.22 +from unittest import TestSuite, makeSuite, main
   53.23 +import Testing
   53.24 +try:
   53.25 +    import Zope2
   53.26 +except ImportError: # BBB: for Zope 2.7
   53.27 +    import Zope as Zope2
   53.28 +Zope2.startup()
   53.29 +
   53.30 +from common import CriterionTestCase
   53.31 +
   53.32 +
   53.33 +class SimpleStringCriterionTests(CriterionTestCase):
   53.34 +
   53.35 +    def _getTargetClass(self):
   53.36 +        from Products.CMFTopic.SimpleStringCriterion \
   53.37 +                import SimpleStringCriterion
   53.38 +
   53.39 +        return SimpleStringCriterion
   53.40 +
   53.41 +    def test_Empty( self ):
   53.42 +        ssc = self._makeOne('foo', 'foofield')
   53.43 +
   53.44 +        self.assertEqual( ssc.getId(), 'foo' )
   53.45 +        self.assertEqual( ssc.field, 'foofield' )
   53.46 +        self.assertEqual( ssc.value, '' )
   53.47 +        self.assertEqual( len( ssc.getCriteriaItems() ), 0 )
   53.48 +
   53.49 +    def test_Nonempty( self ):
   53.50 +        ssc = self._makeOne('foo', 'foofield')
   53.51 +        ssc.edit( 'bar' )
   53.52 +
   53.53 +        self.assertEqual( ssc.getId(), 'foo' )
   53.54 +        self.assertEqual( ssc.field, 'foofield' )
   53.55 +        self.assertEqual( ssc.value, 'bar' )
   53.56 +
   53.57 +        items = ssc.getCriteriaItems()
   53.58 +
   53.59 +        self.assertEqual( len( items ), 1 )
   53.60 +        self.assertEqual( len( items[0] ), 2 )
   53.61 +        self.assertEqual( items[0][0], 'foofield' )
   53.62 +        self.assertEqual( items[0][1], 'bar' )
   53.63 +
   53.64 +
   53.65 +def test_suite():
   53.66 +    return TestSuite((
   53.67 +        makeSuite(SimpleStringCriterionTests),
   53.68 +        ))
   53.69 +
   53.70 +if __name__ == '__main__':
   53.71 +    main(defaultTest='test_suite')
    54.1 new file mode 100644
    54.2 --- /dev/null
    54.3 +++ b/tests/test_SortC.py
    54.4 @@ -0,0 +1,75 @@
    54.5 +##############################################################################
    54.6 +#
    54.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    54.8 +#
    54.9 +# This software is subject to the provisions of the Zope Public License,
   54.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   54.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   54.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   54.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   54.14 +# FOR A PARTICULAR PURPOSE.
   54.15 +#
   54.16 +##############################################################################
   54.17 +""" Unit tests for SortCriterion module.
   54.18 +
   54.19 +$Id: test_SortC.py 37135 2005-07-08 13:24:33Z tseaver $
   54.20 +"""
   54.21 +
   54.22 +from unittest import TestSuite, makeSuite, main
   54.23 +import Testing
   54.24 +try:
   54.25 +    import Zope2
   54.26 +except ImportError: # BBB: for Zope 2.7
   54.27 +    import Zope as Zope2
   54.28 +Zope2.startup()
   54.29 +
   54.30 +from common import CriterionTestCase
   54.31 +
   54.32 +
   54.33 +class SortCriterionTests(CriterionTestCase):
   54.34 +
   54.35 +    def _getTargetClass(self):
   54.36 +        from Products.CMFTopic.SortCriterion import SortCriterion
   54.37 +
   54.38 +        return SortCriterion
   54.39 +
   54.40 +    def test_Empty( self ):
   54.41 +        ssc = self._makeOne('foo', 'foofield')
   54.42 +
   54.43 +        self.assertEqual( ssc.getId(), 'foo' )
   54.44 +        self.assertEqual( ssc.field, None )
   54.45 +        self.assertEqual( ssc.index, 'foofield' )
   54.46 +        self.assertEqual( ssc.Field(), 'foofield' )
   54.47 +        self.assertEqual( ssc.reversed, 0 )
   54.48 +
   54.49 +        items = ssc.getCriteriaItems()
   54.50 +        self.assertEqual( len( items ), 1 )
   54.51 +        self.assertEqual( items[0][0], 'sort_on' )
   54.52 +        self.assertEqual( items[0][1], 'foofield' )
   54.53 +
   54.54 +    def test_Nonempty( self ):
   54.55 +        ssc = self._makeOne('foo', 'foofield')
   54.56 +
   54.57 +        ssc.edit( 1 )
   54.58 +
   54.59 +        self.assertEqual( ssc.getId(), 'foo' )
   54.60 +        self.assertEqual( ssc.field, None )
   54.61 +        self.assertEqual( ssc.index, 'foofield' )
   54.62 +        self.assertEqual( ssc.Field(), 'foofield' )
   54.63 +        self.assertEqual( ssc.reversed, 1 )
   54.64 +
   54.65 +        items = ssc.getCriteriaItems()
   54.66 +        self.assertEqual( len( items ), 2 )
   54.67 +        self.assertEqual( items[0][0], 'sort_on' )
   54.68 +        self.assertEqual( items[0][1], 'foofield' )
   54.69 +        self.assertEqual( items[1][0], 'sort_order' )
   54.70 +        self.assertEqual( items[1][1], 'reverse' )
   54.71 +
   54.72 +
   54.73 +def test_suite():
   54.74 +    return TestSuite((
   54.75 +        makeSuite(SortCriterionTests),
   54.76 +        ))
   54.77 +
   54.78 +if __name__ == '__main__':
   54.79 +    main(defaultTest='test_suite')
    55.1 new file mode 100644
    55.2 --- /dev/null
    55.3 +++ b/tests/test_Topic.py
    55.4 @@ -0,0 +1,353 @@
    55.5 +##############################################################################
    55.6 +#
    55.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    55.8 +#
    55.9 +# This software is subject to the provisions of the Zope Public License,
   55.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   55.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   55.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   55.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   55.14 +# FOR A PARTICULAR PURPOSE.
   55.15 +#
   55.16 +##############################################################################
   55.17 +""" Unit tests for Topic module.
   55.18 +
   55.19 +$Id: test_Topic.py 38418 2005-09-09 08:40:13Z yuppie $
   55.20 +"""
   55.21 +
   55.22 +from unittest import TestSuite, makeSuite, main
   55.23 +import Testing
   55.24 +try:
   55.25 +    import Zope2
   55.26 +except ImportError: # BBB: for Zope 2.7
   55.27 +    import Zope as Zope2
   55.28 +Zope2.startup()
   55.29 +
   55.30 +from Acquisition import Implicit
   55.31 +
   55.32 +from Products.CMFCore.tests.base.dummy import DummySite
   55.33 +from Products.CMFCore.tests.base.testcase import SecurityTest
   55.34 +from Products.CMFCore.TypesTool import FactoryTypeInformation as FTI
   55.35 +from Products.CMFCore.TypesTool import TypesTool
   55.36 +from Products.CMFTopic.Topic import factory_type_information as FTIDATA_TOPIC
   55.37 +
   55.38 +
   55.39 +class FauxBrain( Implicit ):
   55.40 +
   55.41 +    def __init__( self, object ):
   55.42 +
   55.43 +        self._object = object
   55.44 +
   55.45 +    def getObject( self ):
   55.46 +
   55.47 +        return self._object
   55.48 +
   55.49 +
   55.50 +class DummyDocument( Implicit ):
   55.51 +
   55.52 +    def __init__( self, id ):
   55.53 +
   55.54 +        self._id = id
   55.55 +
   55.56 +    def getId( self ):
   55.57 +
   55.58 +        return self._id
   55.59 +
   55.60 +
   55.61 +class DummyCatalog( Implicit ):
   55.62 +
   55.63 +    def __init__( self, index_ids=() ):
   55.64 +
   55.65 +        self._objects = []
   55.66 +        self._index_ids = index_ids
   55.67 +        self._indexes = {}
   55.68 +
   55.69 +        for index_id in index_ids:
   55.70 +            self._indexes[ index_id ] = {}
   55.71 +
   55.72 +    def _index( self, obj ):
   55.73 +
   55.74 +        marker = object()
   55.75 +        self._objects.append( obj )
   55.76 +
   55.77 +        rid = len( self._objects ) - 1
   55.78 +
   55.79 +        for index_id in self._index_ids:
   55.80 +
   55.81 +            value = getattr( obj, index_id, marker )
   55.82 +
   55.83 +            if value is not marker:
   55.84 +                for word in value.split():
   55.85 +                    bucket = self._indexes[ index_id ].setdefault( word, [] )
   55.86 +                    bucket.append( rid )
   55.87 +
   55.88 +    indexObject = _index
   55.89 +
   55.90 +    def searchResults( self, REQUEST=None, **kw ):
   55.91 +
   55.92 +        from sets import Set
   55.93 +        limit = None
   55.94 +
   55.95 +        criteria = kw.copy()
   55.96 +
   55.97 +        if REQUEST is not None:
   55.98 +            for k, v in REQUEST:
   55.99 +                criteria[ k ] = v
  55.100 +
  55.101 +        results = Set( range( len( self._objects ) ) )
  55.102 +
  55.103 +        for k, v in criteria.items():
  55.104 +
  55.105 +            if k == 'sort_limit':
  55.106 +                limit = v
  55.107 +
  55.108 +            else:
  55.109 +                results &= Set( self._indexes[ k ].get( v, [] ) )
  55.110 +
  55.111 +
  55.112 +        results = [ x for x in results ]
  55.113 +
  55.114 +        if limit is not None:
  55.115 +            results = results[ :limit ]
  55.116 +
  55.117 +        return [ FauxBrain( self._objects[ rid ] ) for rid in results ]
  55.118 +
  55.119 +
  55.120 +class DummySyndicationTool( Implicit ):
  55.121 +
  55.122 +    def __init__( self, max_items ):
  55.123 +
  55.124 +        self._max_items = max_items
  55.125 +
  55.126 +    def getMaxItems( self, object ):
  55.127 +
  55.128 +        return self._max_items
  55.129 +
  55.130 +
  55.131 +class TestTopic(SecurityTest):
  55.132 +    """ Test all the general Topic cases.
  55.133 +    """
  55.134 +
  55.135 +    def setUp(self):
  55.136 +        SecurityTest.setUp(self)
  55.137 +        self.site = DummySite('site').__of__(self.root)
  55.138 +
  55.139 +    def _makeOne(self, id, *args, **kw):
  55.140 +        from Products.CMFTopic.Topic import Topic
  55.141 +
  55.142 +        return self.site._setObject( id, Topic(id, *args, **kw) )
  55.143 +
  55.144 +    def _initSite( self, max_items=15, index_ids=() ):
  55.145 +
  55.146 +        self.site.portal_catalog = DummyCatalog( index_ids )
  55.147 +        self.site.portal_syndication = DummySyndicationTool( max_items )
  55.148 +
  55.149 +    def _initDocuments( self, **kw ):
  55.150 +
  55.151 +        for k, v in kw.items():
  55.152 +
  55.153 +            document = DummyDocument( k )
  55.154 +            document.description = v
  55.155 +
  55.156 +            self.site._setObject( k, v )
  55.157 +            self.site.portal_catalog._index( document )
  55.158 +
  55.159 +    def test_z2interfaces(self):
  55.160 +        from Interface.Verify import verifyClass
  55.161 +        from OFS.IOrderSupport import IOrderedContainer
  55.162 +        from webdav.WriteLockInterface import WriteLockInterface
  55.163 +        from Products.CMFCore.interfaces.Dynamic \
  55.164 +                import DynamicType as IDynamicType
  55.165 +        from Products.CMFCore.interfaces.Folderish \
  55.166 +                import Folderish as IFolderish
  55.167 +        from Products.CMFTopic.Topic import Topic
  55.168 +
  55.169 +        verifyClass(IDynamicType, Topic)
  55.170 +        verifyClass(IFolderish, Topic)
  55.171 +        verifyClass(IOrderedContainer, Topic)
  55.172 +        verifyClass(WriteLockInterface, Topic)
  55.173 +
  55.174 +    def test_z3interfaces(self):
  55.175 +        try:
  55.176 +            from zope.interface.verify import verifyClass
  55.177 +            from Products.CMFCore.interfaces import IDynamicType
  55.178 +            from Products.CMFCore.interfaces import IFolderish
  55.179 +        except ImportError:
  55.180 +            # BBB: for Zope 2.7
  55.181 +            return
  55.182 +        from Products.CMFTopic.Topic import Topic
  55.183 +
  55.184 +        verifyClass(IDynamicType, Topic)
  55.185 +        verifyClass(IFolderish, Topic)
  55.186 +
  55.187 +    def test_Empty( self ):
  55.188 +        topic = self._makeOne('top')
  55.189 +
  55.190 +        query = topic.buildQuery()
  55.191 +        self.assertEqual( len( query ), 0 )
  55.192 +
  55.193 +    def test_Simple( self ):
  55.194 +        topic = self._makeOne('top')
  55.195 +        topic.addCriterion( 'foo', 'String Criterion' )
  55.196 +        topic.getCriterion( 'foo' ).edit( 'bar' )
  55.197 +
  55.198 +        query = topic.buildQuery()
  55.199 +        self.assertEqual( len(query), 1 )
  55.200 +        self.assertEqual( query['foo'], 'bar' )
  55.201 +
  55.202 +        topic.addCriterion( 'baz', 'Integer Criterion' )
  55.203 +        topic.getCriterion( 'baz' ).edit( 43 )
  55.204 +
  55.205 +        query = topic.buildQuery()
  55.206 +        self.assertEqual( len( query ), 2 )
  55.207 +        self.assertEqual( query[ 'foo' ], 'bar' )
  55.208 +        self.assertEqual( query[ 'baz' ], 43 )
  55.209 +
  55.210 +    def test_Nested( self ):
  55.211 +        self.site._setObject( 'portal_types', TypesTool() )
  55.212 +        fti = FTIDATA_TOPIC[0].copy()
  55.213 +        self.site.portal_types._setObject( 'Topic', FTI(**fti) )
  55.214 +        topic = self._makeOne('top')
  55.215 +        topic._setPortalTypeName('Topic')
  55.216 +
  55.217 +        topic.addCriterion( 'foo', 'String Criterion' )
  55.218 +        topic.getCriterion( 'foo' ).edit( 'bar' )
  55.219 +
  55.220 +        topic.addSubtopic( 'qux' )
  55.221 +        subtopic = topic.qux
  55.222 +
  55.223 +        subtopic.addCriterion( 'baz', 'String Criterion' )
  55.224 +        subtopic.getCriterion( 'baz' ).edit( 'bam' )
  55.225 +
  55.226 +        query = subtopic.buildQuery()
  55.227 +        self.assertEqual( len( query ), 2 )
  55.228 +        self.assertEqual( query['foo'], 'bar' )
  55.229 +        self.assertEqual( query['baz'], 'bam' )
  55.230 +
  55.231 +        subtopic.acquireCriteria = 0
  55.232 +        query = subtopic.buildQuery()
  55.233 +        self.assertEqual( len( query ), 1 )
  55.234 +        self.assertEqual( query['baz'], 'bam' )
  55.235 +
  55.236 +    def test_selfIndexing(self):
  55.237 +        # The Topic object is CatalogAware and should be in the catalog
  55.238 +        # after it has beeen instantiated.
  55.239 +        self._initSite()
  55.240 +        topic = self._makeOne('top')
  55.241 +
  55.242 +        # A topic without criteria will return a full catalog search result
  55.243 +        # set, so we should not have one result, for the Topic object itself.
  55.244 +        results = topic.queryCatalog()
  55.245 +
  55.246 +        self.assertEquals(len(results), 1)
  55.247 +        self.assertEquals(results[0].getObject().getId(), topic.getId())
  55.248 +        self.assertEquals(results[0].getObject(), topic)
  55.249 +
  55.250 +    def test_searchableText(self):
  55.251 +        # Test the catalog helper
  55.252 +        topic = self._makeOne('top')
  55.253 +        topic.edit(False, title='FOO', description='BAR')
  55.254 +
  55.255 +        st = topic.SearchableText()
  55.256 +        self.failUnless(st.find('BAR') != -1)
  55.257 +        self.failUnless(st.find('FOO') != -1)
  55.258 +
  55.259 +    def test_queryCatalog_noop( self ):
  55.260 +
  55.261 +        self._initSite()
  55.262 +        self._initDocuments( **_DOCUMENTS )
  55.263 +        topic = self._makeOne('top')
  55.264 +
  55.265 +        # Need to filter out the Topic object itself, which is also
  55.266 +        # CatalogAware and will index itself after instantiation.
  55.267 +        brains = [ x for x in topic.queryCatalog()
  55.268 +                      if x.getObject().getId() != 'top' ]
  55.269 +
  55.270 +        self.assertEqual( len( brains ), len( _DOCUMENTS ) )
  55.271 +
  55.272 +        objects = [ brain.getObject() for brain in brains ]
  55.273 +
  55.274 +        for object in objects:
  55.275 +            self.failUnless( object.getId() in _DOCUMENTS.keys() )
  55.276 +            self.failUnless( object.description in _DOCUMENTS.values() )
  55.277 +
  55.278 +    def test_queryCatalog_simple( self ):
  55.279 +
  55.280 +        WORD = 'something'
  55.281 +
  55.282 +        self._initSite( index_ids=( 'description', ) )
  55.283 +        self._initDocuments( **_DOCUMENTS )
  55.284 +        topic = self._makeOne('top')
  55.285 +
  55.286 +        topic.addCriterion( 'description', 'String Criterion' )
  55.287 +        topic.getCriterion( 'description' ).edit( WORD )
  55.288 +
  55.289 +        brains = topic.queryCatalog()
  55.290 +
  55.291 +        self.assertEqual( len( brains )
  55.292 +                        , len( [ x for x in _DOCUMENTS.values()
  55.293 +                                  if WORD in x ] ) )
  55.294 +
  55.295 +        objects = [ brain.getObject() for brain in brains ]
  55.296 +
  55.297 +        for object in objects:
  55.298 +            self.failUnless( object.getId() in _DOCUMENTS.keys() )
  55.299 +            self.failUnless( object.description in _DOCUMENTS.values() )
  55.300 +
  55.301 +    def test_synContentValues_simple( self ):
  55.302 +
  55.303 +        self._initSite()
  55.304 +        self._initDocuments( **_DOCUMENTS )
  55.305 +        topic = self._makeOne('top')
  55.306 +
  55.307 +        #brains = topic.synContentValues()
  55.308 +        # Need to filter out the Topic object itself, which is also
  55.309 +        # CatalogAware and will index itself after instantiation.
  55.310 +        brains = [ x for x in topic.synContentValues()
  55.311 +                      if x.getObject().getId() != 'top' ]
  55.312 +
  55.313 +        self.assertEqual( len( brains ), len( _DOCUMENTS ) )
  55.314 +
  55.315 +        objects = [ brain.getObject() for brain in brains ]
  55.316 +
  55.317 +        for object in objects:
  55.318 +            self.failUnless( object.getId() in _DOCUMENTS.keys() )
  55.319 +            self.failUnless( object.description in _DOCUMENTS.values() )
  55.320 +
  55.321 +    def test_synContentValues_limit( self ):
  55.322 +
  55.323 +        LIMIT = 3
  55.324 +
  55.325 +        self._initSite( max_items=LIMIT )
  55.326 +        self._initDocuments( **_DOCUMENTS )
  55.327 +        topic = self._makeOne('top')
  55.328 +
  55.329 +        brains = topic.synContentValues()
  55.330 +
  55.331 +        self.assertEqual( len( brains ), LIMIT )
  55.332 +
  55.333 +        objects = [ brain.getObject() for brain in brains ]
  55.334 +
  55.335 +        for object in objects:
  55.336 +            self.failUnless( object.getId() in _DOCUMENTS.keys() )
  55.337 +            self.failUnless( object.description in _DOCUMENTS.values() )
  55.338 +
  55.339 +
  55.340 +_DOCUMENTS = \
  55.341 +{ 'one'     : "something in the way she moves"
  55.342 +, 'two'     : "I don't know much about history"
  55.343 +, 'three'   : "something about Mary"
  55.344 +, 'four'    : "something tells me I'm in love"
  55.345 +, 'five'    : "there's a certain wonderful something"
  55.346 +, 'six'     : "gonna wash that man right out of my hair"
  55.347 +, 'seven'   : "I'm so much in love"
  55.348 +}
  55.349 +
  55.350 +
  55.351 +def test_suite():
  55.352 +    return TestSuite((
  55.353 +        makeSuite(TestTopic),
  55.354 +        ))
  55.355 +
  55.356 +if __name__ == '__main__':
  55.357 +    main(defaultTest='test_suite')
    56.1 new file mode 100644
    56.2 --- /dev/null
    56.3 +++ b/tests/test_all.py
    56.4 @@ -0,0 +1,44 @@
    56.5 +##############################################################################
    56.6 +#
    56.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    56.8 +#
    56.9 +# This software is subject to the provisions of the Zope Public License,
   56.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   56.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   56.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   56.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   56.14 +# FOR A PARTICULAR PURPOSE.
   56.15 +#
   56.16 +##############################################################################
   56.17 +""" CMFTopic tests.
   56.18 +
   56.19 +$Id: test_all.py 37135 2005-07-08 13:24:33Z tseaver $
   56.20 +"""
   56.21 +
   56.22 +from unittest import main
   56.23 +import Testing
   56.24 +try:
   56.25 +    import Zope2
   56.26 +except ImportError: # BBB: for Zope 2.7
   56.27 +    import Zope as Zope2
   56.28 +Zope2.startup()
   56.29 +
   56.30 +from Products.CMFCore.tests.base.utils import build_test_suite
   56.31 +
   56.32 +
   56.33 +def suite():
   56.34 +    return build_test_suite('Products.CMFTopic.tests',[
   56.35 +        'test_DateC',
   56.36 +        'test_ListC',
   56.37 +        'test_SIC',
   56.38 +        'test_SortC',
   56.39 +        'test_SSC',
   56.40 +        'test_Topic',
   56.41 +        ])
   56.42 +
   56.43 +def test_suite():
   56.44 +    # Just to silence the top-level test.py
   56.45 +    return None
   56.46 +
   56.47 +if __name__ == '__main__':
   56.48 +    main(defaultTest='suite')
    57.1 new file mode 100644
    57.2 --- /dev/null
    57.3 +++ b/version.txt
    57.4 @@ -0,0 +1,1 @@
    57.5 +CMF-1.5.5-beta