PageRenderTime 36ms CodeModel.GetById 6ms app.highlight 26ms RepoModel.GetById 1ms app.codeStats 0ms

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