From 35fb727d6c1b2a0b8b5556482a0f095839cf9a22 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Mon, 4 May 2020 16:07:56 +0200 Subject: [PATCH] Encode/decode date and datetime in redis sessions In several places, odoo sets a datetime object directly in the session. It works with the default session handler of odoo which uses pickle. But datetime and date are not json serializable by default. Add custom encoder / decoder to handle them (from https://github.com/OCA/queue/blob/dc12a6a20ecfd15c5b90f9b089c9a64cf9d8bbe4/queue_job/fields.py#L57-L99) See the discussion raised by @PCatinean on https://github.com/camptocamp/odoo-cloud-platform/pull/176 --- session_redis/json_encoding.py | 39 ++++++++++++++++++++++++++++++++++ session_redis/session.py | 10 +++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 session_redis/json_encoding.py diff --git a/session_redis/json_encoding.py b/session_redis/json_encoding.py new file mode 100644 index 0000000..88d8bcd --- /dev/null +++ b/session_redis/json_encoding.py @@ -0,0 +1,39 @@ +# Copyright 2016-2020 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +import json + +from datetime import date, datetime + +import dateutil + + +class SessionEncoder(json.JSONEncoder): + """Encode date/datetime objects + + So that we can later recompose them if they were stored in the session + """ + + def default(self, obj): + if isinstance(obj, datetime): + return {"_type": "datetime_isoformat", "value": obj.isoformat()} + elif isinstance(obj, date): + return {"_type": "date_isoformat", "value": obj.isoformat()} + return json.JSONEncoder.default(self, obj) + + +class SessionDecoder(json.JSONDecoder): + """Decode json, recomposing recordsets and date/datetime""" + + def __init__(self, *args, **kwargs): + super().__init__(object_hook=self.object_hook, *args, **kwargs) + + def object_hook(self, obj): + if "_type" not in obj: + return obj + type_ = obj["_type"] + if type_ == "datetime_isoformat": + return dateutil.parser.parse(obj["value"]) + elif type_ == "date_isoformat": + return dateutil.parser.parse(obj["value"]).date() + return obj diff --git a/session_redis/session.py b/session_redis/session.py index 8b9e5af..0d73b0a 100644 --- a/session_redis/session.py +++ b/session_redis/session.py @@ -6,6 +6,8 @@ import logging from werkzeug.contrib.sessions import SessionStore +from . import json_encoding + # this is equal to the duration of the session garbage collector in # odoo.http.session_gc() DEFAULT_SESSION_TIMEOUT = 60 * 60 * 24 * 7 # 7 days in seconds @@ -57,7 +59,9 @@ class RedisSessionStore(SessionStore): "expiration of %s seconds for %s", key, expiration, user_msg) - data = json.dumps(dict(session)).encode('utf-8') + data = json.dumps( + dict(session), cls=json_encoding.SessionEncoder + ).encode('utf-8') if self.redis.set(key, data): return self.redis.expire(key, expiration) @@ -79,7 +83,9 @@ class RedisSessionStore(SessionStore): "returning a new one", key) return self.new() try: - data = json.loads(saved.decode('utf-8')) + data = json.loads( + saved.decode('utf-8'), cls=json_encoding.SessionDecoder + ) except ValueError: _logger.debug("session for key '%s' has been asked but its json " "content could not be read, it has been reset", key)