Jafacak.es

Django - Class Based View CSV Upload

By the end of this post, you will have added the ability to upload a CSV file and process the input in Django using a Class Based View.

The Form

To start, we want to define our CSV Upload Form. This form will be used to specify the file upload field and the validation logic.

# app_name/forms.py
from django import forms

class CSVUploadForm(forms.Form):
    file = forms.FileField()

    def clean(self):
        cleaned_data = super().clean()
        file = cleaned_data.get("file")
        if not file.name.endswith(".csv"):
            raise ValidationError(
                {
                    "file": _("Filetype not supported, the file must be a '.csv'"),
                }
            )
        return cleaned_data

The validation for this form is quite simple, we just want to make sure that the uploaded file is a CSV. We aren't going to do anything clever here, we're just going to check that the file name ends with ".csv".

The View

Now we need to build the view that will present the form to the user and read/process the CSV.

# app_name/views.py
import csv
from io import TextIOWrapper

from django.views.generic.edit import FormView

from .forms import CSVUploadForm


class CustomCSVView(FormView):
    template_name = "app_name/csv_upload.html"
    form_class = CSVUploadForm

    def form_valid(self, form):
        csv_file = form.cleaned_data["file"]
        f = TextIOWrapper(csv_file.file)
        dict_reader = csv.DictReader(f)

        required_columns = ["column_1", "column_2"]
        # Check needed columns exist
        for req_col in required_columns:
            if req_col not in dict_reader.fieldnames:
                raise Exception(
                    f"A required column is missing from the uploaded CSV: '{req_col}'"
                )

        for row, item in enumerate(dict_reader, start=1):
            self.process_item(item)

        return super().form_valid(form)

    def process_item(self, item):
        # TODO: Replace with the code for what you wish to do with the row of data in the CSV.
        print(item["column_1"])
        print(item["column_2"])

Validating the CSV Data

The form_valid method is called when the form is submitted and the form's clean method doesn't raise any errors.

The first thing we will do in the form_valid method is get the CSV file and read it using the DictReader. This will make the CSV data really easy to loop over and process as each row will be a dict keyed by the column name from the first row in the CSV.

The next thing we will do is check that all the available columns are present. To do this I have defined a list of column names that I require required_columns. For each of the strings in this list we check to make sure that they exist in the fieldnames on the DictReader and if they do not we raise an Exception.

Now we are happy that we have all the required columns in the CSV, we can add more validation logic below, or move on to processing the data.

Processing the data from the CSV

This example will run the CustomCSVView.process_item method once for each of the rows in the uploaded CSV. This is important to note as the number of queries you perform in the process_item method will be called for each row in the CSV which can become massively inefficient. If processing the CSV becomes too much to do in this request due to timeouts, you might want to think about making this a smaller task by storing the uploaded data and creating a management command that will process the data on cron.

The Template

Create the following template in `app_name/templates/app_name/csv_upload.html`:

<form method="POST" enctype="multipart/form-data">
    {% csrf_token %}
    {{ form }}
    <button type="submit">Process CSV</button>
</form>

This is the bare minimum you will need to serve the form and process the data.

Serving the view

You will need to add the View to the application's urls.py. You can do this like so:

# urls.py
from django.urls import path

from app_name.views import CustomCSVView

urlpatterns = [
    path("csv/", CustomCSVView.as_view(), name="custom-csv-view"),
]

Now you should be able to access the view at csv/. You should also be able to upload a CSV and test the processing.

Redirecting the user on Success

At the moment, if you try to upload a CSV you will likely get an error because we haven't defined a success_url.

You can fix this by updating the CustomCSVView with a success_url like so:

# app_name/views.py
import csv
from io import TextIOWrapper

from django.urls import reverse
from django.views.generic.edit import FormView

from .forms import CSVUploadForm


class CustomCSVView(FormView):
    template_name = "app_name/csv_upload.html"
    form_class = CSVUploadForm
    success_url = reverse("custom-csv-view")

    def form_valid(self, form):
        csv_file = form.cleaned_data["file"]
        f = TextIOWrapper(csv_file.file)
        dict_reader = csv.DictReader(f)

        required_columns = ["column_1", "column_2"]
        # Check needed columns exist
        for req_col in required_columns:
            if req_col not in dict_reader.fieldnames:
                raise Exception(
                    f"A required column is missing from the uploaded CSV: '{req_col}'"
                )

        for row, item in enumerate(dict_reader, start=1):
            self.process_item(item)

        return super().form_valid(form)

    def process_item(self, item):
        # TODO: Replace with the code for what you wish to do with the row of data in the CSV.
        print(item["column_1"])
        print(item["column_2"])

Note the following changes:

The import at the top from django.urls import reverse added.

The addition of success_url = reverse("custom-csv-view"). This will redirect back to the form once the CSV is processed. You can change custom-csv-view to be a different view name if you wish to send the user elsewhere on completion of the CSV upload

Further reading

I really recommend using the following site to dig further into Class Based Views https://ccbv.co.uk/.