
Wagtail Form Builder 防重复提交用 RoutablePageMixin 实现 Post/Redirect/Get 重定向【免费下载链接】wagtailA Django content management system focused on flexibility and user experience项目地址: https://gitcode.com/GitHub_Trending/wa/wagtail表单提交后的 HTTP 响应通常是302 Found临时重定向到新页面这是经典的 PRGPost/Redirect/Get模式。但 Wagtail 的FormPage默认在 POST 响应中直接渲染落地页landing page用户刷新页面就会重复提交数据。本文基于wagtail.contrib.forms与wagtail.contrib.routable_page两个官方模块给出一个完整的源码级解决方案用RoutablePageMixin把感谢页做成表单页自身的子路由在 POST 成功后重定向过去同时保留提交记录 ID 供落地页展示。读完本文你将掌握如何安装启用routable_page、如何组合继承RoutablePageMixin与AbstractEmailForm、如何重写render_landing_page实现重定向以及reverse_subpage、path装饰器等底层机制的工作原理。问题背景为什么 FormPage 会重复提交Wagtail 表单构建器Form Builder由wagtail.contrib.forms提供其核心页面模型FormPage继承自抽象基类 AbstractEmailForm。从源码可以看到表单的完整请求处理流程集中在 FormMixin.serve 中def serve(self, request, *args, **kwargs): if request.method POST: form self.get_form( request.POST, request.FILES, pageself, userrequest.user ) if form.is_valid(): form_submission self.process_form_submission(form) return self.render_landing_page( request, form_submission, *args, **kwargs ) else: form self.get_form(pageself, userrequest.user) context self.get_context(request) context[form] form return TemplateResponse(request, self.get_template(request), context)关键点在于当 POST 校验通过后serve直接调用 render_landing_page 返回TemplateResponse——也就是把落地页的 HTML 直接作为本次 POST 请求的响应体返回并没有发生浏览器地址栏跳转。此时用户看到的感谢页 URL 仍然是表单页自身的 URL且是 POST 结果此时按 F5 刷新浏览器浏览器会提示并重发上一次的 POST 请求导致同一条表单数据被再次写入数据库、再次发送通知邮件。默认的render_landing_page实现如下源码位置def render_landing_page(self, request, form_submissionNone, *args, **kwargs): context self.get_context(request) context[form_submission] form_submission return TemplateResponse( request, self.get_landing_page_template(request), context )因此本文的目标是不渲染落地页内容作为 POST 响应而是返回一个 302 重定向把用户导向表单页自身的一个子 URL如/thank-you/。落地页内容仍在同一个表单页的 admin 后台中管理只是它的渲染被挪到了重定向后的 GET 请求里。这样地址栏变成了干净的 GET 地址刷新不再重复提交。两种重定向方案概览方案目标页面是否依赖routable_page适用场景本文方案表单页自身的子路由如FormPage/thank-you/需要使用RoutablePageMixin感谢页内容希望留在表单页后台统一管理、URL 保持在同一页面树下替代方案站点内任意其他页面如单独的感谢页不需要已有独立感谢页只想在提交后跳转过去替代方案即 Custom landing page redirect原文档中对应的锚点form_builder_custom_landing_page_redirect做法是给FormPage增加一个thank_you_page外键字段指向目标页面并在render_landing_page中返回redirect(self.thank_you_page.url, permanentFalse)。本文重点讲解第一种方案。第一步安装并启用 routable_pagewagtail.contrib.routable_page是 Wagtail 的官方附加模块允许一个页面在多个子 URL 上响应不同视图。使用前需要把它加入 Django 的INSTALLED_APPS完整说明见 RoutablePageMixin 文档INSTALLED_APPS [ ... wagtail.contrib.routable_page, ]第二步完整实现代码下面是与原文档一致的完整实现。FormPage同时继承RoutablePageMixin和AbstractEmailForm注意继承顺序RoutablePageMixin在前以便它的route/serve方法在 MRO 中生效并接管子 URL 路由。from django.shortcuts import redirect from wagtail.contrib.forms.models import AbstractEmailForm from wagtail.contrib.routable_page.models import RoutablePageMixin, path class FormPage(RoutablePageMixin, AbstractEmailForm): # fields, content_panels, … path() def index_route(self, request, *args, **kwargs): Serve the form, and validate it on POST return super(AbstractEmailForm, self).serve(request, *args, **kwargs) def render_landing_page(self, request, form_submission, *args, **kwargs): Redirect instead to self.thank_you route url self.reverse_subpage(thank_you) # If a form_submission instance is available, append the ID to URL. if form_submission: url ?id%s % form_submission.id return redirect(self.url url, permanentFalse) path(thank-you/) def thank_you(self, request): Return the superclasss landing page, after redirect. form_submission None try: submission_id int(request.GET[id]) except (KeyError, TypeError): pass else: submission_class self.get_submission_class() try: form_submission submission_class.objects.get(idsubmission_id) except submission_class.DoesNotExist: pass return super().render_landing_page(request, form_submission)第三步逐段解析index_route接管表单页根路径path() def index_route(self, request, *args, **kwargs): Serve the form, and validate it on POST return super(AbstractEmailForm, self).serve(request, *args, **kwargs)RoutablePageMixin默认自带一个path()的index_route源码位置其行为等同于普通Page的渲染。这里我们显式覆盖它把请求委托给AbstractEmailForm的serve方法。为什么要用super(AbstractEmailForm, self).serve(...)而不是直接调用self.serve(...)如果写self.serve(request)Python 会先解析到RoutablePageMixin.serve它在 MRO 中靠前而该方法内部又会调用super().serve()最终回到FormMixin.serve——这本身可行但语义不够直白显式写super(AbstractEmailForm, self).serve(...)是跳过AbstractEmailForm本身、从其 MRO 下一级FormMixin开始调用明确指向FormMixin.serve这个表单处理入口让表单校验 成功回调的流程不经过路由逻辑意图更清晰。由于FormMixin.serve在 POST 成功时会调用self.render_landing_page(...)而我们重写了render_landing_page所以验证通过 → 重定向的链路自然成立。render_landing_page把成功响应换成 302 重定向def render_landing_page(self, request, form_submission, *args, **kwargs): Redirect instead to self.thank_you route url self.reverse_subpage(thank_you) # If a form_submission instance is available, append the ID to URL. if form_submission: url ?id%s % form_submission.id return redirect(self.url url, permanentFalse)self.reverse_subpage(thank_you)调用 RoutablePageMixin.reverse_subpage通过页面的 URL resolver 反解出名为thank_you的路由路径。路由名默认取视图方法名因此这里对应thank_you方法返回的是页面内的相对路径thank-you/不含页面自身的 URL。self.url是页面在站点中的完整路径两者拼接得到完整地址例如/contact/thank-you/。当存在form_submission时把提交记录的 ID 以查询参数形式附加到 URL?id123。这样感谢页可以根据 ID 重新取出这条提交记录并展示也便于后续做个性化内容或调试。redirect(url, permanentFalse)返回302 Found临时重定向——这正是 PRG 模式的语义。不要改成永久重定向301因为表单页 URL 并未变化且 301 会被浏览器缓存影响后续正常访问。thank_you重定向后的 GET 落地视图path(thank-you/) def thank_you(self, request): Return the superclasss landing page, after redirect. form_submission None try: submission_id int(request.GET[id]) except (KeyError, TypeError): pass else: submission_class self.get_submission_class() try: form_submission submission_class.objects.get(idsubmission_id) except submission_class.DoesNotExist: pass return super().render_landing_page(request, form_submission)path(thank-you/)把/thank-you/子路径绑定到该方法。从GET参数读取id并转成intKeyError没有该参数和TypeError无法转换都被吞掉此时form_submission保持None只有拿到合法 ID 时才去查询提交记录。self.get_submission_class()返回当前表单使用的提交模型类默认是FormSubmission见 FormMixin.get_submission_class如果 ID 对应的记录不存在DoesNotExist同样回退为None。最后调用super().render_landing_page(request, form_submission)即FormMixin.render_landing_page用标准的落地页模板渲染出感谢页。这套try/except的容错设计非常关键直接访问/contact/thank-you/无id、传入非法id、或记录已被删除时页面都能正常渲染只是不附带具体提交数据不会抛出 500。第四步底层机制——RoutablePageMixin 如何路由理解这套方案需要知道RoutablePageMixin在底层如何工作源码均在 wagtail/contrib/routable_page/models.py。path/re_path装饰器path是 Djangodjango.urls.path的包装re_path对应django.urls.re_path装饰器在注册时会把(URLPattern, creation_counter)追加到视图函数的_routablepage_routes属性上creation_counter用于保证同一类中靠前定义的路径优先级更高源码位置。route是re_path的历史别名仍然可用。get_subpage_urls类方法按 MRO 顺序收集所有被装饰方法的路由源码位置因此基类定义的路由会被子类继承。get_resolver与resolve_subpageget_resolver用收集到的子 URL 构建一个URLResolver挂在RegexPattern(r^/)下resolve_subpage(path)用它对请求路径做匹配并把匹配到的视图方法绑定到页面实例resolver_match.func.__get__(self, type(self))使其成为带self的绑定方法源码位置。route方法这是接入 Wagtail 页面路由的入口源码位置。页面存活self.live时把路径组件拼成子路径尝试resolve_subpage命中则把resolver_match存入request.routable_resolver_match与 Django 的request.resolver_match类似返回RouteResult(self, args(view, args, kwargs))交给 Wagtail 分发Http404或页面未存活则回退到默认Page.route。这也解释了文档中的说明URL 模式都不匹配时控制权会交还给子页面否则才抛 404。serve与renderserve把解析出的视图作为参数调用render复刻普通Page.serve的模板/上下文渲染逻辑可通过context_overrides与template参数覆盖上下文和模板源码位置。从源码结构还可以推断因为route只在页面 live 时尝试子 URL 匹配所以草稿状态下直接访问/thank-you/这类子路径会走默认路由逻辑行为与普通页面一致。第五步URL 反解与模板标签Python 侧反解reverse_subpage(name, argsNone, kwargsNone)只返回页面内的相对路径。要拼完整地址需与self.url站点内路径或self.full_url绝对 URL拼接 event_page.get_url() event_page.reverse_subpage(events_for_year, args(2015, )) /events/year/2015/ event_page.full_url event_page.reverse_subpage(events_for_year, args(2015, )) https://example.com/events/year/2015/路由名默认是视图方法名也可以在path/re_path上用name参数显式指定re_path(r^year/(\d)/$, nameyear) def events_for_year(self, request, year): ...模板侧反解routable_page还提供两个模板标签位于wagtailroutablepage_tags详见 RoutablePageMixin 文档{% load wagtailroutablepage_tags %} {% routablepageurl page feed %} {% routablepageurl page archive 2014 08 14 %} {% routablepageurl page food foobar bazquux %}routablepageurl输出站点内相对路径routablefullpageurl输出带域名的完整 URL。例如在感谢页模板中要生成返回表单页的链接可以直接写{% routablepageurl page index_route %}。替代方案重定向到独立页面无需 routable_page如果不想引入routable_page可以给FormPage增加一个指向任意页面的外键在render_landing_page里直接跳转完整示例见 Custom landing page redirectfrom django.shortcuts import redirect from wagtail.contrib.forms.models import AbstractEmailForm class FormPage(AbstractEmailForm): thank_you_page models.ForeignKey( wagtailcore.Page, nullTrue, blankTrue, on_deletemodels.SET_NULL, related_name, ) def render_landing_page(self, request, form_submissionNone, *args, **kwargs): if self.thank_you_page: url self.thank_you_page.url if form_submission: url ?id%s % form_submission.id return redirect(url, permanentFalse) return super().render_landing_page(request, form_submission, *args, **kwargs)该方案的取舍实现更简单、后台可选择任意感谢页但感谢页是独立页面内容与表单页分离且同样需要自行处理?id参数与容错逻辑。本文主方案的优势是感谢页与表单页共用同一后台页面、URL 归属于表单页路径树。注意事项与最佳实践INSTALLED_APPS 不能遗漏未安装wagtail.contrib.routable_page时导入RoutablePageMixin会直接报错务必先按上文配置。path模式中的标点限制Wagtail 官方文档明确警告path/re_path模式目前不支持标点符号涉及 wagtail#3653 历史问题。子路径请使用字母、数字、连字符和下划线。保持permanentFalse302 临时重定向符合 PRG 语义301会被浏览器/代理缓存后续访问可能不再经过表单页务必不要误用。id参数容错感谢页直接访问、非法 ID、记录已删除三种场景都要兜底为form_submissionNone模板中应相应判断。多选字段的展示落地页模板若要展示提交数据注意FormSubmission中多选Checkboxes等字段保存为列表渲染时需自行join可参考 AbstractEmailForm.render_email 的处理方式value , .join(value)。继承顺序RoutablePageMixin必须写在AbstractEmailForm之前保证路由机制优先生效否则serve解析会命中FormMixin版本子 URL 路由不生效。预览模式FormMixin.serve_preview在landing模式下会调用render_landing_page(request)无form_submission此时返回的是重定向而非页面内容若后台预览需要直接看到感谢页内容可结合request.is_preview做分支处理该属性由RoutablePageMixin.index_route在 源码 中同步到 request。总结本文给出的方案完整落地了 PRG 模式POST 成功后由render_landing_page返回 302浏览器 GET 到FormPage/thank-you/子路由由thank_you视图依据?id参数恢复提交记录并渲染感谢页。整个过程的支撑点包括FormMixin.serve的成功回调钩子wagtail/contrib/forms/models.py、RoutablePageMixin的子 URL 路由与反解机制wagtail/contrib/routable_page/models.py以及两篇官方参考文档 RoutablePageMixin 文档 与 Form Builder 自定义文档。这套代码可以直接放进自定义的FormPage模型中使用既防止用户重复提交又保持感谢页内容在表单页后台统一维护。【免费下载链接】wagtailA Django content management system focused on flexibility and user experience项目地址: https://gitcode.com/GitHub_Trending/wa/wagtail创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考