Импорт и экспорт

Текущее представление — на выход в виде CSV, Excel или JSON, и обратно на вход: обо всех некорректных ячейках сообщается сразу, а запись происходит только если файл разбирается целиком.

Доступно с версии v0.2.0

Экспорт

Включён по умолчанию. Страница списка предлагает CSV, Excel и JSON текущего представления — с учётом применённых фильтров, поиска и сортировки, — поэтому файл соответствует таблице, из которой он получен.

class ProductAdmin(admin.ModelAdmin):
    exportable = True                                   # the default
    export_fields = ("sku", "name", "category", "price", "stock")

По умолчанию export_fields равен list_display. Расширьте его, чтобы включить столбцы, которые есть в записи, но которых слишком много для таблицы; сузьте — чтобы что-то не попало в файл.

exportable = False    # a model whose rows should not leave the admin in a file

Два ограничения

FastFortSettings(admin={"export_limit": 50_000, "export_chunk_size": 1_000})

Экспорт выполняет текущий запрос без пагинации. Без ограничения случайный клик по экспортировать всё на таблице из десяти миллионов строк превращается в запрос, который никогда не завершится, и в базу данных, которая перестаёт отвечать кому-либо ещё.

Строки передаются потоково, чанками, а не собираются в памяти целиком. По той же причине JSON-writer формирует документ запись за записью — вызов json.dumps на всём списке потребовал бы, чтобы весь он сначала находился в памяти.

Геометрия в экспорте

Ячейка списка отображает геометрию кратко: Polygon · 14 points. Экспорт записывает тот же текст, что показывает элемент управления формы: точку — как lat, lng, всё остальное — в формате EWKT.

Это различие сделано намеренно. Краткое описание — то, что удобно читать в таблице, но никакой импортёр не сможет превратить его обратно в полигон, а импорт обещает, что файл, который система записала, она же сможет принять обратно.

Импорт

Выключен по умолчанию, и в этой асимметрии весь смысл: чтение строк — это разрешение, которое уже даёт шлюз доступа; запись нескольких тысяч строк за один запрос — совсем другая вещь, которую нельзя дать кому-то случайно. Модель, строки которой вызывают побочные эффекты — заказ, который отправляется, сообщение, которое рассылается, — должна объявить это явно, а не унаследовать по умолчанию.

class ProductAdmin(admin.ModelAdmin):
    importable = True
    import_fields = ("sku", "name", "price", "cost", "stock", "availability")

Это добавляет /admin/catalog.product/import и рядом с ним ссылку скачать шаблон — файл с правильными заголовками и одной примерной строкой, чтобы первая попытка не была угадыванием.

import_fields никогда не расширяет доступ

FieldSpec.editable проверяется первым. Указание здесь поля, доступного только для чтения, не делает его доступным для записи — такая запись отбрасывается.

Он нужен именно для сужения: прайс-лист может обновлять цены и остатки, но никогда — поставщика. Без него по умолчанию используются все доступные для записи поля, кроме тех, что не может нести файл: пароли, загружаемые файлы, бинарные столбцы и всё, что спецификация помечает как чувствительное.

Что происходит с файлом

  1. Разбирает файл целиком, используя те же парсеры, что и форма. Дата остаётся датой точно так же; геометрия проходит через тот же geo.parse.
  2. Разрешает внешние ключи по имени или id. category может быть 12 или Cables; поиск использует собственные search_fields целевой админки.
  3. Сообщает обо всех некорректных ячейках сразу, с указанием номера строки:
Row 4, price: "12,50" is not a number. Use a full stop for the decimal point.
Row 4, availability: "in stock" is not one of: in_stock, low_stock,
       backorder, discontinued.
Row 9, category: no Category matches "Cabels".
Row 11, sku: a Product with this sku already exists.

4 problems. Nothing was written.
  1. Ничего не записывает, если файл не разобран целиком. Наполовину выполненный импорт хуже, чем отсутствие импорта, потому что оператору придётся разбираться, какая половина была выполнена.

Ни pandas, ни openpyxl на пути чтения

CSV читается стандартной библиотекой. Excel читается путём распаковки книги и обхода XML листа — openpyxl нужен только для записи, и только если вам нужен экспорт в .xlsx.

Фреймворк, который тянет за собой pandas ради функции, которой большинство проектов пользуются пару раз в год, — это фреймворк, переставший быть зависимостью, которую можно оправдать.

Всё вместе, на одной модели

@admin.register(Product, key="catalog.product")
class ProductAdmin(admin.ModelAdmin):
    list_display = ("sku", "name", "category", "price", "stock", "availability")
    list_filter = ("availability", "is_active", "category")
    search_fields = ("sku", "name")

    exportable = True
    export_fields = ("sku", "name", "category", "supplier", "price", "cost", "stock")

    importable = True
    import_fields = ("sku", "name", "price", "cost", "stock", "availability")

Отфильтруйте по backorder, экспортируйте, отредактируйте столбец остатков в таблице, загрузите обратно. Файл, который вышел, — тот же файл, что заходит обратно.