From 48ae5ec713a842d082300d4d7ed4559628cc3270 Mon Sep 17 00:00:00 2001
From: Benjamin Dauvergne <bdauvergne@entrouvert.com>
Date: Tue, 19 Aug 2014 11:09:26 +0200
Subject: [PATCH 04/11] Add middleware to load settings from files based on
 the tenant

* Loaded settings are cached based on the mtime of the setting file
* You can load plain python files using PythonSettingsMiddleware, files
  are loaded from <settings.TENANT_BASE>/<domain_url>/settings.py
* You can load JSON files using JSONSettingsMiddleware, setttings are loaded
  from <settings.TENANT_BASE>/<domain_url>/settings.json
---
 entrouvert/djommon/multitenant/middleware.py |  105 ++++++++++++++++++++++++++
 1 file changed, 105 insertions(+)

diff --git a/entrouvert/djommon/multitenant/middleware.py b/entrouvert/djommon/multitenant/middleware.py
index 988ad92..b3d26f6 100644
--- a/entrouvert/djommon/multitenant/middleware.py
+++ b/entrouvert/djommon/multitenant/middleware.py
@@ -1,3 +1,9 @@
+import os
+import json
+import glob
+import time
+import hashlib
+
 from django.conf import settings, UserSettingsHolder
 
 from tenant_schemas.middleware import TenantMiddleware
@@ -18,3 +24,102 @@ class EOTenantMiddleware(TenantMiddleware):
     def process_response(self, request, response):
         settings._wrapped = self.wrapped
         return response
+
+class DictAdapter(dict):
+    def __init__(self, wrapped):
+        self.wrapped = wrapped
+
+    def __setitem__(self, key, value):
+        setattr(self.wrapped, key, value)
+
+    def __getitem__(self, key):
+        try:
+            return getattr(self.wrapped, key)
+        except AttributeError:
+            raise KeyError
+
+class TenantSettingBaseMiddleware(object):
+    '''Base middleware classe for loading settings based on tenants
+
+       Child classes MUST override the load_tenant_settings() method.
+    '''
+    def __init__(self, *args, **kwargs):
+        self.tenants_settings = {}
+
+    def get_tenant_settings(self, wrapped, tenant):
+        '''Get last loaded settings for tenant, try to update it by loading
+           settings again is last loading time is less recent thant settings data
+           store. Compare with last modification time is done in the
+           load_tenant_settings() method.
+        '''
+        tenant_settings, last_time = self.tenants_settings.get(tenant.schema_name, (None,None))
+        if tenant_settings is None:
+            tenant_settings = UserSettingsHolder(wrapped)
+        tenant_settings, last_time = self.load_tenant_settings(wrapped, tenant, tenant_settings, last_time)
+        self.tenants_settings[tenant.schema_name] = tenant_settings, last_time
+        return tenant_settings
+
+    def load_tenant_settings(self, wrapped, tenant, tenant_settings, last_time):
+        '''Load tenant settings into tenant_settings object, eventually skip if
+           last_time is more recent than last update time for settings and return
+           the new value for tenant_settings and last_time'''
+        raise NotImplemented
+
+    def process_request(self, request):
+        if not hasattr(request, '_old_settings_wrapped'):
+            request._old_settings_wrapped = []
+        request._old_settings_wrapped.append(settings._wrapped)
+        settings._wrapped = self.get_tenant_settings(settings._wrapped, request.tenant)
+
+    def process_response(self, request, response):
+        if hasattr(request, '_old_settings_wrapped') and request._old_settings_wrapped:
+            settings._wrapped = request._old_settings_wrapped.pop()
+        return response
+
+class PythonSettingsMiddleware(TenantSettingBaseMiddleware):
+    '''Load settings from a file whose path is given by:
+
+            os.path.join(settings.TENANT_BASE % schema_name, 'settings.py')
+
+       The file is executed in the same context as the classic settings file
+       using execfile.
+    '''
+    def load_tenant_settings(self, wrapped, tenant, tenant_settings, last_time):
+        path = os.path.join(settings.TENANT_BASE, tenant.schema_name, 'settings.py')
+        try:
+            st_mtime = os.stat(path).st_mtime
+            if not last_time or st_mtime >= last_time:
+                tenant_settings = UserSettingsHolder(wrapped)
+                execfile(path, DictAdapter(tenant_settings))
+                return tenant_settings, st_mtime
+        except OSError:
+            pass
+        return tenant_settings, last_time
+
+class JSONSettingsMiddleware(TenantSettingBaseMiddleware):
+    '''Load settings from a file whose path is given by:
+
+            os.path.join(settings.TENANT_BASE % schema_name, 'settings.json')
+
+       The file is executed in the same context as the classic settings file
+       using execfile.
+    '''
+    def load_tenant_settings(self, wrapped, tenant, tenant_settings, last_time):
+        path = os.path.join(settings.TENANT_BASE, tenant.schema_name)
+        while True:
+            l = glob.glob(os.path.join(path, '*.json'))
+            if not l:
+                h = None
+            else:
+                h = ''.join(map(str, (os.stat(p).st_mtime for p in l)))
+                h += '\0'.join(l)
+                h = hashlib.md5(h).digest
+            if h == last_time:
+                break
+            last_time = h
+            tenant_settings = UserSettingsHolder(wrapped)
+            for p in sorted(glob.glob(os.path.join(path, '*.json'))):
+                json_settings = json.load(file(p))
+                for key in json_settings:
+                    setattr(tenant_settings, key, json_settings[key])
+        return tenant_settings, last_time
-- 
1.7.10.4

