I built a time tracking, leave application and billing application with django, called crowdminder. The product was created in partnership with a consulting company, they paid me and I developed it for them while retaining co-ownership.
I have seen that they have benefited from this simple application and know that many other consulting companies in South Africa could benefit too, so I want to make it into a Software-as-a-service product.
But where to start…
Dynamic Settings
Tenants should also be able to change settings for their instance. With django’s default settings file that would be very difficult and I thought I should take a look at django-constance to see if it may benefit my situation.
Django constance allows you to set some settings as dynamic so they will be retrieved from the database instead of the settings file. For example: TIME_ZONE
, TIMECAPTURE_INTERVAL_MINUTES
, BILLABLE_HOURS_IN_WORKING_DAY
.
I have separated these settings out and it seems to be working quite well so far.
Multi-Tenancy
I realised that to make the product available to many others, it would need to be able to have multiple customers or tenants.
The choice of you multi-tenant strategy needs consideration for your individual case but in my case it made sense to use django-tenant-schemas.
As it would minimise the change to tables (I would need a tenant linked to every record in the db if I went with a shared db). With the shared schemas approach though I would need to switch to postgres
from mysql
.
Setup
Importantly there are a few setup steps like explicitly telling schemas which apps are shared and which are tenant specific. Shared apps will create tables in the public
schema. I have to admit that you will run into problems and it is better to use a rule of thumb keep things in tenant apps unless they definitely will be shared. An important thing to understand is that django.contrib.auth
in both SHARED_APPS
and TENANT APPS
would allow users to be created for the public site and for each tenant.
Furthermore with django-tenant-schemas
you should never use migrate
as that will migrate everything into the current schema instead, migrate_schemas
should be used. Migrate schemas can be run in parallel if a speed up is needed.
Then after migrating just the public schemas, we need to create a public
tenant, whose domain should just be the base domain
of the site. Then other tenants can be created.
Management commands that apply to tenants now need to inherit from BaseTenantCommand
Calling management commands will also need added care: eg. /manage.py tenant_command do_foo --schema=customer1
Thrid Party Apps
Celery also needs to be made to use tenants
Issues while working with postgres
django.db.utils.ProgrammingError: cannot cast type numeric to interval
LINE 1: …R COLUMN “duration” TYPE interval USING “duration”::interval
django.db.utils.OperationalError: cannot DROP TABLE “auth_group_permissions” because it has pending trigger events
It won’t even let you delete the database:
Another gem from the django docs on postgres migrations:
PostgreSQL is the most capable of all the databases here in terms of schema support; the only caveat is that adding columns with default values will cause a full rewrite of the table, for a time proportional to its size.
For this reason, it’s recommended you always create new columns with
null=True
, as this way they will be added immediately.
When running tests the initial migrations panicked with:
django.db.utils.ProgrammingError: relation “users_user” does not exist
Testing Multitenancy
Testing the multitenant sites and public site was a bit tricky in some cases. To test the public schema website the PUBLIC_SCHEMA_URLCONF
were not being used so I had to override the ROOT_URLCONF
to point to the public. Also the public schema was not created automatically so I had to do that in setUp()
.
Another thing is that I sometimes use WebTest for my tests, as it is sometimes better than the standard Django TestCase. When working with tenant tests, there is a Class that can be used that does a lot of the tenant related stuff for you so converting those tests would look like this:
from django_webtest import WebTestMixin
from tenant_schemas.test.cases import TenantTestCase
class EntryUnitTests(WebTestMixin, TenantTestCase):
...
Yet More Issues
Another fucking issue, is that the migrations didn’t seem to be running before the tests. The tables were not created for the tenant with TenantTestCase
.
The ContentType
model needs to be used with care as the model
field is saved in camelcase with MySQL but in lowercase with postgres.
So instead of:
leave_content_type = ContentType.objects.get(
app_label="leave", model="LeaveRequest"
)
leave_content_type = ContentType.objects.get(
app_label__iexact="leave", model__iexact="LeaveRequest"
)
Damn you postgres:
Type 'yes' if you would like to try deleting the test database 'test_crowdminder', or 'no' to cancel: yes
Destroying old test database for alias 'default'...
Got an error recreating the test database: database "test_crowdminder" is being accessed by other users
DETAIL: There is 1 other session using the database.
Ok, I just needed to kill the other test that was running after cancelling an ipdb
session.
So it looks like django-webtest
is not allowing transactions within tests:
def do_request(self, req, status, expect_errors):
# Django closes the database connection after every request;
# this breaks the use of transactions in your tests.
if close_old_connections is not None: # Django 1.6+
signals.request_started.disconnect(close_old_connections)
signals.request_finished.disconnect(close_old_connections)
else: # Django < 1.6
signals.request_finished.disconnect(close_connection)
try:
req.environ.setdefault('REMOTE_ADDR', '127.0.0.1')
But that doesn’t make sense as the tests passed previously (with MySQL) and django-constance
…it turns out if you fix the `HTTP_HOST` so the request happens properly (and doesn’t return a 400). If a 400 is returned the current transaction is aborted.
Site URL and Emails
Previously the app send emails from the `DEFAULT_FROM_EMAIL` and sometimes in the template I had to use a custom settings SITE_URL
to set urls on emails and templates. Now I envision that each tenant will have a <my_company>.crowdminder.co.za
domain, so these settings will need to be dynamic as well (but not editable by the admin user)
Protected Files
I have enabled a way to protect files based on permissions based on the id of the file and if the site is built with an isolated database or schema then there would be duplicated id’s and that would affect file retrieveal. So a tenant based folder would need to be created.
For this I may need to look at using the tenant storage API
Potentially Useful Packages when building a Saas
Apart from the packages mentioned above there may be some more that will make it easier to build a multi-tenant Saas platform with Django.
Django-ikari – Anchored sub-domains
Django-guardian – Per-object permissions
Django-billing – Plan based subscription controls
Django-pricing – Plan based subscription definitions
Django-merlin – multi-step django forms
Django-waffle – A feature flipper, controlling which users can see the features so there is not one massive release / conflict during production deployment.