[REF] Bye bye vim
[odoo/odoo.git] / addons / survey / controllers / main.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2013-Today OpenERP SA (<http://www.openerp.com>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 import json
23 import logging
24 import werkzeug
25 from collections import Counter
26 from datetime import datetime
27 from itertools import product
28 from math import ceil
29
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
35
36
37 _logger = logging.getLogger(__name__)
38
39
40 class WebsiteSurvey(http.Controller):
41
42     ## HELPER METHODS ##
43
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/")
48
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")
52
53         # In case of non open surveys
54         if survey.state != 'open':
55             return request.website.render("survey.notopen")
56
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")
62
63         # Everything seems to be ok
64         return None
65
66     def _check_deadline(self, cr, uid, user_input, context=None):
67         '''Prevent opening of the survey if the deadline has turned out
68
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")
75
76         return None
77
78     ## ROUTES HANDLERS ##
79
80     # Survey start
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']
88
89         # Test mode
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)
96         # END Test mode
97
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)
100         if errpage:
101             return errpage
102
103         # Manual surveying
104         if not token:
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")
110         else:
111             try:
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")
115             else:
116                 user_input = user_input_obj.browse(cr, uid, [user_input_id], context=context)[0]
117
118         # Do not open expired survey
119         errpage = self._check_deadline(cr, uid, user_input, context=context)
120         if errpage:
121             return errpage
122
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)
127         else:
128             return request.redirect('/survey/fill/%s/%s' % (survey.id, user_input.token))
129
130     # Survey displaying
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']
139
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)
142         if errpage:
143             return errpage
144
145         # Load the user_input
146         try:
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")
150         else:
151             user_input = user_input_obj.browse(cr, uid, [user_input_id], context=context)[0]
152
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)
156         if errpage:
157             return errpage
158
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}
163             if last:
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,
168                                                                'token': token,
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}
174             if last:
175                 data.update({'last': True})
176             return request.website.render('survey.survey', data)
177         else:
178             return request.website.render("website.403")
179
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']
187         ret = {}
188
189         # Fetch previous answers
190         if page:
191             ids = user_input_line_obj.search(cr, uid, [('user_input_id.token', '=', token), ('page_id', '=', page.id)], context=context)
192         else:
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)
195
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)
200                 answer_value = None
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
214                 if answer_value:
215                     dict_soft_update(ret, answer_tag, answer_value)
216                 else:
217                     _logger.warning("[survey] No answer has been found for question %s marked as non skipped" % answer_tag)
218         return json.dumps(ret)
219
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):
224
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)
236
237         # Answer validation
238         errors = {}
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))
242
243         ret = {}
244         if (len(errors) != 0):
245             # Return errors messages to webpage
246             ret['errors'] = errors
247         else:
248             # Store answers into database
249             user_input_obj = request.registry['survey.user_input']
250
251             user_input_line_obj = request.registry['survey.user_input_line']
252             try:
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)
259
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'})
266             else:
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'])
270             if go_back:
271                 ret['redirect'] += '/prev'
272         return json.dumps(ret)
273
274     # Printing routes
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',
282                                       {'survey': survey,
283                                        'token': token,
284                                        'page_nr': 0})
285
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'
293         if post:
294             current_filters, filter_display_data = self.filter_input_ids(post)
295         return request.website.render(result_template,
296                                       {'survey': survey,
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
303                                        })
304
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 = [], [], []
314
315         #if user add some random data in query URI
316         try:
317             for ids in filters:
318                 row_id, answer_id = ids.split(',')
319                 question_id = filters[ids]
320                 question = question_obj.browse(cr, uid, int(question_id), context=context)
321                 if row_id == '0':
322                     choice.append(int(answer_id))
323                     labels = label_obj.browse(cr, uid, [int(answer_id)], context=context)
324                 else:
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]})
328             if choice:
329                 domain_filter.insert(0, ('value_suggested.id', 'in', choice))
330             else:
331                 domain_filter = domain_filter[1:]
332         except:
333             #if user add some random data in query URI
334             return([], [])
335
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)
339
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))
344
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'''
347
348         #Calculate and return statistics for choice
349         if question.type in ['simple_choice', 'multiple_choice']:
350             result_summary = {}
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()
356
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()):
363                 res[cell] = 0
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}
368
369         #Calculate and return statistics for free_text, textbox, datetime
370         if question.type in ['free_text', 'textbox', 'datetime']:
371             result_summary = []
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)
375
376         #Calculate and return statistics for numerical_box
377         if question.type == 'numerical_box':
378             result_summary = {'input_lines': []}
379             all_inputs = []
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)})
388
389         return result_summary
390
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', '[]'))
396         result = []
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']:
403                 values = []
404                 for res in data['result']:
405                     if res[1] == answer:
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)
409
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'''
412         result = {}
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']
422         return result
423
424
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)
431     else:
432         dictionary.update({key: [value]})