1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2013-Today OpenERP SA (<http://www.openerp.com>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
25 from datetime import datetime
28 from openerp import SUPERUSER_ID
29 from openerp.addons.web import http
30 from openerp.addons.web.http import request
31 from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT as DTF
32 from openerp.tools.safe_eval import safe_eval
35 _logger = logging.getLogger(__name__)
38 class WebsiteSurvey(http.Controller):
42 def _check_bad_cases(self, cr, uid, request, survey_obj, survey, user_input_obj, context=None):
43 # In case of bad survey, redirect to surveys list
44 if survey_obj.exists(cr, SUPERUSER_ID, survey.id, context=context) == []:
45 return werkzeug.utils.redirect("/survey/")
47 # In case of auth required, block public user
48 if survey.auth_required and uid == request.website.user_id.id:
49 return request.website.render("survey.auth_required", {'survey': survey})
51 # In case of non open surveys
52 if survey.stage_id.closed:
53 return request.website.render("survey.notopen")
55 # If there is no pages
56 if not survey.page_ids:
57 return request.website.render("survey.nopages")
59 # Everything seems to be ok
62 def _check_deadline(self, cr, uid, user_input, context=None):
63 '''Prevent opening of the survey if the deadline has turned out
65 ! This will NOT disallow access to users who have already partially filled the survey !'''
66 if user_input.deadline:
67 dt_deadline = datetime.strptime(user_input.deadline, DTF)
68 dt_now = datetime.now()
69 if dt_now > dt_deadline: # survey is not open anymore
70 return request.website.render("survey.notopen")
77 @http.route(['/survey/start/<model("survey.survey"):survey>',
78 '/survey/start/<model("survey.survey"):survey>/<string:token>'],
79 type='http', auth='public', website=True)
80 def start_survey(self, survey, token=None, **post):
81 cr, uid, context = request.cr, request.uid, request.context
82 survey_obj = request.registry['survey.survey']
83 user_input_obj = request.registry['survey.user_input']
86 if token and token == "phantom":
87 _logger.info("[survey] Phantom mode")
88 user_input_id = user_input_obj.create(cr, uid, {'survey_id': survey.id, 'test_entry': True}, context=context)
89 user_input = user_input_obj.browse(cr, uid, [user_input_id], context=context)[0]
90 data = {'survey': survey, 'page': None, 'token': user_input.token}
91 return request.website.render('survey.survey_init', data)
94 # Controls if the survey can be displayed
95 errpage = self._check_bad_cases(cr, uid, request, survey_obj, survey, user_input_obj, context=context)
101 user_input_id = user_input_obj.create(cr, uid, {'survey_id': survey.id}, context=context)
102 user_input = user_input_obj.browse(cr, uid, [user_input_id], context=context)[0]
105 user_input_id = user_input_obj.search(cr, uid, [('token', '=', token)], context=context)[0]
106 except IndexError: # Invalid token
107 return request.website.render("website.403")
109 user_input = user_input_obj.browse(cr, uid, [user_input_id], context=context)[0]
111 # Do not open expired survey
112 errpage = self._check_deadline(cr, uid, user_input, context=context)
116 # Select the right page
117 if user_input.state == 'new': # Intro page
118 data = {'survey': survey, 'page': None, 'token': user_input.token}
119 return request.website.render('survey.survey_init', data)
121 return request.redirect('/survey/fill/%s/%s' % (survey.id, user_input.token))
124 @http.route(['/survey/fill/<model("survey.survey"):survey>/<string:token>',
125 '/survey/fill/<model("survey.survey"):survey>/<string:token>/<string:prev>'],
126 type='http', auth='public', website=True)
127 def fill_survey(self, survey, token, prev=None, **post):
128 '''Display and validates a survey'''
129 cr, uid, context = request.cr, request.uid, request.context
130 survey_obj = request.registry['survey.survey']
131 user_input_obj = request.registry['survey.user_input']
133 # Controls if the survey can be displayed
134 errpage = self._check_bad_cases(cr, uid, request, survey_obj, survey, user_input_obj, context=context)
138 # Load the user_input
140 user_input_id = user_input_obj.search(cr, uid, [('token', '=', token)])[0]
141 except IndexError: # Invalid token
142 return request.website.render("website.403")
144 user_input = user_input_obj.browse(cr, uid, [user_input_id], context=context)[0]
146 # Do not display expired survey (even if some pages have already been
147 # displayed -- There's a time for everything!)
148 errpage = self._check_deadline(cr, uid, user_input, context=context)
152 # Select the right page
153 if user_input.state == 'new': # First page
154 page, page_nr, last = survey_obj.next_page(cr, uid, user_input, 0, go_back=False, context=context)
155 data = {'survey': survey, 'page': page, 'page_nr': page_nr, 'token': user_input.token}
157 data.update({'last': True})
158 return request.website.render('survey.survey', data)
159 elif user_input.state == 'done': # Display success message
160 return request.website.render('survey.sfinished', {'survey': survey,
162 'user_input': user_input})
163 elif user_input.state == 'skip':
164 flag = (True if prev and prev == 'prev' else False)
165 page, page_nr, last = survey_obj.next_page(cr, uid, user_input, user_input.last_displayed_page_id.id, go_back=flag, context=context)
166 data = {'survey': survey, 'page': page, 'page_nr': page_nr, 'token': user_input.token}
168 data.update({'last': True})
169 return request.website.render('survey.survey', data)
171 return request.website.render("website.403")
173 # AJAX prefilling of a survey
174 @http.route(['/survey/prefill/<model("survey.survey"):survey>/<string:token>',
175 '/survey/prefill/<model("survey.survey"):survey>/<string:token>/<model("survey.page"):page>'],
176 type='http', auth='public', website=True)
177 def prefill(self, survey, token, page=None, **post):
178 cr, uid, context = request.cr, request.uid, request.context
179 user_input_line_obj = request.registry['survey.user_input_line']
182 # Fetch previous answers
184 ids = user_input_line_obj.search(cr, uid, [('user_input_id.token', '=', token), ('page_id', '=', page.id)], context=context)
186 ids = user_input_line_obj.search(cr, uid, [('user_input_id.token', '=', token)], context=context)
187 previous_answers = user_input_line_obj.browse(cr, uid, ids, context=context)
189 # Return non empty answers in a JSON compatible format
190 for answer in previous_answers:
191 if not answer.skipped:
192 answer_tag = '%s_%s_%s' % (answer.survey_id.id, answer.page_id.id, answer.question_id.id)
194 if answer.answer_type == 'free_text':
195 answer_value = answer.value_free_text
196 elif answer.answer_type == 'text' and answer.question_id.type == 'textbox':
197 answer_value = answer.value_text
198 elif answer.answer_type == 'text' and answer.question_id.type != 'textbox':
199 # here come comment answers for matrices, simple choice and multiple choice
200 answer_tag = "%s_%s" % (answer_tag, 'comment')
201 answer_value = answer.value_text
202 elif answer.answer_type == 'number':
203 answer_value = answer.value_number.__str__()
204 elif answer.answer_type == 'date':
205 answer_value = answer.value_date
206 elif answer.answer_type == 'suggestion' and not answer.value_suggested_row:
207 answer_value = answer.value_suggested.id
208 elif answer.answer_type == 'suggestion' and answer.value_suggested_row:
209 answer_tag = "%s_%s" % (answer_tag, answer.value_suggested_row.id)
210 answer_value = answer.value_suggested.id
212 dict_soft_update(ret, answer_tag, answer_value)
214 _logger.warning("[survey] No answer has been found for question %s marked as non skipped" % answer_tag)
215 return json.dumps(ret)
217 # AJAX scores loading for quiz correction mode
218 @http.route(['/survey/scores/<model("survey.survey"):survey>/<string:token>'],
219 type='http', auth='public', website=True)
220 def get_scores(self, survey, token, page=None, **post):
221 cr, uid, context = request.cr, request.uid, request.context
222 user_input_line_obj = request.registry['survey.user_input_line']
226 ids = user_input_line_obj.search(cr, uid, [('user_input_id.token', '=', token)], context=context)
227 previous_answers = user_input_line_obj.browse(cr, uid, ids, context=context)
229 # Compute score for each question
230 for answer in previous_answers:
231 tmp_score = ret.get(answer.question_id.id, 0.0)
232 ret.update({answer.question_id.id: tmp_score + answer.quizz_mark})
233 return json.dumps(ret)
235 # AJAX submission of a page
236 @http.route(['/survey/submit/<model("survey.survey"):survey>'],
237 type='http', methods=['POST'], auth='public', website=True)
238 def submit(self, survey, **post):
239 _logger.debug('Incoming data: %s', post)
240 page_id = int(post['page_id'])
241 cr, uid, context = request.cr, request.uid, request.context
242 survey_obj = request.registry['survey.survey']
243 questions_obj = request.registry['survey.question']
244 questions_ids = questions_obj.search(cr, uid, [('page_id', '=', page_id)], context=context)
245 questions = questions_obj.browse(cr, uid, questions_ids, context=context)
249 for question in questions:
250 answer_tag = "%s_%s_%s" % (survey.id, page_id, question.id)
251 errors.update(questions_obj.validate_question(cr, uid, question, post, answer_tag, context=context))
254 if (len(errors) != 0):
255 # Return errors messages to webpage
256 ret['errors'] = errors
258 # Store answers into database
259 user_input_obj = request.registry['survey.user_input']
261 user_input_line_obj = request.registry['survey.user_input_line']
263 user_input_id = user_input_obj.search(cr, uid, [('token', '=', post['token'])], context=context)[0]
264 except KeyError: # Invalid token
265 return request.website.render("website.403")
266 for question in questions:
267 answer_tag = "%s_%s_%s" % (survey.id, page_id, question.id)
268 user_input_line_obj.save_lines(cr, uid, user_input_id, question, post, answer_tag, context=context)
270 user_input = user_input_obj.browse(cr, uid, user_input_id, context=context)
271 go_back = post['button_submit'] == 'previous'
272 next_page, _, last = survey_obj.next_page(cr, uid, user_input, page_id, go_back=go_back, context=context)
273 vals = {'last_displayed_page_id': page_id}
274 if next_page is None and not go_back:
275 vals.update({'state': 'done'})
277 vals.update({'state': 'skip'})
278 user_input_obj.write(cr, uid, user_input_id, vals, context=context)
279 ret['redirect'] = '/survey/fill/%s/%s' % (survey.id, post['token'])
281 ret['redirect'] += '/prev'
282 return json.dumps(ret)
285 @http.route(['/survey/print/<model("survey.survey"):survey>',
286 '/survey/print/<model("survey.survey"):survey>/<string:token>'],
287 type='http', auth='public', website=True)
288 def print_survey(self, survey, token=None, **post):
289 '''Display an survey in printable view; if <token> is set, it will
290 grab the answers of the user_input_id that has <token>.'''
291 return request.website.render('survey.survey_print',
295 'quizz_correction': True if survey.quizz_mode and token else False})
297 @http.route(['/survey/results/<model("survey.survey"):survey>'],
298 type='http', auth='user', website=True)
299 def survey_reporting(self, survey, token=None, **post):
300 '''Display survey Results & Statistics for given survey.'''
301 result_template ='survey.result'
303 filter_display_data = []
304 filter_finish = False
306 survey_obj = request.registry['survey.survey']
307 if not survey.user_input_ids or not [input_id.id for input_id in survey.user_input_ids if input_id.state != 'new']:
308 result_template = 'survey.no_result'
309 if 'finished' in post:
312 if post or filter_finish:
313 filter_data = self.get_filter_data(post)
314 current_filters = survey_obj.filter_input_ids(request.cr, request.uid, survey, filter_data, filter_finish, context=request.context)
315 filter_display_data = survey_obj.get_filter_display_data(request.cr, request.uid, filter_data, context=request.context)
316 return request.website.render(result_template,
318 'survey_dict': self.prepare_result_dict(survey, current_filters),
319 'page_range': self.page_range,
320 'current_filters': current_filters,
321 'filter_display_data': filter_display_data,
322 'filter_finish': filter_finish
324 # Quick retroengineering of what is injected into the template for now:
325 # (TODO: flatten and simplify this)
327 # survey: a browse record of the survey
328 # survey_dict: very messy dict containing all the info to display answers
333 # {'page': browse record of the page,
338 # {'graph_data': data to be displayed on the graph
339 # 'input_summary': number of answered, skipped...
340 # 'prepare_result': {
341 # answers displayed in the tables
343 # 'question': browse record of the question_ids
356 # page_range: pager helper function
357 # current_filters: a list of ids
358 # filter_display_data: [{'labels': ['a', 'b'], question_text} ... ]
359 # filter_finish: boolean => only finished surveys or not
362 def prepare_result_dict(self,survey, current_filters=[]):
363 """Returns dictionary having values for rendering template"""
364 survey_obj = request.registry['survey.survey']
365 result = {'page_ids': []}
366 for page in survey.page_ids:
367 page_dict = {'page': page, 'question_ids': []}
368 for question in page.question_ids:
369 question_dict = {'question':question, 'input_summary':survey_obj.get_input_summary(request.cr, request.uid, question, current_filters, context=request.context), 'prepare_result':survey_obj.prepare_result(request.cr, request.uid, question, current_filters, context=request.context), 'graph_data': self.get_graph_data(question, current_filters)}
370 page_dict['question_ids'].append(question_dict)
371 result['page_ids'].append(page_dict)
374 def get_filter_data(self, post):
375 """Returns data used for filtering the result"""
378 #if user add some random data in query URI, ignore it
380 row_id, answer_id = ids.split(',')
381 filters.append({'row_id': int(row_id), 'answer_id': int(answer_id)})
386 def page_range(self, total_record, limit):
387 '''Returns number of pages required for pagination'''
388 total = ceil(total_record / float(limit))
389 return range(1, int(total + 1))
391 def get_graph_data(self, question, current_filters=[]):
392 '''Returns formatted data required by graph library on basis of filter'''
393 survey_obj = request.registry['survey.survey']
395 if question.type == 'multiple_choice':
396 result.append({'key': str(question.question),
397 'values': survey_obj.prepare_result(request.cr, request.uid, question, current_filters, context=request.context)['answers']
399 if question.type == 'simple_choice':
400 result = survey_obj.prepare_result(request.cr, request.uid, question, current_filters, context=request.context)['answers']
401 if question.type == 'matrix':
402 data = survey_obj.prepare_result(request.cr, request.uid, question, current_filters, context=request.context)
403 for answer in data['answers']:
405 for res in data['result']:
407 values.append({'text': data['rows'][res[0]], 'count': data['result'][res]})
408 result.append({'key': data['answers'].get(answer), 'values': values})
409 return json.dumps(result)
411 def dict_soft_update(dictionary, key, value):
412 ''' Insert the pair <key>: <value> into the <dictionary>. If <key> is
413 already present, this function will append <value> to the list of
414 existing data (instead of erasing it) '''
415 if key in dictionary:
416 dictionary[key].append(value)
418 dictionary.update({key: [value]})