Browse Source

Added optional kwargs to get_expiry_age/date.

This change allows for cleaner tests: we can test the exact output.

Refs #18194: this change makes it possible to compute session expiry
dates at times other than when the session is saved.

Fixed #18458: the existence of the `modification` kwarg implies that you
must pass it to get_expiry_age/date if you call these functions outside
of a short request - response cycle (the intended use case).
Aymeric Augustin 12 years ago
parent
commit
cd17a24083

+ 32 - 8
django/contrib/sessions/backends/base.py

@@ -170,28 +170,52 @@ class SessionBase(object):
 
     _session = property(_get_session)
 
-    def get_expiry_age(self, expiry=None):
+    def get_expiry_age(self, **kwargs):
         """Get the number of seconds until the session expires.
 
-        expiry is an optional parameter specifying the datetime of expiry.
+        Optionally, this function accepts `modification` and `expiry` keyword
+        arguments specifying the modification and expiry of the session.
         """
-        if expiry is None:
+        try:
+            modification = kwargs['modification']
+        except KeyError:
+            modification = timezone.now()
+        # Make the difference between "expiry=None passed in kwargs" and
+        # "expiry not passed in kwargs", in order to guarantee not to trigger
+        # self.load() when expiry is provided.
+        try:
+            expiry = kwargs['expiry']
+        except KeyError:
             expiry = self.get('_session_expiry')
+
         if not expiry:   # Checks both None and 0 cases
             return settings.SESSION_COOKIE_AGE
         if not isinstance(expiry, datetime):
             return expiry
-        delta = expiry - timezone.now()
+        delta = expiry - modification
         return delta.days * 86400 + delta.seconds
 
-    def get_expiry_date(self):
-        """Get session the expiry date (as a datetime object)."""
-        expiry = self.get('_session_expiry')
+    def get_expiry_date(self, **kwargs):
+        """Get session the expiry date (as a datetime object).
+
+        Optionally, this function accepts `modification` and `expiry` keyword
+        arguments specifying the modification and expiry of the session.
+        """
+        try:
+            modification = kwargs['modification']
+        except KeyError:
+            modification = timezone.now()
+        # Same comment as in get_expiry_age
+        try:
+            expiry = kwargs['expiry']
+        except KeyError:
+            expiry = self.get('_session_expiry')
+
         if isinstance(expiry, datetime):
             return expiry
         if not expiry:   # Checks both None and 0 cases
             expiry = settings.SESSION_COOKIE_AGE
-        return timezone.now() + timedelta(seconds=expiry)
+        return modification + timedelta(seconds=expiry)
 
     def set_expiry(self, value):
         """

+ 1 - 1
django/contrib/sessions/backends/cached_db.py

@@ -40,7 +40,7 @@ class SessionStore(DBStore):
                 )
                 data = self.decode(s.session_data)
                 cache.set(self.cache_key, data,
-                    self.get_expiry_age(s.expire_date))
+                    self.get_expiry_age(expiry=s.expire_date))
             except (Session.DoesNotExist, SuspiciousOperation):
                 self.create()
                 data = {}

+ 29 - 17
django/contrib/sessions/tests.py

@@ -197,31 +197,43 @@ class SessionTestsMixin(object):
         self.assertEqual(self.session.get_expiry_age(), settings.SESSION_COOKIE_AGE)
 
     def test_custom_expiry_seconds(self):
-        # Using seconds
+        modification = timezone.now()
+
         self.session.set_expiry(10)
-        delta = self.session.get_expiry_date() - timezone.now()
-        self.assertIn(delta.seconds, (9, 10))
 
-        age = self.session.get_expiry_age()
-        self.assertIn(age, (9, 10))
+        date = self.session.get_expiry_date(modification=modification)
+        self.assertEqual(date, modification + timedelta(seconds=10))
+
+        age = self.session.get_expiry_age(modification=modification)
+        self.assertEqual(age, 10)
 
     def test_custom_expiry_timedelta(self):
-        # Using timedelta
-        self.session.set_expiry(timedelta(seconds=10))
-        delta = self.session.get_expiry_date() - timezone.now()
-        self.assertIn(delta.seconds, (9, 10))
+        modification = timezone.now()
+
+        # Mock timezone.now, because set_expiry calls it on this code path.
+        original_now = timezone.now
+        try:
+            timezone.now = lambda: modification
+            self.session.set_expiry(timedelta(seconds=10))
+        finally:
+            timezone.now = original_now
+
+        date = self.session.get_expiry_date(modification=modification)
+        self.assertEqual(date, modification + timedelta(seconds=10))
 
-        age = self.session.get_expiry_age()
-        self.assertIn(age, (9, 10))
+        age = self.session.get_expiry_age(modification=modification)
+        self.assertEqual(age, 10)
 
     def test_custom_expiry_datetime(self):
-        # Using fixed datetime
-        self.session.set_expiry(timezone.now() + timedelta(seconds=10))
-        delta = self.session.get_expiry_date() - timezone.now()
-        self.assertIn(delta.seconds, (9, 10))
+        modification = timezone.now()
+
+        self.session.set_expiry(modification + timedelta(seconds=10))
+
+        date = self.session.get_expiry_date(modification=modification)
+        self.assertEqual(date, modification + timedelta(seconds=10))
 
-        age = self.session.get_expiry_age()
-        self.assertIn(age, (9, 10))
+        age = self.session.get_expiry_age(modification=modification)
+        self.assertEqual(age, 10)
 
     def test_custom_expiry_reset(self):
         self.session.set_expiry(None)

+ 11 - 0
docs/topics/http/sessions.txt

@@ -250,12 +250,23 @@ You can edit it multiple times.
       with no custom expiration (or those set to expire at browser close), this
       will equal :setting:`SESSION_COOKIE_AGE`.
 
+      This function accepts two optional keyword arguments:
+
+      - ``modification``: last modification of the session, as a
+        :class:`~datetime.datetime` object. Defaults to the current time.
+      - ``expiry``: expiry information for the session, as a
+        :class:`~datetime.datetime` object, an :class:`int` (in seconds), or
+        ``None``. Defaults to the value stored in the session by
+        :meth:`set_expiry`, if there is one, or ``None``.
+
     .. method:: get_expiry_date
 
       Returns the date this session will expire. For sessions with no custom
       expiration (or those set to expire at browser close), this will equal the
       date :setting:`SESSION_COOKIE_AGE` seconds from now.
 
+      This function accepts the same keyword argumets as :meth:`get_expiry_age`.
+
     .. method:: get_expire_at_browser_close
 
       Returns either ``True`` or ``False``, depending on whether the user's