├── .gitignore ├── README.rst ├── requirements.txt ├── setup.py └── tenant_filter ├── __init__.py ├── middleware.py ├── models.py └── tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .gitignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .cache 41 | nosetests.xml 42 | coverage.xml 43 | 44 | # Translations 45 | *.mo 46 | *.pot 47 | 48 | # Django stuff: 49 | *.log 50 | 51 | # Sphinx documentation 52 | docs/_build/ 53 | 54 | # PyBuilder 55 | target/ 56 | 57 | .idea -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django Tenant Filter 2 | ==================== 3 | 4 | A simple Django app to automatically filter all model's queryset by the tenant ID. 5 | 6 | All models are required to define a foreign key field pointing to the Tenant model and 7 | use the Tenant Filter Manager. 8 | 9 | Mandatory settings:: 10 | 11 | TENANT_FILTER = { 12 | 'TENANT_FK_NAME': 'tenant', 13 | 'TENANT_MODEL': 'my_app.models.Tenant', 14 | 'TENANT_USER_MODEL': 'my_app.models.TenantUser', 15 | 'MODEL_EXCEPTIONS': ( ) 16 | } 17 | 18 | Where `my_app.models.py` is:: 19 | 20 | from django.contrib.auth.models import User 21 | from django.db import models 22 | from tenant_filter.models import TenantFilterManager 23 | 24 | class Tenant(models.Model): 25 | name = models.CharField(max_length=100) 26 | 27 | class TenantUser(models.Model): 28 | user = models.OneToOneField(User) 29 | tenant = models.ForeignKey(Tenant) 30 | objects = TenantFilterManager() 31 | 32 | class OtherModel(models.Model): 33 | tenant = models.ForeignKey(Tenant) 34 | objects = TenantFilterManager() 35 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django<1.8 2 | setuptools 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import os 3 | from setuptools import setup 4 | 5 | with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: 6 | README = readme.read() 7 | 8 | # allow setup.py to be run from any path 9 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 10 | 11 | setup( 12 | name='django-tenant-filter', 13 | version='0.1', 14 | packages=['tenant_filter'], 15 | include_package_data=True, 16 | license='BSD License', 17 | description='A simple Django app to automatically filter all queries by the tenant ID.', 18 | long_description=README, 19 | url='', 20 | author='Hugo Pineda', 21 | author_email='hpineda83@gmail.com', 22 | classifiers=[ 23 | 'Environment :: Web Environment', 24 | 'Framework :: Django', 25 | 'Intended Audience :: Developers', 26 | 'License :: OSI Approved :: BSD License', 27 | 'Operating System :: OS Independent', 28 | 'Programming Language :: Python', 29 | 'Programming Language :: Python :: 2.7', 30 | 'Topic :: Internet :: WWW/HTTP', 31 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 32 | ], 33 | ) -------------------------------------------------------------------------------- /tenant_filter/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | -------------------------------------------------------------------------------- /tenant_filter/middleware.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from threading import local 3 | 4 | _thread_locals = local() 5 | 6 | 7 | def get_current_request(): 8 | """ returns the request object for this thread """ 9 | return getattr(_thread_locals, "request", None) 10 | 11 | 12 | def get_current_user(): 13 | """ returns the current user, if exist, otherwise returns None """ 14 | request = get_current_request() 15 | if request: 16 | return getattr(request, "user", None) 17 | 18 | 19 | class GlobalRequestMiddleware(object): 20 | """ Simple middleware that adds the request object in thread local storage.""" 21 | 22 | def process_request(self, request): 23 | _thread_locals.request = request 24 | 25 | def process_response(self, request, response): 26 | if get_current_request(): 27 | try: 28 | del _thread_locals.request 29 | except AttributeError: 30 | pass 31 | return response 32 | -------------------------------------------------------------------------------- /tenant_filter/models.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from django.db import models 3 | from middleware import get_current_user 4 | from django.conf import settings 5 | 6 | 7 | TENANT_FILTER = getattr(settings, 'TENANT_FILTER') 8 | 9 | 10 | class TenantFilterManager(models.Manager): 11 | def get_queryset(self): 12 | """ 13 | Filter the default queryset by the tenant ID found in the local thread. It requires 14 | 'middleware.GlobalRequestMiddleware' to be activated in settings.py 15 | """ 16 | qs = super(TenantFilterManager, self).get_queryset() 17 | user = get_current_user() 18 | if user and user.is_authenticated(): 19 | tenant_user_obj = getattr(user, TENANT_FILTER['TENANT_USER_MODEL'].split('.')[-1].lower()) 20 | tenant_obj = getattr(tenant_user_obj, TENANT_FILTER['TENANT_MODEL'].split('.')[-1].lower()) 21 | filter_dict = {TENANT_FILTER['TENANT_FK_NAME']: tenant_obj.pk} 22 | qs = qs.filter(**filter_dict) 23 | return qs -------------------------------------------------------------------------------- /tenant_filter/tests.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from django.test import TestCase 3 | from django.apps import apps 4 | from tenant_filter.models import TenantFilterManager 5 | from django.conf import settings 6 | 7 | 8 | class TenantModelManagerTest(TestCase): 9 | def test_model_uses_manager(self): 10 | """ 11 | Check that all models are using the default manager (with some exceptions) 12 | """ 13 | tenant_model = settings.TENANT_FILTER['TENANT_MODEL'].split('.')[-1] 14 | implement_manager_exceptions = (tenant_model, 'LogEntry', 'Permission', 'Group', 'User', 'ContentTypeManager', 'ContentType', 15 | 'Session', 'Manager', 'Source', 'Thumbnail', 'ThumbnailDimensions')\ 16 | + settings.TENANT_FILTER['MODEL_EXCEPTIONS'] 17 | models = apps.get_models() 18 | for model in models: 19 | if model.__name__ not in implement_manager_exceptions: 20 | if not isinstance(model.objects, TenantFilterManager): 21 | print "Model %s does not implement TenantFilterManager" % model 22 | self.assertIsInstance(model.objects, TenantFilterManager) 23 | --------------------------------------------------------------------------------