Skip to content

Instantly share code, notes, and snippets.

@n0phx
Created July 12, 2020 15:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save n0phx/0a85788e3abf607c347ee3bf6508e76a to your computer and use it in GitHub Desktop.
Save n0phx/0a85788e3abf607c347ee3bf6508e76a to your computer and use it in GitHub Desktop.
django-mutant introduction

A few words about django-mutant

Setup

So, I just kicked off with django's standard project layout to get up quickly asap, and created an app named mutantgui. The first thing I did is added the database params for postgres in settings.py(so I can use the awesome pgAdmin tool to examine what mutant is doing exactly), enabled the django admin app, and added south, mutant and our mutantgui app to the INSTALLED_APPS list. After running python manage.py syncdb, a couple of mutant tables were created, specifically:

Creating table mutant_modeldefinition
Creating table mutant_basedefinition
Creating table mutant_orderingfielddefinition
Creating table mutant_uniquetogetherdefinition_field_defs
Creating table mutant_uniquetogetherdefinition
Creating table mutant_fielddefinition
Creating table mutant_fielddefinitionchoice

By examining mutant's source, it has a root folder called models, which has a model and field subfolder. These folders contain an __init__.py file, where mutant has it's base model definitions. The tables that were created by running syncdb were for the models found in these files. The mutant_modeldefinition table will contain information about the database tables created using mutant. However to define columns on these tables, they must be created somewhere as well. At first I thought it is enough to use the mutant_fielddefinition table(which will reflect itself in the admin panel under the 'Fields' name), but I could not create columns the way I wanted, everything was messed up, all because I got it wrong. Mutant has in it's root a separate folder called contrib, which has several subfolders. They hold the models for database column definitions, and I had to add to INSTALLED_APPS those which I wanted to use. So by extending INSTALLED_APPS with these:

'mutant.contrib.boolean',
'mutant.contrib.temporal',
'mutant.contrib.file',
'mutant.contrib.numeric',
'mutant.contrib.text',
'mutant.contrib.web',
'mutant.contrib.related',

and running syncdb again, these tabels were added:

Creating table mutant_foreignkeydefinition
Creating table mutant_manytomanyfielddefinition
Creating table mutant_datefielddefinition
Creating table mutant_charfielddefinition
Creating table mutant_filepathfielddefinition
Creating table mutant_decimalfielddefinition
Creating table mutant_genericipaddressfielddefinition

Table Definitions

We could of course just register these mutant models to show up in django's admin tool, but that would not be a straightforward way to create tables and columns(been there).

The first thing to do is to allow the creation of database tables using django's class based generic views:

from django.core.urlresolvers import reverse_lazy
from django.views.generic import ListView
from django.views.generic.edit import CreateView

from mutant import models


class TableList(ListView):
    model = models.ModelDefinition
    context_object_name = "table_list"
    template_name = 'table_list.html'


list_tables = TableList.as_view()


class TableCreate(CreateView):
    model = models.ModelDefinition
    template_name = 'table_save.html'
    success_url = reverse_lazy('table_list')


create_table = TableCreate.as_view()

Add two simple templates:

table_list.html

{% raw %}

{% extends "base.html" %}
{% load i18n %}
{% block content %}
    {% block table_list %}
        <table>
            {% for db_table in table_list %}
                <tr>
                    <td>{{ db_table.object_name }}</td>
                </tr>
            {% endfor %}
        </table>
    {% endblock table_list %}
    {% block table_controls %}
        <div>
            <a href="{% url 'table_create' %}">{% trans "Create" %}</a>
        </div>
    {% endblock table_controls %}
{% endblock content %}

{% endraw %}

table_save.html

{% raw %}

{% extends "base.html" %}
{% load i18n %}
{% block content %}
    {% block table_save %}
        <form action="" method="post">{% csrf_token %}
            {{ form.as_p }}
            <input type="submit" value="{% trans 'Save' %}" />
        </form>
    {% endblock table_save %}
{% endblock content %}

{% endraw %}

connect them with these url definitions:

url(r'tables/$',
    'mutanttest.views.list_tables',
    name='table_list'),

url(r'tables/create/$',
    'mutanttest.views.create_table',
    name='table_create'),

and it already works! We can list and create database tables. The modelform generated from the model class has a lot of fields, it might look a bit complicated for non-djangonauts, but one could easily customize it, eventually create a custom form with only two fields (app and table name), and fill the rest programatically... Anyway that's too specific, so we won't go into it. The Update / Delete views are pretty straightforward to implement as well, so we won't waste space on them. While I was trying out the implemented features, I always followed each step with pgAdmin, just to be sure that what I'm doing is actually happening in the database as well. So every time you add a new table, the appname you specified will prefix the name of the table you entered, just like django does it, and will appear besides the other "normal" tables. Also, the mutant_modeldefinition table will contain the information you entered in the form too, which is needed so that mutant can register the models dynamically.

Column Definitions

Defining database columns (fields) on the tables we previously created is a bit trickier. First of all, the user must be able to choose the type of the field to add, and then a form needs to be generated dynamically for that field. Because fields may have attributes that are specific to their type, we can't just use a generic form for all field types, we need a specific form for each field type. The previously mentioned FieldDefinition base model comes handy for this task, as fields created in mutant will show up in the mutant_fielddefinition table, besides the table for their specific type. By adding this little admin snippet to the admin.py file inside our app:

from django.contrib import admin

from mutant import models


admin.site.register(models.FieldDefinition)

we are able to see the Fields model in the django admin tool. By clicking on the "Add" option, we can see the form generated for it, and the important thing here is the model_def attribute, which is a ForeignKey used to specify to which table we want to add that field. Otherwise FieldDefinition by itself can't be used to add fields. So to implement a view that will list a specific table's fields, we just have to filter the FieldDefinition model on it's model_def_id field, and we'll get a queryset with fields belonging to that specific table. Also, in this view it could come handy to show the actual table's name, so we extend the context data passed to the template with the name of the table. This is what the view looks like:

class FieldList(ListView):
    model = models.FieldDefinition
    context_object_name = 'field_list'
    template_name = 'mutantgui/field_list.html'

    def get_queryset(self):
        table_pk = self.kwargs.get('table_pk', None)
        return self.model.objects.filter(model_def_id=table_pk)

    def get_context_data(self, **kwargs):
        context = super(FieldList, self).get_context_data(**kwargs)
        table_pk = self.kwargs.get('table_pk', None)
        try:
            parent_table = models.ModelDefinition.objects.get(pk=table_pk)
        except models.ModelDefinition.DoesNotExist:
            pass
        else:
            context['parent_table_name'] = parent_table.name

        context['parent_table_id'] = table_pk
        context['field_type_form'] = AddFieldForm()

        return context


list_fields = FieldList.as_view()

The template code is not that important, only two things are worth mentioning, the first is that we're using a custom template filter to display the field's type:

{% raw %}
{{ field|get_field_type }}
{% endraw %}

which is very simple, we just return the name of the field's model.

@register.filter
def get_field_type(field):
    return field.type_cast().__class__.__name__

type_cast is defined in the polymodels app, look up it's source if you're interested.

The other mentionable thing is that we have added a form to the context data as well, under the key field_type_form. That form will contain only a select field, from which we can choose what kind of field we wish to add to a table. This selectbox will be rendered in the template after the already defined fields are listed and by selecting one of the fields and clicking on the "Add" button, a new form will be dynamically generated for the selected type. As the new form is created using django's CreateView, we're sending the form data from the field list page with a GET request to the url of FieldCreateView:

{% raw %}
<form action="{% url 'field_create' parent_table_id %}" method="get">
{% endraw %}

so when it receives the GET request, the PK of the chosen mutant field type will be available as request data, so it can be used to generate a ModelForm for the selected field type, and return that as the result of the GET request. This is the CreateFieldView code (a bit hackish) but works:

class FieldCreateView(SuccessUrlMixin, CreateView):
    template_name = 'mutantgui/field_save.html'

    def get_success_url(self):
        self.success_url = self.get_reversed_success_url()
        return super(FieldCreateView, self).get_success_url()

    def _prepare_dynamic_form(self, request, table_pk, super_func):
        form = AddFieldForm(request.GET)
        if form.is_valid():
            field_type_pk = form.cleaned_data['field_type']

            table_pk = self.kwargs.get('table_pk', None)
            model_defs = models.ModelDefinition.objects.filter(pk=table_pk)

            self.form_class = get_field_def_form(field_type_pk, model_defs)
            self.model = get_mutant_type(field_type_pk)
            self.initial = {'model_def': table_pk,
                            'content_type': field_type_pk}
            return super_func()
        else:
            return redirect(self.get_success_url())

    def get(self, request, table_pk):
        super_func = lambda: super(FieldCreateView, self).get(request,
                                                              table_pk)
        return self._prepare_dynamic_form(request, table_pk, super_func)

    def post(self, request, table_pk):
        super_func = lambda: super(FieldCreateView, self).post(request,
                                                               table_pk)
        return self._prepare_dynamic_form(request, table_pk, super_func)


create_field = FieldCreateView.as_view()

The SuccessUrlMixin class is not important at all, it's just refactored code because it's used by the FieldUpdateView and FieldDeleteView as well, and it's very simple. As the same code is required for handling both GET and POST requests, it's refactored into a separate method, and it receives a function as an additional parameter to be called in case the form for validating the selected field type is valid and the view's code is executed. This method (_prepare_dynamic_form) uses the same form used in FieldListView to select a field type, but here to validate the sent data. Both the form_class and model attributes of the CreateView class are assigned dynamically, when the following conditions are met:

* the request data is validated
* the table where the field needs to be added is found
* the chosen mutant field type is found

As we previously saw in the admin panel when we opened the form for adding a FieldDefinition object, the model_def attribute of the FieldDefinition form points to the table where the field is defined. Since we know on which table we want to define the field, there is no need to show that on the user interface and we will limit it's queryset to contain only one object (the table in question). There is a content_type attribute as well, which is the type of the field we wish to add, and we know that information too, so we should hide that also. BTW, the initial data for these two fields are provided here too. By looking into the mutantgui/forms.py file, the code for creating the ModelForm dynamically is this:

def get_field_def_form(field_type_pk, model_def_queryset):

    class Meta:
        model = get_mutant_type(field_type_pk)

    form_attrs = {
        'Meta': Meta,
        'content_type': FieldDefinitionTypeField(widget=forms.HiddenInput),
        'model_def': forms.ModelChoiceField(queryset=model_def_queryset,
                                            widget=forms.HiddenInput)
    }

    return type('FieldDefinitionForm', (forms.ModelForm,), form_attrs)

The function receives the primary key of the field type we want to use and a queryset containing only one object (the target table). With a bit of metaprogramming blackmagic, it returns a FieldDefinitionForm, which is a subclass of django's standard ModelForm class. The Meta class of the ModelForm assigns to it's model attribute the return value of the get_mutant_type function, which is defined in our mutantgui/utils.py file. We can access all mutant field types through the mutant.models.FieldDefinitionBase._field_definitions dictionary, which we conveniently copied into a data structure serving our needs in mutantgui/utils.py, and use the get_mutant_type function to return a model class for a mutant field type's primary key.

FieldUpdateView is a bit different. Let's look at the code first:

class FieldUpdateView(SuccessUrlMixin, UpdateView):
    template_name = 'mutantgui/field_save.html'

    def get_success_url(self):
        self.success_url = self.get_reversed_success_url()
        return super(FieldUpdateView, self).get_success_url()

    def get_object(self):
        table_pk = self.kwargs.get('table_pk', None)
        model_defs = models.ModelDefinition.objects.filter(pk=table_pk)

        field_pk = self.kwargs.get('field_pk', None)
        base_field = get_object_or_404(models.FieldDefinition, pk=field_pk)
        field_type_pk = base_field.type_cast().get_content_type().pk

        self.form_class = get_field_def_form(field_type_pk, model_defs)
        self.model = get_mutant_type(field_type_pk)

        field = self.model.objects.get(pk=field_pk)

        return field


update_field = FieldUpdateView.as_view()

We're getting the PK of the field we want to update, but the problem is that we do not know from which model should we get it. We could solve that by providing the field_type_pk in the request as well, or use two queries to retrieve the target field. By getting it from the base FieldDefinition model, we can get the "base object", cast it into it's proper type, retrieve the proper model for the casted object, and then query again using the same field primary key, only now from the proper model.

The FieldDeleteView is simply using the FieldDefinition model, because by deleting a field from the FieldDefinition table, it will be deleted from it's specific table as well (and the column will disappear from the table too).

Well, this intro was vague, and I feel like I haven't said anything useful, cleaner explanations would be better, but, ain't nobody get time for this... So you're probably better off just by examining the code, but how could it become a blog post otherwise...

And of course, grab the sample code from: https://github.com/integricho/mutant-sample-app

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment