Django как загрузить CSV файл с помощью формы для заполнения базы данных postgres и отображения всех элементов в браузере

Django 3.2.1, Python 3.6, база данных Postgres

Я пишу небольшое Django приложение, которое будет использовать браузер для импорта товаров из загруженного csv файла через Form и заполнения базы данных.

В настоящее время я настроил приложение так, что пользователи могут вручную добавлять, редактировать или удалять отдельные продукты. Эти данные сохраняются в базе данных должным образом.

Однако мне также нужна возможность, при которой пользователь может загрузить csv со многими тысячами продуктов, используя Django Form. Я написал логику бэкенда для приложения с помощью Custom Management Commands и прочитал документы , но мне все еще неясно, как достичь этого с помощью Forms, сохранив полную функциональность.

Я перепробовал множество вариантов использования форм и прошел учебники на сайте Django, но не смог добиться успеха в том, чтобы файл загружался через Form, правильно разбирал все строки, сохранялся в базе данных и отображал все новые сохраненные товары в /show_products наряду с товарами, которые были добавлены вручную.

Пример файла csv:

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

Вот пример моего внутреннего кода, который может загружать локальный файл csv, анализировать его, обновлять только определенные поля, если SKU уже существует, создавать статус 'активный/неактивный', и печатать после каждого сохранения, чтобы пользователь мог видеть, что происходит.

/products/management/commands/import_products.py

class Command(BaseCommand):

    def handle(self, *args, **options):

        print('Importing data from:', settings.DATA_IMPORT_LOCATION)

        with open(f'{settings.DATA_IMPORT_LOCATION}/products.csv', '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]

                # Check if sku already exists if this just needs to be updated
                if Product.objects.filter(sku=sku).exists():
                    sku_id = Product.objects.only('id').get(sku=sku).id
                    p = Product(id=sku_id)
                    p.name = name
                    p.description = description
                    p.status = random.choice(['active', 'inactive'])
                    p.save(update_fields=['name', 'description', 'status'])
                    print(p)

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

Вот пример формы, позволяющей выполнять отдельные product CRUD-операции

forms.py

class ProductForm(forms.ModelForm):

    name = forms.CharField(widget= forms.TextInput(attrs={'class':'form-control','placeholder':'Enter name'}))
    sku = forms.CharField(widget= forms.TextInput(attrs={'class':'form-control','placeholder':'Enter sku'}))
    description = forms.CharField(widget= forms.TextInput(attrs={'class':'form-control','placeholder':'Enter description'}))

    radiobutton = [('active','Active'),('inactive','Inactive')]
    status = forms.ChoiceField(widget=forms.RadioSelect, choices=radiobutton)

    class Meta:
        model = Product
        fields = '__all__'

Пример создания новых продуктов в браузере:

products/product/templates/create

    <form method="POST" class="post-form" action="/create_product" enctype="multipart/form-data">
        {% csrf_token %}

        <div class="container">
            <div class="form-group row">
                <label class="col-sm-1 col-form-label"></label>
                <div class="col-sm-4">
                    <h3>Product Details</h3>
                </div>

            <div class="form-group row">
                <label class="col-sm-2 col-form-label">Product Name:</label>
                <div class="col-sm-4">
                    {{ form.name }}
                </div>

            <div class="form-group row">
                <label class="col-sm-1 col-form-label"></label>
                <div class="col-sm-4">
                    <button type="submit" class="btn btn-primary">Submit</button>
                </div>

И views.py показывает show_products и update методы:

def show_product(request):
    product = Product.objects.all()
    return render(request, 'index.html', {'product':product})

def update_product(request, id):
     product = Product(id=id)
     if request.method == "POST" :
        form = ProductForm(request.POST, request.FILES, instance=product)
        if form.is_valid():
            form.save()
            # If update succeeds
            messages.success(request, 'Update successful!')
            # Redirect to show all products
            return redirect("/show_products")

        # If update fails
        message = 'Something went wrong!'
        return render(request, 'edit.html', {'message': message, 'product': form})
     else:
         form = Product.objects.get(id=id)
         product = ProductForm(instance=form)
         content = {'product': product, 'id': id}
         return render(request, 'edit.html', content)

Есть еще много кода, который я могу опубликовать, если это поможет, просто пытаюсь сохранить этот вопрос кратким!

Для сохранения CSV файлов вы создадите функцию для чтения csv файла и сохранения информации о продукте: но вы также можете рефакторить код в соответствии с вашими требованиями.

  • Сначала загрузите и сохраните файл с помощью Product()
  • Получите путь к файлу и прочитайте его содержимое. Будет лучше, если у вас будут одинаковые имена для полей модели и столбцов csv
  • .
  • Пройдитесь по каждой строке и создайте словарь, который содержит только детали продукта на итерации
  • Создайте экземпляр Product(), передайте ему словарь и сохраните
  • .
  • Для внешнего ключа получите объект из Product() с помощью get() соответственно со значением, которое хранится в csv
  • .
# You could save the Product details in two ways

new_product = Product()
new_product.registration_number = fields[0]
new_product.name = fields[1]
# 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
                new_product_details['product'] = 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.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')

Благодаря этому посту SO я смог найти ответ, используя генератор для расшифровки 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')
Вернуться на верх