Using Bootstrap Buttons with Django-Tables2

Lots of my Django projects involve displaying some list of results in a table. Doing that in with a for loop in a template is easy enough, but if a project involves more than one page of results, using django-tables2 can make it even easier. It offers a few advantages over hand-built tables:

  • Clean templates
  • DRY with classes
  • Automatic sorting with clickable headers
  • Automatic pagination
  • Integration with django-filter

When displaying a list of results, I often want to have an open or edit button to let users go to the details form for each item. An example from my training database:

/images/tables_open_button.png

This post won't cover the full use of django-tables2. For that see the docs.

In my settings I've told django-tables2 to use the built-in Bootstrap4 template.

settings.py

DJANGO_TABLES2_TEMPLATE = "django_tables2/bootstrap4.html"

To add the Open button we need to add a column to our table class. In the example below the date, description, tags, and points columns are all part of the model. The edit column is where we add our button.

tables.py

from django_tables2 import tables, TemplateColumn
from my_app.models import Training

class TrainingTable(tables.Table):
    class Meta:
         model = Training
         attrs = {'class': 'table table-sm'}
         fields = ['date', 'description', 'tags', 'points', 'edit']

     edit = TemplateColumn(template_name='pages/tables/training_update_column.html')

The TemplateColumn lets you load arbitrary HTML into a cell. The HTML can be a snippet of a Django template. The column will pass in useful data that you can use in your template. In this case I'm using 'record', but you can also pass in any arbitrary data using the 'extra_context'.

In this case I'm also setting a few Bootstrap classes to style the table the way I like it using attrs in Meta. This is optional.

The template itself is just a single line that defines the button using some Bootstrap classes and builds the link in the normal Django way. 'training_update' is the name of a path in urls.py. The important bit is that record is passed in by the table. It represents the row and because this table is using a model it has access to all the fields in the model; even those not displayed in the table. In this case record.id is the primary key that the path expects.

pages/tables/training_update_column.html

<a class="btn btn-info btn-sm" href="{% url 'training_update' record.id %}">Open</a>

Because the page template is already loading Bootstrap there is no reason to do it here. The templates aren't limited to a single line. You can load as much as you like.

To reuse the template across multiple tables you could easily use the extra_context to pass in the path name for the contents in the table and then access it from the template. In my case, the tables have different styling and since the templates are so small, the duplication in code is minimal and I prefer the simplicity of having a separate template for each table's edit column.

One of the best parts, as mentioned above, is how clean the page's template becomes:

pages/training_list.html

{% extends "base.html" %}
{% load django_tables2 %}

{% block content %}
{% render_table table %}
{% endblock content %}

In this case the template pulls in my base template (where we pull in the Bootstrap stuff), loads the django_tabes2 requirements and then renders the table. The entire table is handled by the {% render_table table %}. All the styling is handled by the classes we added in the TrainingTable class definition.

While this example uses Bootstrap throughout, that isn't at all necessary. You could use another framework or pure, handcrafted, artisanal HTML. Whatever fits your project.

Using Your Django Custom User Model as Foreign Key

The Django docs recommend that when you create a new project you use a custom user model. Doing so lets you do things like add fields to store information about your users quite easily.

There are at least 4 ways to extend the user model on a new project. Each has its own advantages. On an existing project you might try this approach to extend the user model.

If you are using django-cookiecutter like me, you'll get a custom user model automatically. In that case you'll get a custom user model based on the AbstractUser class. You'll find it in {your_app}.users.models and it will look something like this:

class User(AbstractUser):
    class Meta:
        unique_together = (("name", "agency"),)
    # First Name and Last Name do not cover name patterns
    # around the globe.
    name = models.CharField(_('Name of User'), blank=True, max_length=255)
    is_active = models.BooleanField(_('active'), default=True,
                                    help_text=_('Designates whether this user should be treated as '
                                                'active. Unselect this instead of deleting accounts.'))

Adding a new field is as easy as defining it in the class and running the migrations. Simple. But what if you want create a foreign key relationship between the User model and another model you define?

When I tried to do something like this:

class SomeRelatedModel(models.Model):
    user = models.ForeignKey('User' on_delete=models.CASCADE, )

I received this error when I ran my migrations: Field defines a relation with model 'User', which is either not installed, or is abstract.

When I try to import the model first and then use it I get this error: ImportError: cannot import name 'User'. No matter how I did my imports I couldn't get it working correctly.

So what is the solution? It is in the docs of course where they tell us "When you define a foreign key or many-to-many relations to the user model, you should specify the custom model using the AUTH_USER_MODEL setting." In my example that would look like this:

from django.conf import settings

class SomeRelatedModel(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE,)

Changing a Django Model Breaks All Your Tests

I like to think that I learn from my mistakes. But there are a few things that creep up and bite me over and over. This is one of those errors. It gets me when I'm tired or distracted.

If you make a change to a Django model, and you do not create the migrations, when you run your tests all of your database tests will fail. If you are like me, you will experience a moment of deep despair as everything that used to work is now broken and the universe makes no sense. You'll think to yourself "What could I possibly have done to break everything?"

For me, running my tests with PyTest and working with PostgreSQL, after a long list of otuput I see an error like this one:

psycopg2.OperationalError: cursor "_django_curs_7196_10" does not exist

The cursor number is always different, but the cause is always the same. I made a change to some model and didn't make the migrations. It could be adding a new model, adding a field, or making some other model change. It will break all your database-backed tests even if they don't touch the changed model.

Luckily, the fix is easy. Just run makemigrations.

manage.py makemigrations

Rerun your tests, they pass, and suddenly the universe makes sense again.

Using ngrok to Simplify Developing for Alexa

I've recently been toying with building an Alexa skill. Not sure what exactly, something to goof around with the kids with. I was listening to a recent episode of Talk Python To Me and heard of a tool called ngrok which will make my development much easier.

Essentially ngrok creates a tunnel to your computer that is accessible on the internet. You launch ngrok and you get a URL you can access from anywhere. No need to setup port forwarding or anything in your router. This makes working with Alexa much easier, especially in the early stages when I'm trying things to see what works and what doesn't. It means I don't have to push to a public web server to see my changes.

All you need is a free ngrok account. I'll assume you can figure out how to setup an Alexa skill by following Amazon's instructions.

Ngrok Setup

Start by downloading and unzipping ngrok. Then connect your account by running the authtoken command. You can find the exact command you need by signing into your ngrok account and looking at your dashboard.

For Windows ngrok will just be an executable. So you need to either call it from the folder it is placed in or add it to your System Path. Also, for windows you don't need to preface each command with './'. Instead just call ngrok directly.

ngrok authtoken <your_auth_token_goes_here>

Next you just need to launch ngrok to create a new tunnel. I'm working with Flask-Ask and my dev server is running on my localhost on port 5000. So I use this command:

ngrok http 5000

A second or two later my tunnel is up and running and I can see the URL listed in the Forwarding area.

/images/ngrok_online.png

With my free account I get a new random URL each time I launch ngrok. This isn't a problem for what I'm doing here, but I can definitely see how useful it would be to have a paid account if you were doing client demos.

Alexa Setup

In your Alexa dashboard you need to make a couple of settings adjustments.

First, under the 'Configuration' tab set the the Endpoint to HTTPS and set the Default to the HTTPS url that NGROK just gave you. Since this URL changes each time you launch ngrok you'll need to update this field in your skill. Fortunately, this doesn't require any rebuild of the skill and the change is effective immediately.

/images/ngrok_alexa_setup_url.png

Next, under the 'SSL Certificate' tab be sure to pick the option for 'My development endpoint is a sub-domain of a domain that has a wildcard certificate from a certificate authority'. This lets you use the SSL certificate that ngrok provides for the random subdomain they gave you.

/images/ngrok_alexa_ssl_cert.png

If you don't set this option Amazon will refuse to even send traffic to your server. You'll get an error when you test it from the Skill's testing page.

The remote endpoint could not be called, or the response it returned was invalid.

Remember to update your URL in the 'Configuration' tab each time you relaunch ngrok and be sure to checkout the ngrok documentation. Their 'inspector' tool is really helpful to see the values of each request and response, especially when things go wrong.

Formatting Date Fields in Django

By default Django date fields populate with values in the yyyy-mm-dd format. Such as 2017-11-26. This isn't a format that many people actually use. The real problem comes if you want to use a date picker with the date field in your form. If the format used by the date picker, doesn't match the format used by the Django field, you'll get all kinds of issues.

Fortunately there is a really easy fix: set the date format you want in the form itself. As an example let's say we have a model for Appointments that has an appointment_date field.

forms.py

from django import forms
from models import Appointments

class AppointmentForm(forms.ModelForm):

    class Meta:
        model = Appointments
        fields = ['appointment_date', 'name', 'type_of_appointment']

    appointment_date = forms.DateField(
        widget=forms.DateInput(format='%m/%d/%Y'),
        input_formats=('%m/%d/%Y', )
        )

The widget attribute sets the display format for the field. The input_formats attribute lets you define the acceptable formats for date input. You can use all the standard Python string formatting options. You should match this to the format used by your date picker of choice.

While you are at it this can be a convenient place to add a class or two to the HTML so that your date picker can attach itself to your date fields. You'll place the code in the widget, since it defines the display on the page.

Something like this:

appointment_date = forms.DateField(
        widget=forms.DateInput(format='%m/%d/%Y', attrs={'class': 'datepicker'}),
        input_formats=('%m/%d/%Y', )
        )

Read more about Django widgets here.