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(" &nbsp; ")
    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 +}

Impressum / About Us