Our company is oriented towards logistic solutions for big warehouses, with different WCS (warehouse control system) processes like conveyors, RF (radio frequency) terminals, pick/put to light, voice picking and many more. There are customers that are already using our WMS (warehouse management system).
All these systems are implemented using Java programming language with a lot of useful frameworks like Hibernate, Spring, Maven (as a software project management), ActiveMQ and RESTful frameworks. All of them together make our systems really powerful and extensible. But as you might expect, to get all this work done, it costs a lot of money, so taking into consideration small warehouses as our clients, this is not really what we want.
The idea
At ISD, we came up with an idea to build a solution for managing small and medium warehouses – stock management solution of articles during all the possible movements in a warehouse: receiving, storing, selling, returning.
The decision was taken to make it as a new and independent software solution. Of course, we had to chose what technologies we will use in order to implement the product . This question was easily solved when the following suggestion was proposed: “Let’s try something new, let it be Python”.
Components
As the idea was to have this solution as a service for multiple customers, we thought that the best way would be to build a web application hosted by us and later moved to a cloud service. Python is a very flexible language, and it has a lot of frameworks as well. For web applications, we chose Django, because it is the most used web framework for Python, thus, there are a lot of information and “howto’s” on Internet about it. However, using only Django, we wouldn’t have had a nice looking web interface, and we decided to use AngularJS for front-end, with Bootstrap.
We use Postgres as database, since it’s an advanced one, it’s free, easy configurable, easy to use and cross platform.
Django itself has a good way of communicating with the database. It simply requires some configurations to be added to the settings.py
file, and when Django project is initially created, the basic tables for user management are created by simply running a few commands:
[code language=”shell”]
(stockcontrol)$ cd ~/StockControl
(stockcontrol)$ ./manage.py makemigrations
(stockcontrol)$ ./manage.py migrate
(stockcontrol)$ ./manage.py createsuperuser
[/code]
All the other tables, the custom ones, are created based on models we have in our Python application.
Steps to make it work
At first we started implementing the main functionality in Python, we’ve defined the possible models (classes) that might be useful to have for our application, we’ve defined the URLs for our REST API, step by step. Then we had to think on how to make this application to work with multiple clients. And here the most interesting phase began.
First we thought about having separated databases for every new client. Then we started looking on how to configure Django so that it would use the right database for a specific client. While looking for a solution, we found a really big repository with all kinds of packages for Django, Python developers might know about it: https://djangopackages.org
. You can see how much it is used, there is information about which version is most stable, it also says if the chosen package can be used with Python 3.x or it is compatible only with 2.x versions.
Then we found out that there is another approach for multi-client software when using a Postgres database. There are few frameworks that can serve for this, but we chose the Django-tenants one since it was the only available for Python 3.x versions. It’s way of work is about creating separate schemas for every new client, and one default public schema, where few more tables are set up which hold some client data, like tenant name, client name etc.
To make this to work we had to install the framework and add some more configuration to the settings.py
file of the project:
[code language=”python”]
DATABASES = {
‘default’: {
‘ENGINE’: ‘django_tenants.postgresql_backend’,
# ..
}
}
…
DATABASE_ROUTERS = (
‘django_tenants.routers.TenantSyncRouter’,
)
…
MIDDLEWARE = (
‘django_tenants.middleware.main.TenantMainMiddleware’,
#…
)
…
TEMPLATE_CONTEXT_PROCESSORS = (
‘django.core.context_processors.request’,
#…
)
[/code]
To create new clients we’ve implemented a separate page which does some checks for the company name, also some rules for the domain names, validations of the email address, checks if the typed passwords match and so on. If everything is alright, the tenant is created by the application and the user is redirected to the login page of his new company tenant. But what happens in the background would you ask?
So, the Django application triggers the framework to create a new schema in Postgres database with all the tables for a new tenant, beside that, in the public schema, new records are added for the new domain name, tenant name, company name and the user is authorized to use the newly created tenant.
[code language=”python”]
def register_demo(company_name, tenant, username, password, host):
"""
very simplistic register function to show minimal working example
:param company_name: strinc representation of the company name (ex: Inther Software Development)
:param tenant: string representation of a tenant (a.k.a company name a.k.a subdomain)
:param username: string representation of user name
:param password: raw string representation of user password
:param host: string representation of host, like ‘localhost’ or ‘stockcontrol.eu’
:return: resulted StockControlUser instance
"""
with Company(schema_name=’public’):
company = Company(schema_name=tenant, name=company_name)
company.save()
logger.info("Company has been created.")
domain = Domain(domain=tenant + ‘.’ + host, tenant=company, is_primary=True)
domain.save()
logger.info("Domain has been created.")
user = User(username=username)
user.set_password(password)
user.save()
logger.info("User has been created.")
user_profile = UserProfile(user=user, company=company)
user_profile.save()
logger.info("User profile has been created.")
logger.info("Tenant has been created.")
return user_profile
[/code]
So, as you might noticed, every time a client makes changes in his application, only its respective schema is touched.
Now there appears another question, what if we need to make changes to the application and especially in the database? Django has prevented this, but there is an easy way to do so. First we need to create migrations by running makemigrations
command, then execute migrate
command. But this is for simple applications, without tenants. For multi-client applications there’s another command for migrations:
[code language=”shell”]
(stockcontrol)$ ./manage.py makemigrations
(stockcontrol)$ ./manage.py migrate_schemas –shared
[/code]
In our case, we need to make changes only in the clients schemas, not in the public one, and the documentation of the framework says that only this command is allowed to be used, otherwise, if you’d use the default migrate
command, you can harm the public schema.
Unfortunately, it couldn’t be all good, and we got some problems using Django-tenants framework, and that’s because of the migrate_schemas
command, for some reason, it doesn’t really work as expected, the schemas are not always updated correctly, so not all database changes are applied. We are forced to do DDL changes manually, for all the schemas, which could generate some issues when the application will have more clients, but we are investigating it, and hopefully we’ll have a solution soon for it. What I must say is that there is one more framework which works with tenants, but at the time we started using the current one, there wasn’t any version which would work with Python 3.x versions.
Conclusion
Working with Python and it’s frameworks is a pleasure, it’s easy to learn the language, it has a lot of documentation and a lot of users share their experience on forums. The most important thing is to keep monitoring the updates used by the framework, and if possible, update them. This might help us in having a stable application, and not enforcing us to find workarounds for certain cases.