Import and export
The current view out as CSV, Excel or JSON — and back in again, with every bad cell reported at once and nothing written unless the whole file parses.
Available since v0.2.0
Export
On by default. The list page offers CSV, Excel and JSON of the current view — filters, search and ordering applied — so the file matches the table it came from.
class ProductAdmin(admin.ModelAdmin):
exportable = True # the default
export_fields = ("sku", "name", "category", "price", "stock")
export_fields defaults to list_display. Widen it to include columns that are
on the record but too many to put in a table; narrow it to keep something out of
a file.
exportable = False # a model whose rows should not leave the admin in a file
The two limits
FastFortSettings(admin={"export_limit": 50_000, "export_chunk_size": 1_000})
An export runs the current query with no pagination. Without a cap, a mis-clicked export everything on a table of ten million rows is a request that never finishes and a database that stops answering anyone else.
Rows stream in chunks rather than assembling in memory. The JSON writer builds
the document record by record for the same reason — json.dumps on the whole
list would need all of it resident first.
Geometry in an export
A list cell summarises a geometry: Polygon · 14 points. An export writes the
same text the form control shows — a point as lat, lng, everything else as
EWKT.
The difference is deliberate. A summary is the right thing to read in a table and a thing no importer can turn back into a polygon, and the promise import makes is that a file this wrote is a file it can take back.
Import
Off by default, and the asymmetry is the point: reading rows out is a permission the gate already grants; writing several thousand of them in one request is a different thing to hand somebody by accident. A model whose rows have side effects — an order that ships, a message that sends — should say so deliberately rather than inherit it.
class ProductAdmin(admin.ModelAdmin):
importable = True
import_fields = ("sku", "name", "price", "cost", "stock", "availability")
This adds /admin/catalog.product/import and a download a template link beside
it — a file with the right headers and one example row, so the first attempt is
not a guess.
import_fields never widens
FieldSpec.editable is checked first. Naming a read-only field here does not
make it writable; the entry is dropped.
Narrowing is what it is for: a price list that may update prices and stock but never the supplier. Without it, the default is every editable field minus the ones no file can carry — passwords, uploads, binary columns and anything the spec marks sensitive.
What it does with a file
- Parses the whole thing using the same parsers the form uses. A date is a
date the same way; a geometry goes through the same
geo.parse. - Resolves foreign keys by name or id.
categorymay be12orCables; the lookup uses the target admin’s ownsearch_fields. - Reports every bad cell at once, with its line number:
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.
- Writes nothing unless the whole file parses. Half an import is worse than no import, because the operator now has to work out which half.
No pandas, no openpyxl on the read path
CSV is read with the standard library. Excel is read by unzipping the workbook
and walking the sheet XML — openpyxl is only needed to write one, and only if
you want the .xlsx export.
A framework that pulls in pandas for a feature most projects use twice a year is one that has stopped being a dependency you can justify.
The whole thing, on one model
@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")
Filter to backorder, export, edit the stock column in a spreadsheet, upload it back. The file that came out is the file that goes in.