2026, Jan 02 15:00

Django Admin Excel Import Not Working? Intercept POST in changeform_view or add_view and show messages

See why a Django admin file upload does nothing and fix Excel imports by intercepting POST in changeform_view or add_view, showing feedback via Django messages.

When you replace the default Django admin add form with a custom template and post a file upload directly, it is easy to end up with a form submission that never reaches the admin’s internal view pipeline. The result is deceptively quiet: the page reloads, nothing is saved, and no Django messages appear, even though your helper function looks fine on paper.

Problem example

The following admin setup renders a custom add/change template, parses an uploaded Excel file with pandas, and tries to run the import inside the admin’s save hook. Despite that, clicking the submit button refreshes the page with no feedback.

from django.contrib import admin, messages
import pandas as pd
from .models import RmaBgd

@admin.register(RmaBgd)
class RmaBgdAdminPanel(AdminBase):
    list_display = ('name', 'code_RMA', 'type_BGD', 'Partenaire', 'date_creation', 'RMA_BGD_state')
    list_filter = ('type_BGD', 'RMA_BGD_state', 'city')
    search_fields = ('name', 'code_RMA', 'Partenaire')

    add_form_template = "admin/spatial_data/RMABGD/change_form.html"
    change_form_template = "admin/spatial_data/RMABGD/change_form.html"

    def handle_sheet_upload(self, request):
        upload_file = request.FILES.get('sheet_file')
        if not upload_file:
            messages.error(request, "No file was selected. Please choose an Excel file.")
            return False
        try:
            df_sheet = pd.read_excel(upload_file)

            needed_cols = ["code RMA", "code ACAPS", "Dénomination RMA", "Ville", "Adresse", "Longitude", "Latitude", "Type BGD", "Partenaire", "Date création", "Etat BGD RMA"]
            absent_cols = [col for col in needed_cols if col not in df_sheet.columns]

            if absent_cols:
                messages.error(request, f"Missing required fields: {', '.join(absent_cols)}")
                return False
            else:
                imported_count = 0
                error_count = 0
                for idx, record in df_sheet.iterrows():
                    try:
                        entry = RmaBgd(
                            code_ACAPS=record["code ACAPS"],
                            code_RMA=record["code RMA"],
                            name=record["Dénomination RMA"],
                            address=record["Adresse"],
                            city=record["Ville"],
                            location=f'POINT({record["Longitude"]} {record["Latitude"]})',
                            type_BGD=record["Type BGD"],
                            Partenaire=record["Partenaire"],
                            date_creation=record["Date création"],
                            RMA_BGD_state=record["Etat BGD RMA"]
                        )
                        entry.save()
                        imported_count += 1
                    except Exception as exc:
                        messages.error(request, f"Error in row {idx + 1}: {str(exc)}")
                        error_count += 1

                if imported_count > 0:
                    messages.success(request, f"Successfully imported {imported_count} rows")
                    return True
                if error_count > 0:
                    messages.warning(request, f"Failed to import {error_count} rows. See details above.")
                if imported_count == 0:
                    messages.error(request, "No rows were imported. Please check your file and try again.")
                return imported_count > 0
        except Exception as exc:
            messages.error(request, f"Error processing file: {str(exc)}")
            return False

    def persist_model(self, request, obj, form, change):
        self.handle_sheet_upload(request)
        super().save_model(request, obj, form, change)

Here is the paired template that posts the file back to the same URL with multipart form data and shows admin messages.

{% extends "admin/base_site.html" %}
{% load i18n admin_urls static %}

{% block content %}
<div id="content-main">
    {% if messages %}
    <ul class="messagelist">
        {% for message in messages %}
        <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
        {% endfor %}
    </ul>
    {% endif %}

    <form action="." method="post" enctype="multipart/form-data">
        {% csrf_token %}
        <div>
            <fieldset class="module aligned">
                <div class="form-row">
                    <div class="fieldBox">
                        <label for="id_sheet_file" class="required">Excel File:</label>
                        <input type="file" name="sheet_file" id="id_sheet_file" accept=".xlsx,.xls" required>
                        <div class="help">Upload an Excel file with the required columns</div>
                    </div>
                </div>
            </fieldset>

            <div class="help-text">
                <p><strong>{% trans 'Required fields in the imported file:' %}</strong></p>
                <ul>
                    <li>code RMA</li>
                    <li>code ACAPS</li>
                    <li>Dénomination RMA</li>
                    <li>Ville</li>
                    <li>Adresse</li>
                    <li>Longitude</li>
                    <li>Latitude</li>
                    <li>Type BGD</li>
                    <li>Partenaire</li>
                    <li>Date création</li>
                    <li>Etat BGD RMA</li>
                </ul>
            </div>

            <div class="submit-row">
                <input type="submit" value="{% trans 'Import Excel' %}" class="default" name="_import_file">
            </div>
        </div>
    </form>
</div>
{% endblock %}

Why it happens

The upload form posts, but it does not arrive at Django admin’s add_view or changeform_view. Because of that, neither save_model nor the import helper is executed, and Django’s messages never get a chance to render in the next response.

Fix: intercept the POST in the admin view and redirect

The straightforward approach is to handle the special submit action in the admin view layer. Intercept the POST when the “Import Excel” button is used, trigger the Excel processing, then redirect back to the same page so the message framework can display results.

from django.contrib import admin, messages
from django.http import HttpResponseRedirect
import pandas as pd
from .models import RmaBgd

@admin.register(RmaBgd)
class RmaBgdAdmin(admin.ModelAdmin):
    add_form_template = 'admin/spatial_data/RMABGD/change_form.html'
    change_form_template = add_form_template

    def run_sheet_ingest(self, request):
        fobj = request.FILES.get('sheet_file')
        if not fobj:
            messages.error(request, "Please choose an Excel file.")
            return False

        try:
            frame = pd.read_excel(fobj)
            required = ["code RMA","code ACAPS","Dénomination RMA","Ville",
                        "Adresse","Longitude","Latitude","Type BGD",
                        "Partenaire","Date création","Etat BGD RMA"]
            missing = [h for h in required if h not in frame.columns]
            if missing:
                messages.error(request, f"Missing columns: {', '.join(missing)}")
                return False

            imported = 0
            for i, row in frame.iterrows():
                try:
                    RmaBgd.objects.create(
                        code_ACAPS=row["code ACAPS"],
                        code_RMA=row["code RMA"],
                        name=row["Dénomination RMA"],
                        address=row["Adresse"],
                        city=row["Ville"],
                        location=f'POINT({row["Longitude"]} {row["Latitude"]})',
                        type_BGD=row["Type BGD"],
                        Partenaire=row["Partenaire"],
                        date_creation=row["Date création"],
                        RMA_BGD_state=row["Etat BGD RMA"]
                    )
                    imported += 1
                except Exception as e:
                    messages.error(request, f"Row {i+1}: {e}")

            if imported:
                messages.success(request, f"Imported {imported} rows")
            else:
                messages.warning(request, "No rows were imported")
            return imported > 0

        except Exception as e:
            messages.error(request, f"Error processing file: {e}")
            return False

    def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
        if request.method == 'POST' and '_import_file' in request.POST:
            self.run_sheet_ingest(request)
            return HttpResponseRedirect(request.path)
        return super().changeform_view(request, object_id, form_url, extra_context)

If you only need this behavior on the Add page, override add_view instead of changeform_view.

Alternative: integrate django-import-export

Another route is to delegate the import workflow to a library that plugs into Django admin and provides Import and Export buttons, a preview flow, validation, and messages. Install the package, enable it in settings, define a Resource, and register an ImportExportModelAdmin.

pip install django-import-export
INSTALLED_APPS += ['import_export']
from import_export import resources
from import_export.admin import ImportExportModelAdmin
from .models import RmaBgd

class RmaBgdResource(resources.ModelResource):
    class Meta:
        model = RmaBgd
        fields = (
          'code_ACAPS','code_RMA','name','address','city',
          'location','type_BGD','Partenaire','date_creation',
          'RMA_BGD_state',
        )

@admin.register(RmaBgd)
class RmaBgdAdminImportable(ImportExportModelAdmin):
    resource_class = RmaBgdResource

Why this matters

Handling the POST in the admin view guarantees that your import routine actually runs and that Django’s message system persists feedback through a redirect and renders it on the next page load. This keeps the workflow inside the standard admin request cycle, which is exactly where add_view and changeform_view perform their work.

Conclusion

When a custom file upload form in Django admin appears to do nothing, the core issue is usually that the POST bypasses the admin’s view methods. Intercept the import action in changeform_view or add_view, call your processing function, and redirect to let messages display. If you want a more feature-rich and maintainable path, adopt django-import-export to get built-in buttons, preview, and validation with minimal code. Both approaches keep the logic explicit and make sure users see clear, actionable feedback after every import attempt.