Django 4中下载生成CSV文件同时传递模板上下文的实现方案
解决思路:同时实现CSV下载与处理状态提示
在Django中,同一个HTTP请求只能返回一个响应,所以你没法在返回CSV下载的同时直接把处理状态传递给模板渲染。下面提供三种可行的实现方案,按需选择:
方案一:AJAX预请求 + 触发下载
先通过AJAX请求获取处理状态,再触发CSV下载,能实现实时反馈,用户体验最好。
后端修改
from django.views.decorators.csrf import csrf_exempt from django.utils.decorators import method_decorator from django.core.cache import cache from django.http import JsonResponse import uuid from io import StringIO class InvoiceView(TemplateView): model = Cost template_name = "pipeline/invoices_list.html" costs = Cost.objects.all() def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # 原有上下文逻辑 return context @method_decorator(csrf_exempt) def dispatch(self, *args, **kwargs): return super().dispatch(*args, **kwargs) def post(self, request, *args, **kwargs): # 第一步:请求处理状态与缓存CSV if "batch-pay-status" in request.POST: csv_content, batch_payment_status = create_batch_payment_file(self.costs) # 生成唯一缓存键,存储CSV内容(5分钟过期) csv_key = f"batch_csv_{uuid.uuid4().hex}" cache.set(csv_key, csv_content, timeout=300) return JsonResponse({ "processing_status": batch_payment_status, "csv_key": csv_key }) # 第二步:根据缓存键下载CSV elif "batch-pay-download" in request.POST: csv_key = request.POST.get("csv_key") csv_content = cache.get(csv_key) if not csv_content: return JsonResponse({"error": "文件已过期或不存在"}, status=400) response = HttpResponse( csv_content, content_type='text/csv', headers={'Content-Disposition': 'attachment; filename="WISE_BATCH_PAYMENT.csv"'}, ) cache.delete(csv_key) # 下载后清理缓存 return response return super().post(request, *args, **kwargs) # 修改utils.py中的函数,返回CSV字符串而非HttpResponse def create_batch_payment_file(costs): invoices = costs.filter(invoice_status__in=["REC", "REC2"]) processing_status = {} csv_buffer = StringIO() # 这里写CSV写入逻辑,例如: # import csv # writer = csv.writer(csv_buffer) # writer.writerow(["PO编号", "金额", "状态"]) # for invoice in invoices: # # 处理发票数据,写入行 # processing_status[invoice.po_number] = {"status": "success", "message": "已添加"} return csv_buffer.getvalue(), processing_status
前端模板修改
<button id="batch-pay-btn">生成批量付款文件</button> <!-- Toast提示组件 --> <div class="toast position-fixed bottom-0 end-0 m-3" id="processing-toast" role="alert" aria-live="assertive" aria-atomic="true"> <div class="toast-header"> <strong class="me-auto">处理状态</strong> <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button> </div> <div class="toast-body"></div> </div> <script> document.getElementById('batch-pay-btn').addEventListener('click', async function() { // 1. 请求处理状态与缓存键 const statusRes = await fetch(window.location.href, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRFToken': '{{ csrf_token }}' }, body: 'batch-pay-status=1' }); const result = await statusRes.json(); // 2. 显示处理状态Toast let toastHtml = ''; for (const [poNum, status] of Object.entries(result.processing_status)) { toastHtml += `<p>${poNum}: ${status.status} - ${status.message}</p>`; } const toastEl = document.getElementById('processing-toast'); toastEl.querySelector('.toast-body').innerHTML = toastHtml; new bootstrap.Toast(toastEl).show(); // 3. 动态生成表单触发下载 const downloadForm = document.createElement('form'); downloadForm.method = 'POST'; downloadForm.action = window.location.href; downloadForm.innerHTML = ` <input type="hidden" name="batch-pay-download" value="1"> <input type="hidden" name="csv_key" value="${result.csv_key}"> <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}"> `; document.body.appendChild(downloadForm); downloadForm.submit(); document.body.removeChild(downloadForm); }); </script>
方案二:Session存储状态 + 页面刷新
实现最简单,依赖Django的Session存储状态,下载后刷新页面读取状态。
后端修改
import json class InvoiceView(TemplateView): model = Cost template_name = "pipeline/invoices_list.html" costs = Cost.objects.all() def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # 从Session读取状态并清空 if self.request.session.get('show_batch_status'): context['batch_payment_status'] = self.request.session.pop('batch_payment_status', {}) self.request.session.pop('show_batch_status') return context def post(self, request, *args, **kwargs): if "batch-pay-csv" in request.POST: response, batch_payment_status = create_batch_payment_template(self.costs) # 状态存入Session request.session['batch_payment_status'] = batch_payment_status request.session['show_batch_status'] = True # 下载后自动刷新页面 response['Refresh'] = f'0; url={request.path}' return response return super().post(request, *args, **kwargs)
前端模板修改
<form method="POST" action=""> {% csrf_token %} <button type="submit" name="batch-pay-csv">生成批量付款文件</button> </form> <!-- Toast提示组件 --> <div class="toast position-fixed bottom-0 end-0 m-3" id="processing-toast" role="alert" aria-live="assertive" aria-atomic="true"> <div class="toast-header"> <strong class="me-auto">处理状态</strong> <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button> </div> <div class="toast-body"></div> </div> {% if batch_payment_status %} <script> document.addEventListener('DOMContentLoaded', function() { let toastHtml = ''; {% for po_num, status in batch_payment_status.items %} toastHtml += `<p>{{ po_num }}: {{ status.status }} - {{ status.message }}</p>`; {% endfor %} const toastEl = document.getElementById('processing-toast'); toastEl.querySelector('.toast-body').innerHTML = toastHtml; new bootstrap.Toast(toastEl).show(); }); </script> {% endif %}
方案三:Cookie传递状态
无需AJAX和页面刷新,将状态存入Cookie,前端读取后显示提示(注意Cookie大小限制为4KB)。
后端修改
import json class InvoiceView(TemplateView): # 原有属性... def post(self, request, *args, **kwargs): if "batch-pay-csv" in request.POST: response, batch_payment_status = create_batch_payment_template(self.costs) # 状态转JSON存入Cookie,1分钟过期 response.set_cookie( 'batch_payment_status', json.dumps(batch_payment_status), max_age=60, httponly=False # 允许JS读取 ) return response return super().post(request, *args, **kwargs)
前端模板修改
<form method="POST" action=""> {% csrf_token %} <button type="submit" name="batch-pay-csv">生成批量付款文件</button> </form> <!-- Toast提示组件 --> <div class="toast position-fixed bottom-0 end-0 m-3" id="processing-toast" role="alert" aria-live="assertive" aria-atomic="true"> <div class="toast-header"> <strong class="me-auto">处理状态</strong> <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button> </div> <div class="toast-body"></div> </div> <script> document.addEventListener('DOMContentLoaded', function() { // 读取Cookie中的状态 const statusCookie = document.cookie.split('; ').find(row => row.startsWith('batch_payment_status=')); if (statusCookie) { const batchStatus = JSON.parse(decodeURIComponent(statusCookie.split('=')[1])); // 渲染Toast let toastHtml = ''; for (const [poNum, status] of Object.entries(batchStatus)) { toastHtml += `<p>${poNum}: ${status.status} - ${status.message}</p>`; } const toastEl = document.getElementById('processing-toast'); toastEl.querySelector('.toast-body').innerHTML = toastHtml; new bootstrap.Toast(toastEl).show(); // 删除Cookie避免重复显示 document.cookie = 'batch_payment_status=; Max-Age=0; path=/'; } }); </script>
内容的提问来源于stack exchange,提问作者jconnorbuilds
相关产品推荐
相关产品推荐

