33. Refactoring with generic views in Django

32.01 Refactoring the index function

So far we’ve been investigating Django views and templates, and also how we use the URL mapping to map specific user requests into specific functions in our views.

When we write functions like this it gives us a great deal of very fine control over how our functions operate – in other words, how we will handle and manipulate data coming in from the user, and how we handle and manipulate data that we’re getting from the database and sending it to the user.

However, in the case of this application we’ve been building it’s somewhat generic. We’re really only providing the user with a standard set of create, read, update and delete (CRUD) operations.

We’re really only giving the user a way of viewing the database contents and doing some simple operations. If this is all our application is doing, Django thankfully provides a number of convenient classes that we can use to simplify our application, and kind of rearrange it to be a little more logically organised.

We can work through the views.py file and refactor it to use Django’s class-based views.

A reminder that here is the views.py file that we’ve so far created:

from django.shortcuts import render
from .models import *
from django.http import HttpResponseRedirect
from .forms import *

def index(request):
    master_genes = Gene.objects.all()
    return render(request, 'genedata/index.html', {'master_genes': master_genes})

def gene(request, pk):
    gene = Gene.objects.get(pk=pk)
    gene.access += 1
    print("Gene record:", pk, "access count:", str(gene.access))
    gene.save()
    master_genes = Gene.objects.all()
    return render(request, 'genedata/gene.html', {'gene': gene, 'master_genes': master_genes})

def list(request, type):
    genes = Gene.objects.filter(entity__exact=type)
    master_genes = Gene.objects.all()
    return render(request, 'genedata/list.html', {'genes': genes, 'type': type, 'master_genes': master_genes})

def poslist(request):
    genes = Gene.objects.filter(entity__exact='Chromosome').filter(sense__startswith='+')
    master_genes = Gene.objects.all()
    return render(request, 'genedata/list.html', {'genes': genes, 'type':'PosList', 'master_genes': master_genes})

def delete(request, pk):
    GeneAttributeLink.objects.filter(gene_id=pk).delete()
    Gene.objects.filter(pk=pk).delete()
    return HttpResponseRedirect("/")

def create_ec(request):
    master_genes = Gene.objects.all()
    if request.method == 'POST':
        form = ECForm(request.POST)
        if form.is_valid():
            ec = EC()
            ec.ec_name = form.cleaned_data['ec_name']
            ec.save()
            return HttpResponseRedirect('/create_ec/')
    else:
        ecs = EC.objects.all()
        form = ECForm()
    return render(request, 'genedata/ec.html', {'form': form, 'ecs': ecs, 'master_genes': master_genes})

def create_gene(request):
    master_genes = Gene.objects.all()
    if request.method == 'POST':
        form = GeneForm(request.POST)
        if form.is_valid():
            gene = form.save()
            return HttpResponseRedirect('/create_gene/')
        else:
            return render(request, 'genedata/create_gene.html', {'error': "failed", 'master_genes': master_genes, 'form': form})

    else:
        form = GeneForm()
    return render(request, 'genedata/create_gene.html', {'form': form, 'master_genes': master_genes})

Let’s consider our index page:

def index(request):
    master_genes = Gene.objects.all()
    return render(request, 'genedata/index.html', {'master_genes': master_genes})

This gets all the genes, and sends them to our index page – it’s the set of genes on the left hand side of our page.

To refactor this to use the generic list, firstly in views.py we need to import the ListView class:

from django.views.generic import ListView

And then we’ll implement a class of ListView:

class GeneList(ListView):

Then what we need to do is tell it which model our list is pointing at

    model = Gene

We’ve already defined the name of the context_object, so we can override the default context name:

    context_object_name = 'master_genes'

and we have to tell the class which template to use:

    template_name = 'genedata/index.html'

So our class is:

class GeneList(ListView):
    model = Gene
    context_object_name = 'master_genes'
    template_name = 'genedata/index.html'

Our index function is now redundant, so we can delete that.

Finally, our url path no longer functions, so we need to go to urls.py and replace this:

    path('', views.index, name='index'),

with this:

    path('', views.GeneList.as_view(), name='index'),

When we reload the main page, we don’t see any difference, as it’s working exactly the same that it did before.

32.02 Refactoring the gene function

This function returns a single gene record. Firstly we need to import DetailView for this:

from django.views.generic import DetailView

We can now replace this function with a class that makes use of DetailView.

Our old function was:

def gene(request, pk):
    gene = Gene.objects.get(pk=pk)
    gene.access += 1
    print("Gene record:", pk, "access count:", str(gene.access))
    gene.save()
    master_genes = Gene.objects.all()
    return render(request, 'genedata/gene.html', {'gene': gene, 'master_genes': master_genes})

Our new class is:

class GeneDetail(DetailView):
    model = Gene
    context_object_name = 'gene'
    template_name = 'genedata/gene.html'

We also need to correct the path in urls.py:

path('gene/', views.GeneDetail.as_view(), name='gene'),

Note that if we run the server, and click on a gene, we no longer get the navigation because we didn’t get the relevant context data.

As it stands, by default, our gene detail just gets the single record that’s requested, it’s not getting any other data.

We can overwrite functions that the classes provide and one function gets the context object for your template. We can overwrite that to provide a custom context object.

class GeneDetail(DetailView):
    model = Gene
    context_object_name = 'gene'
    template_name = 'genedata/gene.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['master_genes'] = Gene.objects.all()
        return context

Now when we refresh, the list of genes now appears on the gene page.

32.03 Refactoring the create function

At the top of views.py:

from django.views.generic.edit import CreateView

We created a form in previous blogs called GeneForm, so we use that one and allocate it to form_class.

It also needs to know where to go when you’ve successfully submitted the form and it’s been a success, so we create a success_url for this.

We also need the master genes context object and we do this in the same way as for the GeneDetail class.

class GeneCreate(CreateView):
    model = Gene
    template_name = 'genedata/create_gene.html'
    form_class = GeneForm
    success_url = "/create_gene/"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['master_genes'] = Gene.objects.all()
        return context

Finally in urls.py we need to update the path:

path('create_gene/', views.GeneCreate.as_view(), name='create_gene'),

32.04 Refactoring the delete function

At the top of views.py:

from django.views.generic.edit import DeleteView

Then in views.py:

class GeneDelete(DeleteView):
    model = Gene
    success_url = "/"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['master_genes'] = Gene.objects.all()
        return context

So choose model and success_url and copy the context object as before.

Then in urls.py

path('delete/', views.GeneDelete.as_view(), name='delete'),

We can add a new template called gene_confirm_delete.html and add this to it:

{% extends "./base.html" %}

{% block content %}
{% csrf_token %}

Are you sure you want to delete "{{ object }}"?

{% endblock %}

It’s using the class to create a confirm_delete page as we didn’t seem to add this anywhere.

32.04 Refactoring the update function

The last operation to consider is the UPDATE operation. That will complete a set of credit operations we can perform for genes.

Let’s jump back to the code. In our gene.html template we’re just going to add a new route here, just before DELETE RECORD:

UPDATE RECORD 
DELETE RECORD

We need to add this into urls.py as it’s not replacing a route we currently have:

path('update/', views.GeneUpdate.as_view(), name='update'),

We’re promising there’s going to be a class called GeneUpdate, which we need to write in views.py:

First this:

from django.views.generic.edit import UpdateView

Then this. We need to specify which fields are available to be updated, and also a template name suffix called _update_form.

class GeneUpdate(UpdateView):
    model = Gene
    fields = fields = ['gene_id', 'entity', 'start', 'stop', 'sense', 'start_codon', 'sequencing', 'ec']
    template_name_suffix = '_update_form'
 
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['master_genes'] = Gene.objects.all()
        return context

So we need to make a _update_form template too called gene_update_form.html which we will put this code into:

{% extends "./base.html" %}
{% block content %}
    
{% csrf_token %} {{ form.as_p }}
{% endblock %}

{{ form.as_p }} accepts the form object.

Now when we click UPDATE RECORD we can see the form with the values pre-populated, which we can update if we like.

32.05 Refactoring other functions we wrote

On our index page we have some links showing different types of genes – we can refactor these functions too.

We have functions called list and poslist and we can roll the functionality of these into our GeneList class.

In order to do that we need to make our context data objects a bit more complicated in a way that it’s aware of what kind of query that’s being asked of it.

in Class GeneList:

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['master_genes'] = Gene.objects.all()
        if 'type' in self.kwargs:
            if "Chromosome" in self.kwargs['type'] or "Plasmid" in self.kwargs['type']:
                context['genes'] = Gene.objects.filter(entity__exact=self.kwargs['type'])
        return context

For the list of genes we want to use a different template so we can override a different function:

    def get_template_names(self):
        if 'type' in self.kwargs:
            if "Chromosome" in self.kwargs['type'] or "Plasmid" in self.kwargs['type']:
                return 'genedata/list.html'
        return 'genedata/index.html'

We can now delete

template_name = 'genedata/index.html'

from the GeneList class as template name is now being provided by this function.

In urls.py we can amend the list route:

path('list/', views.GeneList.as_view(), name='list'),

Let’s refactor our final function, the positive list search.

We can check to see if poslist is in the full url path like this:

if "poslist" in self.request.get_full_path():

We can then run the context search using that filter:

context['genes'] = Gene.objects.filter(entity__exact='Chromosome').filter(sense__startswith='+')

In our template name function we need to add this too:

        if "poslist" in self.request.get_full_path():
            return 'genedata/list.html'
Wednesday 17 November 2021, 11 views


Leave a Reply

Your email address will not be published. Required fields are marked *