Django 如何使用 Form 上传 CSV 文件来填充 postgres 数据库并在浏览器中显示所有项目

Django how to upload CSV file using Form to populate postgres database and display all items in browser

Django 3.2.1,Python3.6,Postgres 数据库

编辑以考虑下面的评论,谢谢!

我正在编写一个用于存储产品信息的小型 Django 应用程序。我编写了使用 Custom Management Command 上传本地 csv 文件的后端逻辑,并将其连接到前端。

我在执行文件上传时遇到问题 -> 让用户通过 Form 提交上传 products.csv 以用文件填充数据库并在一页上显示所有产品。

我已将我之前的示例以及下面建议的代码精简为最简单的格式,以尝试找出问题所在。

csv 文件示例:

name,sku,description
Brian James,skus-look-like-this,The products will have various descriptions. And multiple lines too.

models.py

class Product(models.Model):
    name = models.CharField(max_length=500)
    sku = models.CharField(max_length=500)
    description = models.TextField(blank=False, null=False)
    status = models.TextField(blank=False, null=False, default='inactive')

    class Meta:
        db_table = 'product'

用于个人 product CRUD 操作和 CSV 文件上传的表单。

forms.py

class UploadForm(forms.Form):
    csv_file = forms.FileField(required=False, widget=forms.FileInput(attrs={'class': 'form-control', 'placeholder':
        'Upload "products.csv"', 'help_text': 'Choose a .csv file with products to enter'}))

/templates/upload.html

<form method="post" enctype="multipart/form-data">
        {% csrf_token %}
    <input type="file" name="sent_file" />
    <input type="submit" name="submit" value="Upload" />
</form>

views.py

# Function to upload the form, parse it, save to database
def create_upload(request):
    if request.method == 'GET':
        form = UploadForm()
        return render(request, 'upload.html', {'form': form})

    # If not GET method then proceed
    form = UploadForm(request.POST, request.FILES)
    print('FIRST FORM', form)

    # Validate the form
    if form.is_valid():
            csv_file = form.cleaned_data['csv_file']
            # Errors begin here ^, print(csv_file) = 'None'

            form.save()
            # Crashes here ^ with error: "AttributeError: 'UploadForm' object has no attribute 'save'
"
            file_path = os.path.join(BASE_DIR, form.csv_file.url)
            # printing `file_path` = `AttributeError: 'InMemoryUploadedFile' object has no attribute 'url'
`
        
            # read the file contents and save the product details
            with open(f'{file_path}, r') as products_csv:

            products_file = csv.reader(products_csv)
            next(products_file)  # skip header row

            for counter, line in enumerate(products_file):

                name = line[0]
                sku = line[1]
                description = line[2]

                p = Product()
                p.name = name
                p.sku = sku
                p.description = description
                p.status = random.choice(['active', 'inactive'])
                p.save()

    return redirect('/show_product')
          

form.cleaned_data['csv_file'] 更改为 request.FILES['sent_file'] 可以正确打印文件名 uploads.csv,但是 url 仍然无法访问并且仍然在 form.save() 上崩溃。我可以将上传文件的内容打印到终端的唯一方法是添加以下内容:

csv_file = request.FILES['sent_file']
for i in csv_file:
    print(i)

输出:

b"'name','sku','description'\n"
b"'Zed','some-skus-more','descriptions. galore.'\n"

但是文件还是无法上传,form.save()无法执行

我不确定如何继续调试它。如果有人能指出我正确的方向,我将不胜感激!

为了保存 CSV 文件,您将创建一个函数来读取 csv 文件并保存产品详细信息:但您也可以重构代码以满足您的需求。

  • 首先使用 Product() 上传并保存文件
  • 获取文件路径并读取内容 如果模型字段和 csv 列的名称相同会​​更好
  • 遍历每一行并创建一个在迭代中仅包含产品详细信息的字典
  • 创建 Product() 实例并将字典传递给它并保存
  • 对于外键,根据存储在 csv 中的值使用 get() 从 Product() 中获取对象
# You could save the Product details in two ways

new_product = Product()
new_product.registration_number = fields[0]
new_product.customer_name = fields[1] # Change this field name the customer
# like so for other fields

new_product.save()
.....
# Create a model object, create a dictionary of key values where keys corresponds to the field names of the model.

# create a dictionary `new_product_details` containing values of a product

new_product = Product()
new_product.__dict__.update(new_product_details)
new_product.save()

import csv
def save_new_product_from_csv(file_path):
    # do try catch accordingly
    # open csv file, read lines
    with open(file_path, 'r') as fp:
        products = csv.reader(fp, delimiter=',')
        row = 0
        for product in products:
            if row==0:
                headers = product
                row = row + 1
            else:
                # create a dictionary of product details
                new_product_details = {}
                for i in range(len(headers)):
                    new_product_details[headers[i]] = product[i]

                # for the foreign key field you should get the object first and reassign the value to the key
                #We will also have to change the product name to customer_name get the foreign key value
                new_product_details['customer_name'] = Product.objects.get() # get the record according to value which is stored in db and csv file

                # create an instance of product model
                new_product = Product()
                new_product.__dict__.update(new_product_details)
                new_product.save()
                row = row + 1
        fp.close()

你的代码应该看起来像这样:

def uploadcsv(request):
    if request.method == 'GET':
        form = UploadForm()
        return render(request, 'upload.html', {'form':form})

    # If not GET method then proceed
    try:
        form = UploadForm(data=request.POST, files=request.FILES)
        if form.is_valid():
            csv_file = form.cleaned_data['csv_file']
            if not csv_file.customer_name.endswith('.csv'):
                messages.error(request, 'File is not CSV type')
                return redirect('/show_product')
            # If file is too large
            if csv_file.multiple_chunks():
                messages.error(request, 'Uploaded file is too big (%.2f MB)' %(csv_file.size(1000*1000),))
                return redirect('/show_product')

            # save and upload file 
            form.save()

            # get the path of the file saved in the server
            file_path = os.path.join(BASE_DIR, form.csv_file.url)

            # a function to read the file contents and save the product details
            save_new_product_from_csv(file_path)
            # do try catch if necessary
                
    except Exception as e:
        logging.getLogger('error_logger').error('Unable to upload file. ' + repr(e))
        messages.error(request, 'Unable to upload file. ' + repr(e))
    return redirect('/show_product')

像这样向您的模型添加客户名称属性:

class Product(models.Model):
    customer_name = models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.CASCADE)
    name = models.CharField(max_length=500)
    sku = models.CharField(max_length=500)
    description = models.TextField(blank=False, null=False)
    status = models.TextField(blank=False, null=False, default='inactive')

    class Meta:
        db_table = 'product'

感谢这个 SO post 我能够通过使用生成器逐行解码 CSV 找到答案。

代码如下: views.py

def decode_utf8(line_iterator):
    for line in line_iterator:
        yield line.decode('utf-8')


    
def create_upload(request):
    if request.method == 'GET':
        form = UploadForm()
        return render(request, 'upload.html', {'form': form})

    form = UploadForm(request.POST, request.FILES)

    # Validate the form
    if form.is_valid():

        # Get the correct type string instead of byte without reading full file into memory with a generator to decode line by line
        products_file = csv.reader(decode_utf8(request.FILES['sent_file']))
        next(products_file)  # Skip header row

        for counter, line in enumerate(products_file):
            name = line[0]
            sku = line[1]
            description = line[2]

            p = Product()
            p.name = name
            p.sku = sku
            p.description = description
            p.save()

        messages.success(request, 'Saved successfully!')

        return redirect('/show_product')