Database-Driven Django Form Creation At Runtime
Modularity. This was one of the key objectives before we started designing our Command Center product. We wanted the user experience, when designing a canvas, to be as simple as dragging and dropping visualization widgets from a palette onto a blank canvas.
Of course, we all know that the simpler the experience for the end user, the more complex the software is on the backend.
We wanted a system where we could easily add widget definitions which would all magically get plugged into the UI at runtime every time a user adds a new widget to a canvas.
For example, we wanted to use Django forms to validate the input for each widget's configuration options simply by storing the module path to the form class in a model field.
The definitions are stored in a model similar to the one below and managed using fixtures:
class Widget(models.Model): name = models.CharField(max_length = 64, blank = False) abbr = models.CharField(max_length = 64, blank = False, unique = True) description = models.CharField(max_length = 256, blank = True) icon_file_name = models.CharField(max_length = 64, blank = False) form_class_name = models.CharField(max_length = 255, blank = False)
Here is an example widget fixture definition:
{ "name": "Heat Map", "abbr": "heat_map", "description": "Visualizes geo-tagged mention activity.", "icon_file_name": "heat_map.png", "form_class_name": "commandcenter.forms.HeatMapForm" }
Notice the value for the form_class_name field: commandcenter.forms.HeatMapForm. In the Widget model class, we define a class function as a property as follows:
@property def form(self): """ Programatically import the form class associated with this widget and return an instance of it. """ split_name = self.form_class_name.split('.') mod_name = '.'.join(split_name[:-1]) class_name = split_name[-1] m = __import__(mod_name, fromlist = [class_name]) return getattr(m, class_name)()
What's happening here? Here is a line by line explanation:
We split the full class name on '.' (since it uses dot notation) into a list.
The module name is the path minus the class name at the end. We slice the split name list excluding the last item and join it using dot. This should yield commandcenter.forms.
We get the class name by taking the last item in the split name list. This should yield HeatMapForm.
We use Python's __import__ built-in giving it the module name and the list of items we want to import from said module. In our case, we only want our one form class.
We use getattr on the imported module with the class name as the attribute to get a handle to the form class we want. We then instantiate it and return an object instance.
It is now so simple for the widget to render it's own settings form when a user clicks on the Edit icon in our canvas designer. In the Django template, it is as simple as: