[CMF-checkins] SVN: CMF/branches/1.5/C - CMFTopic.DateCriteria: The behavior and naming of date criteria

Jens Vagelpohl jens at dataflake.org
Thu Aug 18 18:05:03 EDT 2005


Log message for revision 37996:
  - CMFTopic.DateCriteria: The behavior and naming of date criteria
    operators was very confusing and in many cases unexpected and
    wrong. For that reason e.g. Plone replaced the implementation
    with a more user-friendly handling which is hereby introduced
    to CMF proper. (http://www.zope.org/Collectors/CMF/339)
  

Changed:
  U   CMF/branches/1.5/CHANGES.txt
  U   CMF/branches/1.5/CMFTopic/DateCriteria.py
  U   CMF/branches/1.5/CMFTopic/skins/zpt_topic/friendlydatec_editform.pt
  U   CMF/branches/1.5/CMFTopic/tests/test_DateC.py

-=-
Modified: CMF/branches/1.5/CHANGES.txt
===================================================================
--- CMF/branches/1.5/CHANGES.txt	2005-08-18 19:36:02 UTC (rev 37995)
+++ CMF/branches/1.5/CHANGES.txt	2005-08-18 22:05:03 UTC (rev 37996)
@@ -2,6 +2,12 @@
 
   Bug Fixes
 
+    - CMFTopic.DateCriteria: The behavior and naming of date criteria
+      operators was very confusing and in many cases unexpected and 
+      wrong. For that reason e.g. Plone replaced the implementation
+      with a more user-friendly handling which is hereby introduced
+      to CMF proper. (http://www.zope.org/Collectors/CMF/339)
+
     - CMFCore.PortalContent, CMFCore.FSSTXMethod, CMFTopic.Topic,
       CMFDefault.SkinnedFolder: Cache headers from the Caching Policy Manager
       never got set for DTML-based skins due to the way the view template

Modified: CMF/branches/1.5/CMFTopic/DateCriteria.py
===================================================================
--- CMF/branches/1.5/CMFTopic/DateCriteria.py	2005-08-18 19:36:02 UTC (rev 37995)
+++ CMF/branches/1.5/CMFTopic/DateCriteria.py	2005-08-18 22:05:03 UTC (rev 37996)
@@ -110,19 +110,57 @@
         if self.value is not None:
             field = self.Field()
             value = self.value
+            operation = self.operation
 
             # Negate the value for 'old' days
-            if self.daterange == 'old':
+            if self.daterange == 'old' and value != 0:
                 value = -value
 
+                # Also reverse the operator to match what a user would expect.
+                # Queries such as "More than 2 days ago" should match dates
+                # *earlier* than "today minus 2", and "Less than 2 days ago"
+                # would be expected to return dates *later* then "today minus
+                # two".
+                if operation == 'max':
+                    operation = 'min'
+                elif operation == 'min':
+                    operation = 'max'
+
             date = DateTime() + value
 
-            operation = self.operation
             if operation == 'within_day':
+                # When items within a day are requested, the range is between
+                # the earliest and latest time of that particular day
                 range = ( date.earliestTime(), date.latestTime() )
                 return ( ( field, {'query': range, 'range': 'min:max'} ), )
-            else:
-                return ( ( field, {'query': date, 'range': operation} ), )
+
+            elif operation == 'min':
+                if value != 0:
+                    if self.daterange == 'old':
+                        date_range = (date, DateTime())
+                        return ( ( field, { 'query': date_range
+                                          , 'range': 'min:max'
+                                          } ), )
+                    else:
+                        return ( ( field, { 'query': date.earliestTime()
+                                          , 'range': operation 
+                                          } ), )
+                else:
+                    # Value 0 means "Now", so get everything from now on
+                    return ( ( field, {'query': date,'range': operation } ), )
+
+            elif operation == 'max':
+                if value != 0:
+                    if self.daterange == 'old':
+                        return ((field, {'query': date, 'range': operation}),)
+                    else:
+                        date_range = (DateTime(), date.latestTime())
+                        return ( ( field, { 'query': date_range
+                                          , 'range': 'min:max'
+                                          } ), )
+                else:
+                    # Value is 0, meaning "Now", get everything before "Now"
+                    return ( ( field, {'query': date, 'range': operation} ), )
         else:
             return ()
 

Modified: CMF/branches/1.5/CMFTopic/skins/zpt_topic/friendlydatec_editform.pt
===================================================================
--- CMF/branches/1.5/CMFTopic/skins/zpt_topic/friendlydatec_editform.pt	2005-08-18 19:36:02 UTC (rev 37995)
+++ CMF/branches/1.5/CMFTopic/skins/zpt_topic/friendlydatec_editform.pt	2005-08-18 22:05:03 UTC (rev 37996)
@@ -20,13 +20,13 @@
     <select name="criteria.operation:records">
      <option value="min"
              tal:attributes="selected python:here.operation=='min'"
-     >At the least:</option>
+     >More than</option>
      <option value="max"
              tal:attributes="selected python:here.operation=='max'"
-     >At the most:</option>
+     >Less than</option>
      <option value="within_day"
              tal:attributes="selected python:here.operation=='within_day'"
-     >Within the day:</option>
+     >On the day</option>
     </select>
 
    <select name="criteria.value:records"
@@ -43,7 +43,7 @@
     <select name="criteria.daterange:records">
      <option value="old"
              tal:attributes="selected python:here.daterange == 'old'"
-     >old</option>
+     >ago</option>
      <option value="ahead"
              tal:attributes="selected python:here.daterange == 'ahead'"
      >ahead</option>

Modified: CMF/branches/1.5/CMFTopic/tests/test_DateC.py
===================================================================
--- CMF/branches/1.5/CMFTopic/tests/test_DateC.py	2005-08-18 19:36:02 UTC (rev 37995)
+++ CMF/branches/1.5/CMFTopic/tests/test_DateC.py	2005-08-18 22:05:03 UTC (rev 37996)
@@ -25,17 +25,20 @@
 
 from DateTime.DateTime import DateTime
 
+from Products.CMFCore.tests.base.testcase import RequestTest
+from Products.CMFCore.tests.base.dummy import DummyContent
+from Products.CMFTopic.Topic import Topic
 from common import CriterionTestCase
 
 
 class FriendlyDateCriterionTests(CriterionTestCase):
 
-    lessThanFiveDaysOld = { 'value': 4
-                          , 'operation': 'min'
+    lessThanFiveDaysOld = { 'value': 5
+                          , 'operation': 'max'
                           , 'daterange': 'old'
                           }
 
-    lessThanOneMonthAhead = { 'value': 30
+    lessThanOneMonthAhead = { 'value': 31
                             , 'operation': 'max'
                             , 'daterange': 'ahead'
                             }
@@ -79,8 +82,8 @@
         friendly = self._makeOne('foo', 'foofield')
 
         friendly.apply( self.lessThanFiveDaysOld )
-        self.assertEqual( friendly.value, 4 )
-        self.assertEqual( friendly.operation, 'min' )
+        self.assertEqual( friendly.value, 5 )
+        self.assertEqual( friendly.operation, 'max' )
         self.assertEqual( friendly.daterange, 'old' )
 
     def test_BadInput( self ):
@@ -123,6 +126,7 @@
         self.assertEqual( result[0][1]['range'], 'min:max' )
 
     def test_FiveDaysOld( self ):
+        # This should create a query 
         friendly = self._makeOne('foo', 'foofield')
 
         friendly.apply( self.lessThanFiveDaysOld )
@@ -131,9 +135,10 @@
         result = friendly.getCriteriaItems()
         self.assertEqual( len(result), 1 )
         self.assertEqual( result[0][0], 'foofield' )
-        self.assertEqual( result[0][1]['query'].Date(),
-                          ( DateTime() - 4 ).Date() )
-        self.assertEqual( result[0][1]['range'], 'min' )
+        expect_earliest, expect_now = result[0][1]['query']
+        self.assertEqual( expect_earliest.Date(),
+                          ( DateTime() - 5 ).Date() )
+        self.assertEqual( result[0][1]['range'], 'min:max' )
 
     def test_OneMonthAhead( self ):
         friendly = self._makeOne('foo', 'foofield')
@@ -142,14 +147,235 @@
         self.assertEqual( friendly.daterange, 'ahead' )
 
         result = friendly.getCriteriaItems()
-        self.assertEqual( result[0][1]['query'].Date(),
-                          ( DateTime() + 30 ).Date() )
-        self.assertEqual( result[0][1]['range'], 'max' )
+        expect_now, expect_latest = result[0][1]['query']
+        self.assertEqual( expect_latest.Date(), ( DateTime() + 31 ).Date() )
+        self.assertEqual( expect_now.Date(), DateTime().Date() )
+        self.assertEqual( result[0][1]['range'], 'min:max' )
 
+class FriendlyDateCriterionFunctionalTests(RequestTest):
+    # Test the date criterion using a "real CMF" with catalog etc.
+    selectable_diffs = [0, 1, 2, 5, 7, 14, 31, 93, 186, 365, 730]
+    nonzero_diffs = [1, 2, 5, 7, 14, 31, 93, 186, 365, 730]
+    day_diffs = [-730, -365, -186, -93, -31, -14, -7, -5, -2, -1]
+    day_diffs.extend(selectable_diffs)
 
+    def setUp(self):
+        RequestTest.setUp(self)
+        self.root.manage_addProduct[ 'CMFDefault' ].manage_addCMFSite( 'site' )
+        self.site = self.root.site
+        self.site._setObject( 'topic', Topic('topic') )
+        self.topic = self.site.topic
+        self.topic.addCriterion('modified', 'Friendly Date Criterion')
+        self.topic.addCriterion('portal_type', 'String Criterion')
+        type_crit = self.topic.getCriterion('portal_type')
+        type_crit.edit(value='Dummy Content')
+        self.criterion = self.topic.getCriterion('modified')
+        self.now = DateTime()
+
+        for i in self.day_diffs:
+            dummy_id = 'dummy%i' % i
+            self.site._setObject( dummy_id, DummyContent( id=dummy_id
+                                                        , catalog=1
+                                                        ) )
+            dummy_ob = getattr(self.site, dummy_id)
+            dummy_ob.modified_date = self.now + i
+            dummy_ob.reindexObject()
+
+
+    def test_Harness(self):
+        # Make sure the test harness is set up OK
+        ob_values = self.site.objectValues(['Dummy'])
+        self.assertEqual(len(ob_values), len(self.day_diffs))
+
+        catalog_results = self.site.portal_catalog(portal_type='Dummy Content')
+        self.assertEqual(len(catalog_results), len(self.day_diffs))
+
+    def test_WithinDayAgo(self):
+        # What items were modified "On the day X days ago"
+        for diff in self.selectable_diffs:
+            self.criterion.edit( value=abs(diff)
+                               , operation='within_day'
+                               , daterange='old'
+                               )
+            results = self.topic.queryCatalog()
+
+            # There is only one item with an modified date for this day
+            self.assertEquals(len(results), 1)
+            self.assertEquals( results[0].modified.Date()
+                             , (self.now-diff).Date()
+                             )
+
+    def test_WithinDayAhead(self):
+        # What items were modified "On the day X days ahead"
+        for diff in self.selectable_diffs:
+            self.criterion.edit( value=abs(diff)
+                               , operation='within_day'
+                               , daterange='ahead'
+                               )
+            results = self.topic.queryCatalog()
+
+            # There is only one item with an modified date for this day
+            self.assertEquals(len(results), 1)
+            self.assertEquals( results[0].modified.Date()
+                             , (self.now+diff).Date()
+                             )
+
+    def test_MoreThanDaysAgo(self):
+        # What items are modified "More than X days ago"
+        resultset_size = len(self.nonzero_diffs)
+
+        for diff in self.nonzero_diffs:
+            self.criterion.edit( value=diff
+                               , operation='min'
+                               , daterange='old'
+                               )
+            results = self.topic.queryCatalog()
+            
+            # As we move up in our date difference range, we must find as 
+            # many items as we have "modified" values <= the current value 
+            # in our sequence of user-selectable time differences. As we 
+            # increase the "value", we actually move backwards in time, so 
+            # the expected count of results *decreases*
+            self.assertEquals(len(results), resultset_size)
+            for brain in results:
+                self.failUnless(brain.modified <= self.now-diff)
+
+            resultset_size -= 1
+
+    def test_MoreThanZeroDaysAgo(self):
+        # What items are modified "More than 0 days ago"?
+        # This represents a special case. The "special munging"
+        # that corrects the query terms to what a human would expect
+        # are not applied and the search is a simple 
+        # "everything in the future" search.
+        resultset_size = len(self.selectable_diffs)
+        self.criterion.edit( value=0
+                           , operation='min'
+                           , daterange='old'
+                           )
+        results = self.topic.queryCatalog()
+        self.assertEquals(len(results), resultset_size)
+        for brain in results:
+            self.failUnless(brain.modified >= self.now)
+ 
+
+    def test_MoreThanDaysAhead(self):
+        # What items are modified "More than X days ahead"
+        resultset_size = len(self.nonzero_diffs)
+
+        for diff in self.nonzero_diffs:
+            self.criterion.edit( value=diff
+                               , operation='min'
+                               , daterange='ahead'
+                               )
+            results = self.topic.queryCatalog()
+            
+            # As we move up in our date difference range, we must find as 
+            # many items as we have "modified" values >= the current value 
+            # in our sequence of user-selectable time differences. As we 
+            # increase the "value", we actually move formward in time, so 
+            # the expected count of results *decreases*
+            self.assertEquals(len(results), resultset_size)
+            for brain in results:
+                self.failUnless(brain.modified >= self.now+diff)
+
+            resultset_size -= 1
+
+    def test_MoreThanZeroDaysAhead(self):
+        # What items are modified "More than 0 days ahead"?
+        # This represents a special case. The "special munging"
+        # that corrects the query terms to what a human would expect
+        # are not applied and the search is a simple 
+        # "everything in the future" search.
+        resultset_size = len(self.selectable_diffs)
+        self.criterion.edit( value=0
+                           , operation='min'
+                           , daterange='ahead'
+                           )
+        results = self.topic.queryCatalog()
+        self.assertEquals(len(results), resultset_size)
+        for brain in results:
+            self.failUnless(brain.modified >= self.now)
+
+    def test_LessThanDaysAgo(self):
+        # What items are modified "Less than X days ago"
+        resultset_size = 2
+
+        for diff in self.nonzero_diffs:
+            self.criterion.edit( value=diff
+                               , operation='max'
+                               , daterange='old'
+                               )
+            results = self.topic.queryCatalog()
+            
+            # With this query we are looking for items modified "less than
+            # X days ago", meaning between the given time and now. As we move
+            # through the selectable day values we increase the range to
+            # search through and thus increase the resultset size.
+            self.assertEquals(len(results), resultset_size)
+            for brain in results:
+                self.failUnless(self.now-diff <= brain.modified <= self.now)
+
+            resultset_size += 1
+
+    def test_LessThanZeroDaysAgo(self):
+        # What items are modified "Less than 0 days ago"?
+        # This represents a special case. The "special munging"
+        # that corrects the query terms to what a human would expect
+        # are not applied and the search is a simple 
+        # "everything in the past" search.
+        resultset_size = len(self.selectable_diffs)
+        self.criterion.edit( value=0
+                           , operation='max'
+                           , daterange='old'
+                           )
+        results = self.topic.queryCatalog()
+        self.assertEquals(len(results), resultset_size)
+        for brain in results:
+            self.failUnless(brain.modified <= self.now)
+            
+    def test_LessThanDaysAhead(self):
+        # What items are modified "Less than X days ahead"
+        resultset_size = 2
+
+        for diff in self.nonzero_diffs:
+            self.criterion.edit( value=diff
+                               , operation='max'
+                               , daterange='ahead'
+                               )
+            results = self.topic.queryCatalog()
+            
+            # With this query we are looking for items modified "less than
+            # X days ahead", meaning between now and the given time. As we move
+            # through the selectable day values we increase the range to
+            # search through and thus increase the resultset size.
+            self.assertEquals(len(results), resultset_size)
+            for brain in results:
+                self.failUnless(self.now+diff >= brain.modified >= self.now)
+
+            resultset_size += 1
+
+    def test_LessThanZeroDaysAhead(self):
+        # What items are modified "Less than 0 days ahead"?
+        # This represents a special case. The "special munging"
+        # that corrects the query terms to what a human would expect
+        # are not applied and the search is a simple 
+        # "everything in the past" search.
+        resultset_size = len(self.selectable_diffs)
+        self.criterion.edit( value=0
+                           , operation='max'
+                           , daterange='ahead'
+                           )
+        results = self.topic.queryCatalog()
+        self.assertEquals(len(results), resultset_size)
+        for brain in results:
+            self.failUnless(brain.modified <= self.now)
+
+
 def test_suite():
     return TestSuite((
         makeSuite(FriendlyDateCriterionTests),
+        makeSuite(FriendlyDateCriterionFunctionalTests),
         ))
 
 if __name__ == '__main__':



More information about the CMF-checkins mailing list