/modules/mod_survey/mod_survey.erl

https://code.google.com/p/zotonic/ · Erlang · 366 lines · 254 code · 63 blank · 49 comment · 4 complexity · 57f057b6ef1c933608599986a6de57c2 MD5 · raw file

  1. %% @author Marc Worrell <marc@worrell.nl>
  2. %% @copyright 2010-2011 Marc Worrell
  3. %% @doc Survey module. Define surveys and let people fill them in.
  4. %% Copyright 2010-2011 Marc Worrell
  5. %%
  6. %% Licensed under the Apache License, Version 2.0 (the "License");
  7. %% you may not use this file except in compliance with the License.
  8. %% You may obtain a copy of the License at
  9. %%
  10. %% http://www.apache.org/licenses/LICENSE-2.0
  11. %%
  12. %% Unless required by applicable law or agreed to in writing, software
  13. %% distributed under the License is distributed on an "AS IS" BASIS,
  14. %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. %% See the License for the specific language governing permissions and
  16. %% limitations under the License.
  17. -module(mod_survey).
  18. -author("Marc Worrell <marc@worrell.nl>").
  19. -mod_title("Survey").
  20. -mod_description("Create and publish questionnaires.").
  21. -export([init/1]).
  22. %% interface functions
  23. -export([
  24. event/2,
  25. redraw_questions/2,
  26. new_question/1,
  27. delete_question/3,
  28. render_next_page/6,
  29. question_to_props/1,
  30. module_name/1
  31. ]).
  32. -include_lib("zotonic.hrl").
  33. -include("survey.hrl").
  34. %% @doc Initilize the data model.
  35. init(Context) ->
  36. m_survey:install(Context),
  37. z_datamodel:manage(?MODULE, datamodel(), Context).
  38. %% @doc Handle drag/drop events from the survey admin
  39. event({sort, Items, {dragdrop, {survey, [{id,Id}]}, _Delegate, "survey"}}, Context) ->
  40. event_sort(Id, Items, Context);
  41. event({postback, {survey_start, Args}, _, _}, Context) ->
  42. {id, SurveyId} = proplists:lookup(id, Args),
  43. render_update(render_next_page(SurveyId, 1, exact, [], [], Context), Args, Context);
  44. event({submit, {survey_next, Args}, _, _}, Context) ->
  45. {id, SurveyId} = proplists:lookup(id, Args),
  46. {page_nr, PageNr} = proplists:lookup(page_nr, Args),
  47. {answers, Answers} = proplists:lookup(answers, Args),
  48. {history, History} = proplists:lookup(history, Args),
  49. render_update(render_next_page(SurveyId, PageNr+1, forward, Answers, History, Context), Args, Context);
  50. event({postback, {survey_back, Args}, _, _}, Context) ->
  51. {id, SurveyId} = proplists:lookup(id, Args),
  52. % {page_nr, PageNr} = proplists:lookup(page_nr, Args),
  53. {answers, Answers} = proplists:lookup(answers, Args),
  54. {history, History} = proplists:lookup(history, Args),
  55. case History of
  56. [_,PageNr|History1] ->
  57. render_update(render_next_page(SurveyId, PageNr, exact, Answers, History1, Context), Args, Context);
  58. _History ->
  59. render_update(render_next_page(SurveyId, 0, exact, Answers, [], Context), Args, Context)
  60. end.
  61. %%====================================================================
  62. %% support functions
  63. %%====================================================================
  64. render_update(Render, Args, Context) ->
  65. TargetId = proplists:get_value(element_id, Args, "survey-question"),
  66. z_render:update(TargetId, Render, Context).
  67. %% @doc Handle the sort of a list. First check if there is any new item added.
  68. event_sort(Id, SortItems, Context) ->
  69. case has_new_q(SortItems) of
  70. true ->
  71. %% There is a new question added, redraw the list with the new item in edit state.
  72. {QuestionIds, NewQuestionId, NewQuestion} = items2id_new(SortItems),
  73. {ok, Id} = add_question(Id, QuestionIds, NewQuestionId, NewQuestion, Context),
  74. redraw_questions(Id, Context);
  75. false ->
  76. %% Order changed
  77. save_question_order(Id, items2id(SortItems), Context),
  78. Context
  79. end.
  80. %% @doc Replace the new item in the item list with a new id, return new item and its id
  81. items2id_new(Items) ->
  82. items2id_new(Items, []).
  83. items2id_new([{dragdrop, {q, NewItemOpts}, _, _}|T], Acc) ->
  84. NewItemId = z_ids:identifier(10),
  85. NewItem = new_question(proplists:get_value(type, NewItemOpts)),
  86. {lists:reverse(Acc, [NewItemId|items2id(T)]), NewItemId, NewItem};
  87. items2id_new([{dragdrop, _, _, ItemId}|T], Acc) ->
  88. items2id_new(T, [ItemId|Acc]).
  89. %% @doc Fetch all question ids from the sort list
  90. items2id(Items) ->
  91. items2id(Items, []).
  92. items2id([], Acc) ->
  93. lists:reverse(Acc);
  94. items2id([{dragdrop, _, _, ItemId}|T], Acc) ->
  95. items2id(T, [ItemId|Acc]).
  96. %% @doc Update the rsc with the new question and the new question order.
  97. add_question(Id, QuestionIds, NewQuestionId, NewQuestion, Context) ->
  98. New = case m_rsc:p(Id, survey, Context) of
  99. undefined ->
  100. {survey, [NewQuestionId], [{NewQuestionId, NewQuestion}]};
  101. {survey, _SurveyIds, SurveyQuestions} ->
  102. {survey, QuestionIds, [{NewQuestionId, NewQuestion}|SurveyQuestions]}
  103. end,
  104. m_rsc_update:update(Id, [{survey, New}], Context).
  105. %% @doc Delete a question, redraw the question list.
  106. %% @todo Make this more efficient by only removing the li with QuestionId.
  107. delete_question(Id, QuestionId, Context) ->
  108. case m_rsc:p(Id, survey, Context) of
  109. undefined ->
  110. Context;
  111. {survey, SurveyIds, SurveyQuestions} ->
  112. Ids1 = lists:delete(QuestionId, SurveyIds),
  113. Questions1 = z_utils:prop_delete(QuestionId, SurveyQuestions),
  114. m_rsc:update(Id, [{survey, {survey, Ids1, Questions1}}], Context),
  115. redraw_questions(Id, Context)
  116. end.
  117. %% @doc Update the rsc with the new question order.
  118. save_question_order(Id, QuestionIds, Context) ->
  119. {survey, _SurveyIds, SurveyQuestions} = m_rsc:p(Id, survey, Context),
  120. m_rsc_update:update(Id, [{survey, {survey, QuestionIds, SurveyQuestions}}], Context).
  121. %% @doc Check if the sort list contains a newly dropped question.
  122. has_new_q([]) ->
  123. false;
  124. has_new_q([{dragdrop, {q, _}, _, _}|_]) ->
  125. true;
  126. has_new_q([_|T]) ->
  127. has_new_q(T).
  128. %% @doc Generate the html for the survey editor in the admin, update the displayed survey.
  129. redraw_questions(Id, Context) ->
  130. Html = z_template:render("_admin_survey_questions_edit.tpl", [{id, Id}], Context),
  131. Context1 = z_render:update("survey", Html, Context),
  132. Context1.
  133. %% @doc Return the default state for each item type.
  134. new_question(Type) ->
  135. Mod = module_name(Type),
  136. Mod:new().
  137. %% @doc Fetch the next page from the survey, update the page view
  138. render_next_page(Id, 0, _Direction, Answers, _History, Context) ->
  139. z_render:update("survey-question",
  140. #render{
  141. template="_survey_start.tpl",
  142. vars=[{id,Id},{answers,Answers},{history,[]}]
  143. },
  144. Context);
  145. render_next_page(Id, PageNr, Direction, Answers, History, Context) ->
  146. As = z_context:get_q_all_noz(Context),
  147. Answers1 = lists:foldl(fun({Arg,_Val}, Acc) -> proplists:delete(Arg, Acc) end, Answers, As),
  148. Answers2 = Answers1 ++ As,
  149. case m_rsc:p(Id, survey, Context) of
  150. {survey, QuestionIds, Questions} ->
  151. Qs = [ proplists:get_value(QId, Questions) || QId <- QuestionIds ],
  152. Qs1 = [ Q || Q <- Qs, Q /= undefined ],
  153. case go_page(PageNr, Qs1, Answers2, Direction, Context) of
  154. {L,NewPageNr} when is_list(L) ->
  155. % A new list of questions, PageNr might be another than expected
  156. Vars = [ {id, Id},
  157. {page_nr, NewPageNr},
  158. {questions, [ question_to_props(Q) || Q <- L ]},
  159. {pages, count_pages(Qs1)},
  160. {answers, Answers2},
  161. {history, [NewPageNr|History]}],
  162. #render{template="_survey_question_page.tpl", vars=Vars};
  163. last ->
  164. % That was the last page. Show a thank you and save the result.
  165. case do_submit(Id, QuestionIds, Questions, Answers2, Context) of
  166. ok ->
  167. case z_convert:to_bool(m_rsc:p(Id, survey_show_results, Context)) of
  168. true ->
  169. #render{template="_survey_results.tpl", vars=[{id,Id}, {inline, true}, {history,History}]};
  170. false ->
  171. #render{template="_survey_end.tpl", vars=[{id,Id}, {history,History}]}
  172. end;
  173. {error, _Reason} ->
  174. #render{template="_survey_error.tpl", vars=[{id,Id}, {history,History}]}
  175. end
  176. end;
  177. _NoSurvey ->
  178. % No survey defined, show an error page.
  179. #render{template="_survey_empty.tpl", vars=[{id,Id}]}
  180. end.
  181. %% @doc Count the number of pages in the survey
  182. count_pages([]) ->
  183. 0;
  184. count_pages(L) ->
  185. count_pages(L, 1).
  186. count_pages([], N) ->
  187. N;
  188. count_pages([#survey_question{type=pagebreak}|L], N) ->
  189. L1 = lists:dropwhile(fun(#survey_question{type=pagebreak}) -> true; (_) -> false end, L),
  190. count_pages(L1, N+1);
  191. count_pages([_|L], N) ->
  192. count_pages(L, N).
  193. go_page(Nr, Qs, _Answers, exact, _Context) ->
  194. case fetch_page(Nr, Qs) of
  195. last ->
  196. last;
  197. {L,Nr1} ->
  198. L1 = lists:dropwhile(fun(#survey_question{type=pagebreak}) -> true; (_) -> false end, L),
  199. L2 = lists:takewhile(fun(#survey_question{type=pagebreak}) -> false; (_) -> true end, L1),
  200. {L2,Nr1}
  201. end;
  202. go_page(Nr, Qs, Answers, forward, Context) ->
  203. eval_page_jumps(fetch_page(Nr, Qs), Answers, Context).
  204. eval_page_jumps({[#survey_question{type=pagebreak} = Q|L],Nr}, Answers, Context) ->
  205. case survey_q_pagebreak:test(Q, Answers, Context) of
  206. ok ->
  207. eval_page_jumps({L,Nr}, Answers, Context);
  208. {jump, Name} ->
  209. % Go to question 'name', count pagebreaks in between for the new page nr
  210. % Only allow jumping forward to prevent endless loops.
  211. eval_page_jumps(fetch_question_name(L, z_convert:to_list(Name), Nr, in_pagebreak), Answers, Context);
  212. {error, Reason} ->
  213. {error, Reason}
  214. end;
  215. eval_page_jumps({[], _Nr}, _Answers, _Context) ->
  216. last;
  217. eval_page_jumps(Other, _Answers, _Context) ->
  218. Other.
  219. fetch_question_name([], _Name, Nr, _State) ->
  220. {[], Nr};
  221. fetch_question_name([#survey_question{name=Name}|_] = Qs, Name, Nr, _State) ->
  222. {Qs, Nr};
  223. fetch_question_name([#survey_question{type=pagebreak}|Qs], Name, Nr, in_q) ->
  224. fetch_question_name(Qs, Name, Nr+1, in_pagebreak);
  225. fetch_question_name([#survey_question{type=pagebreak}|Qs], Name, Nr, in_pagebreak) ->
  226. fetch_question_name(Qs, Name, Nr, in_pagebreak);
  227. fetch_question_name([_|Qs], Name, Nr, _State) ->
  228. fetch_question_name(Qs, Name, Nr, in_q).
  229. %% @doc Fetch the Nth page. Multiple page breaks in a row count as a single page break.
  230. %% Returns the question list at the point of the pagebreak, so any pagebreak jumps
  231. %% can be made.
  232. fetch_page(_Nr, []) ->
  233. last;
  234. fetch_page(Nr, L) ->
  235. fetch_page(1, Nr, L).
  236. fetch_page(_, _, []) ->
  237. last;
  238. fetch_page(N, Nr, L) when N >= Nr ->
  239. {L, N};
  240. fetch_page(N, Nr, [#survey_question{type=pagebreak}|_] = L) when N == Nr - 1 ->
  241. {L, Nr};
  242. fetch_page(N, Nr, [#survey_question{type=pagebreak}|L]) when N < Nr ->
  243. L1 = lists:dropwhile(fun(#survey_question{type=pagebreak}) -> true; (_) -> false end, L),
  244. fetch_page(N+1, Nr, L1);
  245. fetch_page(N, Nr, [_|L]) ->
  246. fetch_page(N, Nr, L).
  247. %% @doc Map a question to template friendly properties
  248. question_to_props(Q) ->
  249. [
  250. {name, Q#survey_question.name},
  251. {type, Q#survey_question.type},
  252. {question, Q#survey_question.question},
  253. {text, Q#survey_question.text},
  254. {parts, Q#survey_question.parts},
  255. {html, Q#survey_question.html},
  256. {is_required, Q#survey_question.is_required}
  257. ].
  258. %% @doc Collect all answers per question, save to the database.
  259. do_submit(SurveyId, QuestionIds, Questions, Answers, Context) ->
  260. {FoundAnswers, Missing} = collect_answers(QuestionIds, Questions, Answers),
  261. case Missing of
  262. [] ->
  263. m_survey:insert_survey_submission(SurveyId, FoundAnswers, Context),
  264. z_notifier:notify({survey_submit, SurveyId, FoundAnswers}, Context),
  265. ok;
  266. _ ->
  267. {error, notfound}
  268. end.
  269. %% @doc Collect all answers, report any missing answers.
  270. %% @spec collect_answers(list(), proplist(), Context) -> {AnswerList, MissingIdsList}
  271. collect_answers(QIds, Qs, Answers) ->
  272. collect_answers(QIds, Qs, Answers, [], []).
  273. collect_answers([], _Qs, _Answers, FoundAnswers, Missing) ->
  274. {FoundAnswers, Missing};
  275. collect_answers([QId|QIds], Qs, Answers, FoundAnswers, Missing) ->
  276. Q = proplists:get_value(QId, Qs),
  277. Module = module_name(Q),
  278. case Module:answer(Q, Answers) of
  279. {ok, none} ->
  280. collect_answers(QIds, Qs, Answers, FoundAnswers, Missing);
  281. {ok, AnswerList} ->
  282. collect_answers(QIds, Qs, Answers, [{QId, AnswerList}|FoundAnswers], Missing);
  283. {error, missing} ->
  284. case Q#survey_question.is_required of
  285. true ->
  286. collect_answers(QIds, Qs, Answers, FoundAnswers, [QId|Missing]);
  287. false ->
  288. collect_answers(QIds, Qs, Answers, FoundAnswers, Missing)
  289. end
  290. end.
  291. module_name(L) when is_list(L) ->
  292. module_name(list_to_atom(L));
  293. module_name(Type) when is_atom(Type) ->
  294. module_name(#survey_question{type=Type});
  295. module_name(#survey_question{type=Type}) ->
  296. list_to_atom("survey_q_"++atom_to_list(Type)).
  297. datamodel() ->
  298. [
  299. {categories, [
  300. {survey, undefined, [{title, "Survey"}]},
  301. {poll, survey, [{title, "Poll"}]}
  302. ]}
  303. ].