Garfonzo
Garfonzo

Reputation: 3966

Django - multiple users with unique data

I'm building a SaaS that is something like a property maintenance application. A property maintenance company would use my application to manage multiple buildings and a whole bunch of tenants. So we have buildings and tenants all managed by one management company. However, a management company might have multiple staff (users) who are all managing these buildings/tenants.

Since this is a SaaS, I want to have a whole bunch of management companies all use my application to manage their own set of buildings/tenants. Thus, each management company needs to only be able to see their data and have no way to access another management company's data.

I have no idea how I might accomplish this in Django, though I have an idea:

Perhaps have a model called something like ManagementCompany and all buildings and tenants have a ForeignKey field which points to that ManagementCompany model. The staff of that management company would be connected to the ManagementCompany record via ForeignKey. In this way, the users can only see buildings/tenants whose ManagementCompany matches the users's.

Would that be a reasonable approach, or is there a better way?

Upvotes: 1

Views: 410

Answers (3)

dikamilo
dikamilo

Reputation: 667

It depends on how you want to store data in you SaaS application - single db for all instances or multiple db (each instance have separate database). Multiple db approach is pain when you want add new features, migrations etc. Single db is easier to manage but you need to add bunch of ForeignKey for each model.

For single db you will need:

  1. Middleware that will detect SaaS instance (by subdomain, domain, port, custom url etc.).
  2. Database router that will return database name for read/write depending on SaaS instance.

That's all. Django will read/write to separate databases.

For multiple db you will need:

  1. Middleware that will detect SaaS instance (by subdomain, domain, port, custom url etc.).

Because you probably don't want to add ForeignKey to each model manually and filter it manually:

  1. Abstract model with ForeignKey and custom save method to auto set that ForeignKey.
  2. Custom model manager with custom get_queryset method that will filter all ORM queries with current SaaS instance. This manager should overide create method to auto set ForeignKey for queries like this: Foo.objects.create(**data)

Each model that will be fitlered for SaaS instance should inherit from that abstract model and you will need to set this model manager to that custom model manager.

That's all. Django will filter you ORM queries for current SaaS instance.

Example middleware (uses Domain model to check if domain exists, if not you will get HTTP404):

try:
    from threading import local
except ImportError:
    from django.utils._threading_local import local

_thread_locals = local()

def get_current_saas_instance():
    return getattr(_thread_locals, 'current_instance', None)

class SaaSSubdomainMiddleware(object):
    def process_request(self, request):
        _thread_locals.current_instance = None
        host = request.get_host()

        try:
            domain = Domain.objects.get(name=host)
            _thread_locals.current_instance = domain.company
        except:
            logger.error('Error when checking SaaS domain', exc_info=True)
            raise Http404

Example abstract model:

class SaaSModelAbstract(Model):
    SAAS_FIELD_NAME = 'company'
    company = ForeignKey(Company, null=True, blank=True)

    class Meta:
        abstract = True

    def save(self, *args, **kwargs):
        from .middleware import get_current_saas_instance
        self.company = get_current_saas_instance()
        super(SaaSModelAbstract, self).save(*args, **kwargs)

Example model manager:

class CurrentSaaSInstanceManager(models.Manager):
    def get_current_saas_instance(self):
        from .middleware import get_current_saas_instance
        return get_current_saas_instance()

    def get_queryset(self):
        current_instance = self.get_current_saas_instance()

        if current_instance is not None:
            return super(CurrentSaaSInstanceManager, self).get_queryset().filter(
                **{self.model.SAAS_FIELD_NAME: current_instance})

        return super(CurrentSaaSInstanceManager, self).get_queryset()

    def create(self, **kwargs):
        current_instance = self.get_current_saas_instance()

        if current_instance is not None:
            kwargs[self.model.SAAS_FIELD_NAME] = current_instance

        instance = self.model(**kwargs)
        self._for_write = True
        instance.save(force_insert=True, using=self.db)
        return instance

Example models:

class FooModel(SaaSModelAbstract):
    # model fields, methods 

    objects = CurrentSaaSInstanceManager()

class BarModel(models.Model):
    # model fields, methods
    pass

Example queries:

FooModel.objects.all() # will return query with all objects for current SaaS instance
BarModel.objects.all() # will return all objects withoout SaaS filtering

# Create objects for SaaS instance:
FooModel.objects.create(**data)
# or:
foo = FooModel()
foo.save()

In both cases (single/multiple db) django admin will be working properly.

I'm not posted db router because implementation is trivial and all you need can be found in django docs.

Upvotes: 1

Rajesh Yogeshwar
Rajesh Yogeshwar

Reputation: 2179

You can surely use django-tenant-schemas

Edit:

Given that you mentioned in the comment to my original answer that the database being used is MySQL, django-tenant-schemas is useless in your case. How about using multiple databases with database routers, that ways there can be separate databases for every company and using database routers you can route request for your databases through it.

It might be overwork but you probably can figure out a slick way to do it.

Upvotes: 0

Dan
Dan

Reputation: 314

Have you thought of subdomains?

Custom subdomains are a great way to provide customization for customers of SaaS products and to differentiate content without resorting to long URL paths.

So when a company registers, you either take the company name call to_lower() method, or prompt them to choose the one they like, just like Slack does, or Jira. I suggest you read this article Using Subdomains in Django Applications

Upvotes: 1

Related Questions