Импорт данных на сайт - это частый функционал, который требуется множеством пользователей. В качестве формата файла, для импорта, используется Excel или CSV. Самое частое требование к задаче - это предварительная аутентификация пользователя или выполнение подобного импорта только через панель администратора. В Django, по умолчанию, нет возможности подобного импорта, но вы можете его реализовать не встречая особых сложностей руководствуясь примерами описанными ниже.
Варианты реализации импорта
В статье будет рассмотрена возможность реализации импорта в Django используя как свое решение, так и стороннего модуля. Оба варианта работают через панель администрирования и для аутентифицированных пользователей.
Формат данных, используемый в примерах в статье, CSV. Преимущества CSV над остальными форматами в том, что он поддерживается в качестве экспорта в большинстве программ (SQL/1C/Excel). Сам синтаксис формата простой.
Формат файлов, которые создают программы типа Excel и OpenOffice, для импорта подходят плохо. Каждая из этих программ использует разные форматы файлов, а их импорт может привести к серьезным ошибкам.
В статье используется Django 4.1, но для старых версий так же все должно подойти. Если вы используете Django < 3.0, то обратите внимание на BASE_DIR.
Тестовый проект
Если вы хотите посмотреть на работу примеров локально, то можете создать аналогичный тестовый проект используя код ниже.
# создаем проект
django-admin startproject csvimport
# переходим в проект и создаем приложение
cd csvimport
python manage.py startapp learn
После создания приложения мы должны добавить его в конфигурацию Django проекта. Для этого нужно открыть файл 'csvimport/settings.py' и отредактировать строку 'INSTALLED_APPS', добавив в нее полное название созданного приложения.
INSTALLED_APPS = [
.....
'django.contrib.messages',
# новая строка
'learn.apps.LearnConfig',
]
Для примеров создана модель с названием 'Book'. Для этой модели и будет выполняться импорт. Модель прописывается в файле 'learn/models.py'.
from django.db import models
class Book(models.Model):
name = models.CharField(max_length=150)
author = models.CharField(max_length=150)
publish_date = models.DateField()
Чтобы эта модель отображалась в панели администрирования - ее нужно указать в файле 'learn/admin.py':
from django.contrib import admin
from .models import Book
@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
list_display = ('name', 'author', 'publish_date',)
Для создания таблицы в базе данных, на основе модели выше, нужно выполнить миграции. Кроме этого нужно создать пользователя, который сможет заходить в панель администрирования.
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser
Реализация импорта через свою функцию
Самостоятельно реализовать импорт можно через следующие шаги:
- Создание формы, через которую будет выполняться импорт;
- Создание двух шаблонов (за счет расширения и редактирования существующих шаблонов панели администрирования). Один шаблон будет содержать ссылку на страницу с формой, а второй шаблон - саму форму;
- Создание функции, которая обработает данные и добавит их в базу.
Проект с кодом ниже можно так же посмотреть в моем репозитории на GitHub.
Создание формы
Чтобы хранить историю загруженных файлов и иметь возможность поиска ошибок - я добавил еще одну модель в 'learn/models.py'. Это необязательно, но полезно и используется в некоторых следующих примерах.
...
class BookImport(models.Model):
csv_file = models.FileField(upload_to='uploads/')
date_added = models.DateTimeField(auto_now_add=True)
И выполнил миграцию:
python manage.py makemigrations
python manage.py migrate
Форма, через которую будет загружаться файл, будет привязана к этой модели. Форма объявляется в файле 'learn/forms.py', который создается самостоятельно.
from django.forms import ModelForm
from .models import BookImport
class BookImportForm(ModelForm):
class Meta:
model = BookImport
fields = ('csv_file',)
Создание шаблона
Место, где будут храниться шаблоны, прописывается в файле 'csvimport/settings.py'. Для этого нужно изменить значение в переменной 'TEMPLATES', а именно значение 'DIRS'. У меня оно следующее (в старых версиях Django, а именно младше 3.0, значение BASE_DIR менялось иначе).
'DIRS': [BASE_DIR / 'templates'],
Шаблоны панели администрирования Django работают так же как и обычные, но находятся в папке с самим модулем (папка site-packages/django). Мы можем заменить их, отредактировать или расширить просто разместив по определенному пути в папке 'templates'.
Для работы формы импорта мы создадим новую страницу (шаблон) по следующему пути 'templates/admin/csv_import_page.html'. Эта страница расширяет функционал обычной страницы панели администратора дополнительно выводя форму. В этом файле находится следующий код.
{% extends 'admin/base.html' %}
{% block content %}
<div>
<form action="." method="POST" enctype="multipart/form-data">
{{ form.as_p }}
{% csrf_token %}
<button type="submit">Загрузка CSV</button>
</form>
</div>
{% endblock %}
Что бы пользователь мог попасть на страницу с формой - для него нужно создать кнопку. Пример ниже создает кнопку "Импорт" на странице, где выводятся все объекты из модели 'Book' приложения 'learn'. Такой тип страниц называется 'change_list.html'. Код, соответственно, находится по следующему пути 'templates/admin/learn/book/change_list.html'. В этом файле находится следующий код.
{% extends 'admin/change_list.html' %}
{% load static %}
{% block object-tools-items %}
<li>
<a href="csv-upload/" class="addlink">
Импорт
</a>
</li>
{{ block.super }}
{% endblock %}
Шаблоны, которые были созданы выше, будут отображаться следующим образом. Вы сможете увидеть их работу после создания метода.
Создание метода
Теперь нам нужно создать метод, который будет отправлять пользователю сгенерированный шаблон с формой и получать с формы данные. Один из вариантов реализации - сделать это в файле 'learn/admin.py', в классе для нужной модели.
# обычные модули для панели администрирования
from django.contrib import admin
from .models import Book, BookImport
# обслуживание импорта
import csv
from .forms import BookImportForm
from django.urls import path
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse
from django.contrib import messages
# отображает панель для модели BookImport
@admin.register(BookImport)
class BookImportAdmin(admin.ModelAdmin):
list_display = ('csv_file', 'date_added')
# отображает панель для модели Book и метод для импорта
@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
list_display = ('name', 'author', 'publish_date',)
# даем django(urlpatterns) знать
# о существовании страницы с формой
# иначе будет ошибка
def get_urls(self):
urls = super().get_urls()
urls.insert(-1, path('csv-upload/', self.upload_csv))
return urls
# если пользователь открыл url 'csv-upload/'
# то он выполнит этот метод
# который работает с формой
def upload_csv(self, request):
if request.method == 'POST':
# т.к. это метод POST проводим валидацию данных
form = BookImportForm(request.POST, request.FILES)
if form.is_valid():
# ...
# какая-то ваша реализация обработки формы
# ...
# возвращаем пользователя на главную с сообщением об успехе
url = reverse('admin:index')
messages.success(request, 'Файл успешно импортирован')
return HttpResponseRedirect(url)
# если это не метод POST, то возвращается форма с шаблоном
form = BookImportForm()
return render(request, 'admin/csv_import_page.html', {'form': form})
В целом у вас могут возникнуть вопросы к двум методам:
- get_urls() - это метод используется, что бы уведомить Django о новой странице в панели администрирования. Иначе у нас будет появляться ошибка.
- upload_csv() - это обычный метод обработки формы, который вы так же можете реализовать во views.py и использовать наследование. Мы либо обрабатываем метод POST либо возвращаем саму форму.
Метод 'upload_csv()', выше, показан не полностью т.к. именно отсутствующая часть будет отличаться во всех проектах. Например, у вас может использоваться разделить либо запятая, либо точка с запятой и т.д.
Для тестового проекта, который создавался в начале, можно использовать следующий вариант 'upload_csv'. Он, например, проверяет, что в файле "csv" указаны верные заголовки в виде 'name,author,publish_date' и в качестве разделителя использует запятую.
def upload_csv(self, request):
if request.method == 'POST':
form = BookImportForm(request.POST, request.FILES)
if form.is_valid():
# сохраняем загруженный файл и делаем запись в базу
form_object = form.save()
# обработка csv файла
with form_object.csv_file.open('r') as csv_file:
rows = csv.reader(csv_file, delimiter=',')
if next(rows) != ['name', 'author', 'publish_date']:
# обновляем страницу пользователя
# с информацией о какой-то ошибке
messages.warning(request, 'Неверные заголовки у файла')
return HttpResponseRedirect(request.path_info)
for row in rows:
print(row[2])
# добавляем данные в базу
Book.objects.update_or_create(
name=row[0],
author=row[1],
publish_date=row[2]
)
# конец обработки файлы
# перенаправляем пользователя на главную страницу
# с сообщением об успехе
url = reverse('admin:index')
messages.success(request, 'Файл успешно импортирован')
return HttpResponseRedirect(url)
form = BookImportForm()
return render(request, 'admin/csv_import_page.html', {'form': form})
Вы можете создать файл CSV со следующими данными и попробовать его импортировать через панель администрирования модели 'Book'.
name,author,publish_date
Book1,Author1,2022-12-21
Book2,Author2,2022-12-22
Book3,Author3,2022-12-23
Book4,Author4,2022-12-24
В случае успеха вы увидите изменения на 3 страницах:
- Переадресация на главную страницу с сообщением об успехе;
- Загруженный файл в модели 'BookImport';
- Загруженные данные в модели 'Book'.
В варианте выше проверяются только заголовки (первая строка в CSV). Если эта строка будет неверной, то вы сможете увидеть информацию о проблеме и посмотреть сам файл в 'BookImport'.
Реализация CSV импорта через сторонний модуль
Для Django есть популярный модуль, имеющий более 2600 звезд на github, под названием 'django-import-export'. Его преимущество в том, что в нем реализован не только импорт в формате CSV, но так же YAML, XML и т.д. Кроме этого он содержит возможность оптимизации транзакций и логов, что удобно для случаев, когда вы импортируется большой объем данных. Так же есть предварительный просмотр импорта, экспорт и другие возможности...
Первое, что мы должны сделать - установить модуль.
pip install django-import-export
Установленный модуль мы должны добавить в 'INSTALLED_APPS' по пути 'csvimport/settings.py'.
INSTALLED_APPS = [
...
'import_export',
]
В этом же файле, setting.py, мы должны прописать путь 'STATIC_ROOT'. Эта папка, в которую будут собираться все статические файлы. Это нужно, если вы используете боевой сервер. Переменная для папки, часто, имеет следующий вид (для Django 3.0+).
STATIC_ROOT = BASE_DIR / 'static'
После создания папки - мы должны выполнить сбор всех статических файлов.
python manage.py collectstatic
Самый простой способ создать импорт - это объявить следующие классы в файле 'learn/admin.py'.
from django.contrib import admin
from .models import Book
from import_export import resources
from import_export.admin import ImportExportModelAdmin
# класс обработки данных
class BookResource(resources.ModelResource):
class Meta:
model = Book
# вывод данных на странице
class BookAdmin(ImportExportModelAdmin):
resource_classes = [BookResource]
admin.site.register(Book, BookAdmin)
Используя следующие данные вы можете попробовать выполнить импорт.
name,author,publish_date
Book1,Author1,2022-12-21
Book2,Author2,2022-12-22
Book3,Author3,2022-12-23
Book4,Author4,2022-12-24
Результат будет следующий.
Дополнительные возможности
У модуля есть достаточно много возможностей, которые часто требуются. Ниже описана только часть из них.
Через класс 'BookResource', который был создан в примере выше, можно выполнять не только импорт, но и экспорт. Импорт и экспорт можно выполнять и в обычных функциях. Пример с shell.
python manage.py shell
...
from learn.admin import BookResource
data = BookResource().export()
print(data.csv)
По умолчанию дубликаты, которые могут присутствовать при импорте, не проверяются. Чтобы исключить значения, которые и так присутствуют в таблице, вы можете использовать следующие атрибуты:
- skip_unchanged - будут ли пропускаться строки (row), которые уже присутствуют в базе;
- report_skipped - вывод списка пропущенных строк.
По умолчанию идет проверка по полю 'id'. Учитывая, что идентификатор может не быть в файле импорта, можно указать другое значение. В примере ниже проверка будет проходить по названию 'name'.
# learn/admin.py
...
class BookResource(resources.ModelResource):
class Meta:
model = Book
import_id_fields = ('name',)
skip_unchanged = True
report_skipped = True
Если в вашем файле CSV больше данных, чем требуется, то можно исключить их при импорте. В следующем примере будет импортирована только колонка 'name' т.к. остальные исключены.
# learn/admin.py
...
class BookResource(resources.ModelResource):
class Meta:
model = Book
exclude = ('author', 'publish_date')
...