其他深度

Odoo 问卷为什么把“页面”和“问题”放进同一模型:随机抽题、触发显示与答题次数边界讲透

Odoo Survey 最容易让人看不懂的,不是题型本身,而是为什么页面和问题都叫 survey.question,以及一次答卷为什么还要额外建 survey.user_input。沿着这两层模型看,才能真正理解随机抽题、显示逻辑与答题次数限制。

其他
进阶 开发者 1 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先说结论

Odoo Survey 的核心,不是“支持多少题型”,而是它把问卷结构作答过程分成了两层:

  • survey.question 负责描述题目结构
  • survey.user_input 负责记录某个人的一次作答过程

更有意思的是,survey.question 不只表示“问题”,还同时表示“页面 / section”。

/home/ubuntu/odoo-temp/addons/survey/models/survey_question.pysurvey_user_input.py 来看,这个设计虽然一开始会让人绕,但它解决了三个很实际的问题:

  • 页面与题目可以放在一条有序链上统一编排
  • 随机抽题和触发显示可以围绕同一结构计算
  • 同一个问卷可以有多次作答记录,并且精确限制 attempts

所以 Odoo 问卷真正的重点不是“做表单”,而是:

如何把一套问卷结构,稳定地投射成一次次可追踪、可评分、可限次的作答会话。


第一层:为什么页面和问题要共用 survey.question

源码注释已经直接说明了这一点:

  • 页面本质上也是问卷结构里的一个节点
  • 它只是 is_page = True
  • 这样页面和问题就能一起挂在 question_and_page_ids 里,统一排序和拖拽

这背后最重要的收益,是结构编排变简单

如果页面和问题分成两张完全独立的表,系统在处理这些场景时会很麻烦:

  • 页面之间插题
  • 题目拖动到别的 section
  • 随机题只在某个页面范围内抽
  • 根据上题答案控制后续页面或问题显示

而共用一个模型后,页面与题目都变成“有顺序的节点”,系统只需要维护一条结构链,再通过计算字段把它重新映射成更好理解的视图:

  • 一个页面有哪些问题
  • 某个问题属于哪一页
  • 哪些节点能作为触发题

所以这不是“模型设计偷懒”,而是在用统一结构换更强的编排能力。


第二层:随机抽题为什么离不开“页面也是节点”

survey.question 里有几个字段特别关键:

  • questions_selection
  • random_questions_count
  • page_id
  • sequence

这说明 Odoo 的随机抽题不是简单地“全卷乱抽”,而是更接近:

  • 先按页面 / section 切分结构
  • 再在局部范围内做抽取
  • 最终保持页面与问题的展示顺序可解释

这对考试、测评和培训问卷很重要。

因为真正可用的随机化,一般都需要满足两个条件:

1. 题目乱,但结构不能乱

你可以随机抽题,但不能把“说明页、单选题、矩阵题、结果页”打成一锅粥。

2. 随机范围可控

有时只是某一节想随机抽 5 题,而不是整份问卷随机 5 题。

页面和问题共模之后,Odoo 才能比较自然地做这种局部随机化。


第三层:触发显示为什么很强调“问题顺序”

triggering_question_idsallowed_triggering_question_idstriggering_answer_ids 这组字段能看出,Odoo 对条件显示不是随便放开的。

源码里对触发题有明显限制:

  • 触发题必须在当前题之前
  • 同一问卷内部才允许引用
  • 主要面向可选题型的答案触发

这背后的产品逻辑很合理。

如果允许一个问题去依赖后面的问题,就会出现前端根本无法稳定渲染的情况:

  • 还没答到后题,前题要不要显示?
  • 切页时如何回溯?
  • 随机抽题后依赖关系会不会失效?

所以 Odoo 其实在强迫实施方接受一个原则:

问卷条件逻辑必须是“从前往后”的。

这让前端展示、会话恢复、随机化处理都更稳。


第四层:为什么一次作答要单独建 survey.user_input

很多人初看 Survey 会疑惑:

  • 问卷结构已经有了
  • 为啥还要单独弄一张 survey.user_input
  • 为什么答案不直接挂在问卷上

答案很简单:

一次作答本来就是一个独立业务对象。

survey.user_input 里,官方把这些东西都放进来了:

  • start_datetime / end_datetime
  • deadline
  • state
  • access_token
  • invite_token
  • partner_id / email
  • attempts_count / attempts_number
  • scoring_total / scoring_percentage

这意味着 Odoo 不把“答题”看成问卷的附属小字段,而是看成一次完整会话。

它可以:

  • 被邀请访问
  • 被限制时间
  • 允许或限制重答
  • 输出分数
  • 打印答案
  • 按联系人追踪历史尝试

这也是为什么 Survey 能同时覆盖:

  • 简单收集表单
  • 正式测验
  • 培训认证
  • 限时 live session

第五层:答题次数限制到底是怎么想的

_compute_attempts_info() 非常值得实施时细读。

它不是随便给一条“答过几次”的数字,而是基于:

  • 问卷本身是否开启 attempts limit
  • 该次答卷是否已经 done
  • 是否是测试条目 test_entry
  • partner_id / email
  • invite_token

来计算同一个人到底是第几次答、总共答了多少次。

这套逻辑说明 Odoo 对“同一个人”的判断其实是业务化的:

  • 有联系人就优先按联系人
  • 没联系人可以按邮箱
  • 某些邀请场景还要结合 invite token

也就是说,attempts 管理不是前端按钮级别的限制,而是后端会话识别策略。

这很重要,因为现实里常见这些情况:

  • 同一个人刷新页面继续答
  • 邮件邀请链接下允许某一组重答
  • 培训测试需要严格识别同一参与者

如果没有 survey.user_input 这一层,这些边界会非常难做干净。


第六层:计时为什么也放在作答对象上

survey.user_input 里,系统会根据:

  • 问卷级时间限制
  • 会话开始时间
  • live session 题目级时间限制

来判断:

  • survey_time_limit_reached
  • question_time_limit_reached

这个设计说明一个关键事实:

时间限制不是题目静态属性,而是“某个人在某次作答里是否已经超时”。

所以它必须依赖作答对象,而不是只靠问卷定义。

换句话说,同一份问卷结构可以保持不变,但不同人的作答状态会完全不同。


最容易误解的三个点

误区一:页面只是前端 UI 概念

不是。 在 Odoo Survey 里,页面是结构节点,直接影响排序、随机化与显示逻辑。

误区二:attempts 限制只是“提交后不让再进”

也不是。 它背后是整套用户识别和作答历史计算机制。

误区三:随机抽题就是把题库打乱

Odoo 的随机化更偏向“在结构内有边界地随机”,不是粗暴洗牌。


实施和开发时最该注意什么

如果你要扩展 Survey,最容易踩坑的地方有三个:

  1. 不要轻易把页面和问题拆模,否则随机化和触发显示会一起变复杂。
  2. 不要只在控制器层做 attempts 限制,真正可信的边界在 survey.user_input
  3. 做条件显示时一定保证依赖顺序向前,否则前后端都容易出现状态错乱。

最后一句

Odoo Survey 之所以看起来“绕”,不是因为它设计混乱,而是因为它同时在解决三类事情:

  • 问卷结构怎么表达
  • 单次作答怎么跟踪
  • 限时、限次、随机和条件显示怎么共存

当你接受“页面也是问题节点、作答也是独立对象”这两个前提后,整套设计其实就很顺了。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。