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')