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 collections import Counter
26 from datetime import datetime
27 from itertools import product
30 from openerp import SUPERUSER_ID
31 from openerp.addons.web import http
32 from openerp.addons.web.http import request
33 from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT as DTF
34 from openerp.tools.safe_eval import safe_eval
37 _logger = logging.getLogger(__name__)
40 class WebsiteSurvey(http.Controller):
44 def _check_bad_cases(self, cr, uid, request, survey_obj, survey, user_input_obj, context=None):
45 # In case of bad survey, redirect to surveys list
46 if survey_obj.exists(cr, SUPERUSER_ID, survey.id, context=context) == []:
47 return werkzeug.utils.redirect("/survey/")
49 # In case of auth required, block public user
50 if survey.auth_required and uid == request.registry['website'].get_public_user(cr, uid, context):
51 return request.website.render("website.401")
53 # In case of non open surveys
54 if survey.state != 'open':
55 return request.website.render("survey.notopen")
57 # If enough surveys completed
58 if survey.user_input_limit > 0:
59 completed = user_input_obj.search(cr, uid, [('state', '=', 'done')], count=True)
60 if completed >= survey.user_input_limit:
61 return request.website.render("survey.notopen")
63 # Everything seems to be ok
66 def _check_deadline(self, cr, uid, user_input, context=None):
67 '''Prevent opening of the survey if the deadline has turned out
69 ! This will NOT disallow access to users who have already partially filled the survey !'''
70 if user_input.deadline:
71 dt_deadline = datetime.strptime(user_input.deadline, DTF)
72 dt_now = datetime.now()
73 if dt_now > dt_deadline: # survey is not open anymore
74 return request.website.render("survey.notopen")
81 @http.route(['/survey/start/<model("survey.survey"):survey>',
82 '/survey/start/<model("survey.survey"):survey>/<string:token>'],
83 type='http', auth='public', multilang=True, website=True)
84 def start_survey(self, survey, token=None, **post):
85 cr, uid, context = request.cr, request.uid, request.context
86 survey_obj = request.registry['survey.survey']
87 user_input_obj = request.registry['survey.user_input']
90 if token and token == "phantom":
91 _logger.error("[survey] Phantom mode")
92 user_input_id = user_input_obj.create(cr, uid, {'survey_id': survey.id, 'test_entry': True}, context=context)
93 user_input = user_input_obj.browse(cr, uid, [user_input_id], context=context)[0]
94 data = {'survey': survey, 'page': None, 'token': user_input.token}
95 return request.website.render('survey.survey_init', data)
98 # Controls if the survey can be displayed
99 errpage = self._check_bad_cases(cr, uid, request, survey_obj, survey, user_input_obj, context=context)
105 if survey.visible_to_user:
106 user_input_id = user_input_obj.create(cr, uid, {'survey_id': survey.id}, context=context)
107 user_input = user_input_obj.browse(cr, uid, [user_input_id], context=context)[0]
108 else: # An user cannot open hidden surveys without token
109 return request.website.render("website.403")
112 user_input_id = user_input_obj.search(cr, uid, [('token', '=', token)], context=context)[0]
113 except IndexError: # Invalid token
114 return request.website.render("website.403")
116 user_input = user_input_obj.browse(cr, uid, [user_input_id], context=context)[0]
118 # Do not open expired survey
119 errpage = self._check_deadline(cr, uid, user_input, context=context)
123 # Select the right page
124 if user_input.state == 'new': # Intro page
125 data = {'survey': survey, 'page': None, 'token': user_input.token}
126 return request.website.render('survey.survey_init', data)
128 return request.redirect('/survey/fill/%s/%s' % (survey.id, user_input.token))
131 @http.route(['/survey/fill/<model("survey.survey"):survey>/<string:token>',
132 '/survey/fill/<model("survey.survey"):survey>/<string:token>/<string:prev>'],
133 type='http', auth='public', multilang=True, website=True)
134 def fill_survey(self, survey, token, prev=None, **post):
135 '''Display and validates a survey'''
136 cr, uid, context = request.cr, request.uid, request.context
137 survey_obj = request.registry['survey.survey']
138 user_input_obj = request.registry['survey.user_input']
140 # Controls if the survey can be displayed
141 errpage = self._check_bad_cases(cr, uid, request, survey_obj, survey, user_input_obj, context=context)
145 # Load the user_input
147 user_input_id = user_input_obj.search(cr, uid, [('token', '=', token)])[0]
148 except IndexError: # Invalid token
149 return request.website.render("website.403")
151 user_input = user_input_obj.browse(cr, uid, [user_input_id], context=context)[0]
153 # Do not display expired survey (even if some pages have already been
154 # displayed -- There's a time for everything!)
155 errpage = self._check_deadline(cr, uid, user_input, context=context)
159 # Select the right page
160 if user_input.state == 'new': # First page
161 page, page_nr, last = survey_obj.next_page(cr, uid, user_input, 0, go_back=False, context=context)
162 data = {'survey': survey, 'page': page, 'page_nr': page_nr, 'token': user_input.token}
164 data.update({'last': True})
165 return request.website.render('survey.survey', data)
166 elif user_input.state == 'done': # Display success message
167 return request.website.render('survey.sfinished', {'survey': survey,
169 'user_input': user_input})
170 elif user_input.state == 'skip':
171 flag = (True if prev and prev == 'prev' else False)
172 page, page_nr, last = survey_obj.next_page(cr, uid, user_input, user_input.last_displayed_page_id.id, go_back=flag, context=context)
173 data = {'survey': survey, 'page': page, 'page_nr': page_nr, 'token': user_input.token}
175 data.update({'last': True})
176 return request.website.render('survey.survey', data)
178 return request.website.render("website.403")
180 # AJAX prefilling of a survey
181 @http.route(['/survey/prefill/<model("survey.survey"):survey>/<string:token>',
182 '/survey/prefill/<model("survey.survey"):survey>/<string:token>/<model("survey.page"):page>'],
183 type='http', auth='public', multilang=True, website=True)
184 def prefill(self, survey, token, page=None, **post):
185 cr, uid, context = request.cr, request.uid, request.context
186 user_input_line_obj = request.registry['survey.user_input_line']
189 # Fetch previous answers
191 ids = user_input_line_obj.search(cr, uid, [('user_input_id.token', '=', token), ('page_id', '=', page.id)], context=context)
193 ids = user_input_line_obj.search(cr, uid, [('user_input_id.token', '=', token)], context=context)
194 previous_answers = user_input_line_obj.browse(cr, uid, ids, context=context)
196 # Return non empty answers in a JSON compatible format
197 for answer in previous_answers:
198 if not answer.skipped:
199 answer_tag = '%s_%s_%s' % (answer.survey_id.id, answer.page_id.id, answer.question_id.id)
201 if answer.answer_type == 'free_text':
202 answer_value = answer.value_free_text
203 elif answer.answer_type == 'text':
204 answer_value = answer.value_text
205 elif answer.answer_type == 'number':
206 answer_value = answer.value_number.__str__()
207 elif answer.answer_type == 'date':
208 answer_value = answer.value_date
209 elif answer.answer_type == 'suggestion' and not answer.value_suggested_row:
210 answer_value = answer.value_suggested.id
211 elif answer.answer_type == 'suggestion' and answer.value_suggested_row:
212 answer_tag = "%s_%s" % (answer_tag, answer.value_suggested_row.id)
213 answer_value = answer.value_suggested.id
215 dict_soft_update(ret, answer_tag, answer_value)
217 _logger.warning("[survey] No answer has been found for question %s marked as non skipped" % answer_tag)
218 return json.dumps(ret)
220 # AJAX validation of some questions
221 # @http.route(['/survey/validate/<model("survey.survey"):survey>'],
222 # type='http', auth='public', multilang=True)
223 # def validate(self, survey, **post):
225 # AJAX submission of a page
226 @http.route(['/survey/submit/<model("survey.survey"):survey>'],
227 type='http', auth='public', multilang=True, website=True)
228 def submit(self, survey, **post):
229 _logger.debug('Incoming data: %s', post)
230 page_id = int(post['page_id'])
231 cr, uid, context = request.cr, request.uid, request.context
232 survey_obj = request.registry['survey.survey']
233 questions_obj = request.registry['survey.question']
234 questions_ids = questions_obj.search(cr, uid, [('page_id', '=', page_id)], context=context)
235 questions = questions_obj.browse(cr, uid, questions_ids, context=context)
239 for question in questions:
240 answer_tag = "%s_%s_%s" % (survey.id, page_id, question.id)
241 errors.update(questions_obj.validate_question(cr, uid, question, post, answer_tag, context=context))
244 if (len(errors) != 0):
245 # Return errors messages to webpage
246 ret['errors'] = errors
248 # Store answers into database
249 user_input_obj = request.registry['survey.user_input']
251 user_input_line_obj = request.registry['survey.user_input_line']
253 user_input_id = user_input_obj.search(cr, uid, [('token', '=', post['token'])], context=context)[0]
254 except KeyError: # Invalid token
255 return request.website.render("website.403")
256 for question in questions:
257 answer_tag = "%s_%s_%s" % (survey.id, page_id, question.id)
258 user_input_line_obj.save_lines(cr, uid, user_input_id, question, post, answer_tag, context=context)
260 user_input = user_input_obj.browse(cr, uid, user_input_id, context=context)
261 go_back = post['button_submit'] == 'previous'
262 next_page, _, last = survey_obj.next_page(cr, uid, user_input, page_id, go_back=go_back, context=context)
263 vals = {'last_displayed_page_id': page_id}
264 if next_page is None and not go_back:
265 vals.update({'state': 'done'})
267 vals.update({'state': 'skip'})
268 user_input_obj.write(cr, uid, user_input_id, vals, context=context)
269 ret['redirect'] = '/survey/fill/%s/%s' % (survey.id, post['token'])
271 ret['redirect'] += '/prev'
272 return json.dumps(ret)
275 @http.route(['/survey/print/<model("survey.survey"):survey>/',
276 '/survey/print/<model("survey.survey"):survey>/<string:token>/'],
277 type='http', auth='user', multilang=True, website=True)
278 def print_survey(self, survey, token=None, **post):
279 '''Display an survey in printable view; if <token> is set, it will
280 grab the answers of the user_input_id that has <token>.'''
281 return request.website.render('survey.survey_print',
286 @http.route(['/survey/results/<model("survey.survey"):survey>'],
287 type='http', auth='user', multilang=True, website=True)
288 def survey_reporting(self, survey, token=None, **post):
289 '''Display survey Results & Statistics for given survey.'''
290 result_template, current_filters, filter_display_data = 'survey.result', [], []
291 if not survey.user_input_ids or not [input_id.id for input_id in survey.user_input_ids if input_id.state != 'new']:
292 result_template = 'survey.no_result'
294 current_filters, filter_display_data = self.filter_input_ids(post)
295 return request.website.render(result_template,
297 'prepare_result': self.prepare_result,
298 'get_input_summary': self.get_input_summary,
299 'get_graph_data': self.get_graph_data,
300 'page_range': self.page_range,
301 'current_filters': current_filters,
302 'filter_display_data': filter_display_data
305 def filter_input_ids(self, filters):
306 '''If user applies any filters, then this function returns list of
307 filtered user_input_id and label's strings for display data in web'''
308 cr, uid, context = request.cr, request.uid, request.context
309 question_obj = request.registry['survey.question']
310 input_obj = request.registry['survey.user_input_line']
311 input_line_obj = request.registry['survey.user_input_line']
312 label_obj = request.registry['survey.label']
313 domain_filter, choice, filter_display_data = [], [], []
315 #if user add some random data in query URI
318 row_id, answer_id = ids.split(',')
319 question_id = filters[ids]
320 question = question_obj.browse(cr, uid, int(question_id), context=context)
322 choice.append(int(answer_id))
323 labels = label_obj.browse(cr, uid, [int(answer_id)], context=context)
325 domain_filter.extend(['|', ('value_suggested_row.id', '=', int(row_id)), ('value_suggested.id', '=', int(answer_id))])
326 labels = label_obj.browse(cr, uid, [int(row_id), int(answer_id)], context=context)
327 filter_display_data.append({'question_text': question.question, 'labels': [label.value for label in labels]})
329 domain_filter.insert(0, ('value_suggested.id', 'in', choice))
331 domain_filter = domain_filter[1:]
333 #if user add some random data in query URI
336 line_ids = input_line_obj.search(cr, uid, domain_filter, context=context)
337 filtered_input_ids = [input.user_input_id.id for input in input_obj.browse(cr, uid, line_ids, context=context)]
338 return (filtered_input_ids, filter_display_data)
340 def page_range(self, total_record, limit):
341 '''Returns number of pages required for pagination'''
342 total = ceil(total_record / float(limit))
343 return range(1, int(total + 1))
345 def prepare_result(self, question, current_filters=[]):
346 '''Prepare statistical data for questions by counting number of vote per choice on basis of filter'''
348 #Calculate and return statistics for choice
349 if question.type in ['simple_choice', 'multiple_choice']:
351 [result_summary.update({label.id: {'text': label.value, 'count': 0, 'answer_id': label.id}}) for label in question.labels_ids]
352 for input_line in question.user_input_line_ids:
353 if result_summary.get(input_line.value_suggested.id) and (not(current_filters) or input_line.user_input_id.id in current_filters):
354 result_summary[input_line.value_suggested.id]['count'] += 1
355 result_summary = result_summary.values()
357 #Calculate and return statistics for matrix
358 if question.type == 'matrix':
359 rows, answers, res = {}, {}, {}
360 [rows.update({label.id: label.value}) for label in question.labels_ids_2]
361 [answers.update({label.id: label.value}) for label in question.labels_ids]
362 for cell in product(rows.keys(), answers.keys()):
364 for input_line in question.user_input_line_ids:
365 if not(current_filters) or input_line.user_input_id.id in current_filters:
366 res[(input_line.value_suggested_row.id, input_line.value_suggested.id)] += 1
367 result_summary = {'answers': answers, 'rows': rows, 'result': res}
369 #Calculate and return statistics for free_text, textbox, datetime
370 if question.type in ['free_text', 'textbox', 'datetime']:
372 for input_line in question.user_input_line_ids:
373 if not(current_filters) or input_line.user_input_id.id in current_filters:
374 result_summary.append(input_line)
376 #Calculate and return statistics for numerical_box
377 if question.type == 'numerical_box':
378 result_summary = {'input_lines': []}
380 for input_line in question.user_input_line_ids:
381 if not(current_filters) or input_line.user_input_id.id in current_filters:
382 all_inputs.append(input_line.value_number)
383 result_summary['input_lines'].append(input_line)
384 result_summary.update({'average': round(sum(all_inputs) / len(all_inputs), 2),
385 'max': round(max(all_inputs), 2),
386 'min': round(min(all_inputs), 2),
387 'most_comman': Counter(all_inputs).most_common(5)})
389 return result_summary
391 @http.route(['/survey/results/graph/<model("survey.question"):question>'],
392 type='http', auth='user', multilang=True, website=True)
393 def get_graph_data(self, question, **post):
394 '''Returns appropriate formated data required by graph library on basis of filter'''
395 current_filters = safe_eval(post.get('current_filters', '[]'))
397 if question.type in ['simple_choice', 'multiple_choice']:
398 result.append({'key': str(question.question),
399 'values': self.prepare_result(question, current_filters)})
400 if question.type == 'matrix':
401 data = self.prepare_result(question, current_filters)
402 for answer in data['answers']:
404 for res in data['result']:
406 values.append({'text': data['rows'][res[0]], 'count': data['result'][res]})
407 result.append({'key': data['answers'].get(answer), 'values': values})
408 return json.dumps(result)
410 def get_input_summary(self, question, current_filters=[]):
411 '''Returns overall summary of question e.g. answered, skipped, total_inputs on basis of filter'''
413 if question.survey_id.user_input_ids:
414 total_input_ids = current_filters or [input_id.id for input_id in question.survey_id.user_input_ids if input_id.state != 'new']
415 result['total_inputs'] = len(total_input_ids)
416 question_input_ids = []
417 for user_input in question.user_input_line_ids:
418 if not user_input.skipped:
419 question_input_ids.append(user_input.user_input_id.id)
420 result['answered'] = len(set(question_input_ids) & set(total_input_ids))
421 result['skipped'] = result['total_inputs'] - result['answered']
425 def dict_soft_update(dictionary, key, value):
426 ''' Insert the pair <key>: <value> into the <dictionary>. If <key> is
427 already present, this function will append <value> to the list of
428 existing data (instead of erasing it) '''
429 if key in dictionary:
430 dictionary[key].append(value)
432 dictionary.update({key: [value]})