liquid_feedback_frontend
changeset 1735:5a8a09119865
Added survey feature
author | bsw |
---|---|
date | Fri Oct 08 00:09:23 2021 +0200 (2021-10-08) |
parents | aebc3b064f85 |
children | 40a388c07af9 |
files | app/main/_filter_view/30_navigation.lua app/main/survey/_action/answer.lua app/main/survey/_action/participate.lua app/main/survey/_notification.lua app/main/survey/index.lua app/main/survey/participate.lua db/survey.sql model/survey.lua model/survey_answer.lua model/survey_answer_set.lua model/survey_member.lua model/survey_question.lua |
line diff
1.1 --- a/app/main/_filter_view/30_navigation.lua Thu Sep 30 22:39:46 2021 +0200 1.2 +++ b/app/main/_filter_view/30_navigation.lua Fri Oct 08 00:09:23 2021 +0200 1.3 @@ -116,6 +116,10 @@ 1.4 end) 1.5 end 1.6 1.7 +if request.get_module() ~= "survey" then 1.8 + execute.view{ module = "survey", view = "_notification" } 1.9 +end 1.10 + 1.11 -- show notifications about things the user should take care of 1.12 --[[ 1.13 if app.session.member then
2.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 2.2 +++ b/app/main/survey/_action/answer.lua Fri Oct 08 00:09:23 2021 +0200 2.3 @@ -0,0 +1,73 @@ 2.4 +local id = param.get("question_id", atom.integer) 2.5 + 2.6 +local question = SurveyQuestion:by_id(id) 2.7 + 2.8 +local survey = Survey:get_open() 2.9 + 2.10 +if question.survey_id ~= survey.id then 2.11 + slot.put_into("error", _"Internal error 2") 2.12 + return false 2.13 +end 2.14 + 2.15 +if not question or not question.survey.open then 2.16 + slot.put_into("error", _"Internal error 3") 2.17 + return false 2.18 +end 2.19 + 2.20 +local survey_member = SurveyMember:by_pk(question.survey.id, app.session.member_id) 2.21 +if not survey_member then 2.22 + return execute.view { module = "index", view = "404" } 2.23 +end 2.24 + 2.25 +local answer_set = survey_member.answer_set 2.26 +if not answer_set then 2.27 + return execute.view { module = "index", view = "404" } 2.28 +end 2.29 + 2.30 +local answer = SurveyAnswer:by_pk(answer_set.ident, question.id) 2.31 +if not answer then 2.32 + answer = SurveyAnswer:new() 2.33 + answer.survey_answer_set_ident = answer_set.ident 2.34 + answer.survey_question_id = question.id 2.35 +end 2.36 + 2.37 +local given_answer = param.get("answer") 2.38 + 2.39 +if question.answer_type == "radio" then 2.40 + if not given_answer then 2.41 + slot.put_into("error", _"Please choose an option!") 2.42 + return false 2.43 + end 2.44 + local answer_valid = false 2.45 + for i, answer_option in ipairs(question.answer_options) do 2.46 + if given_answer == answer_option then 2.47 + answer_valid = true 2.48 + end 2.49 + end 2.50 + if not answer_valid then 2.51 + slot.put_into("error", _"Internal error 1") 2.52 + return false 2.53 + end 2.54 +end 2.55 + 2.56 +answer.answer = given_answer 2.57 +answer:save() 2.58 + 2.59 +local question 2.60 +local answers_by_question_id = {} 2.61 +for i, answer in ipairs(answer_set.answers) do 2.62 + answers_by_question_id[answer.survey_question_id] = answer 2.63 +end 2.64 +for i, q in ipairs(survey.questions) do 2.65 + if not question and not answers_by_question_id[q.id] then 2.66 + question = q 2.67 + end 2.68 +end 2.69 + 2.70 +if not question then 2.71 + survey_member.survey_answer_set_ident = nil 2.72 + survey_member.finished = 'now' 2.73 + survey_member:save() 2.74 +end 2.75 + 2.76 +return true
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 3.2 +++ b/app/main/survey/_action/participate.lua Fri Oct 08 00:09:23 2021 +0200 3.3 @@ -0,0 +1,54 @@ 3.4 +local skip_survey = param.get("skip_survey") 3.5 + 3.6 +local survey = Survey:get_open() 3.7 + 3.8 +local survey_member = SurveyMember:by_pk(survey.id, app.session.member_id) 3.9 + 3.10 +if survey_member and not skip_survey then 3.11 + return true 3.12 +end 3.13 + 3.14 +local secret_length = 24 3.15 +local secret_alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' 3.16 +local secret_purposes = { "oauth", "_other" } 3.17 +for idx, purpose in ipairs(secret_purposes) do 3.18 + secret_purposes[purpose] = idx 3.19 +end 3.20 + 3.21 +local function random_string(length_multiplier) 3.22 + return multirand.string( 3.23 + secret_length * (length_multiplier or 1), 3.24 + secret_alphabet 3.25 + ) 3.26 +end 3.27 + 3.28 +if not survey_member then 3.29 + survey_member = SurveyMember:new() 3.30 + survey_member.survey_id = survey.id 3.31 + survey_member.member_id = app.session.member_id 3.32 +end 3.33 + 3.34 +if skip_survey then 3.35 + local answer_set = survey_member.answer_set 3.36 + if answer_set then 3.37 + survey_member.survey_answer_set_ident = nil 3.38 + survey_member:save() 3.39 + answer_set:destroy() 3.40 + end 3.41 + survey_member.rejected = 'now' 3.42 +else 3.43 + local answer_set = SurveyAnswerSet:new() 3.44 + answer_set.ident = random_string() 3.45 + answer_set.survey_id = survey.id 3.46 + answer_set:save() 3.47 + survey_member.survey_answer_set_ident = answer_set.ident 3.48 +end 3.49 + 3.50 +survey_member:save() 3.51 + 3.52 +if skip_survey then 3.53 + return "skip_survey" 3.54 +end 3.55 + 3.56 +return true 3.57 +
4.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 4.2 +++ b/app/main/survey/_notification.lua Fri Oct 08 00:09:23 2021 +0200 4.3 @@ -0,0 +1,61 @@ 4.4 +if not app.session.member then 4.5 + return 4.6 +end 4.7 +local survey = Survey:get_open() 4.8 + 4.9 +if not survey then 4.10 + return 4.11 +end 4.12 + 4.13 +local survey_member = SurveyMember:by_pk(survey.id, app.session.member_id) 4.14 + 4.15 +if not survey_member or survey_member.answer_set and not survey_member.finished then 4.16 + slot.select("motd", function() 4.17 + ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function() 4.18 + ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function() 4.19 + ui.form{ 4.20 + module = "survey", action = "participate", 4.21 + routing = { 4.22 + ok = { mode = "redirect", module = "survey", view = "participate" }, 4.23 + error = { mode = "forward", module = "survey", view = "participate" }, 4.24 + skip_survey = { mode = "redirect", module = "index", view = "index" }, 4.25 + }, 4.26 + content = function() 4.27 + ui.heading{ content = survey.title } 4.28 + ui.container{ content = function() 4.29 + slot.put(survey.text) 4.30 + end } 4.31 + slot.put("<br>") 4.32 + local start_text = _"Start survey" 4.33 + local cancel_text = _"I don't want to particiapte" 4.34 + if survey_member then 4.35 + start_text = _"Continue survey" 4.36 + cancel_text = _"Cancel survey" 4.37 + end 4.38 + ui.tag{ 4.39 + tag = "input", 4.40 + attr = { 4.41 + type = "submit", 4.42 + class = "mdl-button mdl-js-button mdl-button--raised mdl-button--colored", 4.43 + value = start_text 4.44 + }, 4.45 + content = "" 4.46 + } 4.47 + slot.put(" ") 4.48 + ui.tag{ 4.49 + tag = "input", 4.50 + attr = { 4.51 + type = "submit", 4.52 + name = "skip_survey", 4.53 + class = "mdl-button mdl-js-button", 4.54 + value = cancel_text 4.55 + }, 4.56 + content = "" 4.57 + } 4.58 + end 4.59 + } 4.60 + end } 4.61 + end } 4.62 + end) 4.63 +end 4.64 +
5.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 5.2 +++ b/app/main/survey/index.lua Fri Oct 08 00:09:23 2021 +0200 5.3 @@ -0,0 +1,15 @@ 5.4 +ui.heading{ level = 1, content = _"Surveys" } 5.5 + 5.6 +local surveys = Survey:get_open() 5.7 + 5.8 +for i, survey in ipairs(surveys) do 5.9 + 5.10 + ui.container{ content = function() 5.11 + 5.12 + ui.link{ module = "survey", view = "participate", id = survey.id, content = survey.name } 5.13 + 5.14 + ui.container{ content = survey.description } 5.15 + 5.16 + end } 5.17 + 5.18 +end
6.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 6.2 +++ b/app/main/survey/participate.lua Fri Oct 08 00:09:23 2021 +0200 6.3 @@ -0,0 +1,110 @@ 6.4 +local survey = Survey:get_open() 6.5 +if not survey then 6.6 + return execute.view { module = "index", view = "404" } 6.7 +end 6.8 + 6.9 +local survey_member = SurveyMember:by_pk(survey.id, app.session.member_id) 6.10 +if not survey_member then 6.11 + return execute.view { module = "index", view = "404" } 6.12 +end 6.13 + 6.14 +local question 6.15 + 6.16 +if survey_member then 6.17 + local answer_set = survey_member.answer_set 6.18 + if answer_set then 6.19 + local answers_by_question_id = {} 6.20 + for i, answer in ipairs(answer_set.answers) do 6.21 + answers_by_question_id[answer.survey_question_id] = answer 6.22 + end 6.23 + for i, q in ipairs(survey.questions) do 6.24 + if not question and not answers_by_question_id[q.id] then 6.25 + question = q 6.26 + end 6.27 + end 6.28 + end 6.29 +end 6.30 + 6.31 +ui.title(_"Survey") 6.32 +ui.grid{ content = function() 6.33 + ui.cell_main{ content = function() 6.34 + 6.35 + ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function() 6.36 + ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function() 6.37 + ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = survey.title } 6.38 + ui.container{ 6.39 + content = _( 6.40 + "This survey closes in #{closing}.", 6.41 + { closing = format.interval_text(survey.time_left) } 6.42 + ) 6.43 + } 6.44 + end } 6.45 + ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function() 6.46 + if survey_member.finished then 6.47 + ui.container{ content = function() 6.48 + slot.put(survey.finished_text) 6.49 + end } 6.50 + slot.put("<br>") 6.51 + ui.link{ 6.52 + attr = { class = "mdl-button mdl-js-button mdl-button--raised mdl-button--colored" }, 6.53 + module = "index", view = "index", content = _"Go to start page" 6.54 + } 6.55 + return 6.56 + else 6.57 + ui.heading{ level = 2, content = question.question } 6.58 + if question.description then 6.59 + ui.container{ content = question.description } 6.60 + end 6.61 + ui.form{ 6.62 + module = "survey", action = "answer", 6.63 + routing = { 6.64 + ok = { mode = "redirect", module = "survey", view = "participate" }, 6.65 + error = { mode = "forward", module = "survey", view = "participate" }, 6.66 + }, 6.67 + content = function() 6.68 + ui.field.hidden{ name = "question_id", value = question.id } 6.69 + if question.answer_type == "radio" then 6.70 + for i, answer_option in ipairs(question.answer_options) do 6.71 + ui.container{ content = function() 6.72 + ui.tag{ tag = "label", attr = { 6.73 + class = "mdl-radio mdl-js-radio mdl-js-ripple-effect", 6.74 + ["for"] = "answer_" .. i 6.75 + }, 6.76 + content = function() 6.77 + ui.tag{ 6.78 + tag = "input", 6.79 + attr = { 6.80 + id = "answer_" .. i, 6.81 + class = "mdl-radio__button", 6.82 + type = "radio", 6.83 + name = "answer", 6.84 + value = answer_option, 6.85 + checked = param.get("answer") == answer_option and "checked" or nil, 6.86 + } 6.87 + } 6.88 + ui.tag{ 6.89 + attr = { class = "mdl-radio__label", ['for'] = "answer_" .. i }, 6.90 + content = answer_option 6.91 + } 6.92 + end 6.93 + } 6.94 + end } 6.95 + end 6.96 + slot.put("<br>") 6.97 + ui.tag{ 6.98 + tag = "input", 6.99 + attr = { 6.100 + type = "submit", 6.101 + class = "mdl-button mdl-js-button mdl-button--raised mdl-button--colored", 6.102 + value = _"Next step" 6.103 + }, 6.104 + content = "" 6.105 + } 6.106 + end 6.107 + end 6.108 + } 6.109 + end 6.110 + end } 6.111 + end } 6.112 + end } 6.113 +end }
7.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 7.2 +++ b/db/survey.sql Fri Oct 08 00:09:23 2021 +0200 7.3 @@ -0,0 +1,57 @@ 7.4 +CREATE TABLE survey ( 7.5 + id SERIAL4 PRIMARY KEY, 7.6 + title TEXT NOT NULL, 7.7 + text TEXT NOT NULL, 7.8 + finished_text TEXT NOT NULL, 7.9 + created TIMESTAMPTZ NOT NULL DEFAULT now(), 7.10 + open_from TIMESTAMPTZ NOT NULL, 7.11 + open_until TIMESTAMPTZ NOT NULL 7.12 +); 7.13 + 7.14 +CREATE TABLE survey_question ( 7.15 + id SERIAL4 PRIMARY KEY, 7.16 + survey_id INT4 NOT NULL REFERENCES survey(id), 7.17 + position INT4 NOT NULL, 7.18 + question TEXT NOT NULL, 7.19 + description TEXT, 7.20 + answer_type TEXT NOT NULL, 7.21 + answer_options json 7.22 +); 7.23 + 7.24 +CREATE TABLE survey_answer_set ( 7.25 + ident TEXT NOT NULL PRIMARY KEY, 7.26 + survey_id INT4 NOT NULL REFERENCES survey(id) 7.27 +); 7.28 + 7.29 +CREATE TABLE survey_answer ( 7.30 + id SERIAL8 PRIMARY KEY, 7.31 + survey_answer_set_ident TEXT NOT NULL REFERENCES survey_answer_set(ident) ON DELETE CASCADE, 7.32 + survey_question_id INT4 NOT NULL REFERENCES survey_question(id), 7.33 + answer TEXT NOT NULL 7.34 +); 7.35 + 7.36 +CREATE TABLE survey_member ( 7.37 + id SERIAL4 PRIMARY KEY, 7.38 + survey_id INT4 NOT NULL REFERENCES survey(id), 7.39 + member_id INT4 NOT NULL REFERENCES member(id), 7.40 + survey_answer_set_ident TEXT REFERENCES survey_answer_set(ident), 7.41 + started TIMESTAMPTZ DEFAULT now(), 7.42 + finished TIMESTAMPTZ, 7.43 + rejected TIMESTAMPTZ 7.44 +); 7.45 + 7.46 +INSERT INTO survey (id, title, text, finished_text, open_from, open_until) VALUES ( 7.47 + 1, 7.48 + 'Example survey', 7.49 + 'This is just an example', 7.50 + '<strong>Done!</strong><br>You finished it. Thank you very much.', 7.51 + '2021-10-06 12:00', 7.52 + '2021-10-14 12:00' 7.53 +); 7.54 + 7.55 +INSERT INTO survey_question (survey_id, position, question, answer_type, answer_options) VALUES 7.56 + (1, 1, 'What color do you like?', 'radio', '[ "Red", "Green", "Blue" ]'), 7.57 + (1, 2, 'What form do you like?', 'radio', '[ "Square", "Circle", "Hexagon" ]') 7.58 +; 7.59 + 7.60 +
8.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 8.2 +++ b/model/survey.lua Fri Oct 08 00:09:23 2021 +0200 8.3 @@ -0,0 +1,35 @@ 8.4 +Survey = mondelefant.new_class() 8.5 +Survey.table = 'survey' 8.6 + 8.7 +Survey:add_reference{ 8.8 + mode = '1m', 8.9 + to = "SurveyQuestion", 8.10 + this_key = 'id', 8.11 + that_key = 'survey_id', 8.12 + ref = 'questions', 8.13 + back_ref = 'survey', 8.14 + default_order = 'position' 8.15 +} 8.16 + 8.17 + 8.18 +local new_selector = Survey.new_selector 8.19 + 8.20 +function Survey:new_selector() 8.21 + local selector = new_selector(self) 8.22 + selector:add_field("CASE WHEN (open_until NOTNULL AND open_until > now()) THEN open_until - now() ELSE NULL END", "time_left") 8.23 + return selector 8.24 +end 8.25 + 8.26 +function Survey:get_open() 8.27 + return self:new_selector() 8.28 + :add_where("open_from < now() and open_until > now()") 8.29 + :optional_object_mode() 8.30 + :exec() 8.31 +end 8.32 + 8.33 +function Survey.object_get:open() 8.34 + if self.open_from < atom.timestamp:get_current() and self.open_until > atom.timestamp:get_current() then 8.35 + return true 8.36 + end 8.37 + return false 8.38 +end
9.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 9.2 +++ b/model/survey_answer.lua Fri Oct 08 00:09:23 2021 +0200 9.3 @@ -0,0 +1,10 @@ 9.4 +SurveyAnswer = mondelefant.new_class() 9.5 +SurveyAnswer.table = 'survey_answer' 9.6 + 9.7 +function SurveyAnswer:by_pk(survey_answer_set_ident, member_id) 9.8 + return self:new_selector() 9.9 + :add_where{ "survey_answer_set_ident = ?", survey_answer_set_ident } 9.10 + :add_where{ "survey_question_id = ?", question_id } 9.11 + :optional_object_mode() 9.12 + :exec() 9.13 +end
10.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 10.2 +++ b/model/survey_answer_set.lua Fri Oct 08 00:09:23 2021 +0200 10.3 @@ -0,0 +1,12 @@ 10.4 +SurveyAnswerSet = mondelefant.new_class() 10.5 +SurveyAnswerSet.table = 'survey_answer_set' 10.6 +SurveyAnswerSet.primary_key = "ident" 10.7 + 10.8 +SurveyAnswerSet:add_reference{ 10.9 + mode = '1m', 10.10 + to = "SurveyAnswer", 10.11 + this_key = 'ident', 10.12 + that_key = 'survey_answer_set_ident', 10.13 + ref = 'answers', 10.14 + back_ref = 'answer_set' 10.15 +}
11.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 11.2 +++ b/model/survey_member.lua Fri Oct 08 00:09:23 2021 +0200 11.3 @@ -0,0 +1,19 @@ 11.4 +SurveyMember = mondelefant.new_class() 11.5 +SurveyMember.table = 'survey_member' 11.6 + 11.7 +SurveyMember:add_reference{ 11.8 + mode = '11', 11.9 + to = "SurveyAnswerSet", 11.10 + this_key = 'survey_answer_set_ident', 11.11 + that_key = 'ident', 11.12 + ref = 'answer_set', 11.13 + back_ref = 'member' 11.14 +} 11.15 + 11.16 +function SurveyMember:by_pk(survey_id, member_id) 11.17 + return self:new_selector() 11.18 + :add_where{ "survey_id = ?", survey_id } 11.19 + :add_where{ "member_id = ?", member_id } 11.20 + :optional_object_mode() 11.21 + :exec() 11.22 +end
12.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 12.2 +++ b/model/survey_question.lua Fri Oct 08 00:09:23 2021 +0200 12.3 @@ -0,0 +1,10 @@ 12.4 +SurveyQuestion = mondelefant.new_class() 12.5 +SurveyQuestion.table = 'survey_question' 12.6 + 12.7 +SurveyQuestion:add_reference{ 12.8 + mode = 'm1', 12.9 + to = "Survey", 12.10 + this_key = 'survey_id', 12.11 + that_key = 'id', 12.12 + ref = 'survey', 12.13 +}