Django保存模型后触发PDF下载及post_save信号传参问题
问题根因
当前实现存在两个核心问题,导致功能不符合预期:
post_save信号的返回值不会被Django的HTTP响应链路接收。信号本身只用于执行解耦的副作用逻辑(日志、通知、关联数据更新等),接收器的返回值会被直接丢弃。你在信号里构造的PDF响应完全不会返回给前端,admin保存模型后会走默认的重定向逻辑(跳回列表页/编辑页),自然不会弹出下载框。post_save信号默认只携带模型实例、创建状态等固定参数,拿不到ModelForm里的非模型字段、request对象等请求上下文数据,强行给信号传自定义参数会导致其他触发模型保存的场景(命令行操作、异步任务、其他业务代码调用save())因为缺参数报错。
实现方案
不要在信号里处理PDF生成和响应返回,直接把逻辑移到admin的保存流程中实现,步骤如下:
1. 移除原信号逻辑
删掉signals.py里绑定的download_contract_pdf信号接收器,避免无效执行。
2. 抽离公共PDF生成函数
新建utils.py存放PDF生成逻辑,函数只负责接收参数、生成PDF二进制内容,不耦合HTTP响应逻辑:
from datetime import datetime from django.template.loader import render_to_string from weasyprint import HTML from .models import Charge, Control, Extension def generate_contract_pdf(contract_obj, warranty_card_company, warranty_card_number, warranty_card_expiration_date, warranty_card_auth_code): # 格式化时间避免文件名出现特殊字符导致兼容问题 file_name = f"contrato-{datetime.now().strftime('%Y%m%d%H%M%S')}.pdf" charge_qs = Charge.objects.filter(contract=contract_obj.pk) control_qs = Control.objects.filter(contract=contract_obj.pk) extension_qs = Extension.objects.filter(contract=contract_obj.pk) credit_card_data = [ warranty_card_company, warranty_card_number, warranty_card_expiration_date, warranty_card_auth_code ] html_string = render_to_string( 'pdf_template.html', { 'charge_obj': charge_qs, 'contract_obj': contract_obj, 'control_obj': control_qs, 'extension_obj': extension_qs, 'credit_card_data': credit_card_data } ) html = HTML(string=html_string) pdf_content = html.write_pdf( stylesheets=["https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css"] ) return pdf_content, file_name
3. 修改Admin类逻辑
在admin.py里重写admin的保存和响应方法,直接从form中取非模型字段,保存完成后返回PDF响应:
from django.http import HttpRequest, HttpResponse from django.contrib import admin # 导入刚写的生成函数 from .utils import generate_contract_pdf class ContractAdmin(admin.ModelAdmin): form = ContractForm # 原有其他admin配置保留 def save_model(self, request: HttpRequest, obj: Contract, form: ContractForm, change: bool) -> None: if not change and not request.user.is_superuser: obj = MythLabsMultiTenancy.assign_organization_to_obj(obj, request.user) super().save_model(request, obj, form, change) # 把生成PDF需要的表单数据临时挂到request对象上,供后续响应方法使用 request._contract_pdf_data = { 'contract_obj': obj, 'warranty_card_company': form.cleaned_data.get('warranty_card_company', ''), 'warranty_card_number': form.cleaned_data.get('warranty_card_number', ''), 'warranty_card_expiration_date': form.cleaned_data.get('warranty_card_expiration_date', ''), 'warranty_card_auth_code': form.cleaned_data.get('warranty_card_auth_code', 0), } def response_add(self, request, obj, post_url_continue=None): # 新建保存后触发PDF下载 if hasattr(request, '_contract_pdf_data'): pdf_content, file_name = generate_contract_pdf(**request._contract_pdf_data) self.message_user(request, "合同创建成功,PDF已开始下载") return HttpResponse( pdf_content, headers={ 'Content-Type': 'application/pdf', 'Content-Disposition': f'attachment; filename="{file_name}"', } ) return super().response_add(request, obj, post_url_continue) def response_change(self, request, obj): # 修改保存后触发PDF下载 if hasattr(request, '_contract_pdf_data'): pdf_content, file_name = generate_contract_pdf(**request._contract_pdf_data) self.message_user(request, "合同修改成功,PDF已开始下载") return HttpResponse( pdf_content, headers={ 'Content-Type': 'application/pdf', 'Content-Disposition': f'attachment; filename="{file_name}"', } ) return super().response_change(request, obj)
补充说明
如果不需要每次保存都触发下载,可以在admin页面加一个额外的勾选框(比如保存后下载PDF),在save_model里判断勾选状态再决定是否挂PDF生成参数即可,逻辑更灵活。
内容的提问来源于stack exchange,提问作者Santiago Muñoz
相关产品推荐
相关产品推荐

