{"result":{"depiction_url":null,"edges":{"observes":{"objects":[{"created":"2026-09-09T13:47:42Z","object_id":{"id":1468,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_acl_is_allowed","title":"observe_acl_is_allowed\/2","uri":"https:\/\/zotonic.com\/id\/1468"},"seq":1},{"created":"2026-09-09T13:47:42Z","object_id":{"id":1763,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_admin_edit_blocks","title":"observe_admin_edit_blocks\/3","uri":"https:\/\/zotonic.com\/id\/1763"},"seq":2},{"created":"2026-09-09T13:47:42Z","object_id":{"id":1782,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_export_resource_data","title":"observe_export_resource_data\/2","uri":"https:\/\/zotonic.com\/id\/1782"},"seq":3},{"created":"2026-09-09T13:47:42Z","object_id":{"id":1784,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_export_resource_filename","title":"observe_export_resource_filename\/2","uri":"https:\/\/zotonic.com\/id\/1784"},"seq":4},{"created":"2026-09-09T13:47:42Z","object_id":{"id":1786,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_export_resource_header","title":"observe_export_resource_header\/2","uri":"https:\/\/zotonic.com\/id\/1786"},"seq":5},{"created":"2026-09-09T13:47:42Z","object_id":{"id":1997,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_survey_is_submit","title":"observe_survey_is_submit\/2","uri":"https:\/\/zotonic.com\/id\/1997"},"seq":6},{"created":"2026-09-09T13:47:42Z","object_id":{"id":1999,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_rsc_merge","title":"observe_rsc_merge\/2","uri":"https:\/\/zotonic.com\/id\/1999"},"seq":7},{"created":"2026-09-09T13:47:42Z","object_id":{"id":2155,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_admin_rscform","title":"observe_admin_rscform\/3","uri":"https:\/\/zotonic.com\/id\/2155"},"seq":8}],"predicate":{"id":2550,"is_a":["meta","predicate"],"name":"observes","title":{"_type":"trans","tr":{"en":"Observes"}},"uri":"https:\/\/zotonic.com\/id\/observes"}},"references":{"objects":[{"created":"2020-05-30T05:47:43Z","object_id":{"id":1279,"is_a":["text","documentation"],"name":"doc_glossary","title":"Glossary","uri":"https:\/\/zotonic.com\/id\/1279"},"seq":1000000}],"predicate":{"id":332,"is_a":["meta","predicate"],"name":"references","title":{"_type":"trans","tr":{"en":"References"}},"uri":"https:\/\/zotonic.com\/id\/references"}},"subject":{"objects":[{"created":"2026-09-15T12:29:56Z","object_id":{"id":2555,"is_a":["categorization","keyword","keyword_information_type"],"name":"zotonic_topic_reference","title":"Reference","uri":"https:\/\/zotonic.com\/id\/2555"},"seq":1},{"created":"2026-09-15T12:29:56Z","object_id":{"id":2561,"is_a":["categorization","keyword","keyword_audience"],"name":"zotonic_topic_content_editor","title":"Content editor","uri":"https:\/\/zotonic.com\/id\/2561"},"seq":2},{"created":"2026-09-15T12:29:56Z","object_id":{"id":2582,"is_a":["categorization","keyword","keyword_domain"],"name":"zotonic_topic_forms","title":"Forms","uri":"https:\/\/zotonic.com\/id\/2582"},"seq":3},{"created":"2026-09-15T12:29:56Z","object_id":{"id":2584,"is_a":["categorization","keyword","keyword_domain"],"name":"zotonic_topic_surveys","title":"Surveys","uri":"https:\/\/zotonic.com\/id\/2584"},"seq":4},{"created":"2026-09-15T12:29:56Z","object_id":{"id":2635,"is_a":["categorization","keyword","keyword_architecture"],"name":"zotonic_topic_module","title":"Module","uri":"https:\/\/zotonic.com\/id\/2635"},"seq":5},{"created":"2026-09-15T12:29:56Z","object_id":{"id":2651,"is_a":["categorization","keyword","keyword_task"],"name":"zotonic_topic_export","title":"Export","uri":"https:\/\/zotonic.com\/id\/2651"},"seq":6}],"predicate":{"id":308,"is_a":["meta","predicate"],"name":"subject","title":{"_type":"trans","tr":{"en":"Keyword"}},"uri":"http:\/\/purl.org\/dc\/elements\/1.1\/subject"}}},"id":1316,"is_a":["text","documentation","reference","module"],"links":[{"rel":"self","target":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/1316"},{"rel":"hub","target":"https:\/\/zotonic.com\/.zotonic\/websub"}],"medium":null,"medium_url":null,"name":"doc_module_mod_survey","page_url":{"en":"https:\/\/zotonic.com\/docs\/1316\/mod_survey","x-default":"https:\/\/zotonic.com\/docs\/1316\/mod_survey"},"preview_url":null,"resource":{"version":3328,"pivot_location_lat":null,"title":"mod_survey","is_authoritative":true,"body":"<p>Adds survey <a href=\"\/id\/doc_glossary#term-resource\">resources<\/a>: user-defined forms that\ncan be created in the admin interface and filled in by site visitors. The module\nrenders multi-page forms, validates and stores answers, supports quizzes, and\nreports or exports the collected results.<\/p>\n<h2>Survey question types<\/h2>\n<table class=\"table\"><thead><tr><th>Type<\/th><th>Description<\/th><\/tr><\/thead><tbody><tr><td>likert<\/td><td>A five-point scale from completely disagree (1) to completely agree (5).<\/td><\/tr><tr><td>short answer<\/td><td>A single-line text field with optional validation such as email, date, or numeric.<\/td><\/tr><tr><td>long answer<\/td><td>A multi-line text field.<\/td><\/tr><tr><td>matching<\/td><td>Match each item to one of the supplied options.<\/td><\/tr><tr><td>thurstone<\/td><td>Translatable single or multiple choice, optionally submitting immediately after a choice.<\/td><\/tr><tr><td>multiple choice<\/td><td>Simple choices. Numeric choice values can also be totaled in printable and chart results. Use thurstone for translatable choices.<\/td><\/tr><tr><td>true or false<\/td><td>A boolean question with configurable option text.<\/td><\/tr><tr><td>yes or no<\/td><td>A boolean question with configurable option text.<\/td><\/tr><tr><td>narrative<\/td><td>Inline questions embedded in narrative text.<\/td><\/tr><tr><td>category<\/td><td>Select one resource from a category.<\/td><\/tr><tr><td>country<\/td><td>Select a country.<\/td><\/tr><tr><td>hidden<\/td><td>A hidden value, for example to record that a page was submitted.<\/td><\/tr><tr><td>upload<\/td><td>A file upload for the final page. The upload question has no stored result; a custom survey handler must process the file.<\/td><\/tr><tr><td>header<\/td><td>A subheading between questions.<\/td><\/tr><tr><td>prompt<\/td><td>An additional prompt block.<\/td><\/tr><tr><td>text block<\/td><td>Explanatory text between questions.<\/td><\/tr><\/tbody><\/table>\n<h2>Tests and quizzes<\/h2>\n<p>Thurstone and matching questions can be marked as test questions with\n<code>is_test<\/code>. This selects their test rendering and makes the configured correct\nanswers available to the feedback templates. <code>is_test_direct<\/code> shows feedback\nwhile filling in the question.<\/p>\n<p>Scoring is currently implemented for thurstone questions. Each choice can have\nan <code>is_correct<\/code> flag and <code>points_int<\/code>; the default is one point. A single-choice\nquestion awards the selected correct answer. A multiple-choice question can\naward correct selections and correctly omitted wrong selections. With\n<code>is_test_neg<\/code>, wrong selections and omitted correct answers subtract their\nconfigured points. The score for an individual question never goes below zero.\nMatching test questions support direct correctness feedback but currently add\nno points to the stored total.<\/p>\n<p><code>survey_test_results<\/code> stores the calculated total in the answer&#39;s <code>points<\/code>\nfield and annotates the stored answers with their points. Set the survey\nproperty <code>survey_test_percentage<\/code> to the percentage needed to pass. The\n<code>survey_show_results<\/code> setting controls whether the final page shows only the\nthank-you text, the respondent&#39;s result, the respondent&#39;s result only after a\npass, or aggregated results.<\/p>\n<p>Templates can use <code>survey_test_max_points<\/code> to retrieve the maximum score and\nthe <code>survey_any_correct_answer<\/code> and <code>survey_any_wrong_answer<\/code> filters when\nrendering answer feedback.<\/p>\n<h2>Editing submitted answers<\/h2>\n<p>The <code>survey_multiple<\/code> resource property controls repeat submissions and edits:<\/p>\n<ul><li><code>0<\/code> means one final submission; it cannot be edited by the respondent.<\/li><li><code>1<\/code> allows multiple submissions, each stored as a new answer.<\/li><li><code>2<\/code> allows one submission and lets a logged-in respondent edit their own\nstored answer.<\/li><\/ul>\n<p>People who can edit the survey resource can edit any of its saved answers by\nanswer id. A participant can update only their own answer, and only in mode <code>2<\/code>.\nAnonymous respondents cannot edit a final submission in this mode. These checks\nare made server-side with the <code>update_result<\/code> ACL action; hiding an edit button\nis not the access control.<\/p>\n<p><code>m_survey:replace_survey_submission\/4<\/code> recalculates the test score and updates\nthe answer&#39;s <code>modified<\/code> and <code>modifier_id<\/code> fields. The <code>submitted<\/code> date is set\nwhen a new answer is stored. On an edit it is refreshed only when the current\nuser owns the saved answer; an editor changing somebody else&#39;s answer leaves\nthe original submitted date intact.<\/p>\n<p>Saving intermediate progress is separate from editing a submitted answer. Also\nnote that edit-after-submit and submission-count limits depend on the selected\nsurvey handler storing results in <code>survey_answers<\/code>.<\/p>\n<h2>Editor-only questions<\/h2>\n<p>Set <code>is_editor_only<\/code> on a question block when only people with edit permission\non the survey should supply that answer. Editors see the normal input and can\nchange it. A non-editor sees the question in read-only form, including its\nprompt and explanation, and also sees the stored answer when one is present.<\/p>\n<p>This restriction is enforced in both the form flow and the storage layer.\nSubmitted values for editor-only questions are discarded for non-editors,\nincluding the expanded input names used by narrative and matching questions.\nWhen a non-editor updates an answer, any existing editor-only values are copied\nfrom the stored answer instead of being overwritten. An editor-only question\nmarked required is not considered missing for a non-editor. When an editor uses\n<strong>Save &amp; Email<\/strong>, editor-only answers are included in the respondent email even\nif the survey&#39;s normal confirmation settings omit open or all answers.<\/p>\n<h2>Answer status<\/h2>\n<p>Every saved answer has optional workflow metadata, separate from the answer\naudit fields:<\/p>\n<ul><li><code>status<\/code> - a non-negative numeric status index, or <code>undefined<\/code> for no status.\nThe bundled interface presents status values 0 through 5 as color swatches.<\/li><li><code>status_date<\/code> - UTC date and time of the last status change.<\/li><li><code>status_note<\/code> - an optional internal note. It is trimmed and limited to\n65,536 Unicode characters; longer input is truncated safely.<\/li><li><code>status_modifier_id<\/code> - the user who last changed the status.<\/li><\/ul>\n<p>Use <code>m_survey:set_answer_status\/5<\/code> to replace all four status fields together.\nThe answer id must belong to the supplied survey, and the current user must be\nallowed to edit that survey. Changing status does not update <code>modified<\/code> or\n<code>modifier_id<\/code>, because those fields describe changes to the submitted answers.<\/p>\n<p>Status is internal editor information. <code>m_survey:single_result\/3<\/code> and\n<code>single_result\/4<\/code> remove all status fields when the current user cannot edit the\nsurvey. The read-only <code>_survey_answer_status.tpl<\/code> and the editable\n<code>_survey_answer_status_edit.tpl<\/code> apply the same editor check. The latter is\nincluded by <code>_dialog_survey_answer_status.tpl<\/code>, which handles saving through\nthe <code>survey_answer_status<\/code> submit event.<\/p>\n<p><code>_dialog_survey_result_view.tpl<\/code> shows an entire saved result without paging.\nFor editors it combines the status fields with editable editor-only questions;\nall respondent answers are read-only and unanswered respondent questions are\nomitted. Its <code>survey_result_view_save<\/code> event accepts only status and\neditor-only values. Both this view and the final page of the full paged editor\noffer <strong>Save &amp; Email<\/strong>; the extra mail is sent only after the same server-side\nanswer-edit permission check as a normal save. Post-submit content such as the\nreserved <code>survey_feedback<\/code> block, and resultless survey flow-control blocks,\nare omitted from the saved-answer view.<\/p>\n<p>Both status templates accept an optional <code>status_labels<\/code> list for values 0\nthrough 5. Without labels they show only the color swatches. For example:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-django\">{% include &quot;_survey_answer_status.tpl&quot;\n    id=id\n    answer_id=answer_id\n    status_labels=[_&quot;New&quot;, _&quot;Accepted&quot;, _&quot;Rejected&quot;]\n%}\n<\/code><\/pre>\n<h2>Intercepting survey submissions<\/h2>\n<p>When a survey is submitted, the module sends a first <code>#survey_submit{}<\/code>\nnotification with these fields:<\/p>\n<ul><li><code>id<\/code> - id of the submitted survey.<\/li><li><code>handler<\/code> - selected handler name, described below.<\/li><li><code>answers<\/code> - normalized answers.<\/li><li><code>missing<\/code> - names of required answers that were missing.<\/li><li><code>answers_raw<\/code> - unprocessed submitted values.<\/li><\/ul>\n<p>An observer can handle a submission by returning <code>ok<\/code>:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">observe_survey_submit(#survey_submit{id = SurveyId}, Context) -&gt;\n    ?DEBUG(SurveyId),\n    ok.\n<\/code><\/pre>\n<h2>Creating a custom survey handler<\/h2>\n<p>The survey edit page has a dropdown of survey handlers. A handler is a survey\nresource property that selects how a submission is processed. Handlers are\ncollected with the <code>#survey_get_handlers{}<\/code> fold notification:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">observe_survey_get_handlers(#survey_get_handlers{}, All, Context) -&gt;\n    [\n        {&lt;&lt;&quot;email_me&quot;&gt;&gt;,\n            ?__(&lt;&lt;&quot;E-mail me when the survey is submitted&quot;&gt;&gt;, Context)}\n        | All\n    ].\n<\/code><\/pre>\n<p>The selected value is passed in the <code>handler<\/code> field of <code>#survey_submit{}<\/code>. An\nobserver should match its handler and return <code>undefined<\/code> for the others:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">observe_survey_submit(\n        #survey_submit{handler = &lt;&lt;&quot;email_me&quot;&gt;&gt;, id = SurveyId},\n        Context) -&gt;\n    %% Handle and, if desired, store this submission.\n    ok;\nobserve_survey_submit(#survey_submit{}, _Context) -&gt;\n    undefined.\n<\/code><\/pre>\n<h2>Configuration keys<\/h2>\n<p>The result editor can link an answer to a newly created person. Its category\nand content group are configured with:<\/p>\n<ul><li><code>mod_survey.person_category<\/code>, default <code>person<\/code>.<\/li><li><code>mod_survey.person_content_group<\/code>, default <code>default_content_group<\/code>. If empty,\nthe active ACL module selects the default content group.<\/li><\/ul>\n<h2>Accepted events<\/h2>\n<p>This module handles the following notifier callbacks:<\/p>\n<ul><li><code>observe_acl_is_allowed\/2<\/code> checks view, update, delete, and MQTT access for\nsurvey answers.<\/li><li><code>observe_admin_edit_blocks\/3<\/code> adds the survey question blocks to the editor.<\/li><li><code>observe_admin_rscform\/3<\/code> normalizes page jumps into page-break blocks.<\/li><li><code>observe_export_resource_filename\/2<\/code>, <code>observe_export_resource_header\/2<\/code>, and\n<code>observe_export_resource_data\/2<\/code> provide the survey results download.<\/li><li><code>observe_rsc_merge\/2<\/code> moves answers from a merged resource to the winner.<\/li><li><code>observe_survey_get_handlers\/3<\/code> adds built-in survey handlers.<\/li><li><code>observe_survey_is_submit\/2<\/code> checks whether a block submits the survey.<\/li><li><code>observe_survey_submit\/2<\/code> processes submissions for built-in handlers.<\/li><li><code>observe_tick_24h\/2<\/code> removes expired intermediate survey results.<\/li><\/ul>\n<p>Delegate callbacks handled by <code>event\/2<\/code> include the <code>survey_start<\/code>,\n<code>survey_back<\/code>, <code>survey_remove_result<\/code>, and <code>survey_remove_result_confirm<\/code>\npostbacks, and the <code>survey_next<\/code>, <code>survey_answer_status<\/code>, and\n<code>survey_result_view_save<\/code> submit events.<\/p>","slug":"mod_survey","is_protected":false,"visible_for":0,"tz":"UTC","language":["en"],"doc_source_hash":"a28ed48e2a7b0fd13586b0f19a81ad2c35ffc4191963c6417bf436524d3a5894","is_featured":false,"content_group_id":{"id":2551,"is_a":["meta","content_group"],"name":"content_group_imported_docs","title":"Imported documentation","uri":"https:\/\/zotonic.com\/id\/content_group_imported_docs"},"category_id":{"id":320,"is_a":["meta","category"],"name":"module","title":"Modules","uri":"https:\/\/test.zotonic.com\/id\/320"},"doc_source_path":"apps\/zotonic_mod_survey\/src\/mod_survey.erl","publication_start":"2023-01-30T19:23:58Z","github_url":"https:\/\/github.com\/zotonic\/zotonic\/blob\/master\/apps\/zotonic_mod_survey\/src\/mod_survey.erl","pivot_location_lng":null,"doc_source_kind":"module","name":"doc_module_mod_survey","is_unfindable":false,"is_published":true,"pivot_geocode":null,"created":"2020-05-30T05:47:18Z","uri":null,"doc_status":"current","is_dependent":false,"erlang_app":"zotonic_mod_survey","publication_end":"9999-06-01T00:00:00Z","modifier_id":{"id":1,"is_a":["person"],"name":"administrator","title":"Site Administrator","uri":"https:\/\/zotonic.com\/id\/1"},"privacy":0,"doc_source_commit":"66cb145eae7002b184e7d8c4c7bba5e53e8248e5\n","creator_id":{"id":336,"is_a":["person","robot"],"name":"gitbot","title":"Git","uri":"https:\/\/zotonic.com\/id\/336"},"modified":"2026-10-02T12:40:18Z","title_slug":"mod_survey"},"uri":"https:\/\/zotonic.com\/id\/1316","uri_template":"https:\/\/zotonic.com\/id\/:id","websub":{"hub":"https:\/\/zotonic.com\/.zotonic\/websub","topic":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/1316"}},"status":"ok"}