Error in custom Django admin page on csv uploads (object has no attribute 'model')

Viewed 216

I have been trying to create a custom upload button in my Django admin page, but I keep getting an error pointing to my CsvUploader.py file: object has no attribute 'model'

I have a very simple model:

class Link(models.Model):
    name = models.CharField(db_column='Name', max_length=50)
    url = models.URLField(db_column="URL", max_length=500)

    def __str__(self):
        return f"{self.name}: {self.url}"

I also have my admin template modified, as follows:

#Extend Admin portal use

class CsvUploadAdmin(admin.ModelAdmin):

    change_list_template = "custom_admin/csv_form.html"

    def get_urls(self):
        urls = super().get_urls()
        additional_urls = [
            path("upload-csv/", self.upload_csv),
        ]
        return additional_urls + urls

    urls = property(get_urls)

    def changelist_view(self, request, extra_context=None):
        extra = extra_context or {}
        extra["csv_upload_form"] = CsvUploadForm()
        return super(CsvUploadAdmin, self).changelist_view(request, extra_context=extra)

    def upload_csv(self, request):
        if request.method == "POST":
            form = CsvUploadForm(request.POST, request.FILES)
            if form.is_valid():
                if request.FILES['csv_file'].name.endswith('csv'):

                    try:
                        decoded_file = request.FILES['csv_file'].read().decode('utf-8')
                    except UnicodeDecodeError as e:
                        self.message_user(
                            request,
                            "There was an error decoding the file:{}".format(e),
                            level=messages.ERROR
                        )
                        return redirect("..")

                    # Here we will call our class method
                    io_string = io.StringIO(decoded_file)
                uploader = CsvUploader(io_string, self.model, portal)
                result = uploader.create_records()

            else:
                self.message_user(
                request,
                "Incorrect file type: {}".format(
                    request.FILES['csv_file'].name.split(".")[1]
                    ),
                level=messages.ERROR
                )

        else:
            self.message_user(
                request,
                "There was an error in the form {}".format(form.errors),
                level=messages.ERROR
            )
            return redirect("..")

@admin.register(Link)
class LinkAdmin(CsvUploadAdmin):
    pass

Finally, here is my CsvUploader.py file:

import csv
from portal.models import Link
from django.db.utils import IntegrityError
from django.core.exceptions import FieldDoesNotExist
from django.db import transaction
from django.conf.urls import *

class CsvUploader:

    def __init__(self, csv_file, model_name, app_name):
        self.reader = list(csv.DictReader(csv_file, delimiter=','))
        self.keys = [k for k in self.reader[0]]
        self.model_fields = [f.name for f in  self.model._meta.get_fields()]
        self.valid = self._validate_csv()
        self.csv_pos = 0

    def _validate_csv(self):

        keys = []
        for k in self.keys:
            if k.endswith("_id"):
                keys.append(k[:-3])
            else:
                keys.append(k)
        return set(keys).issubset(self.model_fields)

    def read_chunk(self):
        chunk = []
        for i in range(1000):
            try:
                chunk.append(self.model(**self.reader[self.csv_pos]))
            except IndexError as e:
                print(e)
                break
            self.csv_pos += 1
        return chunk

    def create_records(self):

        if not self.valid:
            return "Invalid csv file"

        while True:
            chunk = self.read_chunk()

            if not chunk:
                break

            try:
                with transaction.atomic():
                    self.model.objects.bulk_create(chunk)
            except IntegrityError as e:
                for i in chunk:
                    try:
                        i.save()
                    except IntegrityError:
                        continue
                print("Exception: {}".format(e))

        return "records successfully saved!"

I'm hoping to get a second set of eyes on this that can spot the error from line 13 above:

'CsvUploader' object has no attribute 'model' Request Method: POST Request URL: http://127.0.0.1:8000/admin/portal/link/upload-csv/ Django Version: 3.2.4 Exception Type: AttributeError Exception Value:
'CsvUploader' object has no attribute 'model'

Any help is greatly appreciated!

2 Answers

The issue is you are expecting self.model to be Link model object because you calling like below in your CsvUploadAdmin :-

uploader = CsvUploader(io_string, self.model, portal)

which is not correct, you have the self.model value in model_name variable which you define in the __init__ method such as :-

def __init__(self, csv_file, model_name, app_name):

so in order to fix your issue you need to add below line you in __init__ method

self.model = model_name

so the final method will be

def __init__(self, csv_file, model_name, app_name):
    self.reader = list(csv.DictReader(csv_file, delimiter=','))
    self.keys = [k for k in self.reader[0]]
    self.model = model_name
    self.model_fields = [f.name for f in  self.model._meta.get_fields()]
    self.valid = self._validate_csv()
    self.csv_pos = 0

I think there is a simpler method to create an upload / import capability on the admin page, if I understand this post correctly.

In your Admin template, you could do something similar to the following:

from django.contrib import messages, admin
from django.contrib.contenttypes.models import ContentType
from django.http import *
from django.shortcuts import redirect, render, HttpResponseRedirect
from django.urls import include, path, reverse
from portals.models import Links
from django.conf.urls import *
from django import forms
from portals.actions import export_as_csv_action

#Extend Admin portal use
class CsvImportForm(forms.Form):
    csv_upload = forms.FileField()

@admin.register(Links)
class LinksAdmin(admin.ModelAdmin):
    actions = [export_as_csv_action("Export Selected as CSV", fields=['name', 'url'], index=False)]

    def get_urls(self):
        urls = super().get_urls()
        new_urls = [path('upload-csv/', self.upload_csv),]
        return new_urls + urls

    def upload_csv(self, request):

        if request.method == "POST":
            csv_file = request.FILES["csv_upload"]
            
            if not csv_file.name.endswith('.csv'):
                messages.warning(request, 'The wrong file type was uploaded.')
                return HttpResponseRedirect(request.path_info)
            
            file_data = csv_file.read().decode("utf-8")
            csv_data = file_data.split("\n")

            for x in csv_data:
                flds = x.split(",")
                if x:
                    Links.objects.update_or_create(
                        name = flds[0],
                        url = flds[1],                    
                        )
                
            return redirect("..")
         
        form = CsvImportForm()
        data = {"form": form}
        return render(request, "admin/csv_upload.html", data)

**Note: The above doesn't have validation, such as to ensure the same number of columns are present, etc. You would also want to do that...

Then inside your templates folder, you create a folder called admin, inside of which you create another folder called portals and a file called csv_upload.html The csv_upload.html file would be something like this:

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

{% block content %}
<div>
    <form action="." method="POST" enctype="multipart/form-data">
        {{ form.as_p }}
        {% csrf_token %}
        <button type='submit'>Upload File</button>
    </form>
    
</div>

{% endblock%}

Then inside your portals folder, you create a links folder, and inside the links folder you create a file called change_list.html, as follows:

{
  "%" "extends",
  "admin/change_list.html" "%"
}
{% load static %}
{% block content %}

<b><a href='upload-csv/'>Upload a CSV file.</a></b>

{{block.super}}

{% endblock %}

I have tested this, and it functions on my end.

Related