# HG changeset patch
# User bsw
# Date 1633644563 -7200
# Node ID 5a8a09119865ee72964e47806f30b47ed10e91c1
# Parent aebc3b064f857e5b60d7739c5567d50945aa500f
Added survey feature
diff -r aebc3b064f85 -r 5a8a09119865 app/main/_filter_view/30_navigation.lua
--- a/app/main/_filter_view/30_navigation.lua Thu Sep 30 22:39:46 2021 +0200
+++ b/app/main/_filter_view/30_navigation.lua Fri Oct 08 00:09:23 2021 +0200
@@ -116,6 +116,10 @@
end)
end
+if request.get_module() ~= "survey" then
+ execute.view{ module = "survey", view = "_notification" }
+end
+
-- show notifications about things the user should take care of
--[[
if app.session.member then
diff -r aebc3b064f85 -r 5a8a09119865 app/main/survey/_action/answer.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/survey/_action/answer.lua Fri Oct 08 00:09:23 2021 +0200
@@ -0,0 +1,73 @@
+local id = param.get("question_id", atom.integer)
+
+local question = SurveyQuestion:by_id(id)
+
+local survey = Survey:get_open()
+
+if question.survey_id ~= survey.id then
+ slot.put_into("error", _"Internal error 2")
+ return false
+end
+
+if not question or not question.survey.open then
+ slot.put_into("error", _"Internal error 3")
+ return false
+end
+
+local survey_member = SurveyMember:by_pk(question.survey.id, app.session.member_id)
+if not survey_member then
+ return execute.view { module = "index", view = "404" }
+end
+
+local answer_set = survey_member.answer_set
+if not answer_set then
+ return execute.view { module = "index", view = "404" }
+end
+
+local answer = SurveyAnswer:by_pk(answer_set.ident, question.id)
+if not answer then
+ answer = SurveyAnswer:new()
+ answer.survey_answer_set_ident = answer_set.ident
+ answer.survey_question_id = question.id
+end
+
+local given_answer = param.get("answer")
+
+if question.answer_type == "radio" then
+ if not given_answer then
+ slot.put_into("error", _"Please choose an option!")
+ return false
+ end
+ local answer_valid = false
+ for i, answer_option in ipairs(question.answer_options) do
+ if given_answer == answer_option then
+ answer_valid = true
+ end
+ end
+ if not answer_valid then
+ slot.put_into("error", _"Internal error 1")
+ return false
+ end
+end
+
+answer.answer = given_answer
+answer:save()
+
+local question
+local answers_by_question_id = {}
+for i, answer in ipairs(answer_set.answers) do
+ answers_by_question_id[answer.survey_question_id] = answer
+end
+for i, q in ipairs(survey.questions) do
+ if not question and not answers_by_question_id[q.id] then
+ question = q
+ end
+end
+
+if not question then
+ survey_member.survey_answer_set_ident = nil
+ survey_member.finished = 'now'
+ survey_member:save()
+end
+
+return true
diff -r aebc3b064f85 -r 5a8a09119865 app/main/survey/_action/participate.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/survey/_action/participate.lua Fri Oct 08 00:09:23 2021 +0200
@@ -0,0 +1,54 @@
+local skip_survey = param.get("skip_survey")
+
+local survey = Survey:get_open()
+
+local survey_member = SurveyMember:by_pk(survey.id, app.session.member_id)
+
+if survey_member and not skip_survey then
+ return true
+end
+
+local secret_length = 24
+local secret_alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
+local secret_purposes = { "oauth", "_other" }
+for idx, purpose in ipairs(secret_purposes) do
+ secret_purposes[purpose] = idx
+end
+
+local function random_string(length_multiplier)
+ return multirand.string(
+ secret_length * (length_multiplier or 1),
+ secret_alphabet
+ )
+end
+
+if not survey_member then
+ survey_member = SurveyMember:new()
+ survey_member.survey_id = survey.id
+ survey_member.member_id = app.session.member_id
+end
+
+if skip_survey then
+ local answer_set = survey_member.answer_set
+ if answer_set then
+ survey_member.survey_answer_set_ident = nil
+ survey_member:save()
+ answer_set:destroy()
+ end
+ survey_member.rejected = 'now'
+else
+ local answer_set = SurveyAnswerSet:new()
+ answer_set.ident = random_string()
+ answer_set.survey_id = survey.id
+ answer_set:save()
+ survey_member.survey_answer_set_ident = answer_set.ident
+end
+
+survey_member:save()
+
+if skip_survey then
+ return "skip_survey"
+end
+
+return true
+
diff -r aebc3b064f85 -r 5a8a09119865 app/main/survey/_notification.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/survey/_notification.lua Fri Oct 08 00:09:23 2021 +0200
@@ -0,0 +1,61 @@
+if not app.session.member then
+ return
+end
+local survey = Survey:get_open()
+
+if not survey then
+ return
+end
+
+local survey_member = SurveyMember:by_pk(survey.id, app.session.member_id)
+
+if not survey_member or survey_member.answer_set and not survey_member.finished then
+ slot.select("motd", function()
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ ui.form{
+ module = "survey", action = "participate",
+ routing = {
+ ok = { mode = "redirect", module = "survey", view = "participate" },
+ error = { mode = "forward", module = "survey", view = "participate" },
+ skip_survey = { mode = "redirect", module = "index", view = "index" },
+ },
+ content = function()
+ ui.heading{ content = survey.title }
+ ui.container{ content = function()
+ slot.put(survey.text)
+ end }
+ slot.put("
")
+ local start_text = _"Start survey"
+ local cancel_text = _"I don't want to particiapte"
+ if survey_member then
+ start_text = _"Continue survey"
+ cancel_text = _"Cancel survey"
+ end
+ ui.tag{
+ tag = "input",
+ attr = {
+ type = "submit",
+ class = "mdl-button mdl-js-button mdl-button--raised mdl-button--colored",
+ value = start_text
+ },
+ content = ""
+ }
+ slot.put(" ")
+ ui.tag{
+ tag = "input",
+ attr = {
+ type = "submit",
+ name = "skip_survey",
+ class = "mdl-button mdl-js-button",
+ value = cancel_text
+ },
+ content = ""
+ }
+ end
+ }
+ end }
+ end }
+ end)
+end
+
diff -r aebc3b064f85 -r 5a8a09119865 app/main/survey/index.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/survey/index.lua Fri Oct 08 00:09:23 2021 +0200
@@ -0,0 +1,15 @@
+ui.heading{ level = 1, content = _"Surveys" }
+
+local surveys = Survey:get_open()
+
+for i, survey in ipairs(surveys) do
+
+ ui.container{ content = function()
+
+ ui.link{ module = "survey", view = "participate", id = survey.id, content = survey.name }
+
+ ui.container{ content = survey.description }
+
+ end }
+
+end
diff -r aebc3b064f85 -r 5a8a09119865 app/main/survey/participate.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/survey/participate.lua Fri Oct 08 00:09:23 2021 +0200
@@ -0,0 +1,110 @@
+local survey = Survey:get_open()
+if not survey then
+ return execute.view { module = "index", view = "404" }
+end
+
+local survey_member = SurveyMember:by_pk(survey.id, app.session.member_id)
+if not survey_member then
+ return execute.view { module = "index", view = "404" }
+end
+
+local question
+
+if survey_member then
+ local answer_set = survey_member.answer_set
+ if answer_set then
+ local answers_by_question_id = {}
+ for i, answer in ipairs(answer_set.answers) do
+ answers_by_question_id[answer.survey_question_id] = answer
+ end
+ for i, q in ipairs(survey.questions) do
+ if not question and not answers_by_question_id[q.id] then
+ question = q
+ end
+ end
+ end
+end
+
+ui.title(_"Survey")
+ui.grid{ content = function()
+ ui.cell_main{ content = function()
+
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = survey.title }
+ ui.container{
+ content = _(
+ "This survey closes in #{closing}.",
+ { closing = format.interval_text(survey.time_left) }
+ )
+ }
+ end }
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ if survey_member.finished then
+ ui.container{ content = function()
+ slot.put(survey.finished_text)
+ end }
+ slot.put("
")
+ ui.link{
+ attr = { class = "mdl-button mdl-js-button mdl-button--raised mdl-button--colored" },
+ module = "index", view = "index", content = _"Go to start page"
+ }
+ return
+ else
+ ui.heading{ level = 2, content = question.question }
+ if question.description then
+ ui.container{ content = question.description }
+ end
+ ui.form{
+ module = "survey", action = "answer",
+ routing = {
+ ok = { mode = "redirect", module = "survey", view = "participate" },
+ error = { mode = "forward", module = "survey", view = "participate" },
+ },
+ content = function()
+ ui.field.hidden{ name = "question_id", value = question.id }
+ if question.answer_type == "radio" then
+ for i, answer_option in ipairs(question.answer_options) do
+ ui.container{ content = function()
+ ui.tag{ tag = "label", attr = {
+ class = "mdl-radio mdl-js-radio mdl-js-ripple-effect",
+ ["for"] = "answer_" .. i
+ },
+ content = function()
+ ui.tag{
+ tag = "input",
+ attr = {
+ id = "answer_" .. i,
+ class = "mdl-radio__button",
+ type = "radio",
+ name = "answer",
+ value = answer_option,
+ checked = param.get("answer") == answer_option and "checked" or nil,
+ }
+ }
+ ui.tag{
+ attr = { class = "mdl-radio__label", ['for'] = "answer_" .. i },
+ content = answer_option
+ }
+ end
+ }
+ end }
+ end
+ slot.put("
")
+ ui.tag{
+ tag = "input",
+ attr = {
+ type = "submit",
+ class = "mdl-button mdl-js-button mdl-button--raised mdl-button--colored",
+ value = _"Next step"
+ },
+ content = ""
+ }
+ end
+ end
+ }
+ end
+ end }
+ end }
+ end }
+end }
diff -r aebc3b064f85 -r 5a8a09119865 db/survey.sql
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/db/survey.sql Fri Oct 08 00:09:23 2021 +0200
@@ -0,0 +1,57 @@
+CREATE TABLE survey (
+ id SERIAL4 PRIMARY KEY,
+ title TEXT NOT NULL,
+ text TEXT NOT NULL,
+ finished_text TEXT NOT NULL,
+ created TIMESTAMPTZ NOT NULL DEFAULT now(),
+ open_from TIMESTAMPTZ NOT NULL,
+ open_until TIMESTAMPTZ NOT NULL
+);
+
+CREATE TABLE survey_question (
+ id SERIAL4 PRIMARY KEY,
+ survey_id INT4 NOT NULL REFERENCES survey(id),
+ position INT4 NOT NULL,
+ question TEXT NOT NULL,
+ description TEXT,
+ answer_type TEXT NOT NULL,
+ answer_options json
+);
+
+CREATE TABLE survey_answer_set (
+ ident TEXT NOT NULL PRIMARY KEY,
+ survey_id INT4 NOT NULL REFERENCES survey(id)
+);
+
+CREATE TABLE survey_answer (
+ id SERIAL8 PRIMARY KEY,
+ survey_answer_set_ident TEXT NOT NULL REFERENCES survey_answer_set(ident) ON DELETE CASCADE,
+ survey_question_id INT4 NOT NULL REFERENCES survey_question(id),
+ answer TEXT NOT NULL
+);
+
+CREATE TABLE survey_member (
+ id SERIAL4 PRIMARY KEY,
+ survey_id INT4 NOT NULL REFERENCES survey(id),
+ member_id INT4 NOT NULL REFERENCES member(id),
+ survey_answer_set_ident TEXT REFERENCES survey_answer_set(ident),
+ started TIMESTAMPTZ DEFAULT now(),
+ finished TIMESTAMPTZ,
+ rejected TIMESTAMPTZ
+);
+
+INSERT INTO survey (id, title, text, finished_text, open_from, open_until) VALUES (
+ 1,
+ 'Example survey',
+ 'This is just an example',
+ 'Done!
You finished it. Thank you very much.',
+ '2021-10-06 12:00',
+ '2021-10-14 12:00'
+);
+
+INSERT INTO survey_question (survey_id, position, question, answer_type, answer_options) VALUES
+ (1, 1, 'What color do you like?', 'radio', '[ "Red", "Green", "Blue" ]'),
+ (1, 2, 'What form do you like?', 'radio', '[ "Square", "Circle", "Hexagon" ]')
+;
+
+
diff -r aebc3b064f85 -r 5a8a09119865 model/survey.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/model/survey.lua Fri Oct 08 00:09:23 2021 +0200
@@ -0,0 +1,35 @@
+Survey = mondelefant.new_class()
+Survey.table = 'survey'
+
+Survey:add_reference{
+ mode = '1m',
+ to = "SurveyQuestion",
+ this_key = 'id',
+ that_key = 'survey_id',
+ ref = 'questions',
+ back_ref = 'survey',
+ default_order = 'position'
+}
+
+
+local new_selector = Survey.new_selector
+
+function Survey:new_selector()
+ local selector = new_selector(self)
+ selector:add_field("CASE WHEN (open_until NOTNULL AND open_until > now()) THEN open_until - now() ELSE NULL END", "time_left")
+ return selector
+end
+
+function Survey:get_open()
+ return self:new_selector()
+ :add_where("open_from < now() and open_until > now()")
+ :optional_object_mode()
+ :exec()
+end
+
+function Survey.object_get:open()
+ if self.open_from < atom.timestamp:get_current() and self.open_until > atom.timestamp:get_current() then
+ return true
+ end
+ return false
+end
diff -r aebc3b064f85 -r 5a8a09119865 model/survey_answer.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/model/survey_answer.lua Fri Oct 08 00:09:23 2021 +0200
@@ -0,0 +1,10 @@
+SurveyAnswer = mondelefant.new_class()
+SurveyAnswer.table = 'survey_answer'
+
+function SurveyAnswer:by_pk(survey_answer_set_ident, member_id)
+ return self:new_selector()
+ :add_where{ "survey_answer_set_ident = ?", survey_answer_set_ident }
+ :add_where{ "survey_question_id = ?", question_id }
+ :optional_object_mode()
+ :exec()
+end
diff -r aebc3b064f85 -r 5a8a09119865 model/survey_answer_set.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/model/survey_answer_set.lua Fri Oct 08 00:09:23 2021 +0200
@@ -0,0 +1,12 @@
+SurveyAnswerSet = mondelefant.new_class()
+SurveyAnswerSet.table = 'survey_answer_set'
+SurveyAnswerSet.primary_key = "ident"
+
+SurveyAnswerSet:add_reference{
+ mode = '1m',
+ to = "SurveyAnswer",
+ this_key = 'ident',
+ that_key = 'survey_answer_set_ident',
+ ref = 'answers',
+ back_ref = 'answer_set'
+}
diff -r aebc3b064f85 -r 5a8a09119865 model/survey_member.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/model/survey_member.lua Fri Oct 08 00:09:23 2021 +0200
@@ -0,0 +1,19 @@
+SurveyMember = mondelefant.new_class()
+SurveyMember.table = 'survey_member'
+
+SurveyMember:add_reference{
+ mode = '11',
+ to = "SurveyAnswerSet",
+ this_key = 'survey_answer_set_ident',
+ that_key = 'ident',
+ ref = 'answer_set',
+ back_ref = 'member'
+}
+
+function SurveyMember:by_pk(survey_id, member_id)
+ return self:new_selector()
+ :add_where{ "survey_id = ?", survey_id }
+ :add_where{ "member_id = ?", member_id }
+ :optional_object_mode()
+ :exec()
+end
diff -r aebc3b064f85 -r 5a8a09119865 model/survey_question.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/model/survey_question.lua Fri Oct 08 00:09:23 2021 +0200
@@ -0,0 +1,10 @@
+SurveyQuestion = mondelefant.new_class()
+SurveyQuestion.table = 'survey_question'
+
+SurveyQuestion:add_reference{
+ mode = 'm1',
+ to = "Survey",
+ this_key = 'survey_id',
+ that_key = 'id',
+ ref = 'survey',
+}