Dica 36 - Exportando CSV e XLSX pelo front no projeto
Versões usadas no vídeo: Django 4.1, Python 3.10, django-import-export 3.x, PyExcelerate e Tailwind CSS com o template Windster (Flowbite).
Importante: nas versões antigas desta página as tags de template apareciam com uma \ no meio ({%). Aqui elas já estão escritas do jeito certo, sem a barra.
Nas dicas anteriores vimos como exportar dados pela linha de comando: CSV com o módulo csv do Python (dica 34) e XLSX com o PyExcelerate (dica 35). Agora vamos levar isso para o front do Projeto Dicas de Django: um botão Exportar na lista de produtos abre um modal onde o usuário escolhe CSV ou XLSX, e cada opção chama uma view que gera o arquivo.
No fim da dica veremos também como exportar apenas os itens selecionados pelo Admin, usando o django-import-export.
Pré-requisitos
- O projeto das dicas anteriores (repositório rg3915/dicas-de-django), com a app
product, a lista de produtos (product_list.html) e o modal de importação da dica 30. - O model
Productcom os campostitleeprice. - O PyExcelerate instalado (dica 35):
pip install pyexcelerate
pip freeze | grep pyexcelerate >> requirements.txt
- O
django-import-exportinstalado e configurado no Admin (dica 30), com'import_export'emINSTALLED_APPS.
O botão Exportar
Na lista de produtos já existia um link Exportar que não fazia nada (<a href="">). Troque esse link por um botão que chama a função openExportModal().
Edite product/templates/product/product_list.html, no bloco dos botões do cabeçalho (<!-- START: Adicionar -->):
<!-- product/templates/product/product_list.html -->
<!-- START: Adicionar -->
<div class="flex items-center space-x-2 sm:space-x-3 ml-auto">
<a href="{% url 'product:product_create' %}" data-modal-toggle="add-user-modal" class="w-1/2 text-white bg-cyan-600 hover:bg-cyan-700 focus:ring-4 focus:ring-cyan-200 font-medium inline-flex items-center justify-center rounded-lg text-sm px-3 py-2 text-center sm:w-auto">
<svg class="-ml-1 mr-2 h-6 w-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd"></path></svg>
Adicionar
</a>
<button onclick="openImportModal()" class="w-1/2 text-gray-900 bg-white border border-gray-300 hover:bg-gray-100 focus:ring-4 focus:ring-cyan-200 font-medium inline-flex items-center justify-center rounded-lg text-sm px-3 py-2 text-center sm:w-auto">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M5.293 7.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L6.707 7.707a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
</svg>
Importar
</button>
<button onclick="openExportModal()" class="w-1/2 text-gray-900 bg-white border border-gray-300 hover:bg-gray-100 focus:ring-4 focus:ring-cyan-200 font-medium inline-flex items-center justify-center rounded-lg text-sm px-3 py-2 text-center sm:w-auto">
<svg class="-ml-1 mr-2 h-6 w-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V7.414A2 2 0 0015.414 6L12 2.586A2 2 0 0010.586 2H6zm5 6a1 1 0 10-2 0v3.586l-1.293-1.293a1 1 0 10-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L11 11.586V8z" clip-rule="evenodd"></path></svg>
Exportar
</button>
</div>
<!-- END: Adicionar -->
No fim do bloco content, junto dos outros modais, inclua o novo modal de exportação:
{% include "./includes/delete_modal.html" %}
{% include "./includes/import_modal.html" %}
{% include "./includes/export_modal.html" %}
{% endblock content %}
E no bloco js, depois do código que já abre e fecha o modal de importação, acrescente as funções do modal de exportação. Elas usam a classe Modal do Flowbite, do mesmo jeito que os modais de exclusão e de importação:
{% block js %}
<script>
// ... código dos modais de exclusão e de importação
const targetElExport = document.getElementById('export-modal')
const exportModal = new Modal(targetElExport)
openExportModal = () => {
exportModal.show()
}
closeExportModal = () => {
exportModal.hide()
}
</script>
{% endblock js %}
document.getElementById('export-modal')pega odivdo modal peloid(definido no include abaixo);new Modal(...)cria o objeto do Flowbite que sabe mostrar (show()) e esconder (hide()) o modal;openExportModalé chamada pelo botão Exportar, ecloseExportModalpelo X e pelo link Não, cancelar do modal.
O modal de exportação
Crie product/templates/product/includes/export_modal.html. É uma cópia do modal de importação, trocando o formulário de upload por dois links, um para cada formato:
<!-- includes/export_modal.html -->
<!-- export_modal.html -->
<div class="hidden overflow-x-hidden overflow-y-auto fixed top-4 left-0 right-0 md:inset-0 z-50 justify-center items-center h-modal sm:h-full" id="export-modal">
<div class="relative w-full max-w-md px-4 h-full md:h-auto">
<!-- Modal content -->
<div class="bg-white rounded-lg shadow relative">
<!-- Modal header -->
<div class="flex justify-end p-2">
<button type="button" onclick="closeExportModal()" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center" data-modal-toggle="import-modal">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</button>
</div>
<!-- Modal body -->
<div class="p-6 pt-0 text-center">
<h3 class="text-xl font-normal text-gray-500 mt-5">Exportar dados</h3>
<p class="text-base font-normal text-gray-500 mb-6">Escolha uma opção:</p>
<a href="{% url 'product:export_csv' %}" class="text-white bg-cyan-600 hover:bg-cyan-800 focus:ring-4 focus:ring-cyan-300 font-medium rounded-lg text-base inline-flex items-center px-3 py-2.5 text-center mr-2">
CSV
</a>
<a href="{% url 'product:export_xlsx' %}" class="text-white bg-cyan-600 hover:bg-cyan-800 focus:ring-4 focus:ring-cyan-300 font-medium rounded-lg text-base inline-flex items-center px-3 py-2.5 text-center mr-2">
XLSX
</a>
<a href="#" onclick="closeExportModal()" class="text-gray-900 bg-white hover:bg-gray-100 focus:ring-4 focus:ring-cyan-200 border border-gray-200 font-medium inline-flex items-center rounded-lg text-base px-3 py-2.5 text-center" data-modal-toggle="import-modal">
Não, cancelar
</a>
</div>
</div>
</div>
</div>
Os pontos importantes:
- o
id="export-modal"é o que o JavaScript usa para encontrar o modal; - o botão X e o link Não, cancelar chamam
closeExportModal(); - os links CSV e XLSX apontam para as urls
product:export_csveproduct:export_xlsx, que vamos criar agora.
As urls
Edite product/urls.py e acrescente as duas rotas no fim de product_patterns:
# product/urls.py
from django.urls import include, path
from backend.product import views as v
app_name = 'product'
# A ordem das urls é importante por causa do slug, quando existir.
product_patterns = [
# path('', v.ProductListView.as_view(), name='product_list'), # noqa E501
path('', v.product_list, name='product_list'), # noqa E501
path('create/', v.product_create, name='product_create'), # noqa E501
path('<int:pk>/', v.product_detail, name='product_detail'), # noqa E501
path('<int:pk>/update/', v.product_update, name='product_update'), # noqa E501
path('<int:pk>/delete/', v.product_delete, name='product_delete'), # noqa E501
path('import/', v.import_view, name='import_view'), # noqa E501
path('export/csv/', v.export_csv, name='export_csv'), # noqa E501
path('export/xlsx/', v.export_xlsx, name='export_xlsx'), # noqa E501
]
urlpatterns = [
path('', include(product_patterns)),
]
As views
Edite product/views.py. No topo, importe o módulo csv e o Workbook do PyExcelerate:
# product/views.py
import csv
# from django.views.generic import ListView
import openpyxl
from django.core.paginator import Paginator
from django.db.models import Q
from django.shortcuts import get_object_or_404, redirect, render
from django.views.decorators.http import require_http_methods
from pyexcelerate import Workbook
from .forms import ProductForm
from .models import Product
from .services import csv_to_list_in_memory, save_data
E no fim do arquivo escreva as duas views. O código é o mesmo que usamos no shell nas dicas 34 e 35, agora dentro de uma função que recebe o request e, no fim, redireciona de volta para a lista de produtos:
# product/views.py
def export_csv(request):
with open('/tmp/products_out.csv', 'w') as f:
csv_writer = csv.writer(f)
products = Product.objects.all()
csv_writer.writerow(['title', 'price'])
for product in products:
csv_writer.writerow([product.title, product.price])
return redirect('product:product_list')
def export_xlsx(request):
products = Product.objects.all()
data = [(product.title, product.price) for product in products]
data.insert(0, ('title', 'price'))
wb = Workbook()
wb.new_sheet("sheet name", data=data)
wb.save("/tmp/products_out.xlsx")
return redirect('product:product_list')
export_csvabre/tmp/products_out.csvpara escrita, grava o cabeçalho (title,price) e depois uma linha por produto;export_xlsxmonta uma lista de tuplas(title, price), insere o cabeçalho na posição 0, cria a planilha comwb.new_sheet(...)e salva em/tmp/products_out.xlsx.
Testando
Rode o servidor, entre em Produtos e clique em Exportar. O modal Exportar dados aparece com as opções CSV, XLSX e Não, cancelar.
Ao clicar em CSV ou XLSX a página simplesmente volta para a lista: a view não avisa onde salvou o arquivo. Ele está na pasta temporária, em /tmp/products_out.csv e /tmp/products_out.xlsx. No vídeo, a pasta /tmp é aberta no gerenciador de arquivos para conferir que os dois arquivos foram criados.
Repare que o arquivo é salvo no servidor, e não baixado pelo navegador. Para o usuário receber o arquivo, a view teria que devolver um HttpResponse com o conteúdo, mas isso não faz parte deste vídeo.
Exportar apenas itens selecionados no Admin
Na dica 30 configuramos o ImportExportModelAdmin, que coloca os botões Importar e Exportar no Admin. Mas esse Exportar exporta todos os registros. Para exportar só alguns, use também o ExportActionModelAdmin, que acrescenta uma ação na lista do Admin.
Edite product/admin.py:
# product/admin.py
from django.contrib import admin
from import_export import resources
from import_export.admin import ExportActionModelAdmin, ImportExportModelAdmin
from .models import Category, Photo, Product
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('__str__',)
search_fields = ('title',)
class PhotoInline(admin.TabularInline):
model = Photo
extra = 0
class ProductResource(resources.ModelResource):
class Meta:
model = Product
@admin.register(Product)
class ProductAdmin(ImportExportModelAdmin, ExportActionModelAdmin):
resource_classes = [ProductResource]
inlines = (PhotoInline,)
list_display = ('__str__', 'slug', 'category')
readonly_fields = ('slug',)
search_fields = ('title',)
list_filter = ('category',)
# date_hierarchy = 'created'
A única mudança é o ExportActionModelAdmin, no import e na herança de ProductAdmin.
Agora, em /admin/product/product/, marque alguns produtos, escolha a ação Exportar produtos selecionados, escolha o formato (CSV, XLSX etc.) e clique em Ir. O arquivo gerado (no vídeo, Product-2023-03-15.csv, aberto no LibreOffice Calc) traz só os produtos marcados, com todas as colunas do model: id, created, modified, title, description, price, category e slug.
Conclusão
Com um modal, duas urls e duas views, o usuário passa a exportar os produtos em CSV ou XLSX pela interface do projeto, reaproveitando o código das dicas 34 e 35. E, no Admin, o ExportActionModelAdmin permite exportar apenas os registros selecionados. Esta é a última aula da série do Projeto Dicas de Django; o código completo está no repositório rg3915/dicas-de-django.