Upgrade a Django based application from 1.11 to 2.0

Posted by Harald Nezbeda on Tue 05 December 2017

Keeping dependencies updated is crucial to the project's health. Newer package version close bugs, fix security issues and bring new feature sets from which applications can benefit. As code complexity and structure can vary, so does the time required to do this operation. An interesting fact is the age and how an application evolved. Chances are that the upgrade is much smoother to Django 2.0 if the project was built with the 1.11 release, as when it started with 1.8 and got upgraded to 1.11, i describe such a case with migrations bellow. Please note that I assume you are using Python 3.x. If not, this is certain the first think to consider upgrading, as the Python 2.x support was dropped.

For sure, the application that is going to be upgraded has other dependencies besides Django. So, what I recommend is to first get through the list of packages and upgrade them one by one, and also run the tests and make sure it’s still behaving correctly. This task would be less of a headache when using some handy console utilities:

  • updatable – returns all available updates for the packages installed in the current environment (virtualenv or global)
  • pipdeptree – returns a tree of dependencies, which will help to remove installed packages that aren’t required anymore.

It’s now time to upgrade Django and I will continue with the issues I faced and fixed during this process. Consider also to take a look at the list of feature that got removed in Django 2.0.

Foreign key on_delete is mandatory

Probably it will be the first that pops out in the console when trying to run the application after the upgrade. on_delete was a keyword argument that could be passed to the model, which defaulted to CASCADE. With 1.9 it was introduced as a second position argument with None as a default value that gets evaluated to CASCADE. During this check, it raised a warning that this field will be mandatory in the 2.0 release.

For newer apps this is no big deal, you just have to explicitly add it to the foreign keys definition:

from django.db.models import Model, ForeignKey, CASCADE

class MyModel(Model):
    group = ForeignKey(
        'MyGroup',
        on_delete=CASCADE,
    )
    # ...

class MyGroup(Model):
    # ...

This should not affect migrations if the app started with Django 1.11. However, this will not be the case if you have upgraded from an older code base. It appears that the automatic migrations have not used the on_delete argument, as it defaulted to CASCADE. This is done after 1.11, but even if not defined, the app will work well as there is no change in the model states. Since in 2.0 is no default value defined, it will cause inconsistency from the migration to the model states, as the field gets defined in the models (as Django demands) but it is missing in the migrations. To fix this, you must add the on_delete argument with the value CASCADE in all migrations that create a foreign key:

migrations.CreateModel(
    name='MyModel',
    fields=[
        ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
        ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='my_app.MyGroup')),
    ],
),

This way the migration is similar to the one created with Django 1.11 and the model states can be rendered. Since it uses the default value, it will not affect the database.

Routes with namespace require app_name

Let’s assume the app uses this route configuration:

urlpatterns = [
    url(r'^myapp/', include('my_app.urls', namespace='my_app', app_name='my_app')),
]

Usually, this will be a list of URL that gets included from a third party package, but it can also be used with the internal apps. This construct will not work anymore in Django 2.0, as the app_name was removed from the list of arguments of the include function. Instead, it must use a construct that has been introduced with Dajngo 1.9, which uses the app_name defined in the originating app URL configuration. This means that the ‘my_app.urls’ must define the app_name, like this:

app_name='my_app'
urlpatterns = [
    # ...
]

The other route configuration will then change to this:

urlpatterns = [
    url(r'^myapp/', include('my_app.urls', namespace='my_app')),
]

If the code relies on a django package, you can use another construct that will allow defining the app_namespace, which is available in earlier versions of Django: include((pattern_list, app_namespace), namespace=None)

This way, the route configuration of my_app can stay as it is, and the other configuration will change to:

urlpatterns = [
    url(r'^myapp/', include(
    ('my_app.urls', 'my_app'),
    namespace='my_app',)
    ),
]

At this step, it might be worth moving to the new route configuration by using path and re_path instead of url:

urlpatterns = [
    path('myapp/', include(
    ('my_app.urls', 'my_app'),
    namespace='my_app',)
    ),
]

is_authenticated() and is_anonymous() are no longer available for User object

This two functions have become model properties, which means that they can be accessed now without the parentheses:

  • user.is_authenticated() → user.is_authenticated
  • user.is_anonymous() → user.is_anonymous

This was introduced in Django 1.10 and the methods where still available until 1.11 for Backward compatibilyty.

Custom expressions must define the output_field

I haven't found much about this so I assume this relates to the move of output_field in BaseExpression: Renamed BaseExpression._output_field to output_field. You may need to update custom expressions.

Here is a code sample that was working in 1.11:

@property
    def total_price(self):
        return self.related_items.aggregate(
            total_price=Sum(
                F('price') * F('amount') - Coalesce(F('discount'), 0)
            )
        )['total_price']

In the related model price and discount are of type DecimalField and amount is a PositiveIntegerField and this was evaluated by Django to a decimal field without any specification. Since 2.0 this requires an explicit definition and will raise an exception: Expression contains mixed types. You must set output_field.

This can be corrected by defining the output_field in the SUM:

@property
def total_price(self):
    return self.receipt_items.aggregate(
        total_price=Sum(
            F('price') * F('amount') - Coalesce(F('discount'), 0),
            output_field=DecimalField(decimal_places=3, max_digits=10,)
        )
    )['total_price']

This is a good practice and also allows you to control the output in a more pythonic way.

Dealing with Django packages that use removed features

For the second issue above, I found a solution which allows the usage of the package in a Django 2.0 application. Fixing on_delete in models or switch from functions to properties in User model will be more complicated. Of course you can start monkey patching the libraries, but this is not a solution I would recommend, as it will only bring messy stuff to the code and it does not really solve the issue for future applications.

The best approach would be to find the package repository on Github and make a Fork of it, where you can do all the changes required to make the application run with Django 2.0. The next step would be to do a pull request with this patch and in an ideal world, you would then use the new release of the package. If this does not happen there are some options left:

  • Find a replacement package that is actively maintained, which fits the purpose.
  • Temporarily use the fork in your requirements until the pull request gets merged.
  • Release a new package and maintain the fork - make sure to respect the licensing.

Not all the solutions will fit within the time allocated for the project, but on the other hand, it can be seen as a great way to give something back to the open source community.