diff --git a/addons/survey/__openerp__.py b/addons/survey/__openerp__.py index 7208c21ab32..92dec026508 100644 --- a/addons/survey/__openerp__.py +++ b/addons/survey/__openerp__.py @@ -36,12 +36,13 @@ sent mails with personal token for the invitation of the survey. 'summary': 'Create surveys, collect answers and print statistics', 'author': 'OpenERP SA', 'website': 'https://www.openerp.com/apps/survey/', - 'depends': ['email_template', 'mail', 'website', 'web_kanban_gauge'], + 'depends': ['email_template', 'mail', 'website', 'web_kanban_gauge', 'web_graph'], 'data': [ 'security/survey_security.xml', 'security/ir.model.access.csv', 'views/survey_views.xml', 'views/survey_templates.xml', + 'views/survey_result.xml', 'wizard/survey_email_compose_message.xml', 'data/survey_cron.xml' ], diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py index e75833832cb..0ea3a160786 100644 --- a/addons/survey/controllers/main.py +++ b/addons/survey/controllers/main.py @@ -19,16 +19,19 @@ # ############################################################################## -from openerp import SUPERUSER_ID -from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT as DTF -from datetime import datetime -import werkzeug import json import logging +import werkzeug +from collections import Counter +from datetime import datetime +from itertools import product +from math import ceil + +from openerp import SUPERUSER_ID from openerp.addons.web import http from openerp.addons.web.http import request -from openerp.addons.website.controllers.main import Website as controllers -controllers = controllers() +from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT as DTF +from openerp.tools.safe_eval import safe_eval _logger = logging.getLogger(__name__) @@ -280,6 +283,144 @@ class WebsiteSurvey(http.Controller): 'token': token, 'page_nr': 0}) + @http.route(['/survey/results/'], + type='http', auth='user', multilang=True, website=True) + def survey_reporting(self, survey, token=None, **post): + '''Display survey Results & Statistics for given survey.''' + result_template, current_filters, filter_display_data = 'survey.result', [], [] + if not survey.user_input_ids or not [input_id.id for input_id in survey.user_input_ids if input_id.state != 'new']: + result_template = 'survey.no_result' + if post: + current_filters, filter_display_data = self.filter_input_ids(post) + return request.website.render(result_template, + {'survey': survey, + 'prepare_result': self.prepare_result, + 'get_input_summary': self.get_input_summary, + 'get_graph_data': self.get_graph_data, + 'page_range': self.page_range, + 'current_filters': current_filters, + 'filter_display_data': filter_display_data + }) + + def filter_input_ids(self, filters): + '''If user applies any filters, then this function returns list of + filtered user_input_id and label's strings for display data in web''' + cr, uid, context = request.cr, request.uid, request.context + question_obj = request.registry['survey.question'] + input_obj = request.registry['survey.user_input_line'] + input_line_obj = request.registry['survey.user_input_line'] + label_obj = request.registry['survey.label'] + domain_filter, choice, filter_display_data = [], [], [] + + #if user add some random data in query URI + try: + for ids in filters: + row_id, answer_id = ids.split(',') + question_id = filters[ids] + question = question_obj.browse(cr, uid, int(question_id), context=context) + if row_id == '0': + choice.append(int(answer_id)) + labels = label_obj.browse(cr, uid, [int(answer_id)], context=context) + else: + domain_filter.extend(['|', ('value_suggested_row.id', '=', int(row_id)), ('value_suggested.id', '=', int(answer_id))]) + labels = label_obj.browse(cr, uid, [int(row_id), int(answer_id)], context=context) + filter_display_data.append({'question_text': question.question, 'labels': [label.value for label in labels]}) + if choice: + domain_filter.insert(0, ('value_suggested.id', 'in', choice)) + else: + domain_filter = domain_filter[1:] + except: + #if user add some random data in query URI + return([], []) + + line_ids = input_line_obj.search(cr, uid, domain_filter, context=context) + filtered_input_ids = [input.user_input_id.id for input in input_obj.browse(cr, uid, line_ids, context=context)] + return (filtered_input_ids, filter_display_data) + + def page_range(self, total_record, limit): + '''Returns number of pages required for pagination''' + total = ceil(total_record / float(limit)) + return range(1, int(total + 1)) + + def prepare_result(self, question, current_filters=[]): + '''Prepare statistical data for questions by counting number of vote per choice on basis of filter''' + + #Calculate and return statistics for choice + if question.type in ['simple_choice', 'multiple_choice']: + result_summary = {} + [result_summary.update({label.id: {'text': label.value, 'count': 0, 'answer_id': label.id}}) for label in question.labels_ids] + for input_line in question.user_input_line_ids: + if result_summary.get(input_line.value_suggested.id) and (not(current_filters) or input_line.user_input_id.id in current_filters): + result_summary[input_line.value_suggested.id]['count'] += 1 + result_summary = result_summary.values() + + #Calculate and return statistics for matrix + if question.type == 'matrix': + rows, answers, res = {}, {}, {} + [rows.update({label.id: label.value}) for label in question.labels_ids_2] + [answers.update({label.id: label.value}) for label in question.labels_ids] + for cell in product(rows.keys(), answers.keys()): + res[cell] = 0 + for input_line in question.user_input_line_ids: + if not(current_filters) or input_line.user_input_id.id in current_filters: + res[(input_line.value_suggested_row.id, input_line.value_suggested.id)] += 1 + result_summary = {'answers': answers, 'rows': rows, 'result': res} + + #Calculate and return statistics for free_text, textbox, datetime + if question.type in ['free_text', 'textbox', 'datetime']: + result_summary = [] + for input_line in question.user_input_line_ids: + if not(current_filters) or input_line.user_input_id.id in current_filters: + result_summary.append(input_line) + + #Calculate and return statistics for numerical_box + if question.type == 'numerical_box': + result_summary = {'input_lines': []} + all_inputs = [] + for input_line in question.user_input_line_ids: + if not(current_filters) or input_line.user_input_id.id in current_filters: + all_inputs.append(input_line.value_number) + result_summary['input_lines'].append(input_line) + result_summary.update({'average': round(sum(all_inputs) / len(all_inputs), 2), + 'max': round(max(all_inputs), 2), + 'min': round(min(all_inputs), 2), + 'most_comman': Counter(all_inputs).most_common(5)}) + + return result_summary + + @http.route(['/survey/results/graph/'], + type='http', auth='user', multilang=True, website=True) + def get_graph_data(self, question, **post): + '''Returns appropriate formated data required by graph library on basis of filter''' + current_filters = safe_eval(post.get('current_filters', '[]')) + result = [] + if question.type in ['simple_choice', 'multiple_choice']: + result.append({'key': str(question.question), + 'values': self.prepare_result(question, current_filters)}) + if question.type == 'matrix': + data = self.prepare_result(question, current_filters) + for answer in data['answers']: + values = [] + for res in data['result']: + if res[1] == answer: + values.append({'text': data['rows'][res[0]], 'count': data['result'][res]}) + result.append({'key': data['answers'].get(answer), 'values': values}) + return json.dumps(result) + + def get_input_summary(self, question, current_filters=[]): + '''Returns overall summary of question e.g. answered, skipped, total_inputs on basis of filter''' + result = {} + if question.survey_id.user_input_ids: + total_input_ids = current_filters or [input_id.id for input_id in question.survey_id.user_input_ids if input_id.state != 'new'] + result['total_inputs'] = len(total_input_ids) + question_input_ids = [] + for user_input in question.user_input_line_ids: + if not user_input.skipped: + question_input_ids.append(user_input.user_input_id.id) + result['answered'] = len(set(question_input_ids) & set(total_input_ids)) + result['skipped'] = result['total_inputs'] - result['answered'] + return result + def dict_soft_update(dictionary, key, value): ''' Insert the pair : into the . If is diff --git a/addons/survey/static/src/css/survey_result.css b/addons/survey/static/src/css/survey_result.css new file mode 100644 index 00000000000..2033d9275fd --- /dev/null +++ b/addons/survey/static/src/css/survey_result.css @@ -0,0 +1,42 @@ +.only_right_radius{ + border-top-right-radius:2em; + border-bottom-right-radius:2em; + border-top-left-radius:0; + border-bottom-left-radius:0; +} + +.only_left_radius{ + border-top-right-radius:0; + border-bottom-right-radius:0; + border-top-left-radius:2em; + border-bottom-left-radius:2em; +} + +.no_radius{ + border-radius:0; +} + +.clear_survey_filter{ + cursor:pointer; +} + +.nvtooltip h5 { + margin: 0; + line-height: 18px; + font-weight: bold; + background-color: rgba(247,247,247,0.75); + text-align: center; + border-bottom: 1px solid #ebebeb; + -webkit-border-radius: 5px 5px 0 0; + -moz-border-radius: 5px 5px 0 0; + border-radius: 5px 5px 0 0; +} + + +.survey_answer i{ + padding:3px; + cursor:pointer; +} +.survey_answer i.invisible{ + visibility: hidden!important; +} diff --git a/addons/survey/static/src/js/survey_result.js b/addons/survey/static/src/js/survey_result.js new file mode 100644 index 00000000000..72ec404d0a2 --- /dev/null +++ b/addons/survey/static/src/js/survey_result.js @@ -0,0 +1,144 @@ +/* + * OpenERP, Open Source Management Solution + * Copyright (C) 2004-TODAY OpenERP S.A. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +$(document).ready(function () { + 'use strict'; + console.debug("[survey] Survey Result JS is loading..."); + + //Script For Pagination + var survey_pagination = $('.pagination'); + $.each(survey_pagination, function(index, pagination){ + var question_id = $(pagination).attr("data-question_id"); + var limit = $(pagination).attr("data-record_limit"); //Number of Record Par Page. If you want to change number of record per page, change record_limit in pagination template. + $('#table_question_'+ question_id +' tbody tr:lt('+limit+')').removeClass('hidden'); + $('#pagination_'+question_id+' li:first').addClass('active'); + $('#pagination_'+question_id+' li a').click(function(event){ + event.preventDefault(); + $('#pagination_'+question_id+' li').removeClass('active'); + $(this).parent('li').addClass('active'); + $('#table_question_'+ question_id +' tbody tr').addClass('hidden'); + var num = $(this).text(); + var min = (limit * (num-1))-1; + if (min == -1){ + $('#table_question_'+ question_id +' tbody tr:lt('+ limit * num +')').removeClass('hidden'); + } + else{ + $('#table_question_'+question_id+' tbody tr:lt('+ limit * num +'):gt('+min+')').removeClass('hidden'); + } + }); + }); + + //initialize MultiBar Chart + function init_multibar_chart(){ + var chart = nv.models.multiBarChart() + .x(function(d) { return d.text; }) + .y(function(d) { return d.count; }) + .staggerLabels(true); + + // Replacing Library's Default Tooltip with our Custom One + chart.tooltip(function(key, x, y, e, graph) { + return '
' + x + '
' + + '

' + 'Responses : ' + key + '

' + + '

' + "Total Vote : " + y + '

'; + }); + return chart; + } + + //initialize discreteBar Chart + function init_bar_chart(){ + return nv.models.discreteBarChart() + .x(function(d) { return d.text; }) + .y(function(d) { return d.count; }) + .staggerLabels(true) + .tooltips(false) + .showValues(true); + } + + //load chart to svg element chart:initialized chart, response:AJAX response, quistion_id:if of survey question, tick_limit:text length limit + function load_chart(chart, response, question_id, tick_limit){ + // Custom Tick fuction for replacing long text with '...' + var customtick_function = function(d){ + if(! this || d.length <= tick_limit){ + return d; + } + else{ + return d.slice(0,tick_limit)+'...'; + } + }; + chart.xAxis + .tickFormat(customtick_function); + chart.yAxis + .tickFormat(d3.format('d')); + + d3.select('#graph_question_' + question_id + ' svg') + .datum(response) + .transition().duration(500).call(chart); + nv.utils.windowResize(chart.update); + return chart; + } + //Script For Graph + var survey_graphs = $('.survey_graph'); + $.each(survey_graphs, function(index, graph){ + var question_id = $(graph).attr("data-question_id"); + var graph_type = $(graph).attr("data-graph_type"); + var current_filters = $(graph).attr("data-current_filters"); + console.log(current_filters); + $.ajax({ + url: '/survey/results/graph/'+question_id, + type: 'POST', // TODO check that something is posted + dataType: 'json', + data:{'current_filters': current_filters}, + success: function(response, status, xhr, wfe){ + if(graph_type == 'multi_bar'){ + nv.addGraph(function(){ + var chart = init_multibar_chart(); + return load_chart(chart,response,question_id,25); + }); + } + else if(graph_type == 'bar'){ + nv.addGraph(function() { + var chart = init_bar_chart(); + return load_chart(chart,response,question_id,35); + }); + } + } + }); + }); + + // Script for filter + $('td.survey_answer').hover(function(){$(this).find('i.fa-filter').removeClass('invisible');},function(){$(this).find('i.fa-filter').addClass('invisible');}); + $('td.survey_answer i.fa-filter').click(function(){ + var cell=$(this); + var question_id = cell.attr('data-question_id'); + var row_id = cell.attr('data-row_id') | 0; + var answer_id = cell.attr('data-answer_id'); + if(document.URL.indexOf("?") == -1){ + window.location.href = document.URL+'?' + encodeURI(row_id + ','+answer_id + '=' + question_id); + } + else{ + window.location.href = document.URL+'&' + encodeURI(row_id + ','+answer_id + '=' + question_id); + } + }); + + // for clear all filters + $('.clear_survey_filter').click(function(){ + window.location.href = document.URL.substring(0,document.URL.indexOf("?")); + }); + + console.debug("[survey] Survey Result JS loaded!"); +}); \ No newline at end of file diff --git a/addons/survey/survey.py b/addons/survey/survey.py index e2e88e0094d..03539d6f9b3 100644 --- a/addons/survey/survey.py +++ b/addons/survey/survey.py @@ -104,7 +104,7 @@ class survey_survey(osv.Model): """ Computes an URL for the survey results """ base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url') - return {id: urljoin(base_url, "survey/result/%s/" % id) for id in ids} + return {id: urljoin(base_url, "survey/results/%s/" % id) for id in ids} # Model fields # @@ -479,6 +479,9 @@ class survey_question(osv.Model): oldname='minimum_req_ans'), 'constr_error_msg': fields.char("Error message", oldname='req_error_msg'), + 'user_input_line_ids': fields.one2many('survey.user_input_line', + 'question_id', 'Answers', + domain=[('skipped', '=', False)]), } _defaults = { 'page_id': lambda s, cr, uid, c: c.get('page_id'), diff --git a/addons/survey/views/survey_result.xml b/addons/survey/views/survey_result.xml new file mode 100644 index 00000000000..2577077d132 --- /dev/null +++ b/addons/survey/views/survey_result.xml @@ -0,0 +1,300 @@ + + + +