summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/survey/__manifest__.py26
-rw-r--r--addons/survey/data/ir_actions_data.xml16
-rw-r--r--addons/survey/data/survey_data.xml (renamed from addons/survey/data/survey_stages.xml)0
-rw-r--r--addons/survey/models/__init__.py6
-rw-r--r--addons/survey/models/survey.py1053
-rw-r--r--addons/survey/models/survey_page.py27
-rw-r--r--addons/survey/models/survey_question.py299
-rw-r--r--addons/survey/models/survey_stage.py21
-rw-r--r--addons/survey/models/survey_survey.py354
-rw-r--r--addons/survey/models/survey_user.py386
-rw-r--r--addons/survey/views/assets.xml30
-rw-r--r--addons/survey/views/survey_menus.xml10
-rw-r--r--addons/survey/views/survey_page_views.xml71
-rw-r--r--addons/survey/views/survey_question_views.xml228
-rw-r--r--addons/survey/views/survey_result.xml346
-rw-r--r--addons/survey/views/survey_stage_views.xml19
-rw-r--r--addons/survey/views/survey_survey_views.xml182
-rw-r--r--addons/survey/views/survey_templates.xml361
-rw-r--r--addons/survey/views/survey_user_views.xml222
-rw-r--r--addons/survey/views/survey_views.xml732
-rw-r--r--addons/survey/wizard/__init__.py2
-rw-r--r--addons/survey/wizard/survey_mail_compose_message.py (renamed from addons/survey/wizard/survey_email_compose_message.py)0
-rw-r--r--addons/survey/wizard/survey_mail_compose_message_views.xml (renamed from addons/survey/wizard/survey_email_compose_message.xml)0
23 files changed, 2229 insertions, 2162 deletions
diff --git a/addons/survey/__manifest__.py b/addons/survey/__manifest__.py
index 6304e6957d7..2c2e45bf93d 100644
--- a/addons/survey/__manifest__.py
+++ b/addons/survey/__manifest__.py
@@ -18,19 +18,27 @@ sent mails with personal token for the invitation of the survey.
'website': 'https://www.odoo.com/page/survey',
'depends': ['http_routing', 'mail'],
'data': [
+ 'data/mail_template_data.xml',
+ 'data/survey_data.xml',
+ 'data/ir_actions_data.xml',
'security/survey_security.xml',
'security/ir.model.access.csv',
- 'views/survey_views.xml',
+ 'views/assets.xml',
+ 'views/survey_menus.xml',
+ 'views/survey_stage_views.xml',
+ 'views/survey_survey_views.xml',
+ 'views/survey_user_views.xml',
+ 'views/survey_question_views.xml',
+ 'views/survey_page_views.xml',
'views/survey_templates.xml',
- 'views/survey_result.xml',
- 'data/mail_template_data.xml',
- 'wizard/survey_email_compose_message.xml',
- 'data/survey_stages.xml',
+ 'wizard/survey_mail_compose_message_views.xml',
+ ],
+ 'demo': [
+ 'data/survey_demo_user.xml',
+ 'data/survey_demo_feedback.xml',
+ 'data/survey.user_input.csv',
+ 'data/survey.user_input_line.csv'
],
- 'demo': ['data/survey_demo_user.xml',
- 'data/survey_demo_feedback.xml',
- 'data/survey.user_input.csv',
- 'data/survey.user_input_line.csv'],
'installable': True,
'auto_install': False,
'application': True,
diff --git a/addons/survey/data/ir_actions_data.xml b/addons/survey/data/ir_actions_data.xml
new file mode 100644
index 00000000000..7e0473fdbeb
--- /dev/null
+++ b/addons/survey/data/ir_actions_data.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data noupdate="1">
+ <record id="survey_action_server_clean_test_answers" model="ir.actions.server">
+ <field name="name">Survey: Clean test answers</field>
+ <field name="type">ir.actions.server</field>
+ <field name="model_id" ref="model_survey_survey" />
+ <field name="binding_model_id" ref="model_survey_survey" />
+ <field name="state">code</field>
+ <field name="code">
+if records:
+ env['survey.user_input'].search([('survey_id', 'in', records.ids), ('test_entry', '=', 'True')]).unlink()
+ </field>
+ </record>
+</data>
+</odoo> \ No newline at end of file
diff --git a/addons/survey/data/survey_stages.xml b/addons/survey/data/survey_data.xml
index 87cefb946c9..87cefb946c9 100644
--- a/addons/survey/data/survey_stages.xml
+++ b/addons/survey/data/survey_data.xml
diff --git a/addons/survey/models/__init__.py b/addons/survey/models/__init__.py
index 26edfafe8ad..d6a0e6c499b 100644
--- a/addons/survey/models/__init__.py
+++ b/addons/survey/models/__init__.py
@@ -1,5 +1,9 @@
# -*- encoding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from . import survey
from . import ir_autovacuum
+from . import survey_survey
+from . import survey_page
+from . import survey_question
+from . import survey_stage
+from . import survey_user
diff --git a/addons/survey/models/survey.py b/addons/survey/models/survey.py
deleted file mode 100644
index d24c124f735..00000000000
--- a/addons/survey/models/survey.py
+++ /dev/null
@@ -1,1053 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-import datetime
-import logging
-import re
-import uuid
-from collections import Counter, OrderedDict
-from itertools import product
-from werkzeug import urls
-
-from odoo import api, fields, models, tools, SUPERUSER_ID, _
-from odoo.addons.http_routing.models.ir_http import slug
-from odoo.exceptions import UserError, ValidationError
-email_validator = re.compile(r"[^@]+@[^@]+\.[^@]+")
-_logger = logging.getLogger(__name__)
-
-
-def dict_keys_startswith(dictionary, string):
- """Returns a dictionary containing the elements of <dict> whose keys start with <string>.
- .. note::
- This function uses dictionary comprehensions (Python >= 2.7)
- """
- return {k: v for k, v in dictionary.items() if k.startswith(string)}
-
-
-class SurveyStage(models.Model):
- """Stages for Kanban view of surveys"""
-
- _name = 'survey.stage'
- _description = 'Survey Stage'
- _order = 'sequence,id'
-
- name = fields.Char(required=True, translate=True)
- sequence = fields.Integer(default=1)
- closed = fields.Boolean(help="If closed, people won't be able to answer to surveys in this column.")
- fold = fields.Boolean(string="Folded in kanban view")
-
- _sql_constraints = [
- ('positive_sequence', 'CHECK(sequence >= 0)', 'Sequence number MUST be a natural')
- ]
-
-
-class Survey(models.Model):
- """ Settings for a multi-page/multi-question survey.
- Each survey can have one or more attached pages, and each page can display
- one or more questions.
- """
-
- _name = 'survey.survey'
- _description = 'Survey'
- _rec_name = 'title'
- _inherit = ['mail.thread', 'mail.activity.mixin']
-
- def _default_stage(self):
- return self.env['survey.stage'].search([], limit=1).id
-
- title = fields.Char('Title', required=True, translate=True)
- page_ids = fields.One2many('survey.page', 'survey_id', string='Pages', copy=True)
- stage_id = fields.Many2one('survey.stage', string="Stage", default=_default_stage,
- ondelete="restrict", copy=False, group_expand='_read_group_stage_ids')
- auth_required = fields.Boolean('Login required', help="Users with a public link will be requested to login before taking part to the survey",
- oldname="authenticate")
- users_can_go_back = fields.Boolean('Users can go back', help="If checked, users can go back to previous pages.")
- tot_sent_survey = fields.Integer("Number of sent surveys", compute="_compute_survey_statistic")
- tot_start_survey = fields.Integer("Number of started surveys", compute="_compute_survey_statistic")
- tot_comp_survey = fields.Integer("Number of completed surveys", compute="_compute_survey_statistic")
- description = fields.Html("Description", translate=True, help="A long description of the purpose of the survey")
- color = fields.Integer('Color Index', default=0)
- user_input_ids = fields.One2many('survey.user_input', 'survey_id', string='User responses', readonly=True)
- designed = fields.Boolean("Is designed?", compute="_is_designed")
- public_url = fields.Char("Public link", compute="_compute_survey_url")
- public_url_html = fields.Char("Public link (html version)", compute="_compute_survey_url")
- print_url = fields.Char("Print link", compute="_compute_survey_url")
- result_url = fields.Char("Results link", compute="_compute_survey_url")
- email_template_id = fields.Many2one('mail.template', string='Email Template', ondelete='set null')
- thank_you_message = fields.Html("Thanks Message", translate=True, help="This message will be displayed when survey is completed")
- quizz_mode = fields.Boolean("Quizz Mode")
- active = fields.Boolean("Active", default=True)
- is_closed = fields.Boolean("Is closed", related='stage_id.closed', readonly=False)
-
- def _is_designed(self):
- for survey in self:
- if not survey.page_ids or not [page.question_ids for page in survey.page_ids if page.question_ids]:
- survey.designed = False
- else:
- survey.designed = True
-
- @api.multi
- def _compute_survey_statistic(self):
- UserInput = self.env['survey.user_input']
-
- sent_survey = UserInput.search([('survey_id', 'in', self.ids), ('input_type', '=', 'link')])
- start_survey = UserInput.search(['&', ('survey_id', 'in', self.ids), '|', ('state', '=', 'skip'), ('state', '=', 'done')])
- complete_survey = UserInput.search([('survey_id', 'in', self.ids), ('state', '=', 'done')])
-
- for survey in self:
- survey.tot_sent_survey = len(sent_survey.filtered(lambda user_input: user_input.survey_id == survey))
- survey.tot_start_survey = len(start_survey.filtered(lambda user_input: user_input.survey_id == survey))
- survey.tot_comp_survey = len(complete_survey.filtered(lambda user_input: user_input.survey_id == survey))
-
- def _compute_survey_url(self):
- """ Computes a public URL for the survey """
- base_url = '/' if self.env.context.get('relative_url') else \
- self.env['ir.config_parameter'].sudo().get_param('web.base.url')
- for survey in self:
- survey.public_url = urls.url_join(base_url, "survey/start/%s" % (slug(survey)))
- survey.print_url = urls.url_join(base_url, "survey/print/%s" % (slug(survey)))
- survey.result_url = urls.url_join(base_url, "survey/results/%s" % (slug(survey)))
- survey.public_url_html = '<a href="%s">%s</a>' % (survey.public_url, _("Click here to start survey"))
-
- @api.model
- def _read_group_stage_ids(self, stages, domain, order):
- """ Read group customization in order to display all the stages in the
- kanban view, even if they are empty
- """
- stage_ids = stages._search([], order=order, access_rights_uid=SUPERUSER_ID)
- return stages.browse(stage_ids)
-
- # Public methods #
- def copy_data(self, default=None):
- title = _("%s (copy)") % (self.title)
- default = dict(default or {}, title=title)
- return super(Survey, self).copy_data(default)
-
- @api.model
- def next_page(self, user_input, page_id, go_back=False):
- """ The next page to display to the user, knowing that page_id is the id
- of the last displayed page.
-
- If page_id == 0, it will always return the first page of the survey.
-
- If all the pages have been displayed and go_back == False, it will
- return None
-
- If go_back == True, it will return the *previous* page instead of the
- next page.
-
- .. note::
- It is assumed here that a careful user will not try to set go_back
- to True if she knows that the page to display is the first one!
- (doing this will probably cause a giant worm to eat her house)
- """
- survey = user_input.survey_id
- pages = list(enumerate(survey.page_ids))
-
- # First page
- if page_id == 0:
- return (pages[0][1], 0, len(pages) == 1)
-
- current_page_index = pages.index(next(p for p in pages if p[1].id == page_id))
-
- # All the pages have been displayed
- if current_page_index == len(pages) - 1 and not go_back:
- return (None, -1, False)
- # Let's get back, baby!
- elif go_back and survey.users_can_go_back:
- return (pages[current_page_index - 1][1], current_page_index - 1, False)
- else:
- # This will show the last page
- if current_page_index == len(pages) - 2:
- return (pages[current_page_index + 1][1], current_page_index + 1, True)
- # This will show a regular page
- else:
- return (pages[current_page_index + 1][1], current_page_index + 1, False)
-
- @api.multi
- def filter_input_ids(self, filters, finished=False):
- """If user applies any filters, then this function returns list of
- filtered user_input_id and label's strings for display data in web.
- :param filters: list of dictionary (having: row_id, ansewr_id)
- :param finished: True for completely filled survey,Falser otherwise.
- :returns list of filtered user_input_ids.
- """
- self.ensure_one()
- if filters:
- domain_filter, choice = [], []
- for current_filter in filters:
- row_id, answer_id = current_filter['row_id'], current_filter['answer_id']
- if row_id == 0:
- choice.append(answer_id)
- else:
- domain_filter.extend(['|', ('value_suggested_row.id', '=', row_id), ('value_suggested.id', '=', answer_id)])
- if choice:
- domain_filter.insert(0, ('value_suggested.id', 'in', choice))
- else:
- domain_filter = domain_filter[1:]
- input_lines = self.env['survey.user_input_line'].search(domain_filter)
- filtered_input_ids = [input_line.user_input_id.id for input_line in input_lines]
- else:
- filtered_input_ids = []
- if finished:
- UserInput = self.env['survey.user_input']
- if not filtered_input_ids:
- user_inputs = UserInput.search([('survey_id', '=', self.id)])
- else:
- user_inputs = UserInput.browse(filtered_input_ids)
- return user_inputs.filtered(lambda input_item: input_item.state == 'done').ids
- return filtered_input_ids
-
- @api.model
- def get_filter_display_data(self, filters):
- """Returns data to display current filters
- :param filters: list of dictionary (having: row_id, answer_id)
- :returns list of dict having data to display filters.
- """
- filter_display_data = []
- if filters:
- Label = self.env['survey.label']
- for current_filter in filters:
- row_id, answer_id = current_filter['row_id'], current_filter['answer_id']
- label = Label.browse(answer_id)
- question = label.question_id
- if row_id == 0:
- labels = label
- else:
- labels = Label.browse([row_id, answer_id])
- filter_display_data.append({'question_text': question.question,
- 'labels': labels.mapped('value')})
- return filter_display_data
-
- @api.model
- def prepare_result(self, question, current_filters=None):
- """ Compute statistical data for questions by counting number of vote per choice on basis of filter """
- current_filters = current_filters if current_filters else []
- result_summary = {}
-
- # Calculate and return statistics for choice
- if question.question_type in ['simple_choice', 'multiple_choice']:
- comments = []
- answers = OrderedDict((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 input_line.answer_type == 'suggestion' and answers.get(input_line.value_suggested.id) and (not(current_filters) or input_line.user_input_id.id in current_filters):
- answers[input_line.value_suggested.id]['count'] += 1
- if input_line.answer_type == 'text' and (not(current_filters) or input_line.user_input_id.id in current_filters):
- comments.append(input_line)
- result_summary = {'answers': list(answers.values()), 'comments': comments}
-
- # Calculate and return statistics for matrix
- if question.question_type == 'matrix':
- rows = OrderedDict()
- answers = OrderedDict()
- res = dict()
- comments = []
- [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, answers):
- res[cell] = 0
- for input_line in question.user_input_line_ids:
- if input_line.answer_type == 'suggestion' and (not(current_filters) or input_line.user_input_id.id in current_filters) and input_line.value_suggested_row:
- res[(input_line.value_suggested_row.id, input_line.value_suggested.id)] += 1
- if input_line.answer_type == 'text' and (not(current_filters) or input_line.user_input_id.id in current_filters):
- comments.append(input_line)
- result_summary = {'answers': answers, 'rows': rows, 'result': res, 'comments': comments}
-
- # Calculate and return statistics for free_text, textbox, date
- if question.question_type in ['free_text', 'textbox', 'date']:
- 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.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)
- if all_inputs:
- result_summary.update({'average': round(sum(all_inputs) / len(all_inputs), 2),
- 'max': round(max(all_inputs), 2),
- 'min': round(min(all_inputs), 2),
- 'sum': sum(all_inputs),
- 'most_common': Counter(all_inputs).most_common(5)})
- return result_summary
-
- @api.model
- def get_input_summary(self, question, current_filters=None):
- """ Returns overall summary of question e.g. answered, skipped, total_inputs on basis of filter """
- current_filters = current_filters if current_filters else []
- 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
-
- # Actions
-
- @api.multi
- def action_start_survey(self):
- """ Open the website page with the survey form """
- self.ensure_one()
- token = self.env.context.get('survey_token')
- trail = "/%s" % token if token else ""
- return {
- 'type': 'ir.actions.act_url',
- 'name': "Start Survey",
- 'target': 'self',
- 'url': self.with_context(relative_url=True).public_url + trail
- }
-
- @api.multi
- def action_send_survey(self):
- """ Open a window to compose an email, pre-filled with the survey message """
- # Ensure that this survey has at least one page with at least one question.
- if not self.page_ids or not [page.question_ids for page in self.page_ids if page.question_ids]:
- raise UserError(_('You cannot send an invitation for a survey that has no questions.'))
-
- if self.stage_id.closed:
- raise UserError(_("You cannot send invitations for closed surveys."))
-
- template = self.env.ref('survey.email_template_survey', raise_if_not_found=False)
-
- local_context = dict(
- self.env.context,
- default_model='survey.survey',
- default_res_id=self.id,
- default_survey_id=self.id,
- default_use_template=bool(template),
- default_template_id=template and template.id or False,
- default_composition_mode='comment',
- notif_layout='mail.mail_notification_light',
- )
- return {
- 'type': 'ir.actions.act_window',
- 'view_type': 'form',
- 'view_mode': 'form',
- 'res_model': 'survey.mail.compose.message',
- 'target': 'new',
- 'context': local_context,
- }
-
- @api.multi
- def action_print_survey(self):
- """ Open the website page with the survey printable view """
- self.ensure_one()
- token = self.env.context.get('survey_token')
- trail = "/" + token if token else ""
- return {
- 'type': 'ir.actions.act_url',
- 'name': "Print Survey",
- 'target': 'self',
- 'url': self.with_context(relative_url=True).print_url + trail
- }
-
- @api.multi
- def action_result_survey(self):
- """ Open the website page with the survey results view """
- self.ensure_one()
- return {
- 'type': 'ir.actions.act_url',
- 'name': "Results of the Survey",
- 'target': 'self',
- 'url': self.with_context(relative_url=True).result_url
- }
-
- @api.multi
- def action_test_survey(self):
- ''' Open the website page with the survey form into test mode'''
- self.ensure_one()
- return {
- 'type': 'ir.actions.act_url',
- 'name': "Results of the Survey",
- 'target': 'self',
- 'url': self.with_context(relative_url=True).public_url + "/phantom"
- }
-
- @api.multi
- def action_survey_user_input(self):
- action_rec = self.env.ref('survey.action_survey_user_input')
- action = action_rec.read()[0]
- ctx = dict(self.env.context)
- ctx.update({'search_default_survey_id': self.ids[0],
- 'search_default_completed': 1})
- action['context'] = ctx
- return action
-
-
-class SurveyPage(models.Model):
- """ A page for a survey.
-
- Pages are essentially containers, allowing to group questions by ordered
- screens.
-
- .. note::
- A page should be deleted if the survey it belongs to is deleted.
- """
- _name = 'survey.page'
- _description = 'Survey Page'
- _rec_name = 'title'
- _order = 'sequence,id'
-
- # Model Fields #
-
- title = fields.Char('Page Title', required=True, translate=True)
- survey_id = fields.Many2one('survey.survey', string='Survey', ondelete='cascade', required=True)
- question_ids = fields.One2many('survey.question', 'page_id', string='Questions', copy=True)
- sequence = fields.Integer('Page number', default=10)
- description = fields.Html('Description', translate=True, oldname="note", help="An introductory text to your page")
-
-
-class SurveyQuestion(models.Model):
- """ Questions that will be asked in a survey.
-
- Each question can have one of more suggested answers (eg. in case of
- dropdown choices, multi-answer checkboxes, radio buttons...).
- """
-
- _name = 'survey.question'
- _description = 'Survey Question'
- _rec_name = 'question'
- _order = 'sequence,id'
-
- # Model fields #
-
- # Question metadata
- page_id = fields.Many2one('survey.page', string='Survey page',
- ondelete='cascade', required=True, default=lambda self: self.env.context.get('page_id'))
- survey_id = fields.Many2one('survey.survey', related='page_id.survey_id', string='Survey', readonly=False)
- sequence = fields.Integer('Sequence', default=10)
-
- # Question
- question = fields.Char('Question Name', required=True, translate=True)
- description = fields.Html('Description', help="Use this field to add \
- additional explanations about your question", translate=True,
- oldname='descriptive_text')
-
- # Answer
- question_type = fields.Selection([
- ('free_text', 'Multiple Lines Text Box'),
- ('textbox', 'Single Line Text Box'),
- ('numerical_box', 'Numerical Value'),
- ('date', 'Date'),
- ('simple_choice', 'Multiple choice: only one answer'),
- ('multiple_choice', 'Multiple choice: multiple answers allowed'),
- ('matrix', 'Matrix')], string='Type of Question', default='free_text', required=True, oldname='type')
- matrix_subtype = fields.Selection([('simple', 'One choice per row'),
- ('multiple', 'Multiple choices per row')], string='Matrix Type', default='simple')
- labels_ids = fields.One2many('survey.label', 'question_id', string='Types of answers', oldname='answer_choice_ids', copy=True)
- labels_ids_2 = fields.One2many('survey.label', 'question_id_2', string='Rows of the Matrix', copy=True)
- # labels are used for proposed choices
- # if question.type == simple choice | multiple choice
- # -> only labels_ids is used
- # if question.type == matrix
- # -> labels_ids are the columns of the matrix
- # -> labels_ids_2 are the rows of the matrix
-
- # Display options
- column_nb = fields.Selection([('12', '1'),
- ('6', '2'),
- ('4', '3'),
- ('3', '4'),
- ('2', '6')],
- 'Number of columns', default='12')
- # These options refer to col-xx-[12|6|4|3|2] classes in Bootstrap
- display_mode = fields.Selection([('columns', 'Radio Buttons'),
- ('dropdown', 'Selection Box')],
- default='columns')
-
- # Comments
- comments_allowed = fields.Boolean('Show Comments Field',
- oldname="allow_comment")
- comments_message = fields.Char('Comment Message', translate=True, default=lambda self: _("If other, please specify:"))
- comment_count_as_answer = fields.Boolean('Comment Field is an Answer Choice',
- oldname='make_comment_field')
-
- # Validation
- validation_required = fields.Boolean('Validate entry', oldname='is_validation_require')
- validation_email = fields.Boolean('Input must be an email')
- validation_length_min = fields.Integer('Minimum Text Length')
- validation_length_max = fields.Integer('Maximum Text Length')
- validation_min_float_value = fields.Float('Minimum value')
- validation_max_float_value = fields.Float('Maximum value')
- validation_min_date = fields.Date('Minimum Date')
- validation_max_date = fields.Date('Maximum Date')
- validation_error_msg = fields.Char('Validation Error message', oldname='validation_valid_err_msg',
- translate=True, default=lambda self: _("The answer you entered has an invalid format."))
-
- # Constraints on number of answers (matrices)
- constr_mandatory = fields.Boolean('Mandatory Answer', oldname="is_require_answer")
- constr_error_msg = fields.Char('Error message', oldname='req_error_msg', translate=True, default=lambda self: _("This question requires an answer."))
- user_input_line_ids = fields.One2many('survey.user_input_line', 'question_id', string='Answers', domain=[('skipped', '=', False)])
-
- _sql_constraints = [
- ('positive_len_min', 'CHECK (validation_length_min >= 0)', 'A length must be positive!'),
- ('positive_len_max', 'CHECK (validation_length_max >= 0)', 'A length must be positive!'),
- ('validation_length', 'CHECK (validation_length_min <= validation_length_max)', 'Max length cannot be smaller than min length!'),
- ('validation_float', 'CHECK (validation_min_float_value <= validation_max_float_value)', 'Max value cannot be smaller than min value!'),
- ('validation_date', 'CHECK (validation_min_date <= validation_max_date)', 'Max date cannot be smaller than min date!')
- ]
-
- @api.onchange('validation_email')
- def onchange_validation_email(self):
- if self.validation_email:
- self.validation_required = False
-
- # Validation methods
-
- @api.multi
- def validate_question(self, post, answer_tag):
- """ Validate question, depending on question type and parameters """
- self.ensure_one()
- try:
- checker = getattr(self, 'validate_' + self.question_type)
- except AttributeError:
- _logger.warning(self.question_type + ": This type of question has no validation method")
- return {}
- else:
- return checker(post, answer_tag)
-
- @api.multi
- def validate_free_text(self, post, answer_tag):
- self.ensure_one()
- errors = {}
- answer = post[answer_tag].strip()
- # Empty answer to mandatory question
- if self.constr_mandatory and not answer:
- errors.update({answer_tag: self.constr_error_msg})
- return errors
-
- @api.multi
- def validate_textbox(self, post, answer_tag):
- self.ensure_one()
- errors = {}
- answer = post[answer_tag].strip()
- # Empty answer to mandatory question
- if self.constr_mandatory and not answer:
- errors.update({answer_tag: self.constr_error_msg})
- # Email format validation
- # Note: this validation is very basic:
- # all the strings of the form
- # <something>@<anything>.<extension>
- # will be accepted
- if answer and self.validation_email:
- if not email_validator.match(answer):
- errors.update({answer_tag: _('This answer must be an email address')})
- # Answer validation (if properly defined)
- # Length of the answer must be in a range
- if answer and self.validation_required:
- if not (self.validation_length_min <= len(answer) <= self.validation_length_max):
- errors.update({answer_tag: self.validation_error_msg})
- return errors
-
- @api.multi
- def validate_numerical_box(self, post, answer_tag):
- self.ensure_one()
- errors = {}
- answer = post[answer_tag].strip()
- # Empty answer to mandatory question
- if self.constr_mandatory and not answer:
- errors.update({answer_tag: self.constr_error_msg})
- # Checks if user input is a number
- if answer:
- try:
- floatanswer = float(answer)
- except ValueError:
- errors.update({answer_tag: _('This is not a number')})
- # Answer validation (if properly defined)
- if answer and self.validation_required:
- # Answer is not in the right range
- with tools.ignore(Exception):
- floatanswer = float(answer) # check that it is a float has been done hereunder
- if not (self.validation_min_float_value <= floatanswer <= self.validation_max_float_value):
- errors.update({answer_tag: self.validation_error_msg})
- return errors
-
- @api.multi
- def validate_date(self, post, answer_tag):
- self.ensure_one()
- errors = {}
- answer = post[answer_tag].strip()
- # Empty answer to mandatory question
- if self.constr_mandatory and not answer:
- errors.update({answer_tag: self.constr_error_msg})
- # Checks if user input is a date
- if answer:
- try:
- dateanswer = fields.Date.from_string(answer)
- except ValueError:
- errors.update({answer_tag: _('This is not a date')})
- return errors
- # Answer validation (if properly defined)
- if answer and self.validation_required:
- # Answer is not in the right range
- try:
- date_from_string = fields.Date.from_string
- dateanswer = date_from_string(answer)
- min_date = date_from_string(self.validation_min_date)
- max_date = date_from_string(self.validation_max_date)
-
- if min_date and max_date and not (min_date <= dateanswer <= max_date):
- # If Minimum and Maximum Date are entered
- errors.update({answer_tag: self.validation_error_msg})
- elif min_date and not min_date <= dateanswer:
- # If only Minimum Date is entered and not Define Maximum Date
- errors.update({answer_tag: self.validation_error_msg})
- elif max_date and not dateanswer <= max_date:
- # If only Maximum Date is entered and not Define Minimum Date
- errors.update({answer_tag: self.validation_error_msg})
- except ValueError: # check that it is a date has been done hereunder
- pass
- return errors
-
- @api.multi
- def validate_simple_choice(self, post, answer_tag):
- self.ensure_one()
- errors = {}
- if self.comments_allowed:
- comment_tag = "%s_%s" % (answer_tag, 'comment')
- # Empty answer to mandatory self
- if self.constr_mandatory and answer_tag not in post:
- errors.update({answer_tag: self.constr_error_msg})
- if self.constr_mandatory and answer_tag in post and not post[answer_tag].strip():
- errors.update({answer_tag: self.constr_error_msg})
- # Answer is a comment and is empty
- if self.constr_mandatory and answer_tag in post and post[answer_tag] == "-1" and self.comment_count_as_answer and comment_tag in post and not post[comment_tag].strip():
- errors.update({answer_tag: self.constr_error_msg})
- return errors
-
- @api.multi
- def validate_multiple_choice(self, post, answer_tag):
- self.ensure_one()
- errors = {}
- if self.constr_mandatory:
- answer_candidates = dict_keys_startswith(post, answer_tag)
- comment_flag = answer_candidates.pop(("%s_%s" % (answer_tag, -1)), None)
- if self.comments_allowed:
- comment_answer = answer_candidates.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
- # Preventing answers with blank value
- if all(not answer.strip() for answer in answer_candidates.values()) and answer_candidates:
- errors.update({answer_tag: self.constr_error_msg})
- # There is no answer neither comments (if comments count as answer)
- if not answer_candidates and self.comment_count_as_answer and (not comment_flag or not comment_answer):
- errors.update({answer_tag: self.constr_error_msg})
- # There is no answer at all
- if not answer_candidates and not self.comment_count_as_answer:
- errors.update({answer_tag: self.constr_error_msg})
- return errors
-
- @api.multi
- def validate_matrix(self, post, answer_tag):
- self.ensure_one()
- errors = {}
- if self.constr_mandatory:
- lines_number = len(self.labels_ids_2)
- answer_candidates = dict_keys_startswith(post, answer_tag)
- answer_candidates.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
- # Number of lines that have been answered
- if self.matrix_subtype == 'simple':
- answer_number = len(answer_candidates)
- elif self.matrix_subtype == 'multiple':
- answer_number = len({sk.rsplit('_', 1)[0] for sk in answer_candidates})
- else:
- raise RuntimeError("Invalid matrix subtype")
- # Validate that each line has been answered
- if answer_number != lines_number:
- errors.update({answer_tag: self.constr_error_msg})
- return errors
-
-
-class SurveyLabel(models.Model):
- """ A suggested answer for a question """
-
- _name = 'survey.label'
- _rec_name = 'value'
- _order = 'sequence,id'
- _description = 'Survey Label'
-
- question_id = fields.Many2one('survey.question', string='Question', ondelete='cascade')
- question_id_2 = fields.Many2one('survey.question', string='Question 2', ondelete='cascade')
- sequence = fields.Integer('Label Sequence order', default=10)
- value = fields.Char('Suggested value', translate=True, required=True)
- quizz_mark = fields.Float('Score for this choice', help="A positive score indicates a correct choice; a negative or null score indicates a wrong answer")
-
- @api.one
- @api.constrains('question_id', 'question_id_2')
- def _check_question_not_empty(self):
- """Ensure that field question_id XOR field question_id_2 is not null"""
- if not bool(self.question_id) != bool(self.question_id_2):
- raise ValidationError(_("A label must be attached to only one question."))
-
-
-class SurveyUserInput(models.Model):
- """ Metadata for a set of one user's answers to a particular survey """
-
- _name = "survey.user_input"
- _rec_name = 'date_create'
- _description = 'Survey User Input'
-
- survey_id = fields.Many2one('survey.survey', string='Survey', required=True, readonly=True, ondelete='restrict')
- date_create = fields.Datetime('Creation Date', default=fields.Datetime.now, required=True, readonly=True, copy=False)
- deadline = fields.Datetime('Deadline', help="Date by which the person can open the survey and submit answers", oldname="date_deadline")
- input_type = fields.Selection([('manually', 'Manually'), ('link', 'Link')], string='Answer Type', default='manually', required=True, readonly=True, oldname="type")
- state = fields.Selection([
- ('new', 'Not started yet'),
- ('skip', 'Partially completed'),
- ('done', 'Completed')], string='Status', default='new', readonly=True)
- test_entry = fields.Boolean(readonly=True)
- token = fields.Char('Identification token', default=lambda self: str(uuid.uuid4()), readonly=True, required=True, copy=False)
-
- # Optional Identification data
- partner_id = fields.Many2one('res.partner', string='Partner', readonly=True)
- email = fields.Char('E-mail', readonly=True)
-
- # Displaying data
- last_displayed_page_id = fields.Many2one('survey.page', string='Last displayed page')
- # The answers !
- user_input_line_ids = fields.One2many('survey.user_input_line', 'user_input_id', string='Answers', copy=True)
-
- # URLs used to display the answers
- result_url = fields.Char("Public link to the survey results", related='survey_id.result_url', readonly=False)
- print_url = fields.Char("Public link to the empty survey", related='survey_id.print_url', readonly=False)
-
- quizz_score = fields.Float("Score for the quiz", compute="_compute_quizz_score", default=0.0)
-
- @api.depends('user_input_line_ids.quizz_mark')
- def _compute_quizz_score(self):
- for user_input in self:
- user_input.quizz_score = sum(user_input.user_input_line_ids.mapped('quizz_mark'))
-
- _sql_constraints = [
- ('unique_token', 'UNIQUE (token)', 'A token must be unique!'),
- ]
-
- @api.model
- def do_clean_emptys(self):
- """ Remove empty user inputs that have been created manually
- (used as a cronjob declared in data/survey_cron.xml)
- """
- an_hour_ago = fields.Datetime.to_string(datetime.datetime.now() - datetime.timedelta(hours=1))
- self.search([('input_type', '=', 'manually'), ('state', '=', 'new'),
- ('date_create', '<', an_hour_ago)]).unlink()
-
- @api.multi
- def action_survey_resend(self):
- """ Send again the invitation """
- self.ensure_one()
- local_context = {
- 'survey_resent_token': True,
- 'default_partner_ids': self.partner_id and [self.partner_id.id] or [],
- 'default_multi_email': self.email or "",
- 'default_public': 'email_private',
- }
- return self.survey_id.with_context(local_context).action_send_survey()
-
- @api.multi
- def action_view_answers(self):
- """ Open the website page with the survey form """
- self.ensure_one()
- return {
- 'type': 'ir.actions.act_url',
- 'name': "View Answers",
- 'target': 'self',
- 'url': '%s/%s' % (self.print_url, self.token)
- }
-
- @api.multi
- def action_survey_results(self):
- """ Open the website page with the survey results """
- self.ensure_one()
- return {
- 'type': 'ir.actions.act_url',
- 'name': "Survey Results",
- 'target': 'self',
- 'url': self.result_url
- }
-
-
-class SurveyUserInputLine(models.Model):
- _name = 'survey.user_input_line'
- _description = 'Survey User Input Line'
- _rec_name = 'date_create'
-
- user_input_id = fields.Many2one('survey.user_input', string='User Input', ondelete='cascade', required=True)
- question_id = fields.Many2one('survey.question', string='Question', ondelete='restrict', required=True)
- page_id = fields.Many2one(related='question_id.page_id', string="Page", readonly=False)
- survey_id = fields.Many2one(related='user_input_id.survey_id', string='Survey', store=True, readonly=False)
- date_create = fields.Datetime('Create Date', default=fields.Datetime.now, required=True)
- skipped = fields.Boolean('Skipped')
- answer_type = fields.Selection([
- ('text', 'Text'),
- ('number', 'Number'),
- ('date', 'Date'),
- ('free_text', 'Free Text'),
- ('suggestion', 'Suggestion')], string='Answer Type')
- value_text = fields.Char('Text answer')
- value_number = fields.Float('Numerical answer')
- value_date = fields.Date('Date answer')
- value_free_text = fields.Text('Free Text answer')
- value_suggested = fields.Many2one('survey.label', string="Suggested answer")
- value_suggested_row = fields.Many2one('survey.label', string="Row answer")
- quizz_mark = fields.Float('Score given for this choice')
-
- @api.constrains('skipped', 'answer_type')
- def _answered_or_skipped(self):
- for uil in self:
- if not uil.skipped != bool(uil.answer_type):
- raise ValidationError(_('This question cannot be unanswered or skipped.'))
-
- @api.constrains('answer_type')
- def _check_answer_type(self):
- for uil in self:
- fields_type = {
- 'text': bool(uil.value_text),
- 'number': (bool(uil.value_number) or uil.value_number == 0),
- 'date': bool(uil.value_date),
- 'free_text': bool(uil.value_free_text),
- 'suggestion': bool(uil.value_suggested)
- }
- if not fields_type.get(uil.answer_type, True):
- raise ValidationError(_('The answer must be in the right type'))
-
- def _get_mark(self, value_suggested):
- label = self.env['survey.label'].browse(int(value_suggested))
- mark = label.quizz_mark if label.exists() else 0.0
- return mark
-
- @api.model_create_multi
- def create(self, vals_list):
- for vals in vals_list:
- value_suggested = vals.get('value_suggested')
- if value_suggested:
- vals.update({'quizz_mark': self._get_mark(value_suggested)})
- return super(SurveyUserInputLine, self).create(vals_list)
-
- @api.multi
- def write(self, vals):
- value_suggested = vals.get('value_suggested')
- if value_suggested:
- vals.update({'quizz_mark': self._get_mark(value_suggested)})
- return super(SurveyUserInputLine, self).write(vals)
-
- @api.model
- def save_lines(self, user_input_id, question, post, answer_tag):
- """ Save answers to questions, depending on question type
-
- If an answer already exists for question and user_input_id, it will be
- overwritten (in order to maintain data consistency).
- """
- try:
- saver = getattr(self, 'save_line_' + question.question_type)
- except AttributeError:
- _logger.error(question.question_type + ": This type of question has no saving function")
- return False
- else:
- saver(user_input_id, question, post, answer_tag)
-
- @api.model
- def save_line_free_text(self, user_input_id, question, post, answer_tag):
- vals = {
- 'user_input_id': user_input_id,
- 'question_id': question.id,
- 'survey_id': question.survey_id.id,
- 'skipped': False,
- }
- if answer_tag in post and post[answer_tag].strip():
- vals.update({'answer_type': 'free_text', 'value_free_text': post[answer_tag]})
- else:
- vals.update({'answer_type': None, 'skipped': True})
- old_uil = self.search([
- ('user_input_id', '=', user_input_id),
- ('survey_id', '=', question.survey_id.id),
- ('question_id', '=', question.id)
- ])
- if old_uil:
- old_uil.write(vals)
- else:
- old_uil.create(vals)
- return True
-
- @api.model
- def save_line_textbox(self, user_input_id, question, post, answer_tag):
- vals = {
- 'user_input_id': user_input_id,
- 'question_id': question.id,
- 'survey_id': question.survey_id.id,
- 'skipped': False
- }
- if answer_tag in post and post[answer_tag].strip():
- vals.update({'answer_type': 'text', 'value_text': post[answer_tag]})
- else:
- vals.update({'answer_type': None, 'skipped': True})
- old_uil = self.search([
- ('user_input_id', '=', user_input_id),
- ('survey_id', '=', question.survey_id.id),
- ('question_id', '=', question.id)
- ])
- if old_uil:
- old_uil.write(vals)
- else:
- old_uil.create(vals)
- return True
-
- @api.model
- def save_line_numerical_box(self, user_input_id, question, post, answer_tag):
- vals = {
- 'user_input_id': user_input_id,
- 'question_id': question.id,
- 'survey_id': question.survey_id.id,
- 'skipped': False
- }
- if answer_tag in post and post[answer_tag].strip():
- vals.update({'answer_type': 'number', 'value_number': float(post[answer_tag])})
- else:
- vals.update({'answer_type': None, 'skipped': True})
- old_uil = self.search([
- ('user_input_id', '=', user_input_id),
- ('survey_id', '=', question.survey_id.id),
- ('question_id', '=', question.id)
- ])
- if old_uil:
- old_uil.write(vals)
- else:
- old_uil.create(vals)
- return True
-
- @api.model
- def save_line_date(self, user_input_id, question, post, answer_tag):
- vals = {
- 'user_input_id': user_input_id,
- 'question_id': question.id,
- 'survey_id': question.survey_id.id,
- 'skipped': False
- }
- if answer_tag in post and post[answer_tag].strip():
- vals.update({'answer_type': 'date', 'value_date': post[answer_tag]})
- else:
- vals.update({'answer_type': None, 'skipped': True})
- old_uil = self.search([
- ('user_input_id', '=', user_input_id),
- ('survey_id', '=', question.survey_id.id),
- ('question_id', '=', question.id)
- ])
- if old_uil:
- old_uil.write(vals)
- else:
- old_uil.create(vals)
- return True
-
- @api.model
- def save_line_simple_choice(self, user_input_id, question, post, answer_tag):
- vals = {
- 'user_input_id': user_input_id,
- 'question_id': question.id,
- 'survey_id': question.survey_id.id,
- 'skipped': False
- }
- old_uil = self.search([
- ('user_input_id', '=', user_input_id),
- ('survey_id', '=', question.survey_id.id),
- ('question_id', '=', question.id)
- ])
- old_uil.sudo().unlink()
-
- if answer_tag in post and post[answer_tag].strip():
- vals.update({'answer_type': 'suggestion', 'value_suggested': post[answer_tag]})
- else:
- vals.update({'answer_type': None, 'skipped': True})
-
- # '-1' indicates 'comment count as an answer' so do not need to record it
- if post.get(answer_tag) and post.get(answer_tag) != '-1':
- self.create(vals)
-
- comment_answer = post.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
- if comment_answer:
- vals.update({'answer_type': 'text', 'value_text': comment_answer, 'skipped': False, 'value_suggested': False})
- self.create(vals)
-
- return True
-
- @api.model
- def save_line_multiple_choice(self, user_input_id, question, post, answer_tag):
- vals = {
- 'user_input_id': user_input_id,
- 'question_id': question.id,
- 'survey_id': question.survey_id.id,
- 'skipped': False
- }
- old_uil = self.search([
- ('user_input_id', '=', user_input_id),
- ('survey_id', '=', question.survey_id.id),
- ('question_id', '=', question.id)
- ])
- old_uil.sudo().unlink()
-
- ca_dict = dict_keys_startswith(post, answer_tag + '_')
- comment_answer = ca_dict.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
- if len(ca_dict) > 0:
- for key in ca_dict:
- # '-1' indicates 'comment count as an answer' so do not need to record it
- if key != ('%s_%s' % (answer_tag, '-1')):
- vals.update({'answer_type': 'suggestion', 'value_suggested': ca_dict[key]})
- self.create(vals)
- if comment_answer:
- vals.update({'answer_type': 'text', 'value_text': comment_answer, 'value_suggested': False})
- self.create(vals)
- if not ca_dict and not comment_answer:
- vals.update({'answer_type': None, 'skipped': True})
- self.create(vals)
- return True
-
- @api.model
- def save_line_matrix(self, user_input_id, question, post, answer_tag):
- vals = {
- 'user_input_id': user_input_id,
- 'question_id': question.id,
- 'survey_id': question.survey_id.id,
- 'skipped': False
- }
- old_uil = self.search([
- ('user_input_id', '=', user_input_id),
- ('survey_id', '=', question.survey_id.id),
- ('question_id', '=', question.id)
- ])
- old_uil.sudo().unlink()
-
- no_answers = True
- ca_dict = dict_keys_startswith(post, answer_tag + '_')
-
- comment_answer = ca_dict.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
- if comment_answer:
- vals.update({'answer_type': 'text', 'value_text': comment_answer})
- self.create(vals)
- no_answers = False
-
- if question.matrix_subtype == 'simple':
- for row in question.labels_ids_2:
- a_tag = "%s_%s" % (answer_tag, row.id)
- if a_tag in ca_dict:
- no_answers = False
- vals.update({'answer_type': 'suggestion', 'value_suggested': ca_dict[a_tag], 'value_suggested_row': row.id})
- self.create(vals)
-
- elif question.matrix_subtype == 'multiple':
- for col in question.labels_ids:
- for row in question.labels_ids_2:
- a_tag = "%s_%s_%s" % (answer_tag, row.id, col.id)
- if a_tag in ca_dict:
- no_answers = False
- vals.update({'answer_type': 'suggestion', 'value_suggested': col.id, 'value_suggested_row': row.id})
- self.create(vals)
- if no_answers:
- vals.update({'answer_type': None, 'skipped': True})
- self.create(vals)
- return True
diff --git a/addons/survey/models/survey_page.py b/addons/survey/models/survey_page.py
new file mode 100644
index 00000000000..b518ff01f16
--- /dev/null
+++ b/addons/survey/models/survey_page.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class SurveyPage(models.Model):
+ """ A page for a survey.
+
+ Pages are essentially containers, allowing to group questions by ordered
+ screens.
+
+ .. note::
+ A page should be deleted if the survey it belongs to is deleted.
+ """
+ _name = 'survey.page'
+ _description = 'Survey Page'
+ _rec_name = 'title'
+ _order = 'sequence,id'
+
+ # Model Fields #
+
+ title = fields.Char('Page Title', required=True, translate=True)
+ survey_id = fields.Many2one('survey.survey', string='Survey', ondelete='cascade', required=True)
+ question_ids = fields.One2many('survey.question', 'page_id', string='Questions', copy=True)
+ sequence = fields.Integer('Page number', default=10)
+ description = fields.Html('Description', translate=True, oldname="note", help="An introductory text to your page") \ No newline at end of file
diff --git a/addons/survey/models/survey_question.py b/addons/survey/models/survey_question.py
new file mode 100644
index 00000000000..3f207c2719e
--- /dev/null
+++ b/addons/survey/models/survey_question.py
@@ -0,0 +1,299 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import logging
+import re
+
+from odoo import api, fields, models, tools, _
+from odoo.exceptions import ValidationError
+
+email_validator = re.compile(r"[^@]+@[^@]+\.[^@]+")
+_logger = logging.getLogger(__name__)
+
+
+def dict_keys_startswith(dictionary, string):
+ """Returns a dictionary containing the elements of <dict> whose keys start with <string>.
+ .. note::
+ This function uses dictionary comprehensions (Python >= 2.7)
+ """
+ return {k: v for k, v in dictionary.items() if k.startswith(string)}
+
+
+class SurveyQuestion(models.Model):
+ """ Questions that will be asked in a survey.
+
+ Each question can have one of more suggested answers (eg. in case of
+ dropdown choices, multi-answer checkboxes, radio buttons...).
+ """
+
+ _name = 'survey.question'
+ _description = 'Survey Question'
+ _rec_name = 'question'
+ _order = 'sequence,id'
+
+ # Model fields #
+
+ # Question metadata
+ page_id = fields.Many2one('survey.page', string='Survey page',
+ ondelete='cascade', required=True, default=lambda self: self.env.context.get('page_id'))
+ survey_id = fields.Many2one('survey.survey', related='page_id.survey_id', string='Survey', readonly=False)
+ sequence = fields.Integer('Sequence', default=10)
+
+ # Question
+ question = fields.Char('Question Name', required=True, translate=True)
+ description = fields.Html('Description', help="Use this field to add \
+ additional explanations about your question", translate=True,
+ oldname='descriptive_text')
+
+ # Answer
+ question_type = fields.Selection([
+ ('free_text', 'Multiple Lines Text Box'),
+ ('textbox', 'Single Line Text Box'),
+ ('numerical_box', 'Numerical Value'),
+ ('date', 'Date'),
+ ('simple_choice', 'Multiple choice: only one answer'),
+ ('multiple_choice', 'Multiple choice: multiple answers allowed'),
+ ('matrix', 'Matrix')], string='Type of Question', default='free_text', required=True, oldname='type')
+ matrix_subtype = fields.Selection([('simple', 'One choice per row'),
+ ('multiple', 'Multiple choices per row')], string='Matrix Type', default='simple')
+ labels_ids = fields.One2many('survey.label', 'question_id', string='Types of answers', oldname='answer_choice_ids', copy=True)
+ labels_ids_2 = fields.One2many('survey.label', 'question_id_2', string='Rows of the Matrix', copy=True)
+ # labels are used for proposed choices
+ # if question.type == simple choice | multiple choice
+ # -> only labels_ids is used
+ # if question.type == matrix
+ # -> labels_ids are the columns of the matrix
+ # -> labels_ids_2 are the rows of the matrix
+
+ # Display options
+ column_nb = fields.Selection([('12', '1'),
+ ('6', '2'),
+ ('4', '3'),
+ ('3', '4'),
+ ('2', '6')],
+ 'Number of columns', default='12')
+ # These options refer to col-xx-[12|6|4|3|2] classes in Bootstrap
+ display_mode = fields.Selection([('columns', 'Radio Buttons'),
+ ('dropdown', 'Selection Box')],
+ default='columns')
+
+ # Comments
+ comments_allowed = fields.Boolean('Show Comments Field',
+ oldname="allow_comment")
+ comments_message = fields.Char('Comment Message', translate=True, default=lambda self: _("If other, please specify:"))
+ comment_count_as_answer = fields.Boolean('Comment Field is an Answer Choice',
+ oldname='make_comment_field')
+
+ # Validation
+ validation_required = fields.Boolean('Validate entry', oldname='is_validation_require')
+ validation_email = fields.Boolean('Input must be an email')
+ validation_length_min = fields.Integer('Minimum Text Length')
+ validation_length_max = fields.Integer('Maximum Text Length')
+ validation_min_float_value = fields.Float('Minimum value')
+ validation_max_float_value = fields.Float('Maximum value')
+ validation_min_date = fields.Date('Minimum Date')
+ validation_max_date = fields.Date('Maximum Date')
+ validation_error_msg = fields.Char('Validation Error message', oldname='validation_valid_err_msg',
+ translate=True, default=lambda self: _("The answer you entered has an invalid format."))
+
+ # Constraints on number of answers (matrices)
+ constr_mandatory = fields.Boolean('Mandatory Answer', oldname="is_require_answer")
+ constr_error_msg = fields.Char('Error message', oldname='req_error_msg', translate=True, default=lambda self: _("This question requires an answer."))
+ user_input_line_ids = fields.One2many('survey.user_input_line', 'question_id', string='Answers', domain=[('skipped', '=', False)])
+
+ _sql_constraints = [
+ ('positive_len_min', 'CHECK (validation_length_min >= 0)', 'A length must be positive!'),
+ ('positive_len_max', 'CHECK (validation_length_max >= 0)', 'A length must be positive!'),
+ ('validation_length', 'CHECK (validation_length_min <= validation_length_max)', 'Max length cannot be smaller than min length!'),
+ ('validation_float', 'CHECK (validation_min_float_value <= validation_max_float_value)', 'Max value cannot be smaller than min value!'),
+ ('validation_date', 'CHECK (validation_min_date <= validation_max_date)', 'Max date cannot be smaller than min date!')
+ ]
+
+ @api.onchange('validation_email')
+ def onchange_validation_email(self):
+ if self.validation_email:
+ self.validation_required = False
+
+ # Validation methods
+
+ @api.multi
+ def validate_question(self, post, answer_tag):
+ """ Validate question, depending on question type and parameters """
+ self.ensure_one()
+ try:
+ checker = getattr(self, 'validate_' + self.question_type)
+ except AttributeError:
+ _logger.warning(self.question_type + ": This type of question has no validation method")
+ return {}
+ else:
+ return checker(post, answer_tag)
+
+ @api.multi
+ def validate_free_text(self, post, answer_tag):
+ self.ensure_one()
+ errors = {}
+ answer = post[answer_tag].strip()
+ # Empty answer to mandatory question
+ if self.constr_mandatory and not answer:
+ errors.update({answer_tag: self.constr_error_msg})
+ return errors
+
+ @api.multi
+ def validate_textbox(self, post, answer_tag):
+ self.ensure_one()
+ errors = {}
+ answer = post[answer_tag].strip()
+ # Empty answer to mandatory question
+ if self.constr_mandatory and not answer:
+ errors.update({answer_tag: self.constr_error_msg})
+ # Email format validation
+ # Note: this validation is very basic:
+ # all the strings of the form
+ # <something>@<anything>.<extension>
+ # will be accepted
+ if answer and self.validation_email:
+ if not email_validator.match(answer):
+ errors.update({answer_tag: _('This answer must be an email address')})
+ # Answer validation (if properly defined)
+ # Length of the answer must be in a range
+ if answer and self.validation_required:
+ if not (self.validation_length_min <= len(answer) <= self.validation_length_max):
+ errors.update({answer_tag: self.validation_error_msg})
+ return errors
+
+ @api.multi
+ def validate_numerical_box(self, post, answer_tag):
+ self.ensure_one()
+ errors = {}
+ answer = post[answer_tag].strip()
+ # Empty answer to mandatory question
+ if self.constr_mandatory and not answer:
+ errors.update({answer_tag: self.constr_error_msg})
+ # Checks if user input is a number
+ if answer:
+ try:
+ floatanswer = float(answer)
+ except ValueError:
+ errors.update({answer_tag: _('This is not a number')})
+ # Answer validation (if properly defined)
+ if answer and self.validation_required:
+ # Answer is not in the right range
+ with tools.ignore(Exception):
+ floatanswer = float(answer) # check that it is a float has been done hereunder
+ if not (self.validation_min_float_value <= floatanswer <= self.validation_max_float_value):
+ errors.update({answer_tag: self.validation_error_msg})
+ return errors
+
+ @api.multi
+ def validate_date(self, post, answer_tag):
+ self.ensure_one()
+ errors = {}
+ answer = post[answer_tag].strip()
+ # Empty answer to mandatory question
+ if self.constr_mandatory and not answer:
+ errors.update({answer_tag: self.constr_error_msg})
+ # Checks if user input is a date
+ if answer:
+ try:
+ dateanswer = fields.Date.from_string(answer)
+ except ValueError:
+ errors.update({answer_tag: _('This is not a date')})
+ return errors
+ # Answer validation (if properly defined)
+ if answer and self.validation_required:
+ # Answer is not in the right range
+ try:
+ date_from_string = fields.Date.from_string
+ dateanswer = date_from_string(answer)
+ min_date = date_from_string(self.validation_min_date)
+ max_date = date_from_string(self.validation_max_date)
+
+ if min_date and max_date and not (min_date <= dateanswer <= max_date):
+ # If Minimum and Maximum Date are entered
+ errors.update({answer_tag: self.validation_error_msg})
+ elif min_date and not min_date <= dateanswer:
+ # If only Minimum Date is entered and not Define Maximum Date
+ errors.update({answer_tag: self.validation_error_msg})
+ elif max_date and not dateanswer <= max_date:
+ # If only Maximum Date is entered and not Define Minimum Date
+ errors.update({answer_tag: self.validation_error_msg})
+ except ValueError: # check that it is a date has been done hereunder
+ pass
+ return errors
+
+ @api.multi
+ def validate_simple_choice(self, post, answer_tag):
+ self.ensure_one()
+ errors = {}
+ if self.comments_allowed:
+ comment_tag = "%s_%s" % (answer_tag, 'comment')
+ # Empty answer to mandatory self
+ if self.constr_mandatory and answer_tag not in post:
+ errors.update({answer_tag: self.constr_error_msg})
+ if self.constr_mandatory and answer_tag in post and not post[answer_tag].strip():
+ errors.update({answer_tag: self.constr_error_msg})
+ # Answer is a comment and is empty
+ if self.constr_mandatory and answer_tag in post and post[answer_tag] == "-1" and self.comment_count_as_answer and comment_tag in post and not post[comment_tag].strip():
+ errors.update({answer_tag: self.constr_error_msg})
+ return errors
+
+ @api.multi
+ def validate_multiple_choice(self, post, answer_tag):
+ self.ensure_one()
+ errors = {}
+ if self.constr_mandatory:
+ answer_candidates = dict_keys_startswith(post, answer_tag)
+ comment_flag = answer_candidates.pop(("%s_%s" % (answer_tag, -1)), None)
+ if self.comments_allowed:
+ comment_answer = answer_candidates.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
+ # Preventing answers with blank value
+ if all(not answer.strip() for answer in answer_candidates.values()) and answer_candidates:
+ errors.update({answer_tag: self.constr_error_msg})
+ # There is no answer neither comments (if comments count as answer)
+ if not answer_candidates and self.comment_count_as_answer and (not comment_flag or not comment_answer):
+ errors.update({answer_tag: self.constr_error_msg})
+ # There is no answer at all
+ if not answer_candidates and not self.comment_count_as_answer:
+ errors.update({answer_tag: self.constr_error_msg})
+ return errors
+
+ @api.multi
+ def validate_matrix(self, post, answer_tag):
+ self.ensure_one()
+ errors = {}
+ if self.constr_mandatory:
+ lines_number = len(self.labels_ids_2)
+ answer_candidates = dict_keys_startswith(post, answer_tag)
+ answer_candidates.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
+ # Number of lines that have been answered
+ if self.matrix_subtype == 'simple':
+ answer_number = len(answer_candidates)
+ elif self.matrix_subtype == 'multiple':
+ answer_number = len({sk.rsplit('_', 1)[0] for sk in answer_candidates})
+ else:
+ raise RuntimeError("Invalid matrix subtype")
+ # Validate that each line has been answered
+ if answer_number != lines_number:
+ errors.update({answer_tag: self.constr_error_msg})
+ return errors
+
+class SurveyLabel(models.Model):
+ """ A suggested answer for a question """
+
+ _name = 'survey.label'
+ _rec_name = 'value'
+ _order = 'sequence,id'
+ _description = 'Survey Label'
+
+ question_id = fields.Many2one('survey.question', string='Question', ondelete='cascade')
+ question_id_2 = fields.Many2one('survey.question', string='Question 2', ondelete='cascade')
+ sequence = fields.Integer('Label Sequence order', default=10)
+ value = fields.Char('Suggested value', translate=True, required=True)
+ quizz_mark = fields.Float('Score for this choice', help="A positive score indicates a correct choice; a negative or null score indicates a wrong answer")
+
+ @api.one
+ @api.constrains('question_id', 'question_id_2')
+ def _check_question_not_empty(self):
+ """Ensure that field question_id XOR field question_id_2 is not null"""
+ if not bool(self.question_id) != bool(self.question_id_2):
+ raise ValidationError(_("A label must be attached to only one question."))
diff --git a/addons/survey/models/survey_stage.py b/addons/survey/models/survey_stage.py
new file mode 100644
index 00000000000..b73e28fbc51
--- /dev/null
+++ b/addons/survey/models/survey_stage.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class SurveyStage(models.Model):
+ """Stages for Kanban view of surveys"""
+
+ _name = 'survey.stage'
+ _description = 'Survey Stage'
+ _order = 'sequence,id'
+
+ name = fields.Char(required=True, translate=True)
+ sequence = fields.Integer(default=1)
+ closed = fields.Boolean(help="If closed, people won't be able to answer to surveys in this column.")
+ fold = fields.Boolean(string="Folded in kanban view")
+
+ _sql_constraints = [
+ ('positive_sequence', 'CHECK(sequence >= 0)', 'Sequence number MUST be a natural')
+ ]
diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py
new file mode 100644
index 00000000000..65955df273e
--- /dev/null
+++ b/addons/survey/models/survey_survey.py
@@ -0,0 +1,354 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from collections import Counter, OrderedDict
+from itertools import product
+from werkzeug import urls
+
+from odoo import api, fields, models, SUPERUSER_ID, _
+from odoo.addons.http_routing.models.ir_http import slug
+from odoo.exceptions import UserError
+
+
+class Survey(models.Model):
+ """ Settings for a multi-page/multi-question survey.
+ Each survey can have one or more attached pages, and each page can display
+ one or more questions.
+ """
+
+ _name = 'survey.survey'
+ _description = 'Survey'
+ _rec_name = 'title'
+ _inherit = ['mail.thread', 'mail.activity.mixin']
+
+ def _default_stage(self):
+ return self.env['survey.stage'].search([], limit=1).id
+
+ title = fields.Char('Title', required=True, translate=True)
+ page_ids = fields.One2many('survey.page', 'survey_id', string='Pages', copy=True)
+ stage_id = fields.Many2one('survey.stage', string="Stage", default=_default_stage,
+ ondelete="restrict", copy=False, group_expand='_read_group_stage_ids')
+ auth_required = fields.Boolean('Login required', help="Users with a public link will be requested to login before taking part to the survey",
+ oldname="authenticate")
+ users_can_go_back = fields.Boolean('Users can go back', help="If checked, users can go back to previous pages.")
+ tot_sent_survey = fields.Integer("Number of sent surveys", compute="_compute_survey_statistic")
+ tot_start_survey = fields.Integer("Number of started surveys", compute="_compute_survey_statistic")
+ tot_comp_survey = fields.Integer("Number of completed surveys", compute="_compute_survey_statistic")
+ description = fields.Html("Description", translate=True, help="A long description of the purpose of the survey")
+ color = fields.Integer('Color Index', default=0)
+ user_input_ids = fields.One2many('survey.user_input', 'survey_id', string='User responses', readonly=True)
+ designed = fields.Boolean("Is designed?", compute="_is_designed")
+ public_url = fields.Char("Public link", compute="_compute_survey_url")
+ public_url_html = fields.Char("Public link (html version)", compute="_compute_survey_url")
+ print_url = fields.Char("Print link", compute="_compute_survey_url")
+ result_url = fields.Char("Results link", compute="_compute_survey_url")
+ email_template_id = fields.Many2one('mail.template', string='Email Template', ondelete='set null')
+ thank_you_message = fields.Html("Thanks Message", translate=True, help="This message will be displayed when survey is completed")
+ quizz_mode = fields.Boolean("Quizz Mode")
+ active = fields.Boolean("Active", default=True)
+ is_closed = fields.Boolean("Is closed", related='stage_id.closed', readonly=False)
+
+ def _is_designed(self):
+ for survey in self:
+ if not survey.page_ids or not [page.question_ids for page in survey.page_ids if page.question_ids]:
+ survey.designed = False
+ else:
+ survey.designed = True
+
+ @api.multi
+ def _compute_survey_statistic(self):
+ UserInput = self.env['survey.user_input']
+
+ sent_survey = UserInput.search([('survey_id', 'in', self.ids), ('input_type', '=', 'link')])
+ start_survey = UserInput.search(['&', ('survey_id', 'in', self.ids), '|', ('state', '=', 'skip'), ('state', '=', 'done')])
+ complete_survey = UserInput.search([('survey_id', 'in', self.ids), ('state', '=', 'done')])
+
+ for survey in self:
+ survey.tot_sent_survey = len(sent_survey.filtered(lambda user_input: user_input.survey_id == survey))
+ survey.tot_start_survey = len(start_survey.filtered(lambda user_input: user_input.survey_id == survey))
+ survey.tot_comp_survey = len(complete_survey.filtered(lambda user_input: user_input.survey_id == survey))
+
+ def _compute_survey_url(self):
+ """ Computes a public URL for the survey """
+ base_url = '/' if self.env.context.get('relative_url') else \
+ self.env['ir.config_parameter'].sudo().get_param('web.base.url')
+ for survey in self:
+ survey.public_url = urls.url_join(base_url, "survey/start/%s" % (slug(survey)))
+ survey.print_url = urls.url_join(base_url, "survey/print/%s" % (slug(survey)))
+ survey.result_url = urls.url_join(base_url, "survey/results/%s" % (slug(survey)))
+ survey.public_url_html = '<a href="%s">%s</a>' % (survey.public_url, _("Click here to start survey"))
+
+ @api.model
+ def _read_group_stage_ids(self, stages, domain, order):
+ """ Read group customization in order to display all the stages in the
+ kanban view, even if they are empty
+ """
+ stage_ids = stages._search([], order=order, access_rights_uid=SUPERUSER_ID)
+ return stages.browse(stage_ids)
+
+ # Public methods #
+ def copy_data(self, default=None):
+ title = _("%s (copy)") % (self.title)
+ default = dict(default or {}, title=title)
+ return super(Survey, self).copy_data(default)
+
+ @api.model
+ def next_page(self, user_input, page_id, go_back=False):
+ """ The next page to display to the user, knowing that page_id is the id
+ of the last displayed page.
+
+ If page_id == 0, it will always return the first page of the survey.
+
+ If all the pages have been displayed and go_back == False, it will
+ return None
+
+ If go_back == True, it will return the *previous* page instead of the
+ next page.
+
+ .. note::
+ It is assumed here that a careful user will not try to set go_back
+ to True if she knows that the page to display is the first one!
+ (doing this will probably cause a giant worm to eat her house)
+ """
+ survey = user_input.survey_id
+ pages = list(enumerate(survey.page_ids))
+
+ # First page
+ if page_id == 0:
+ return (pages[0][1], 0, len(pages) == 1)
+
+ current_page_index = pages.index(next(p for p in pages if p[1].id == page_id))
+
+ # All the pages have been displayed
+ if current_page_index == len(pages) - 1 and not go_back:
+ return (None, -1, False)
+ # Let's get back, baby!
+ elif go_back and survey.users_can_go_back:
+ return (pages[current_page_index - 1][1], current_page_index - 1, False)
+ else:
+ # This will show the last page
+ if current_page_index == len(pages) - 2:
+ return (pages[current_page_index + 1][1], current_page_index + 1, True)
+ # This will show a regular page
+ else:
+ return (pages[current_page_index + 1][1], current_page_index + 1, False)
+
+ @api.multi
+ def filter_input_ids(self, filters, finished=False):
+ """If user applies any filters, then this function returns list of
+ filtered user_input_id and label's strings for display data in web.
+ :param filters: list of dictionary (having: row_id, ansewr_id)
+ :param finished: True for completely filled survey,Falser otherwise.
+ :returns list of filtered user_input_ids.
+ """
+ self.ensure_one()
+ if filters:
+ domain_filter, choice = [], []
+ for current_filter in filters:
+ row_id, answer_id = current_filter['row_id'], current_filter['answer_id']
+ if row_id == 0:
+ choice.append(answer_id)
+ else:
+ domain_filter.extend(['|', ('value_suggested_row.id', '=', row_id), ('value_suggested.id', '=', answer_id)])
+ if choice:
+ domain_filter.insert(0, ('value_suggested.id', 'in', choice))
+ else:
+ domain_filter = domain_filter[1:]
+ input_lines = self.env['survey.user_input_line'].search(domain_filter)
+ filtered_input_ids = [input_line.user_input_id.id for input_line in input_lines]
+ else:
+ filtered_input_ids = []
+ if finished:
+ UserInput = self.env['survey.user_input']
+ if not filtered_input_ids:
+ user_inputs = UserInput.search([('survey_id', '=', self.id)])
+ else:
+ user_inputs = UserInput.browse(filtered_input_ids)
+ return user_inputs.filtered(lambda input_item: input_item.state == 'done').ids
+ return filtered_input_ids
+
+ @api.model
+ def get_filter_display_data(self, filters):
+ """Returns data to display current filters
+ :param filters: list of dictionary (having: row_id, answer_id)
+ :returns list of dict having data to display filters.
+ """
+ filter_display_data = []
+ if filters:
+ Label = self.env['survey.label']
+ for current_filter in filters:
+ row_id, answer_id = current_filter['row_id'], current_filter['answer_id']
+ label = Label.browse(answer_id)
+ question = label.question_id
+ if row_id == 0:
+ labels = label
+ else:
+ labels = Label.browse([row_id, answer_id])
+ filter_display_data.append({'question_text': question.question,
+ 'labels': labels.mapped('value')})
+ return filter_display_data
+
+ @api.model
+ def prepare_result(self, question, current_filters=None):
+ """ Compute statistical data for questions by counting number of vote per choice on basis of filter """
+ current_filters = current_filters if current_filters else []
+ result_summary = {}
+
+ # Calculate and return statistics for choice
+ if question.question_type in ['simple_choice', 'multiple_choice']:
+ comments = []
+ answers = OrderedDict((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 input_line.answer_type == 'suggestion' and answers.get(input_line.value_suggested.id) and (not(current_filters) or input_line.user_input_id.id in current_filters):
+ answers[input_line.value_suggested.id]['count'] += 1
+ if input_line.answer_type == 'text' and (not(current_filters) or input_line.user_input_id.id in current_filters):
+ comments.append(input_line)
+ result_summary = {'answers': list(answers.values()), 'comments': comments}
+
+ # Calculate and return statistics for matrix
+ if question.question_type == 'matrix':
+ rows = OrderedDict()
+ answers = OrderedDict()
+ res = dict()
+ comments = []
+ [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, answers):
+ res[cell] = 0
+ for input_line in question.user_input_line_ids:
+ if input_line.answer_type == 'suggestion' and (not(current_filters) or input_line.user_input_id.id in current_filters) and input_line.value_suggested_row:
+ res[(input_line.value_suggested_row.id, input_line.value_suggested.id)] += 1
+ if input_line.answer_type == 'text' and (not(current_filters) or input_line.user_input_id.id in current_filters):
+ comments.append(input_line)
+ result_summary = {'answers': answers, 'rows': rows, 'result': res, 'comments': comments}
+
+ # Calculate and return statistics for free_text, textbox, date
+ if question.question_type in ['free_text', 'textbox', 'date']:
+ 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.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)
+ if all_inputs:
+ result_summary.update({'average': round(sum(all_inputs) / len(all_inputs), 2),
+ 'max': round(max(all_inputs), 2),
+ 'min': round(min(all_inputs), 2),
+ 'sum': sum(all_inputs),
+ 'most_common': Counter(all_inputs).most_common(5)})
+ return result_summary
+
+ @api.model
+ def get_input_summary(self, question, current_filters=None):
+ """ Returns overall summary of question e.g. answered, skipped, total_inputs on basis of filter """
+ current_filters = current_filters if current_filters else []
+ 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
+
+ # Actions
+
+ @api.multi
+ def action_start_survey(self):
+ """ Open the website page with the survey form """
+ self.ensure_one()
+ token = self.env.context.get('survey_token')
+ trail = "/%s" % token if token else ""
+ return {
+ 'type': 'ir.actions.act_url',
+ 'name': "Start Survey",
+ 'target': 'self',
+ 'url': self.with_context(relative_url=True).public_url + trail
+ }
+
+ @api.multi
+ def action_send_survey(self):
+ """ Open a window to compose an email, pre-filled with the survey message """
+ # Ensure that this survey has at least one page with at least one question.
+ if not self.page_ids or not [page.question_ids for page in self.page_ids if page.question_ids]:
+ raise UserError(_('You cannot send an invitation for a survey that has no questions.'))
+
+ if self.stage_id.closed:
+ raise UserError(_("You cannot send invitations for closed surveys."))
+
+ template = self.env.ref('survey.email_template_survey', raise_if_not_found=False)
+
+ local_context = dict(
+ self.env.context,
+ default_model='survey.survey',
+ default_res_id=self.id,
+ default_survey_id=self.id,
+ default_use_template=bool(template),
+ default_template_id=template and template.id or False,
+ default_composition_mode='comment',
+ notif_layout='mail.mail_notification_light',
+ )
+ return {
+ 'type': 'ir.actions.act_window',
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'res_model': 'survey.mail.compose.message',
+ 'target': 'new',
+ 'context': local_context,
+ }
+
+ @api.multi
+ def action_print_survey(self):
+ """ Open the website page with the survey printable view """
+ self.ensure_one()
+ token = self.env.context.get('survey_token')
+ trail = "/" + token if token else ""
+ return {
+ 'type': 'ir.actions.act_url',
+ 'name': "Print Survey",
+ 'target': 'self',
+ 'url': self.with_context(relative_url=True).print_url + trail
+ }
+
+ @api.multi
+ def action_result_survey(self):
+ """ Open the website page with the survey results view """
+ self.ensure_one()
+ return {
+ 'type': 'ir.actions.act_url',
+ 'name': "Results of the Survey",
+ 'target': 'self',
+ 'url': self.with_context(relative_url=True).result_url
+ }
+
+ @api.multi
+ def action_test_survey(self):
+ ''' Open the website page with the survey form into test mode'''
+ self.ensure_one()
+ return {
+ 'type': 'ir.actions.act_url',
+ 'name': "Results of the Survey",
+ 'target': 'self',
+ 'url': self.with_context(relative_url=True).public_url + "/phantom"
+ }
+
+ @api.multi
+ def action_survey_user_input(self):
+ action_rec = self.env.ref('survey.action_survey_user_input')
+ action = action_rec.read()[0]
+ ctx = dict(self.env.context)
+ ctx.update({'search_default_survey_id': self.ids[0],
+ 'search_default_completed': 1})
+ action['context'] = ctx
+ return action
+
diff --git a/addons/survey/models/survey_user.py b/addons/survey/models/survey_user.py
new file mode 100644
index 00000000000..32250825bd3
--- /dev/null
+++ b/addons/survey/models/survey_user.py
@@ -0,0 +1,386 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import datetime
+import logging
+import re
+import uuid
+
+from odoo import api, fields, models, _
+from odoo.exceptions import ValidationError
+
+email_validator = re.compile(r"[^@]+@[^@]+\.[^@]+")
+_logger = logging.getLogger(__name__)
+
+
+def dict_keys_startswith(dictionary, string):
+ """Returns a dictionary containing the elements of <dict> whose keys start with <string>.
+ .. note::
+ This function uses dictionary comprehensions (Python >= 2.7)
+ """
+ return {k: v for k, v in dictionary.items() if k.startswith(string)}
+
+
+class SurveyUserInput(models.Model):
+ """ Metadata for a set of one user's answers to a particular survey """
+
+ _name = "survey.user_input"
+ _rec_name = 'date_create'
+ _description = 'Survey User Input'
+
+ survey_id = fields.Many2one('survey.survey', string='Survey', required=True, readonly=True, ondelete='restrict')
+ date_create = fields.Datetime('Creation Date', default=fields.Datetime.now, required=True, readonly=True, copy=False)
+ deadline = fields.Datetime('Deadline', help="Date by which the person can open the survey and submit answers", oldname="date_deadline")
+ input_type = fields.Selection([('manually', 'Manually'), ('link', 'Link')], string='Answer Type', default='manually', required=True, readonly=True, oldname="type")
+ state = fields.Selection([
+ ('new', 'Not started yet'),
+ ('skip', 'Partially completed'),
+ ('done', 'Completed')], string='Status', default='new', readonly=True)
+ test_entry = fields.Boolean(readonly=True)
+ token = fields.Char('Identification token', default=lambda self: str(uuid.uuid4()), readonly=True, required=True, copy=False)
+
+ # Optional Identification data
+ partner_id = fields.Many2one('res.partner', string='Partner', readonly=True)
+ email = fields.Char('E-mail', readonly=True)
+
+ # Displaying data
+ last_displayed_page_id = fields.Many2one('survey.page', string='Last displayed page')
+ # The answers !
+ user_input_line_ids = fields.One2many('survey.user_input_line', 'user_input_id', string='Answers', copy=True)
+
+ # URLs used to display the answers
+ result_url = fields.Char("Public link to the survey results", related='survey_id.result_url', readonly=False)
+ print_url = fields.Char("Public link to the empty survey", related='survey_id.print_url', readonly=False)
+
+ quizz_score = fields.Float("Score for the quiz", compute="_compute_quizz_score", default=0.0)
+
+ @api.depends('user_input_line_ids.quizz_mark')
+ def _compute_quizz_score(self):
+ for user_input in self:
+ user_input.quizz_score = sum(user_input.user_input_line_ids.mapped('quizz_mark'))
+
+ _sql_constraints = [
+ ('unique_token', 'UNIQUE (token)', 'A token must be unique!'),
+ ]
+
+ @api.model
+ def do_clean_emptys(self):
+ """ Remove empty user inputs that have been created manually
+ (used as a cronjob declared in data/survey_cron.xml)
+ """
+ an_hour_ago = fields.Datetime.to_string(datetime.datetime.now() - datetime.timedelta(hours=1))
+ self.search([('input_type', '=', 'manually'), ('state', '=', 'new'),
+ ('date_create', '<', an_hour_ago)]).unlink()
+
+ @api.multi
+ def action_survey_resend(self):
+ """ Send again the invitation """
+ self.ensure_one()
+ local_context = {
+ 'survey_resent_token': True,
+ 'default_partner_ids': self.partner_id and [self.partner_id.id] or [],
+ 'default_multi_email': self.email or "",
+ 'default_public': 'email_private',
+ }
+ return self.survey_id.with_context(local_context).action_send_survey()
+
+ @api.multi
+ def action_view_answers(self):
+ """ Open the website page with the survey form """
+ self.ensure_one()
+ return {
+ 'type': 'ir.actions.act_url',
+ 'name': "View Answers",
+ 'target': 'self',
+ 'url': '%s/%s' % (self.print_url, self.token)
+ }
+
+ @api.multi
+ def action_survey_results(self):
+ """ Open the website page with the survey results """
+ self.ensure_one()
+ return {
+ 'type': 'ir.actions.act_url',
+ 'name': "Survey Results",
+ 'target': 'self',
+ 'url': self.result_url
+ }
+
+
+class SurveyUserInputLine(models.Model):
+ _name = 'survey.user_input_line'
+ _description = 'Survey User Input Line'
+ _rec_name = 'date_create'
+
+ user_input_id = fields.Many2one('survey.user_input', string='User Input', ondelete='cascade', required=True)
+ question_id = fields.Many2one('survey.question', string='Question', ondelete='restrict', required=True)
+ page_id = fields.Many2one(related='question_id.page_id', string="Page", readonly=False)
+ survey_id = fields.Many2one(related='user_input_id.survey_id', string='Survey', store=True, readonly=False)
+ date_create = fields.Datetime('Create Date', default=fields.Datetime.now, required=True)
+ skipped = fields.Boolean('Skipped')
+ answer_type = fields.Selection([
+ ('text', 'Text'),
+ ('number', 'Number'),
+ ('date', 'Date'),
+ ('free_text', 'Free Text'),
+ ('suggestion', 'Suggestion')], string='Answer Type')
+ value_text = fields.Char('Text answer')
+ value_number = fields.Float('Numerical answer')
+ value_date = fields.Date('Date answer')
+ value_free_text = fields.Text('Free Text answer')
+ value_suggested = fields.Many2one('survey.label', string="Suggested answer")
+ value_suggested_row = fields.Many2one('survey.label', string="Row answer")
+ quizz_mark = fields.Float('Score given for this choice')
+
+ @api.constrains('skipped', 'answer_type')
+ def _answered_or_skipped(self):
+ for uil in self:
+ if not uil.skipped != bool(uil.answer_type):
+ raise ValidationError(_('This question cannot be unanswered or skipped.'))
+
+ @api.constrains('answer_type')
+ def _check_answer_type(self):
+ for uil in self:
+ fields_type = {
+ 'text': bool(uil.value_text),
+ 'number': (bool(uil.value_number) or uil.value_number == 0),
+ 'date': bool(uil.value_date),
+ 'free_text': bool(uil.value_free_text),
+ 'suggestion': bool(uil.value_suggested)
+ }
+ if not fields_type.get(uil.answer_type, True):
+ raise ValidationError(_('The answer must be in the right type'))
+
+ def _get_mark(self, value_suggested):
+ label = self.env['survey.label'].browse(int(value_suggested))
+ mark = label.quizz_mark if label.exists() else 0.0
+ return mark
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ for vals in vals_list:
+ value_suggested = vals.get('value_suggested')
+ if value_suggested:
+ vals.update({'quizz_mark': self._get_mark(value_suggested)})
+ return super(SurveyUserInputLine, self).create(vals_list)
+
+ @api.multi
+ def write(self, vals):
+ value_suggested = vals.get('value_suggested')
+ if value_suggested:
+ vals.update({'quizz_mark': self._get_mark(value_suggested)})
+ return super(SurveyUserInputLine, self).write(vals)
+
+ @api.model
+ def save_lines(self, user_input_id, question, post, answer_tag):
+ """ Save answers to questions, depending on question type
+
+ If an answer already exists for question and user_input_id, it will be
+ overwritten (in order to maintain data consistency).
+ """
+ try:
+ saver = getattr(self, 'save_line_' + question.question_type)
+ except AttributeError:
+ _logger.error(question.question_type + ": This type of question has no saving function")
+ return False
+ else:
+ saver(user_input_id, question, post, answer_tag)
+
+ @api.model
+ def save_line_free_text(self, user_input_id, question, post, answer_tag):
+ vals = {
+ 'user_input_id': user_input_id,
+ 'question_id': question.id,
+ 'survey_id': question.survey_id.id,
+ 'skipped': False,
+ }
+ if answer_tag in post and post[answer_tag].strip():
+ vals.update({'answer_type': 'free_text', 'value_free_text': post[answer_tag]})
+ else:
+ vals.update({'answer_type': None, 'skipped': True})
+ old_uil = self.search([
+ ('user_input_id', '=', user_input_id),
+ ('survey_id', '=', question.survey_id.id),
+ ('question_id', '=', question.id)
+ ])
+ if old_uil:
+ old_uil.write(vals)
+ else:
+ old_uil.create(vals)
+ return True
+
+ @api.model
+ def save_line_textbox(self, user_input_id, question, post, answer_tag):
+ vals = {
+ 'user_input_id': user_input_id,
+ 'question_id': question.id,
+ 'survey_id': question.survey_id.id,
+ 'skipped': False
+ }
+ if answer_tag in post and post[answer_tag].strip():
+ vals.update({'answer_type': 'text', 'value_text': post[answer_tag]})
+ else:
+ vals.update({'answer_type': None, 'skipped': True})
+ old_uil = self.search([
+ ('user_input_id', '=', user_input_id),
+ ('survey_id', '=', question.survey_id.id),
+ ('question_id', '=', question.id)
+ ])
+ if old_uil:
+ old_uil.write(vals)
+ else:
+ old_uil.create(vals)
+ return True
+
+ @api.model
+ def save_line_numerical_box(self, user_input_id, question, post, answer_tag):
+ vals = {
+ 'user_input_id': user_input_id,
+ 'question_id': question.id,
+ 'survey_id': question.survey_id.id,
+ 'skipped': False
+ }
+ if answer_tag in post and post[answer_tag].strip():
+ vals.update({'answer_type': 'number', 'value_number': float(post[answer_tag])})
+ else:
+ vals.update({'answer_type': None, 'skipped': True})
+ old_uil = self.search([
+ ('user_input_id', '=', user_input_id),
+ ('survey_id', '=', question.survey_id.id),
+ ('question_id', '=', question.id)
+ ])
+ if old_uil:
+ old_uil.write(vals)
+ else:
+ old_uil.create(vals)
+ return True
+
+ @api.model
+ def save_line_date(self, user_input_id, question, post, answer_tag):
+ vals = {
+ 'user_input_id': user_input_id,
+ 'question_id': question.id,
+ 'survey_id': question.survey_id.id,
+ 'skipped': False
+ }
+ if answer_tag in post and post[answer_tag].strip():
+ vals.update({'answer_type': 'date', 'value_date': post[answer_tag]})
+ else:
+ vals.update({'answer_type': None, 'skipped': True})
+ old_uil = self.search([
+ ('user_input_id', '=', user_input_id),
+ ('survey_id', '=', question.survey_id.id),
+ ('question_id', '=', question.id)
+ ])
+ if old_uil:
+ old_uil.write(vals)
+ else:
+ old_uil.create(vals)
+ return True
+
+ @api.model
+ def save_line_simple_choice(self, user_input_id, question, post, answer_tag):
+ vals = {
+ 'user_input_id': user_input_id,
+ 'question_id': question.id,
+ 'survey_id': question.survey_id.id,
+ 'skipped': False
+ }
+ old_uil = self.search([
+ ('user_input_id', '=', user_input_id),
+ ('survey_id', '=', question.survey_id.id),
+ ('question_id', '=', question.id)
+ ])
+ old_uil.sudo().unlink()
+
+ if answer_tag in post and post[answer_tag].strip():
+ vals.update({'answer_type': 'suggestion', 'value_suggested': post[answer_tag]})
+ else:
+ vals.update({'answer_type': None, 'skipped': True})
+
+ # '-1' indicates 'comment count as an answer' so do not need to record it
+ if post.get(answer_tag) and post.get(answer_tag) != '-1':
+ self.create(vals)
+
+ comment_answer = post.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
+ if comment_answer:
+ vals.update({'answer_type': 'text', 'value_text': comment_answer, 'skipped': False, 'value_suggested': False})
+ self.create(vals)
+
+ return True
+
+ @api.model
+ def save_line_multiple_choice(self, user_input_id, question, post, answer_tag):
+ vals = {
+ 'user_input_id': user_input_id,
+ 'question_id': question.id,
+ 'survey_id': question.survey_id.id,
+ 'skipped': False
+ }
+ old_uil = self.search([
+ ('user_input_id', '=', user_input_id),
+ ('survey_id', '=', question.survey_id.id),
+ ('question_id', '=', question.id)
+ ])
+ old_uil.sudo().unlink()
+
+ ca_dict = dict_keys_startswith(post, answer_tag + '_')
+ comment_answer = ca_dict.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
+ if len(ca_dict) > 0:
+ for key in ca_dict:
+ # '-1' indicates 'comment count as an answer' so do not need to record it
+ if key != ('%s_%s' % (answer_tag, '-1')):
+ vals.update({'answer_type': 'suggestion', 'value_suggested': ca_dict[key]})
+ self.create(vals)
+ if comment_answer:
+ vals.update({'answer_type': 'text', 'value_text': comment_answer, 'value_suggested': False})
+ self.create(vals)
+ if not ca_dict and not comment_answer:
+ vals.update({'answer_type': None, 'skipped': True})
+ self.create(vals)
+ return True
+
+ @api.model
+ def save_line_matrix(self, user_input_id, question, post, answer_tag):
+ vals = {
+ 'user_input_id': user_input_id,
+ 'question_id': question.id,
+ 'survey_id': question.survey_id.id,
+ 'skipped': False
+ }
+ old_uil = self.search([
+ ('user_input_id', '=', user_input_id),
+ ('survey_id', '=', question.survey_id.id),
+ ('question_id', '=', question.id)
+ ])
+ old_uil.sudo().unlink()
+
+ no_answers = True
+ ca_dict = dict_keys_startswith(post, answer_tag + '_')
+
+ comment_answer = ca_dict.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
+ if comment_answer:
+ vals.update({'answer_type': 'text', 'value_text': comment_answer})
+ self.create(vals)
+ no_answers = False
+
+ if question.matrix_subtype == 'simple':
+ for row in question.labels_ids_2:
+ a_tag = "%s_%s" % (answer_tag, row.id)
+ if a_tag in ca_dict:
+ no_answers = False
+ vals.update({'answer_type': 'suggestion', 'value_suggested': ca_dict[a_tag], 'value_suggested_row': row.id})
+ self.create(vals)
+
+ elif question.matrix_subtype == 'multiple':
+ for col in question.labels_ids:
+ for row in question.labels_ids_2:
+ a_tag = "%s_%s_%s" % (answer_tag, row.id, col.id)
+ if a_tag in ca_dict:
+ no_answers = False
+ vals.update({'answer_type': 'suggestion', 'value_suggested': col.id, 'value_suggested_row': row.id})
+ self.create(vals)
+ if no_answers:
+ vals.update({'answer_type': None, 'skipped': True})
+ self.create(vals)
+ return True
diff --git a/addons/survey/views/assets.xml b/addons/survey/views/assets.xml
new file mode 100644
index 00000000000..099877c34d6
--- /dev/null
+++ b/addons/survey/views/assets.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+<data>
+ <!-- survey assets -->
+ <template id="survey_assets" name="Survey Results assets">
+ <script src="/web/static/lib/nvd3/d3.v3.js"></script>
+ <script src="/web/static/lib/nvd3/nv.d3.js"></script>
+ <script type="text/javascript" src="/web/static/src/js/services/session.js"></script>
+ <script type="text/javascript" src="/survey/static/src/js/survey_result.js" />
+ <script type="text/javascript" src="/survey/static/src/js/survey.js" />
+ <script type="text/javascript" src="/survey/static/src/js/tour_test_survey.js"></script>
+ <script type="text/javascript" src="/web/static/src/js/fields/field_utils.js"></script>
+
+ <t t-call="web._assets_frontend_helpers"/>
+ <link href="/web/static/lib/nvd3/nv.d3.css" rel="stylesheet" type="text/css"></link>
+ <t t-call="web._assets_bootstrap"/>
+ <link rel="stylesheet" type="text/less" href="/web/static/src/scss/fonts.scss" />
+ <!-- Some helper classes (like mt- mb-) are required for survey templates -->
+ <link rel="stylesheet" type="text/less" href="/web_editor/static/src/scss/web_editor.common.scss"/>
+ <link href="/survey/static/src/css/survey_print.css" rel="stylesheet" type="text/css"/>
+ <link href="/survey/static/src/css/survey_result.css" rel="stylesheet" type="text/css"></link>
+ </template>
+
+ <template id="assets_backend" name="survey assets" inherit_id="web.assets_backend">
+ <xpath expr="." position="inside">
+ <link rel="stylesheet" href="/survey/static/src/css/survey_result.css"/>
+ </xpath>
+ </template>
+</data>
+</odoo> \ No newline at end of file
diff --git a/addons/survey/views/survey_menus.xml b/addons/survey/views/survey_menus.xml
new file mode 100644
index 00000000000..5ec28117b59
--- /dev/null
+++ b/addons/survey/views/survey_menus.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+<data>
+ <!-- Main menu -->
+ <menuitem name="Surveys" id="menu_surveys" sequence="70" groups="group_survey_user" web_icon="survey,static/description/icon.png"/>
+
+ <!-- Parent menus -->
+ <menuitem name="Configuration" id="menu_surveys_configuration" parent="menu_surveys" groups="base.group_no_one" sequence="100"/>
+</data>
+</odoo> \ No newline at end of file
diff --git a/addons/survey/views/survey_page_views.xml b/addons/survey/views/survey_page_views.xml
new file mode 100644
index 00000000000..2f1ae7411fb
--- /dev/null
+++ b/addons/survey/views/survey_page_views.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+<data>
+ <!-- PAGES -->
+ <record model="ir.ui.view" id="survey_page_form">
+ <field name="name">Form view for survey page</field>
+ <field name="model">survey.page</field>
+ <field name="arch" type="xml">
+ <form string="Survey Page" create="false">
+ <sheet>
+ <div class="oe_title" style="width: 100%;">
+ <label for="title" class="oe_edit_only"/>
+ <h1><field name="title" placeholder="Page Title"/></h1>
+ </div>
+ <field name="question_ids" nolabel="1" mode="tree" context="{'default_page_id': active_id}">
+ <tree>
+ <field name="sequence" widget="handle"/>
+ <field name="question"/>
+ <field name="question_type"/>
+ <field name="constr_mandatory"/>
+ </tree>
+ </field>
+ </sheet>
+ </form>
+ </field>
+ </record>
+ <record model="ir.ui.view" id="survey_page_tree">
+ <field name="name">Tree view for survey page</field>
+ <field name="model">survey.page</field>
+ <field name="arch" type="xml">
+ <tree string="Survey Page" create="false">
+ <field name="sequence" widget="handle"/>
+ <field name="title"/>
+ <field name="survey_id"/>
+ <field name="question_ids" string="#Questions"/>
+ </tree>
+ </field>
+ </record>
+ <record model="ir.ui.view" id="survey_page_search" >
+ <field name="name">Search view for survey page</field>
+ <field name="model">survey.page</field>
+ <field name="arch" type="xml">
+ <search string="Search Page">
+ <field name="title" string="Page"/>
+ <field name="survey_id" string="Survey"/>
+ <group expand="0" string="Group By">
+ <filter name="group_by_survey" string="Survey" domain="[]" context="{'group_by':'survey_id'}"/>
+ </group>
+ </search>
+ </field>
+ </record>
+
+ <record model="ir.actions.act_window" id="action_survey_page_form">
+ <field name="name">Pages</field>
+ <field name="res_model">survey.page</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">tree,form</field>
+ <field name="search_view_id" ref="survey_page_search"/>
+ <field name="context">{'search_default_group_by_survey': True}</field>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_empty_folder">
+ No pages found
+ </p>
+ </field>
+ </record>
+
+ <act_window context="{'search_default_page_id': active_id, 'default_page_id': active_id}" id="act_survey_page_question" name="Questions" res_model="survey.question" src_model="survey.page"/>
+
+ <menuitem name="Pages" id="menu_survey_page_form1" action="action_survey_page_form" parent="menu_surveys_configuration" sequence="1"/>
+</data>
+</odoo> \ No newline at end of file
diff --git a/addons/survey/views/survey_question_views.xml b/addons/survey/views/survey_question_views.xml
new file mode 100644
index 00000000000..b66fc391558
--- /dev/null
+++ b/addons/survey/views/survey_question_views.xml
@@ -0,0 +1,228 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+<data>
+ <!-- QUESTIONS -->
+ <record model="ir.ui.view" id="survey_question_form">
+ <field name="name">Form view for survey question</field>
+ <field name="model">survey.question</field>
+ <field name="arch" type="xml">
+ <form string="Survey Question" create="false">
+ <sheet>
+ <div class="oe_title" style="width: 100%;">
+ <label for="question" string="Question name" class="oe_edit_only"/>
+ <h1><field name="question" colspan="4" placeholder="Question Name"/></h1>
+ <separator />
+ </div>
+ <group>
+ <group>
+ <field name="question_type" widget="radio" />
+ </group>
+ <group>
+ <div class="col-lg-6 offset-lg-3 o_preview_questions">
+ <!-- Multiple Lines Text Zone -->
+ <div attrs="{'invisible': [('question_type', '!=', 'free_text')]}">
+ <i class="fa fa-align-justify fa-4x" role="img" aria-label="Multiple lines" title="Multiple Lines"/>
+ </div>
+ <!-- Single Line Text Zone -->
+ <div attrs="{'invisible': [('question_type', '!=', 'textbox')]}">
+ <i class="fa fa-minus fa-4x" role="img" aria-label="Single Line" title="Single Line"/>
+ </div>
+ <!-- Numerical Value -->
+ <div attrs="{'invisible': [('question_type', '!=', 'numerical_box')]}">
+ <i class="fa fa-2x" role="img" aria-label="Numeric" title="Numeric">123..</i>
+ </div>
+ <!-- Date and Time -->
+ <div attrs="{'invisible': [('question_type', '!=', 'date')]}">
+ <p class="o_datetime">YYYY-MM-DD
+ <i class="fa fa-calendar fa-2x" role="img" aria-label="Calendar" title="Calendar"/>
+ </p>
+ </div>
+ <!-- Multiple choice: only one answer -->
+ <div attrs="{'invisible': [('question_type', '!=', 'simple_choice')]}" role="img" aria-label="Multiple choice with one answer" title="Multiple choice with one answer">
+ <div class="row"><i class="fa fa-circle-o fa-lg"/> answer</div>
+ <div class="row"><i class="fa fa-dot-circle-o fa-lg"/> answer</div>
+ <div class="row"><i class="fa fa-circle-o fa-lg"/> answer</div>
+ </div>
+ <!-- Multiple choice: multiple answers allowed -->
+ <div attrs="{'invisible': [('question_type', '!=', 'multiple_choice')]}" role="img" aria-label="Multiple choice with multiple answers" title="Multiple choice with multiple answers">
+ <div class="row"><i class="fa fa-square-o fa-lg"/> answer</div>
+ <div class="row"><i class="fa fa-check-square-o fa-lg"/> answer</div>
+ <div class="row"><i class="fa fa-square-o fa-lg"/> answer</div>
+ </div>
+ <!-- Matrix -->
+ <div attrs="{'invisible': [('question_type', '!=', 'matrix')]}">
+ <div class="row o_matrix_head">
+ <div class="col-lg-3"></div>
+ <div class="col-lg-3">ans</div>
+ <div class="col-lg-3">ans</div>
+ <div class="col-lg-3">ans</div>
+ </div>
+ <div class="row o_matrix_row">
+ <div class="col-lg-3">Row1</div>
+ <div class="col-lg-3"><i class="fa fa-circle-o fa-lg" role="img" aria-label="Not checked" title="Not checked"/></div>
+ <div class="col-lg-3"><i class="fa fa-dot-circle-o fa-lg" role="img" aria-label="Checked" title="Checked"/></div>
+ <div class="col-lg-3"><i class="fa fa-circle-o fa-lg" role="img" aria-label="Not checked" title="Not checked"/></div>
+ </div>
+ <div class="row o_matrix_row">
+ <div class="col-lg-3">Row2</div>
+ <div class="col-lg-3"><i class="fa fa-circle-o fa-lg" role="img" aria-label="Not checked" title="Not checked"/></div>
+ <div class="col-lg-3"><i class="fa fa-circle-o fa-lg" role="img" aria-label="Not checked" title="Not checked"/></div>
+ <div class="col-lg-3"><i class="fa fa-dot-circle-o fa-lg" role="img" aria-label="Checked" title="Checked"/></div>
+ </div>
+ <div class="row o_matrix_row">
+ <div class="col-lg-3">Row3</div>
+ <div class="col-lg-3"><i class="fa fa-dot-circle-o fa-lg" role="img" aria-label="Checked" title="Checked"/></div>
+ <div class="col-lg-3"><i class="fa fa-circle-o fa-lg" role="img" aria-label="Not checked" title="Not checked"/></div>
+ <div class="col-lg-3"><i class="fa fa-circle-o fa-lg" role="img" aria-label="Not checked" title="Not checked"/></div>
+ </div>
+ </div>
+ </div>
+ </group>
+ </group>
+ <notebook>
+ <page string="Answers">
+ <field name="validation_email" attrs="{'invisible': [('question_type', '!=', 'textbox')]}"/>
+ <label for="validation_email" attrs="{'invisible': [('question_type', '!=', 'textbox')]}"/>
+ <separator />
+ <field name="labels_ids" string="Type of answers" context="{'default_question_id': active_id}" attrs="{'invisible': [('question_type', 'not in', ['simple_choice', 'multiple_choice', 'matrix'])]}">
+ <tree editable="bottom">
+ <field name="sequence" widget="handle"/>
+ <field name="value" string="Choices"/>
+ <field name="quizz_mark" groups="base.group_no_one"/>
+ </tree>
+ </field>
+ <separator />
+ <label for="labels_ids_2" attrs="{'invisible': [('question_type', '!=', 'matrix')]}" />
+ <field name="labels_ids_2" context="{'default_question_id_2': active_id}" attrs="{'invisible': [('question_type', '!=', 'matrix')]}">
+ <tree editable="bottom">
+ <field name="sequence" widget="handle"/>
+ <field name="value" string="Rows"/>
+ </tree>
+ </field>
+ </page>
+ <page string="Options">
+ <group string="Constraints">
+ <group colspan="2" col="4">
+ <field name="constr_mandatory" string="Mandatory Answer"/>
+ <field name="constr_error_msg" attrs="{'invisible': [('constr_mandatory', '=', False)]}"/>
+ </group>
+ <div colspan="2" attrs="{'invisible': [('question_type', 'not in', ['textbox', 'numerical_box', 'date'])]}">
+ <group>
+ <field name="validation_required" attrs="{'invisible': [('validation_email', '=', True), ('question_type', '=', 'textbox')]}"/>
+ </group>
+ <group col="4" attrs="{'invisible': [('validation_required', '=', False)]}">
+ <field name="validation_length_min" attrs="{'invisible': [('question_type', '!=', 'textbox')]}"/>
+ <field name="validation_length_max" attrs="{'invisible': [('question_type', '!=', 'textbox')]}"/>
+ <field name="validation_min_float_value" attrs="{'invisible': [('question_type', '!=', 'numerical_box')]}"/>
+ <field name="validation_max_float_value" attrs="{'invisible': [('question_type', '!=', 'numerical_box')]}"/>
+ <field name="validation_min_date" attrs="{'invisible': [('question_type', '!=', 'date')]}"/>
+ <field name="validation_max_date" attrs="{'invisible': [('question_type', '!=', 'date')]}"/>
+ <field name="validation_error_msg" colspan="4"/>
+ </group>
+ </div>
+ <group>
+ <field name="matrix_subtype" attrs="{'invisible':[('question_type','not in',['matrix'])]}"/>
+ </group>
+ </group>
+ <group string="Display mode" attrs="{'invisible':[('question_type','not in',['simple_choice', 'multiple_choice'])]}">
+ <field name="display_mode" string="Format" attrs="{'invisible':[('question_type','not in',['simple_choice'])],'required':[('question_type','=','simple_choice')]}"/>
+ <field name="column_nb" string="Number of columns" attrs="{'invisible':[('display_mode','=','dropdown'), ('question_type','=','simple_choice')]}"/>
+ </group>
+ <group string="Allow Comments" attrs="{'invisible':[('question_type','not in',['simple_choice','multiple_choice', 'matrix'])]}">
+ <field name='comments_allowed' />
+ <field name='comments_message' attrs="{'invisible': [('comments_allowed', '=', False)]}"/>
+ <field name='comment_count_as_answer' attrs="{'invisible': ['|', ('comments_allowed', '=', False), ('question_type', 'in', ['matrix'])]}" />
+ </group>
+ </page>
+ </notebook>
+ </sheet>
+ </form>
+ </field>
+ </record>
+ <record model="ir.ui.view" id="survey_question_tree">
+ <field name="name">Tree view for survey question</field>
+ <field name="model">survey.question</field>
+ <field name="arch" type="xml">
+ <tree string="Survey Question" create="false">
+ <field name="sequence" widget="handle"/>
+ <field name="question"/>
+ <field name="page_id"/>
+ <field name="survey_id"/>
+ <field name="question_type"/>
+ </tree>
+ </field>
+ </record>
+ <record model="ir.ui.view" id="survey_question_search">
+ <field name="name">Search view for survey question</field>
+ <field name="model">survey.question</field>
+ <field name="arch" type="xml">
+ <search string="Search Question">
+ <field name="question" string="Question"/>
+ <field name="survey_id" string="Survey"/>
+ <field name="page_id" string="Page"/>
+ <field name="question_type" string="Type"/>
+ <group expand="1" string="Group By">
+ <filter name="group_by_page" string="Page" domain="[]" context="{'group_by':'page_id'}"/>
+ <filter name="group_by_type" string="Type" domain="[]" context="{'group_by':'question_type'}"/>
+ </group>
+ </search>
+ </field>
+ </record>
+
+ <record model="ir.actions.act_window" id="action_survey_question_form">
+ <field name="name">Questions</field>
+ <field name="res_model">survey.question</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">tree,form</field>
+ <field name="search_view_id" ref="survey_question_search"/>
+ <field name="context">{'search_default_group_by_page': True}</field>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_empty_folder">
+ No questions found
+ </p>
+ </field>
+ </record>
+
+ <!-- LABELS -->
+ <record model="ir.ui.view" id="survey_label_tree">
+ <field name="name">survey_label_tree</field>
+ <field name="model">survey.label</field>
+ <field name="arch" type="xml">
+ <tree string="Survey Label" create="false">
+ <field name="sequence" widget="handle"/>
+ <field name="question_id"/>
+ <field name="question_id_2"/>
+ <field name="value"/>
+ <field name="quizz_mark" groups="base.group_no_one"/>
+ </tree>
+ </field>
+ </record>
+ <record id="survey_label_search" model="ir.ui.view">
+ <field name="name">survey_label_search</field>
+ <field name="model">survey.label</field>
+ <field name="arch" type="xml">
+ <search string="Search Label">
+ <field name="question_id" string="Question"/>
+ <filter name="group_by_question" string="Question" domain="[]" context="{'group_by':'question_id'}"/>
+ </search>
+ </field>
+ </record>
+
+ <record model="ir.actions.act_window" id="action_survey_label_form">
+ <field name="name">Labels</field>
+ <field name="res_model">survey.label</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">tree,form</field>
+ <field name="search_view_id" ref="survey_label_search"/>
+ <field name="context">{'search_default_group_by_question': True}</field>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_empty_folder">
+ No survey labels found
+ </p>
+ </field>
+ </record>
+
+ <menuitem name="Questions" id="menu_survey_question_form1" action="action_survey_question_form" parent="menu_surveys_configuration" sequence="2"/>
+ <menuitem name="Labels" id="menu_survey_label_form1" action="action_survey_label_form" parent="menu_surveys_configuration" sequence="3"/>
+</data>
+</odoo> \ No newline at end of file
diff --git a/addons/survey/views/survey_result.xml b/addons/survey/views/survey_result.xml
deleted file mode 100644
index 38e784e1e52..00000000000
--- a/addons/survey/views/survey_result.xml
+++ /dev/null
@@ -1,346 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-<data>
- <template id="result" name="Survey Result">
- <t t-call="survey.layout">
- <div class="oe_structure" id="oe_structure_survey_result_1"/>
- <div class="container js_surveyresult">
- <t t-call="survey.back" />
- <div class="jumbotron mt32">
- <h1><span t-field="survey.title" /> <span style="font-size:1.5em;" class="fa fa-bar-chart-o float-right " role="img" aria-label="Chart" title="Chart"/></h1>
- <div t-field="survey.description" class="oe_no_empty" />
- </div>
- <div class="card d-print-none">
- <div class="card-header"><span class="fa fa-filter"></span> Filters <span t-if="filter_display_data" class="float-right text-primary clear_survey_filter"><i class="fa fa-times"></i> Clear All Filters</span></div>
- <div class="card-body">
- <span t-if="filter_finish == True">
- <span class="badge badge-secondary only_left_radius filter-all">All surveys</span><span class="badge badge-primary only_right_radius filter-finished">Finished surveys</span>
- </span>
- <span t-if="filter_finish == False">
- <span class="badge badge-primary only_left_radius filter-all">All surveys</span><span class="badge badge-secondary only_right_radius filter-finished">Finished surveys</span>
- </span>
- <span t-foreach="filter_display_data" t-as="filter_data">
- <span class="badge badge-primary only_left_radius"><i class="fa fa-filter" role="img" aria-label="Filter" title="Filter"></i></span><span class="badge badge-primary no_radius" t-esc="filter_data['question_text']"></span><span class="badge badge-success only_right_radius" t-esc="' > '.join(filter_data['labels'])"></span>
- </span>
- </div>
- </div>
- <div t-foreach="survey_dict['page_ids']" t-as='page_ids'>
- <t t-set="page" t-value="page_ids['page']"/>
- <h1 class="mt16" t-field='page.title'></h1>
- <div t-field="page.description" class="oe_no_empty" />
- <hr/>
- <div t-foreach="page_ids['question_ids']" t-as='question_ids' class="mt16">
- <t t-set="input_summary" t-value="question_ids['input_summary']"/>
- <t t-set="question" t-value="question_ids['question']"/>
- <t t-set="graph_data" t-value="question_ids['graph_data']"/>
- <t t-set="prepare_result" t-value="question_ids['prepare_result']"/>
- <h4>
- <b>Question </b>
- <span t-field='question.question'></span>
- <t t-if="question.question_type == 'matrix'">
- <small><span class="badge badge-secondary">Matrix: <span t-field='question.matrix_subtype'/></span></small>
- </t>
- <t t-if="question.question_type in ['simple_choice', 'multiple_choice']">
- <small><span t-field='question.question_type' class="badge badge-secondary"></span></small>
- </t>
- <span class="float-right">
- <span class="badge badge-success"><span t-esc="input_summary['answered']"></span> Answered</span>
- <span class="badge badge-danger"><span t-esc="input_summary['skipped']"></span> Skipped</span>
- </span>
- </h4>
- <t t-if="input_summary['answered'] != 0">
- <t t-if="question.description">
- <div class="text-muted oe_no_empty" t-field="question.description" />
- </t>
- <t t-if="question.question_type in ['textbox', 'free_text', 'date']">
- <t t-call="survey.result_text"></t>
- </t>
- <t t-if="question.question_type in ['simple_choice', 'multiple_choice']">
- <t t-call="survey.result_choice"></t>
- </t>
- <t t-if="question.question_type == 'matrix'">
- <t t-call="survey.result_matrix"></t>
- </t>
- <t t-if="question.question_type == 'numerical_box'">
- <t t-call="survey.result_number"></t>
- </t>
- </t>
- <t t-if="input_summary['answered'] == 0">
- <h2 style="padding-top:30px;padding-bottom:30px;text-align:center;" class="text-muted">Sorry, No one answered this question.</h2>
- </t>
- </div>
- </div>
- </div>
- <div class="oe_structure" id="oe_structure_survey_result_2"/>
- </t>
- </template>
-
- <!-- Result for free_text,textbox and date -->
- <template id="result_text" name="Text Result">
- <table class="table table-hover table-sm" t-att-id="'table_question_%d' % question.id">
- <thead>
- <tr>
- <th>#</th>
- <th>User Responses</th>
- </tr>
- </thead>
- <tbody>
- <t t-set="text_result" t-value="prepare_result"/>
- <tr t-foreach="text_result" t-as="user_input">
- <td><a t-att-href="'%s/%s' % (user_input.user_input_id.print_url, user_input.user_input_id.token)"><t t-esc="user_input_index + 1"></t></a></td>
- <t t-if="question.question_type == 'free_text'">
- <td>
- <a t-att-href="'%s/%s' % (user_input.user_input_id.print_url, user_input.user_input_id.token)" t-field="user_input.value_free_text"></a><br/>
- </td>
- </t>
- <t t-if="question.question_type == 'textbox'">
- <td>
- <a t-att-href="'%s/%s' % (user_input.user_input_id.print_url, user_input.user_input_id.token)" t-field="user_input.value_text"></a><br/>
- </td>
- </t>
- <t t-if="question.question_type == 'date'">
- <td>
- <a t-att-href="'%s/%s' % (user_input.user_input_id.print_url, user_input.user_input_id.token)" t-field="user_input.value_date"></a><br/>
- </td>
- </t>
- </tr>
- </tbody>
- </table>
- <t t-call="survey.pagination" />
- </template>
-
- <!-- Result for comments -->
- <template id="result_comments" name="Text Result">
- <!-- a 'comments' variable must be set an must contain a list of browse records of user input lines -->
- <table class="table table-hover table-sm" t-att-id="'table_question_%d' % question.id">
- <thead>
- <tr>
- <th>#</th>
- <th>Comment</th>
- </tr>
- </thead>
- <tbody>
- <tr t-foreach="comments" t-as="user_input">
- <td><a t-att-href="'%s/%s' % (user_input.user_input_id.print_url, user_input.user_input_id.token)"><t t-esc="user_input_index + 1"></t></a></td>
- <td>
- <span t-field="user_input.value_text"></span><br/>
- </td>
- </tr>
- </tbody>
- </table>
- </template>
-
-
- <!-- Result for simple_choice and multiple_choice -->
- <template id="result_choice" name="Choice Result">
- <div>
- <!-- Tabs -->
- <ul class="nav nav-tabs d-print-none" role="tablist">
- <li class="nav-item" t-if="question.question_type != 'simple_choice'">
- <a t-att-href="'#graph_question_%d' % question.id" t-att-aria-controls="'graph_question_%d' % question.id" class="nav-link active" data-toggle="tab" role="tab">
- <i class="fa fa-bar-chart-o"></i> Graph
- </a>
- </li>
- <li class="nav-item" t-if="question.question_type == 'simple_choice'">
- <a t-att-href="'#graph_question_%d' % question.id" t-att-aria-controls="'graph_question_%d' % question.id" class="nav-link active" data-toggle="tab" role="tab">
- <i class="fa fa-bar-chart-o"></i> Pie Chart
- </a>
- </li>
- <li class="nav-item">
- <a t-att-href="'#data_question_%d' % question.id" t-att-aria-controls="'data_question_%d' % question.id" class="nav-link" data-toggle="tab" role="tab">
- <i class="fa fa-list-alt"></i> Data
- </a>
- </li>
- </ul>
- <div class="tab-content">
- <div role="tabpanel" class="tab-pane active survey_graph" t-if="question.question_type != 'simple_choice'" t-att-id="'graph_question_%d' % question.id" t-att-data-question_id="question.id" data-graph_type="bar" t-att-graph-data="graph_data">
- <!-- svg element for drawing bar chart -->
- <svg style="height:20em"></svg>
- </div>
- <div role="tabpanel" class="tab-pane active survey_graph" t-if="question.question_type == 'simple_choice'" t-att-id="'graph_question_%d' % question.id" t-att-data-question_id="question.id" data-graph_type="pie" t-att-graph-data="graph_data">
- <!-- svg element for drawing pie chart -->
- <svg style="height:20em"></svg>
- </div>
- <div role="tabpanel" class="tab-pane" t-att-id="'data_question_%d' % question.id">
- <table class="table table-hover table-sm">
- <thead>
- <tr>
- <th>Answer Choices</th>
- <th>User Responses</th>
- </tr>
- </thead>
- <tbody>
- <tr t-foreach="prepare_result['answers']" t-as="user_input">
- <td>
- <p t-esc="user_input['text']"></p>
- </td>
- <td class="survey_answer">
- <span t-esc="round(user_input['count']*100.0/(input_summary['answered'] or 1),2)"></span> %
- <span t-esc="user_input['count']" class="badge badge-primary">Vote</span>
- <i class="fa fa-filter text-primary invisible survey_filter" t-att-data-question_id="question.id" t-att-data-answer_id="user_input['answer_id']" role="img" aria-label="Filter question" title="Filter question"/>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- </div>
- <!-- handle comments -->
- <div>
- <t t-set="comments" t-value="prepare_result['comments']" />
- <t t-if="comments">
- <t t-call="survey.result_comments" />
- </t>
- </div>
-
- </div>
- </template>
-
- <!-- Result for matrix -->
- <template id="result_matrix" name="Matrix Result">
- <t t-set="matrix_result" t-value="prepare_result"/>
- <!-- Tabs -->
- <ul class="nav nav-tabs d-print-none" role="tablist">
- <li class="nav-item">
- <a t-att-href="'#graph_question_%d' % question.id" t-att-aria-controls="'graph_question_%d' % question.id" class="nav-link active" data-toggle="tab" role="tab">
- <i class="fa fa-bar-chart"></i>
- Graph
- </a>
- </li>
- <li class="nav-item">
- <a t-att-href="'#data_question_%d' % question.id" t-att-aria-controls="'data_question_%d' % question.id" class="nav-link" data-toggle="tab" role="tab">
- <i class="fa fa-list-alt"></i>
- Data
- </a>
- </li>
- </ul>
- <div class="tab-content">
- <div role="tabpanel" class="tab-pane active with-3d-shadow with-transitions survey_graph" t-att-id="'graph_question_%d' % question.id" t-att-data-question_id= "question.id" data-graph_type= "multi_bar" t-att-graph-data="graph_data">
- <!-- svg element for drawing Multibar chart -->
- <svg style="height:20em"></svg>
- </div>
- <div role="tabpanel" class="tab-pane" t-att-id="'data_question_%d' % question.id">
- <table class="table table-hover table-sm text-right">
- <thead>
- <tr>
- <th></th>
- <th class="text-right" t-foreach="matrix_result['answers']" t-as="answer_id">
- <span t-esc="matrix_result['answers'][answer_id]"></span>
- </th>
- </tr>
- </thead>
- <tbody>
- <tr t-foreach="matrix_result['rows']" t-as="row_id">
- <td>
- <span t-esc="matrix_result['rows'][row_id]"></span>
- </td>
- <td class="survey_answer" t-foreach="matrix_result['answers']" t-as="answer_id">
- <span t-esc="round(matrix_result['result'][(row_id,answer_id)]*100.0/(input_summary['answered'] or 1),2)"></span> %
- <span class="badge badge-primary" t-esc="matrix_result['result'][(row_id,answer_id)]"></span><i class="fa fa-filter text-primary invisible survey_filter" t-att-data-question_id="question.id" t-att-data-row_id="row_id" t-att-data-answer_id="answer_id" role="img" aria-label="Survey filter" title="Survey filter"></i>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!-- handle comments to matrix -->
- <div>
- <t t-set="comments" t-value="matrix_result['comments']" />
- <t t-if="comments">
- <t t-call="survey.result_comments" />
- </t>
- </div>
- </div>
- </template>
-
- <!-- Result for Numeric Data -->
- <template id="result_number" name="Number Result">
- <t t-set="number_result" t-value="prepare_result"/>
- <t t-set="text_result" t-value="number_result['input_lines']" />
- <span class="float-right mt8">
- <span class="badge badge-secondary only_left_radius">Sum </span> <span class="badge badge-info only_right_radius" t-esc="number_result['sum']"></span>
- <span class="badge badge-secondary only_left_radius">Maximum </span> <span class="badge badge-success only_right_radius" t-esc="number_result['max']"></span>
- <span class="badge badge-secondary only_left_radius">Minimum </span> <span class="badge badge-danger only_right_radius" t-esc="number_result['min']"></span>
- <span class="badge badge-secondary only_left_radius">Average </span> <span class="badge badge-warning only_right_radius" t-esc="number_result['average']"></span>
- </span>
- <ul class="nav nav-tabs d-print-none" role="tablist">
- <li class="nav-item">
- <a t-att-href="'#most_common_%d' % question.id" t-att-aria-controls="'most_common_%d' % question.id" class="nav-link active" data-toggle="tab" role="tab">
- <i class="fa fa-list-ol"></i>
- Most Common <span t-esc="len(number_result['most_common'])"></span>
- </a>
- </li>
- <li class="nav-item">
- <a t-att-href="'#data_question_%d' % question.id" t-att-aria-controls="'data_question_%d' % question.id" class="nav-link" data-toggle="tab" role="tab">
- <i class="fa fa-list-alt"></i>
- All Data
- </a>
- </li>
- </ul>
- <div class="tab-content">
- <div role="tabpanel" class="tab-pane active with-3d-shadow with-transitions" t-att-id="'most_common_%d' % question.id">
- <table class="table table-hover table-sm">
- <thead>
- <tr>
- <th>User Responses</th>
- <th>Occurence</th>
- </tr>
- </thead>
- <tbody>
- <tr t-foreach="number_result['most_common']" t-as="row">
- <td>
- <span t-esc="row[0]"></span>
- </td>
- <td>
- <span t-esc="row[1]"></span>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <div role="tabpanel" class="tab-pane" t-att-id="'data_question_%d' % question.id">
- <table class="table table-hover table-sm" t-att-id="'table_question_%d' % question.id">
- <thead>
- <tr>
- <th>#</th>
- <th>User Responses</th>
- </tr>
- </thead>
- <tbody>
- <tr class="d-none" t-foreach="number_result['input_lines']" t-as="user_input">
- <td><a t-att-href="'%s/%s' % (user_input.user_input_id.print_url, user_input.user_input_id.token)"><t t-esc="user_input_index + 1"></t></a></td>
- <td><span t-field="user_input.value_number"></span><br/></td>
- </tr>
- </tbody>
- </table>
- <t t-call="survey.pagination"/>
- </div>
- </div>
- </template>
- <template id="no_result" name="Survey Result">
- <t t-call="survey.layout">
- <div class="container">
- <t t-call="survey.back" />
- <h2><span t-field='survey.title'></span></h2>
- <div class="jumbotron mt32">
- <h2 class="text-muted">
- <span>
- <i style="font-size:1.8em" class="fa fa-users float-right" role="img" aria-label="No answer" title="No answer"></i>
- </span>
- Sorry, No one answered this survey yet
- </h2>
- </div>
- </div>
- </t>
- </template>
- <!-- Pagination Element -->
- <template id="pagination" name="Survey Result">
- <t t-set="record_limit" t-value="10"/><!-- Change This record_limit to change number of record per page-->
- <ul t-att-id="'pagination_%d' % question.id" class="pagination" t-att-data-question_id="question.id" t-att-data-record_limit="record_limit">
- <t t-if="len(text_result) > record_limit">
- <li t-foreach="page_range(len(text_result), record_limit)" t-as="num" class="page-item">
- <a href="#" class="page-link" t-esc="num"></a>
- </li>
- </t>
- </ul>
- </template>
-</data>
-</odoo>
diff --git a/addons/survey/views/survey_stage_views.xml b/addons/survey/views/survey_stage_views.xml
new file mode 100644
index 00000000000..f536981b260
--- /dev/null
+++ b/addons/survey/views/survey_stage_views.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+<data>
+ <record model="ir.ui.view" id="survey_stage_form">
+ <field name="name">Form view for survey stage</field>
+ <field name="model">survey.stage</field>
+ <field name="arch" type="xml">
+ <form string="Stage">
+ <group col='4' colspan='4'>
+ <field name="name" />
+ <field name="closed" />
+ <field name="sequence" />
+ <field name="fold" />
+ </group>
+ </form>
+ </field>
+ </record>
+</data>
+</odoo> \ No newline at end of file
diff --git a/addons/survey/views/survey_survey_views.xml b/addons/survey/views/survey_survey_views.xml
new file mode 100644
index 00000000000..6e10e60398f
--- /dev/null
+++ b/addons/survey/views/survey_survey_views.xml
@@ -0,0 +1,182 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+<data>
+ <record model="ir.ui.view" id="survey_form">
+ <field name="name">Form view for survey</field>
+ <field name="model">survey.survey</field>
+ <field name="arch" type="xml">
+ <form string="Survey">
+ <field name="id" invisible="1"/>
+ <field name="tot_start_survey" invisible="1"/>
+ <field name="is_closed" invisible="1"/>
+ <header groups="survey.group_survey_manager">
+ <button name="action_test_survey" string="Test Survey" type="object" attrs="{'invisible': [('id', '=', False)]}"/>
+ <button name="action_print_survey" string="Print Survey" type="object" attrs="{'invisible': [('id', '=', False)]}"/>
+ <button name="action_send_survey" string="Share and invite by email" type="object" class="oe_highlight" attrs="{'invisible': [('id', '=', False)]}"/>
+ <button name="action_result_survey" string="View results" type="object" attrs="{'invisible': ['|',('id', '=', False), ('tot_start_survey', '!=', 0)]}"/>
+ <button name="action_result_survey" string="View results" type="object" class="oe_highlight" attrs="{'invisible': [('tot_start_survey', '=', 0)]}"/>
+ <field name="stage_id" widget="statusbar" options="{'clickable': '1'}"/>
+ </header>
+ <sheet>
+ <div class="oe_button_box" name="button_box">
+ <button name="action_survey_user_input"
+ type="object"
+ class="oe_stat_button"
+ icon="fa-pencil-square-o">
+ <field string="Answers" name="tot_comp_survey" widget="statinfo"/>
+ </button>
+ <button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive" attrs="{'invisible': [('is_closed', '=', False)]}">
+ <field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
+ </button>
+ </div>
+ <div class="oe_title" style="width: 100%;">
+ <label for="title" class="oe_edit_only"/>
+ <h1><field name="title" placeholder="Survey Title"/></h1>
+ </div>
+ <notebook>
+ <page string="Edit Pages and Questions">
+ <field name="page_ids" mode="tree" context="{'default_survey_id': active_id}" nolabel="1">
+ <tree>
+ <field name="sequence" widget="handle"/>
+ <field name="title"/>
+ <field name="question_ids"/>
+ </tree>
+ </field>
+ </page>
+ <page string="Options">
+ <group class="o_label_nowrap">
+ <field name="users_can_go_back" string="User can come back in the previous page"/>
+ <field name="auth_required"/>
+ <field name="quizz_mode" groups="base.group_no_one"/>
+ </group>
+ </page>
+ </notebook>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers"/>
+ <field name="activity_ids" widget="mail_activity"/>
+ <field name="message_ids" widget="mail_thread"/>
+ </div>
+ </form>
+ </field>
+ </record>
+ <record model="ir.ui.view" id= "survey_tree">
+ <field name="name">Tree view for survey</field>
+ <field name="model">survey.survey</field>
+ <field name="arch" type="xml">
+ <tree string="Survey">
+ <field name="active" invisible="1"/>
+ <field name="title" />
+ <field name="stage_id" />
+ <field name="tot_sent_survey" string="Invitations sent" />
+ <field name="tot_start_survey" string="Started" />
+ <field name="tot_comp_survey" string="Completed" />
+ </tree>
+ </field>
+ </record>
+ <record model="ir.ui.view" id="survey_kanban">
+ <field name="name">Kanban view for survey</field>
+ <field name="model">survey.survey</field>
+ <field name="arch" type="xml">
+ <kanban default_group_by="stage_id">
+ <field name="stage_id" />
+ <field name="designed" />
+ <field name="title" />
+ <field name="tot_sent_survey" />
+ <field name="tot_start_survey" />
+ <field name="tot_comp_survey" />
+ <field name="color" />
+ <field name="auth_required" />
+ <field name="public_url" />
+ <field name="activity_ids" />
+ <field name="activity_state" />
+ <templates>
+ <div t-name="kanban-box" t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click">
+ <div class="o_dropdown_kanban dropdown" t-if="widget.editable">
+
+ <a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
+ <span class="fa fa-ellipsis-v"/>
+ </a>
+ <div class="dropdown-menu" role="menu">
+ <a role="menuitem" type="edit" class="dropdown-item">Edit Survey</a>
+ <a t-if="widget.deletable" role="menuitem" type="delete" class="dropdown-item">Delete</a>
+ <div role="separator" class="dropdown-divider"/>
+ <div role="separator" class="dropdown-item-text">Color</div>
+ <ul class="oe_kanban_colorpicker" data-field="color"/>
+ </div>
+ </div>
+ <div class="o_kanban_record_top">
+ <h4 class="o_kanban_record_title mb4"><field name="title" /></h4>
+ </div>
+ <div class="o_kanban_record_bottom">
+ <ul class="list-unstyled">
+ <li id="survey_test">
+ <t t-if="record.designed.raw_value"><a type="object" name="action_test_survey">Test</a></t>
+ <t t-if="! record.designed.raw_value"><a style="color: #aaaaaa;">Test</a></t>
+ </li>
+ <li>
+ <t t-if="record.designed.raw_value"><a type="object" name="action_send_survey">Share &amp;amp; Invite</a></t>
+ <t t-if="! record.designed.raw_value"><a style="color: #aaaaaa;">Share &amp;amp; Invite</a></t>
+ </li>
+ <li>
+ <t t-if="record.tot_start_survey.raw_value &gt; 0"><a name="action_result_survey" type="object">Analyze Answers</a> <span t-if="record.tot_start_survey.raw_value &gt; 0">(<field name="tot_start_survey" />)</span></t>
+ <t t-if="record.tot_start_survey.raw_value &lt;= 0"><a style="color: #aaaaaa;">Analyze Answers</a></t>
+ </li>
+ </ul>
+ </div>
+ <div class="o_kanban_record_bottom">
+ <div class="oe_kanban_bottom_left"/>
+ <div class="oe_kanban_bottom_right">
+ <field name="activity_ids" widget="kanban_activity" />
+ </div>
+ </div>
+ </div>
+ </templates>
+ </kanban>
+ </field>
+ </record>
+ <record id="survey_survey_view_search" model="ir.ui.view">
+ <field name="name">survey.survey.search</field>
+ <field name="model">survey.survey</field>
+ <field name="arch" type="xml">
+ <search string="Survey">
+ <field string="Title" name="title"/>
+ <filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
+ <separator/>
+ <filter string="My Activities" name="activities_my"
+ domain="[('activity_ids.user_id', '=', uid)]"/>
+ <separator/>
+ <filter string="Late Activities" name="activities_overdue"
+ domain="[('activity_ids.date_deadline', '&lt;', context_today().strftime('%Y-%m-%d'))]"
+ help="Show all records which has next action date is before today"/>
+ <filter string="Today Activities" name="activities_today"
+ domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
+ <filter string="Upcoming Activities" name="activities_upcoming_all"
+ domain="[('activity_ids.date_deadline', '&gt;', context_today().strftime('%Y-%m-%d'))
+ ]"/>
+ </search>
+ </field>
+ </record>
+
+ <record model="ir.actions.act_window" id="action_survey_form">
+ <field name="name">Surveys</field>
+ <field name="res_model">survey.survey</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">kanban,tree,form,activity</field>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_smiling_face">
+ Add a new survey
+ </p><p>
+ You can create surveys for different purposes: customer opinion, services feedback, recruitment interviews, employee's periodical evaluations, marketing campaigns, etc.
+ </p><p>
+ Design easily your survey, send invitations to answer by email and analyse answers.
+ </p>
+ </field>
+ </record>
+ <act_window context="{'search_default_survey_id': [active_id], 'default_survey_id': active_id}" id="act_survey_pages" name="Pages" res_model="survey.page" src_model="survey.survey"/>
+ <act_window context="{'search_default_survey_id': [active_id], 'default_survey_id': active_id}" id="act_survey_question" name="Questions" res_model="survey.question" src_model="survey.survey"/>
+
+ <menuitem name="Surveys" id="menu_survey_form" action="action_survey_form" parent="menu_surveys" sequence="1"/>
+
+</data>
+</odoo>
diff --git a/addons/survey/views/survey_templates.xml b/addons/survey/views/survey_templates.xml
index ff34950df09..2b438713c8e 100644
--- a/addons/survey/views/survey_templates.xml
+++ b/addons/survey/views/survey_templates.xml
@@ -1,26 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
- <!-- survey assets -->
- <template id="survey_assets" name="Survey Results assets">
- <script src="/web/static/lib/nvd3/d3.v3.js"></script>
- <script src="/web/static/lib/nvd3/nv.d3.js"></script>
- <script type="text/javascript" src="/web/static/src/js/services/session.js"></script>
- <script type="text/javascript" src="/survey/static/src/js/survey_result.js" />
- <script type="text/javascript" src="/survey/static/src/js/survey.js" />
- <script type="text/javascript" src="/survey/static/src/js/tour_test_survey.js"></script>
- <script type="text/javascript" src="/web/static/src/js/fields/field_utils.js"></script>
-
- <t t-call="web._assets_frontend_helpers"/>
- <link href="/web/static/lib/nvd3/nv.d3.css" rel="stylesheet" type="text/css"></link>
- <t t-call="web._assets_bootstrap"/>
- <link rel="stylesheet" type="text/less" href="/web/static/src/scss/fonts.scss" />
- <!-- Some helper classes (like mt- mb-) are required for survey templates -->
- <link rel="stylesheet" type="text/less" href="/web_editor/static/src/scss/web_editor.common.scss"/>
- <link href="/survey/static/src/css/survey_print.css" rel="stylesheet" type="text/css"/>
- <link href="/survey/static/src/css/survey_result.css" rel="stylesheet" type="text/css"></link>
- </template>
-
<!-- Forbidden error messages-->
<template id="403">
<t t-call="survey.layout">
@@ -379,5 +359,346 @@
</div>
</t>
</template>
+
+ <template id="result" name="Survey Result">
+ <t t-call="survey.layout">
+ <div class="oe_structure" id="oe_structure_survey_result_1"/>
+ <div class="container js_surveyresult">
+ <t t-call="survey.back" />
+ <div class="jumbotron mt32">
+ <h1><span t-field="survey.title" /> <span style="font-size:1.5em;" class="fa fa-bar-chart-o float-right " role="img" aria-label="Chart" title="Chart"/></h1>
+ <div t-field="survey.description" class="oe_no_empty" />
+ </div>
+ <div class="card d-print-none">
+ <div class="card-header"><span class="fa fa-filter"></span> Filters <span t-if="filter_display_data" class="float-right text-primary clear_survey_filter"><i class="fa fa-times"></i> Clear All Filters</span></div>
+ <div class="card-body">
+ <span t-if="filter_finish == True">
+ <span class="badge badge-secondary only_left_radius filter-all">All surveys</span><span class="badge badge-primary only_right_radius filter-finished">Finished surveys</span>
+ </span>
+ <span t-if="filter_finish == False">
+ <span class="badge badge-primary only_left_radius filter-all">All surveys</span><span class="badge badge-secondary only_right_radius filter-finished">Finished surveys</span>
+ </span>
+ <span t-foreach="filter_display_data" t-as="filter_data">
+ <span class="badge badge-primary only_left_radius"><i class="fa fa-filter" role="img" aria-label="Filter" title="Filter"></i></span><span class="badge badge-primary no_radius" t-esc="filter_data['question_text']"></span><span class="badge badge-success only_right_radius" t-esc="' > '.join(filter_data['labels'])"></span>
+ </span>
+ </div>
+ </div>
+ <div t-foreach="survey_dict['page_ids']" t-as='page_ids'>
+ <t t-set="page" t-value="page_ids['page']"/>
+ <h1 class="mt16" t-field='page.title'></h1>
+ <div t-field="page.description" class="oe_no_empty" />
+ <hr/>
+ <div t-foreach="page_ids['question_ids']" t-as='question_ids' class="mt16">
+ <t t-set="input_summary" t-value="question_ids['input_summary']"/>
+ <t t-set="question" t-value="question_ids['question']"/>
+ <t t-set="graph_data" t-value="question_ids['graph_data']"/>
+ <t t-set="prepare_result" t-value="question_ids['prepare_result']"/>
+ <h4>
+ <b>Question </b>
+ <span t-field='question.question'></span>
+ <t t-if="question.question_type == 'matrix'">
+ <small><span class="badge badge-secondary">Matrix: <span t-field='question.matrix_subtype'/></span></small>
+ </t>
+ <t t-if="question.question_type in ['simple_choice', 'multiple_choice']">
+ <small><span t-field='question.question_type' class="badge badge-secondary"></span></small>
+ </t>
+ <span class="float-right">
+ <span class="badge badge-success"><span t-esc="input_summary['answered']"></span> Answered</span>
+ <span class="badge badge-danger"><span t-esc="input_summary['skipped']"></span> Skipped</span>
+ </span>
+ </h4>
+ <t t-if="input_summary['answered'] != 0">
+ <t t-if="question.description">
+ <div class="text-muted oe_no_empty" t-field="question.description" />
+ </t>
+ <t t-if="question.question_type in ['textbox', 'free_text', 'date']">
+ <t t-call="survey.result_text"></t>
+ </t>
+ <t t-if="question.question_type in ['simple_choice', 'multiple_choice']">
+ <t t-call="survey.result_choice"></t>
+ </t>
+ <t t-if="question.question_type == 'matrix'">
+ <t t-call="survey.result_matrix"></t>
+ </t>
+ <t t-if="question.question_type == 'numerical_box'">
+ <t t-call="survey.result_number"></t>
+ </t>
+ </t>
+ <t t-if="input_summary['answered'] == 0">
+ <h2 style="padding-top:30px;padding-bottom:30px;text-align:center;" class="text-muted">Sorry, No one answered this question.</h2>
+ </t>
+ </div>
+ </div>
+ </div>
+ <div class="oe_structure" id="oe_structure_survey_result_2"/>
+ </t>
+ </template>
+
+ <!-- Result for free_text,textbox and date -->
+ <template id="result_text" name="Text Result">
+ <table class="table table-hover table-sm" t-att-id="'table_question_%d' % question.id">
+ <thead>
+ <tr>
+ <th>#</th>
+ <th>User Responses</th>
+ </tr>
+ </thead>
+ <tbody>
+ <t t-set="text_result" t-value="prepare_result"/>
+ <tr t-foreach="text_result" t-as="user_input">
+ <td><a t-att-href="'%s/%s' % (user_input.user_input_id.print_url, user_input.user_input_id.token)"><t t-esc="user_input_index + 1"></t></a></td>
+ <t t-if="question.question_type == 'free_text'">
+ <td>
+ <a t-att-href="'%s/%s' % (user_input.user_input_id.print_url, user_input.user_input_id.token)" t-field="user_input.value_free_text"></a><br/>
+ </td>
+ </t>
+ <t t-if="question.question_type == 'textbox'">
+ <td>
+ <a t-att-href="'%s/%s' % (user_input.user_input_id.print_url, user_input.user_input_id.token)" t-field="user_input.value_text"></a><br/>
+ </td>
+ </t>
+ <t t-if="question.question_type == 'date'">
+ <td>
+ <a t-att-href="'%s/%s' % (user_input.user_input_id.print_url, user_input.user_input_id.token)" t-field="user_input.value_date"></a><br/>
+ </td>
+ </t>
+ </tr>
+ </tbody>
+ </table>
+ <t t-call="survey.pagination" />
+ </template>
+
+ <!-- Result for comments -->
+ <template id="result_comments" name="Text Result">
+ <!-- a 'comments' variable must be set an must contain a list of browse records of user input lines -->
+ <table class="table table-hover table-sm" t-att-id="'table_question_%d' % question.id">
+ <thead>
+ <tr>
+ <th>#</th>
+ <th>Comment</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr t-foreach="comments" t-as="user_input">
+ <td><a t-att-href="'%s/%s' % (user_input.user_input_id.print_url, user_input.user_input_id.token)"><t t-esc="user_input_index + 1"></t></a></td>
+ <td>
+ <span t-field="user_input.value_text"></span><br/>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </template>
+
+ <!-- Result for simple_choice and multiple_choice -->
+ <template id="result_choice" name="Choice Result">
+ <div>
+ <!-- Tabs -->
+ <ul class="nav nav-tabs d-print-none" role="tablist">
+ <li class="nav-item" t-if="question.question_type != 'simple_choice'">
+ <a t-att-href="'#graph_question_%d' % question.id" t-att-aria-controls="'graph_question_%d' % question.id" class="nav-link active" data-toggle="tab" role="tab">
+ <i class="fa fa-bar-chart-o"></i> Graph
+ </a>
+ </li>
+ <li class="nav-item" t-if="question.question_type == 'simple_choice'">
+ <a t-att-href="'#graph_question_%d' % question.id" t-att-aria-controls="'graph_question_%d' % question.id" class="nav-link active" data-toggle="tab" role="tab">
+ <i class="fa fa-bar-chart-o"></i> Pie Chart
+ </a>
+ </li>
+ <li class="nav-item">
+ <a t-att-href="'#data_question_%d' % question.id" t-att-aria-controls="'data_question_%d' % question.id" class="nav-link" data-toggle="tab" role="tab">
+ <i class="fa fa-list-alt"></i> Data
+ </a>
+ </li>
+ </ul>
+ <div class="tab-content">
+ <div role="tabpanel" class="tab-pane active survey_graph" t-if="question.question_type != 'simple_choice'" t-att-id="'graph_question_%d' % question.id" t-att-data-question_id="question.id" data-graph_type="bar" t-att-graph-data="graph_data">
+ <!-- svg element for drawing bar chart -->
+ <svg style="height:20em"></svg>
+ </div>
+ <div role="tabpanel" class="tab-pane active survey_graph" t-if="question.question_type == 'simple_choice'" t-att-id="'graph_question_%d' % question.id" t-att-data-question_id="question.id" data-graph_type="pie" t-att-graph-data="graph_data">
+ <!-- svg element for drawing pie chart -->
+ <svg style="height:20em"></svg>
+ </div>
+ <div role="tabpanel" class="tab-pane" t-att-id="'data_question_%d' % question.id">
+ <table class="table table-hover table-sm">
+ <thead>
+ <tr>
+ <th>Answer Choices</th>
+ <th>User Responses</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr t-foreach="prepare_result['answers']" t-as="user_input">
+ <td>
+ <p t-esc="user_input['text']"></p>
+ </td>
+ <td class="survey_answer">
+ <span t-esc="round(user_input['count']*100.0/(input_summary['answered'] or 1),2)"></span> %
+ <span t-esc="user_input['count']" class="badge badge-primary">Vote</span>
+ <i class="fa fa-filter text-primary invisible survey_filter" t-att-data-question_id="question.id" t-att-data-answer_id="user_input['answer_id']" role="img" aria-label="Filter question" title="Filter question"/>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ <!-- handle comments -->
+ <div>
+ <t t-set="comments" t-value="prepare_result['comments']" />
+ <t t-if="comments">
+ <t t-call="survey.result_comments" />
+ </t>
+ </div>
+
+ </div>
+ </template>
+
+ <!-- Result for matrix -->
+ <template id="result_matrix" name="Matrix Result">
+ <t t-set="matrix_result" t-value="prepare_result"/>
+ <!-- Tabs -->
+ <ul class="nav nav-tabs d-print-none" role="tablist">
+ <li class="nav-item">
+ <a t-att-href="'#graph_question_%d' % question.id" t-att-aria-controls="'graph_question_%d' % question.id" class="nav-link active" data-toggle="tab" role="tab">
+ <i class="fa fa-bar-chart"></i>
+ Graph
+ </a>
+ </li>
+ <li class="nav-item">
+ <a t-att-href="'#data_question_%d' % question.id" t-att-aria-controls="'data_question_%d' % question.id" class="nav-link" data-toggle="tab" role="tab">
+ <i class="fa fa-list-alt"></i>
+ Data
+ </a>
+ </li>
+ </ul>
+ <div class="tab-content">
+ <div role="tabpanel" class="tab-pane active with-3d-shadow with-transitions survey_graph" t-att-id="'graph_question_%d' % question.id" t-att-data-question_id= "question.id" data-graph_type= "multi_bar" t-att-graph-data="graph_data">
+ <!-- svg element for drawing Multibar chart -->
+ <svg style="height:20em"></svg>
+ </div>
+ <div role="tabpanel" class="tab-pane" t-att-id="'data_question_%d' % question.id">
+ <table class="table table-hover table-sm text-right">
+ <thead>
+ <tr>
+ <th></th>
+ <th class="text-right" t-foreach="matrix_result['answers']" t-as="answer_id">
+ <span t-esc="matrix_result['answers'][answer_id]"></span>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr t-foreach="matrix_result['rows']" t-as="row_id">
+ <td>
+ <span t-esc="matrix_result['rows'][row_id]"></span>
+ </td>
+ <td class="survey_answer" t-foreach="matrix_result['answers']" t-as="answer_id">
+ <span t-esc="round(matrix_result['result'][(row_id,answer_id)]*100.0/(input_summary['answered'] or 1),2)"></span> %
+ <span class="badge badge-primary" t-esc="matrix_result['result'][(row_id,answer_id)]"></span><i class="fa fa-filter text-primary invisible survey_filter" t-att-data-question_id="question.id" t-att-data-row_id="row_id" t-att-data-answer_id="answer_id" role="img" aria-label="Survey filter" title="Survey filter"></i>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!-- handle comments to matrix -->
+ <div>
+ <t t-set="comments" t-value="matrix_result['comments']" />
+ <t t-if="comments">
+ <t t-call="survey.result_comments" />
+ </t>
+ </div>
+ </div>
+ </template>
+
+ <!-- Result for Numeric Data -->
+ <template id="result_number" name="Number Result">
+ <t t-set="number_result" t-value="prepare_result"/>
+ <t t-set="text_result" t-value="number_result['input_lines']" />
+ <span class="float-right mt8">
+ <span class="badge badge-secondary only_left_radius">Sum </span> <span class="badge badge-info only_right_radius" t-esc="number_result['sum']"></span>
+ <span class="badge badge-secondary only_left_radius">Maximum </span> <span class="badge badge-success only_right_radius" t-esc="number_result['max']"></span>
+ <span class="badge badge-secondary only_left_radius">Minimum </span> <span class="badge badge-danger only_right_radius" t-esc="number_result['min']"></span>
+ <span class="badge badge-secondary only_left_radius">Average </span> <span class="badge badge-warning only_right_radius" t-esc="number_result['average']"></span>
+ </span>
+ <ul class="nav nav-tabs d-print-none" role="tablist">
+ <li class="nav-item">
+ <a t-att-href="'#most_common_%d' % question.id" t-att-aria-controls="'most_common_%d' % question.id" class="nav-link active" data-toggle="tab" role="tab">
+ <i class="fa fa-list-ol"></i>
+ Most Common <span t-esc="len(number_result['most_common'])"></span>
+ </a>
+ </li>
+ <li class="nav-item">
+ <a t-att-href="'#data_question_%d' % question.id" t-att-aria-controls="'data_question_%d' % question.id" class="nav-link" data-toggle="tab" role="tab">
+ <i class="fa fa-list-alt"></i>
+ All Data
+ </a>
+ </li>
+ </ul>
+ <div class="tab-content">
+ <div role="tabpanel" class="tab-pane active with-3d-shadow with-transitions" t-att-id="'most_common_%d' % question.id">
+ <table class="table table-hover table-sm">
+ <thead>
+ <tr>
+ <th>User Responses</th>
+ <th>Occurence</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr t-foreach="number_result['most_common']" t-as="row">
+ <td>
+ <span t-esc="row[0]"></span>
+ </td>
+ <td>
+ <span t-esc="row[1]"></span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div role="tabpanel" class="tab-pane" t-att-id="'data_question_%d' % question.id">
+ <table class="table table-hover table-sm" t-att-id="'table_question_%d' % question.id">
+ <thead>
+ <tr>
+ <th>#</th>
+ <th>User Responses</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr class="d-none" t-foreach="number_result['input_lines']" t-as="user_input">
+ <td><a t-att-href="'%s/%s' % (user_input.user_input_id.print_url, user_input.user_input_id.token)"><t t-esc="user_input_index + 1"></t></a></td>
+ <td><span t-field="user_input.value_number"></span><br/></td>
+ </tr>
+ </tbody>
+ </table>
+ <t t-call="survey.pagination"/>
+ </div>
+ </div>
+ </template>
+ <template id="no_result" name="Survey Result">
+ <t t-call="survey.layout">
+ <div class="container">
+ <t t-call="survey.back" />
+ <h2><span t-field='survey.title'></span></h2>
+ <div class="jumbotron mt32">
+ <h2 class="text-muted">
+ <span>
+ <i style="font-size:1.8em" class="fa fa-users float-right" role="img" aria-label="No answer" title="No answer"></i>
+ </span>
+ Sorry, No one answered this survey yet
+ </h2>
+ </div>
+ </div>
+ </t>
+ </template>
+ <!-- Pagination Element -->
+ <template id="pagination" name="Survey Result">
+ <t t-set="record_limit" t-value="10"/><!-- Change This record_limit to change number of record per page-->
+ <ul t-att-id="'pagination_%d' % question.id" class="pagination" t-att-data-question_id="question.id" t-att-data-record_limit="record_limit">
+ <t t-if="len(text_result) > record_limit">
+ <li t-foreach="page_range(len(text_result), record_limit)" t-as="num" class="page-item">
+ <a href="#" class="page-link" t-esc="num"></a>
+ </li>
+ </t>
+ </ul>
+ </template>
</data>
</odoo>
diff --git a/addons/survey/views/survey_user_views.xml b/addons/survey/views/survey_user_views.xml
new file mode 100644
index 00000000000..1e35a182e06
--- /dev/null
+++ b/addons/survey/views/survey_user_views.xml
@@ -0,0 +1,222 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+<data>
+ <!-- USER INPUTS -->
+ <record id="survey_user_input_search" model="ir.ui.view">
+ <field name="name">survey_user_input_search</field>
+ <field name="model">survey.user_input</field>
+ <field name="arch" type="xml">
+ <search string="Search Survey">
+ <field name="survey_id"/>
+ <field name="email"/>
+ <field name="partner_id"/>
+ <filter name="completed" string="Completed" domain="[('state', '=', 'done')]"/>
+ <filter string="Partially Completed" name="partially_completed" domain="[('state', '=', 'skip')]"/>
+ <filter string="New" name="new" domain="[('state', '=', 'new')]"/>
+ <separator/>
+ <filter string="Test Entries" name="test" domain="[('test_entry','=', True)]"/>
+ <group expand="0" string="Group By">
+ <filter name="group_by_survey" string="Survey" domain="[]" context="{'group_by': 'survey_id'}"/>
+ <filter string="Email" name="group_by_email" domain="[]" context="{'group_by': 'email'}"/>
+ <filter string="Partner" name="group_by_partner" domain="[]" context="{'group_by': 'partner_id'}"/>
+ </group>
+ </search>
+ </field>
+ </record>
+ <record model="ir.ui.view" id="survey_user_input_form">
+ <field name="name">survey_user_input_form</field>
+ <field name="model">survey.user_input</field>
+ <field name="arch" type="xml">
+ <form string="Survey User inputs" create="false">
+ <header>
+ <button name="action_survey_resend" string="Send Invitation Again" type="object" class="oe_highlight" attrs="{'invisible': ['|', ('input_type','=','manually'), ('state', '=', 'done')]}"/>
+ <button name="action_view_answers" states="done" string="Print These Answers" type="object" class="oe_highlight" />
+ <button name="action_survey_results" string="View Results" type="object" />
+ <field name="state" widget="statusbar"/>
+ </header>
+ <sheet>
+ <div class="oe_button_box" name="button_box"/>
+ <group col="2">
+ <group>
+ <field name="survey_id"/>
+ <field name="date_create"/>
+ <field name="input_type"/>
+ <field name="token" groups="base.group_no_one"/>
+ </group>
+ <group>
+ <field name="deadline"/>
+ <field name="partner_id"/>
+ <field name="email" widget="email"/>
+ <field name="test_entry" groups="base.group_no_one"/>
+ <field name="quizz_score" groups="base.group_no_one"/>
+ </group>
+ </group>
+ <field name="user_input_line_ids" mode="tree" attrs="{'readonly': False}">
+ <tree>
+ <field name="page_id"/>
+ <field name="question_id"/>
+ <field name="answer_type"/>
+ <field name="skipped"/>
+ <field name="date_create"/>
+ <field name="quizz_mark" groups="base.group_no_one"/>
+ </tree>
+ </field>
+ </sheet>
+ </form>
+ </field>
+ </record>
+ <record model="ir.ui.view" id="survey_user_input_tree">
+ <field name="name">survey_user_input_tree</field>
+ <field name="model">survey.user_input</field>
+ <field name="arch" type="xml">
+ <tree string="Survey User inputs" decoration-muted="test_entry == True" create="false">
+ <field name="survey_id"/>
+ <field name="date_create"/>
+ <field name="deadline"/>
+ <field name="partner_id"/>
+ <field name="email"/>
+ <field name="input_type"/>
+ <field name="state"/>
+ <field name="test_entry" invisible="True"/>
+ <field name="quizz_score" groups="base.group_no_one"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="view_survey_user_input_kanban" model="ir.ui.view">
+ <field name="name">survey.user_input.kanban</field>
+ <field name="model">survey.user_input</field>
+ <field name="arch" type="xml">
+ <kanban create="false">
+ <field name="survey_id"/>
+ <field name="date_create"/>
+ <field name="partner_id"/>
+ <field name="email"/>
+ <field name="input_type"/>
+ <field name="state"/>
+ <templates>
+ <t t-name="kanban-box">
+ <div t-attf-class="oe_kanban_global_click">
+ <div class="o_kanban_record_top">
+ <div class="o_kanban_record_headings">
+ <strong class="o_kanban_record_title"><t t-esc="record.survey_id.value"/></strong>
+ </div>
+ <span class="badge badge-pill"><t t-esc="record.input_type.value"/></span>
+ </div>
+ <div class="o_kanban_record_bottom">
+ <div class="oe_kanban_bottom_left">
+ <field name="date_create"/>
+ </div>
+ <div class="oe_kanban_bottom_right mr4">
+ <field name="state" widget="kanban_label_selection" options="{'classes': {'new': 'default', 'done': 'success', 'skip':'warning'}}"/>
+ </div>
+ </div>
+ </div>
+ </t>
+ </templates>
+ </kanban>
+ </field>
+ </record>
+
+ <record model="ir.actions.act_window" id="action_survey_user_input">
+ <field name="name">Answers</field>
+ <field name="res_model">survey.user_input</field>
+ <field name="view_mode">tree,kanban,form</field>
+ <field name="view_id" ref="survey_user_input_tree"></field>
+ <field name="search_view_id" ref="survey_user_input_search"/>
+ <field name="context">{'search_default_group_by_survey': True}</field>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_empty_folder">
+ Nobody has replied to your surveys yet
+ </p>
+ </field>
+ </record>
+ <record id="action_selected_survey_user_input" model="ir.actions.act_window">
+ <field name="name">Survey User input</field>
+ <field name="res_model">survey.user_input</field>
+ <field name="view_mode">tree,form</field>
+ <field name="view_id" ref="survey_user_input_tree"></field>
+ <field name="search_view_id" ref="survey_user_input_search"/>
+ <field name="context">{'search_default_survey_id': active_id, 'default_survey_id': active_id}</field>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_empty_folder">
+ Nobody has replied to your surveys yet
+ </p>
+ </field>
+ </record>
+
+ <!-- USER INPUT LINES
+ .. note:: these views are useful mainly for technical users/administrators -->
+ <record model="ir.ui.view" id="survey_user_input_line_form">
+ <field name="name">survey_user_input_line_form</field>
+ <field name="model">survey.user_input_line</field>
+ <field name="arch" type="xml">
+ <form string="User input line details" create="false">
+ <sheet>
+ <group col="4">
+ <field name="question_id"/>
+ <field name="date_create"/>
+ <field name="answer_type"/>
+ <field name="skipped" />
+ <field name="quizz_mark" groups="base.group_no_one"/>
+ </group>
+ <group>
+ <field name="value_text" colspan='2' attrs="{'invisible': [('answer_type','!=','text')]}"/>
+ <field name="value_number" colspan='2' attrs="{'invisible': [('answer_type','!=','number')]}"/>
+ <field name="value_date" colspan='2' attrs="{'invisible': [('answer_type','!=','date')]}"/>
+ <field name="value_free_text" colspan='2' attrs="{'invisible': [('answer_type','!=','free_text')]}"/>
+ <field name="value_suggested_row" colspan='2' />
+ <field name="value_suggested" colspan='2' attrs="{'invisible': [('answer_type','!=','suggestion')]}"/>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+ <record model="ir.ui.view" id="survey_response_line_tree">
+ <field name="name">survey_response_line_tree</field>
+ <field name="model">survey.user_input_line</field>
+ <field name="arch" type="xml">
+ <tree string="Survey Answer Line" create="false">
+ <field name="survey_id"/>
+ <field name="user_input_id"/>
+ <field name="page_id"/>
+ <field name="question_id"/>
+ <field name="date_create"/>
+ <field name="answer_type"/>
+ <field name="skipped"/>
+ <field name="quizz_mark" groups="base.group_no_one"/>
+ </tree>
+ </field>
+ </record>
+ <record id="survey_response_line_search" model="ir.ui.view">
+ <field name="name">survey_response_line_search</field>
+ <field name="model">survey.user_input_line</field>
+ <field name="arch" type="xml">
+ <search string="Search User input lines">
+ <field name="user_input_id"/>
+ <field name="survey_id"/>
+ <group expand="1" string="Group By">
+ <filter name="group_by_survey" string="Survey" domain="[]" context="{'group_by':'survey_id'}"/>
+ <filter name="group_by_user_input" string="User Input" domain="[]" context="{'group_by':'user_input_id'}"/>
+ </group>
+ </search>
+ </field>
+ </record>
+
+ <record model="ir.actions.act_window" id="action_survey_user_input_line">
+ <field name="name">Survey User Input lines</field>
+ <field name="res_model">survey.user_input_line</field>
+ <field name="view_mode">tree,form</field>
+ <field name="search_view_id" ref="survey_response_line_search"/>
+ <field name="context">{'search_default_group_by_survey': True, 'search_default_group_by_user_input': True}</field>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_empty_folder">
+ No user input lines found
+ </p>
+ </field>
+ </record>
+
+ <menuitem name="Answers" id="menu_survey_type_form1" action="action_survey_user_input" parent="menu_surveys" sequence="1"/>
+ <menuitem name="User Input Lines" id="menu_survey_response_line_form" action="action_survey_user_input_line" parent="menu_surveys_configuration" sequence="4"/>
+</data>
+</odoo> \ No newline at end of file
diff --git a/addons/survey/views/survey_views.xml b/addons/survey/views/survey_views.xml
deleted file mode 100644
index e3e03fc96d8..00000000000
--- a/addons/survey/views/survey_views.xml
+++ /dev/null
@@ -1,732 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
- <data>
-
- <!-- USER INPUTS -->
- <record id="survey_user_input_search" model="ir.ui.view">
- <field name="name">survey_user_input_search</field>
- <field name="model">survey.user_input</field>
- <field name="arch" type="xml">
- <search string="Search Survey">
- <field name="survey_id"/>
- <field name="email"/>
- <field name="partner_id"/>
- <filter name="completed" string="Completed" domain="[('state', '=', 'done')]"/>
- <filter string="Partially Completed" name="partially_completed" domain="[('state', '=', 'skip')]"/>
- <filter string="New" name="new" domain="[('state', '=', 'new')]"/>
- <separator/>
- <filter string="Test Entries" name="test" domain="[('test_entry','=', True)]"/>
- <group expand="0" string="Group By">
- <filter name="group_by_survey" string="Survey" domain="[]" context="{'group_by': 'survey_id'}"/>
- <filter string="Email" name="group_by_email" domain="[]" context="{'group_by': 'email'}"/>
- <filter string="Partner" name="group_by_partner" domain="[]" context="{'group_by': 'partner_id'}"/>
- </group>
- </search>
- </field>
- </record>
- <record model="ir.ui.view" id="survey_user_input_form">
- <field name="name">survey_user_input_form</field>
- <field name="model">survey.user_input</field>
- <field name="arch" type="xml">
- <form string="Survey User inputs" create="false">
- <header>
- <button name="action_survey_resend" string="Send Invitation Again" type="object" class="oe_highlight" attrs="{'invisible': ['|', ('input_type','=','manually'), ('state', '=', 'done')]}"/>
- <button name="action_view_answers" states="done" string="Print These Answers" type="object" class="oe_highlight" />
- <button name="action_survey_results" string="View Results" type="object" />
- <field name="state" widget="statusbar"/>
- </header>
- <sheet>
- <div class="oe_button_box" name="button_box"/>
- <group col="2">
- <group>
- <field name="survey_id"/>
- <field name="date_create"/>
- <field name="input_type"/>
- <field name="token" groups="base.group_no_one"/>
- </group>
- <group>
- <field name="deadline"/>
- <field name="partner_id"/>
- <field name="email" widget="email"/>
- <field name="test_entry" groups="base.group_no_one"/>
- <field name="quizz_score" groups="base.group_no_one"/>
- </group>
- </group>
- <field name="user_input_line_ids" mode="tree" attrs="{'readonly': False}">
- <tree>
- <field name="page_id"/>
- <field name="question_id"/>
- <field name="answer_type"/>
- <field name="skipped"/>
- <field name="date_create"/>
- <field name="quizz_mark" groups="base.group_no_one"/>
- </tree>
- </field>
- </sheet>
- </form>
- </field>
- </record>
- <record model="ir.ui.view" id="survey_user_input_tree">
- <field name="name">survey_user_input_tree</field>
- <field name="model">survey.user_input</field>
- <field name="arch" type="xml">
- <tree string="Survey User inputs" decoration-muted="test_entry == True" create="false">
- <field name="survey_id"/>
- <field name="date_create"/>
- <field name="deadline"/>
- <field name="partner_id"/>
- <field name="email"/>
- <field name="input_type"/>
- <field name="state"/>
- <field name="test_entry" invisible="True"/>
- <field name="quizz_score" groups="base.group_no_one"/>
- </tree>
- </field>
- </record>
-
- <record id="view_survey_user_input_kanban" model="ir.ui.view">
- <field name="name">survey.user_input.kanban</field>
- <field name="model">survey.user_input</field>
- <field name="arch" type="xml">
- <kanban create="false">
- <field name="survey_id"/>
- <field name="date_create"/>
- <field name="partner_id"/>
- <field name="email"/>
- <field name="input_type"/>
- <field name="state"/>
- <templates>
- <t t-name="kanban-box">
- <div t-attf-class="oe_kanban_global_click">
- <div class="o_kanban_record_top">
- <div class="o_kanban_record_headings">
- <strong class="o_kanban_record_title"><t t-esc="record.survey_id.value"/></strong>
- </div>
- <span class="badge badge-pill"><t t-esc="record.input_type.value"/></span>
- </div>
- <div class="o_kanban_record_bottom">
- <div class="oe_kanban_bottom_left">
- <field name="date_create"/>
- </div>
- <div class="oe_kanban_bottom_right mr4">
- <field name="state" widget="kanban_label_selection" options="{'classes': {'new': 'default', 'done': 'success', 'skip':'warning'}}"/>
- </div>
- </div>
- </div>
- </t>
- </templates>
- </kanban>
- </field>
- </record>
-
- <record model="ir.actions.act_window" id="action_survey_user_input">
- <field name="name">Answers</field>
- <field name="res_model">survey.user_input</field>
- <field name="view_mode">tree,kanban,form</field>
- <field name="view_id" ref="survey_user_input_tree"></field>
- <field name="search_view_id" ref="survey_user_input_search"/>
- <field name="context">{'search_default_group_by_survey': True}</field>
- <field name="help" type="html">
- <p class="o_view_nocontent_empty_folder">
- Nobody has replied to your surveys yet
- </p>
- </field>
- </record>
- <record id="action_selected_survey_user_input" model="ir.actions.act_window">
- <field name="name">Survey User input</field>
- <field name="res_model">survey.user_input</field>
- <field name="view_mode">tree,form</field>
- <field name="view_id" ref="survey_user_input_tree"></field>
- <field name="search_view_id" ref="survey_user_input_search"/>
- <field name="context">{'search_default_survey_id': active_id, 'default_survey_id': active_id}</field>
- <field name="help" type="html">
- <p class="o_view_nocontent_empty_folder">
- Nobody has replied to your surveys yet
- </p>
- </field>
- </record>
-
- <!-- SURVEY -->
- <record model="ir.ui.view" id="survey_form">
- <field name="name">Form view for survey</field>
- <field name="model">survey.survey</field>
- <field name="arch" type="xml">
- <form string="Survey">
- <field name="id" invisible="1"/>
- <field name="tot_start_survey" invisible="1"/>
- <field name="is_closed" invisible="1"/>
- <header groups="survey.group_survey_manager">
- <button name="action_test_survey" string="Test Survey" type="object" attrs="{'invisible': [('id', '=', False)]}"/>
- <button name="action_print_survey" string="Print Survey" type="object" attrs="{'invisible': [('id', '=', False)]}"/>
- <button name="action_send_survey" string="Share and invite by email" type="object" class="oe_highlight" attrs="{'invisible': [('id', '=', False)]}"/>
- <button name="action_result_survey" string="View results" type="object" attrs="{'invisible': ['|',('id', '=', False), ('tot_start_survey', '!=', 0)]}"/>
- <button name="action_result_survey" string="View results" type="object" class="oe_highlight" attrs="{'invisible': [('tot_start_survey', '=', 0)]}"/>
- <field name="stage_id" widget="statusbar" options="{'clickable': '1'}"/>
- </header>
- <sheet>
- <div class="oe_button_box" name="button_box">
- <button name="action_survey_user_input"
- type="object"
- class="oe_stat_button"
- icon="fa-pencil-square-o">
- <field string="Answers" name="tot_comp_survey" widget="statinfo"/>
- </button>
- <button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive" attrs="{'invisible': [('is_closed', '=', False)]}">
- <field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
- </button>
- </div>
- <div class="oe_title" style="width: 100%;">
- <label for="title" class="oe_edit_only"/>
- <h1><field name="title" placeholder="Survey Title"/></h1>
- </div>
- <notebook>
- <page string="Edit Pages and Questions">
- <field name="page_ids" mode="tree" context="{'default_survey_id': active_id}" nolabel="1">
- <tree>
- <field name="sequence" widget="handle"/>
- <field name="title"/>
- <field name="question_ids"/>
- </tree>
- </field>
- </page>
- <page string="Options">
- <group class="o_label_nowrap">
- <field name="users_can_go_back" string="User can come back in the previous page"/>
- <field name="auth_required"/>
- <field name="quizz_mode" groups="base.group_no_one"/>
- </group>
- </page>
- </notebook>
- </sheet>
- <div class="oe_chatter">
- <field name="message_follower_ids" widget="mail_followers"/>
- <field name="activity_ids" widget="mail_activity"/>
- <field name="message_ids" widget="mail_thread"/>
- </div>
- </form>
- </field>
- </record>
- <record model="ir.ui.view" id= "survey_tree">
- <field name="name">Tree view for survey</field>
- <field name="model">survey.survey</field>
- <field name="arch" type="xml">
- <tree string="Survey">
- <field name="active" invisible="1"/>
- <field name="title" />
- <field name="stage_id" />
- <field name="tot_sent_survey" string="Invitations sent" />
- <field name="tot_start_survey" string="Started" />
- <field name="tot_comp_survey" string="Completed" />
- </tree>
- </field>
- </record>
- <record model="ir.ui.view" id="survey_kanban">
- <field name="name">Kanban view for survey</field>
- <field name="model">survey.survey</field>
- <field name="arch" type="xml">
- <kanban default_group_by="stage_id">
- <field name="stage_id" />
- <field name="designed" />
- <field name="title" />
- <field name="tot_sent_survey" />
- <field name="tot_start_survey" />
- <field name="tot_comp_survey" />
- <field name="color" />
- <field name="auth_required" />
- <field name="public_url" />
- <field name="activity_ids" />
- <field name="activity_state" />
- <templates>
- <div t-name="kanban-box" t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click">
- <div class="o_dropdown_kanban dropdown" t-if="widget.editable">
-
- <a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
- <span class="fa fa-ellipsis-v"/>
- </a>
- <div class="dropdown-menu" role="menu">
- <a role="menuitem" type="edit" class="dropdown-item">Edit Survey</a>
- <a t-if="widget.deletable" role="menuitem" type="delete" class="dropdown-item">Delete</a>
- <div role="separator" class="dropdown-divider"/>
- <div role="separator" class="dropdown-item-text">Color</div>
- <ul class="oe_kanban_colorpicker" data-field="color"/>
- </div>
- </div>
- <div class="o_kanban_record_top">
- <h4 class="o_kanban_record_title mb4"><field name="title" /></h4>
- </div>
- <div class="o_kanban_record_bottom">
- <ul class="list-unstyled">
- <li id="survey_test">
- <t t-if="record.designed.raw_value"><a type="object" name="action_test_survey">Test</a></t>
- <t t-if="! record.designed.raw_value"><a style="color: #aaaaaa;">Test</a></t>
- </li>
- <li>
- <t t-if="record.designed.raw_value"><a type="object" name="action_send_survey">Share &amp;amp; Invite</a></t>
- <t t-if="! record.designed.raw_value"><a style="color: #aaaaaa;">Share &amp;amp; Invite</a></t>
- </li>
- <li>
- <t t-if="record.tot_start_survey.raw_value &gt; 0"><a name="action_result_survey" type="object">Analyze Answers</a> <span t-if="record.tot_start_survey.raw_value &gt; 0">(<field name="tot_start_survey" />)</span></t>
- <t t-if="record.tot_start_survey.raw_value &lt;= 0"><a style="color: #aaaaaa;">Analyze Answers</a></t>
- </li>
- </ul>
- </div>
- <div class="o_kanban_record_bottom">
- <div class="oe_kanban_bottom_left"/>
- <div class="oe_kanban_bottom_right">
- <field name="activity_ids" widget="kanban_activity" />
- </div>
- </div>
- </div>
- </templates>
- </kanban>
- </field>
- </record>
- <record id="survey_survey_view_search" model="ir.ui.view">
- <field name="name">survey.survey.search</field>
- <field name="model">survey.survey</field>
- <field name="arch" type="xml">
- <search string="Survey">
- <field string="Title" name="title"/>
- <filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
- <separator/>
- <filter string="My Activities" name="activities_my"
- domain="[('activity_ids.user_id', '=', uid)]"/>
- <separator/>
- <filter string="Late Activities" name="activities_overdue"
- domain="[('activity_ids.date_deadline', '&lt;', context_today().strftime('%Y-%m-%d'))]"
- help="Show all records which has next action date is before today"/>
- <filter string="Today Activities" name="activities_today"
- domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
- <filter string="Upcoming Activities" name="activities_upcoming_all"
- domain="[('activity_ids.date_deadline', '&gt;', context_today().strftime('%Y-%m-%d'))
- ]"/>
- </search>
- </field>
- </record>
-
- <record model="ir.actions.act_window" id="action_survey_form">
- <field name="name">Surveys</field>
- <field name="res_model">survey.survey</field>
- <field name="view_type">form</field>
- <field name="view_mode">kanban,tree,form,activity</field>
- <field name="help" type="html">
- <p class="o_view_nocontent_smiling_face">
- Add a new survey
- </p><p>
- You can create surveys for different purposes: customer opinion, services feedback, recruitment interviews, employee's periodical evaluations, marketing campaigns, etc.
- </p><p>
- Design easily your survey, send invitations to answer by email and analyse answers.
- </p>
- </field>
- </record>
- <act_window context="{'search_default_survey_id': [active_id], 'default_survey_id': active_id}" id="act_survey_pages" name="Pages" res_model="survey.page" src_model="survey.survey"/>
- <act_window context="{'search_default_survey_id': [active_id], 'default_survey_id': active_id}" id="act_survey_question" name="Questions" res_model="survey.question" src_model="survey.survey"/>
-
- <!-- PAGES -->
- <record model="ir.ui.view" id="survey_page_form">
- <field name="name">Form view for survey page</field>
- <field name="model">survey.page</field>
- <field name="arch" type="xml">
- <form string="Survey Page" create="false">
- <sheet>
- <div class="oe_title" style="width: 100%;">
- <label for="title" class="oe_edit_only"/>
- <h1><field name="title" placeholder="Page Title"/></h1>
- </div>
- <field name="question_ids" nolabel="1" mode="tree" context="{'default_page_id': active_id}">
- <tree>
- <field name="sequence" widget="handle"/>
- <field name="question"/>
- <field name="question_type"/>
- <field name="constr_mandatory"/>
- </tree>
- </field>
- </sheet>
- </form>
- </field>
- </record>
- <record model="ir.ui.view" id="survey_page_tree">
- <field name="name">Tree view for survey page</field>
- <field name="model">survey.page</field>
- <field name="arch" type="xml">
- <tree string="Survey Page" create="false">
- <field name="sequence" widget="handle"/>
- <field name="title"/>
- <field name="survey_id"/>
- <field name="question_ids" string="#Questions"/>
- </tree>
- </field>
- </record>
- <record model="ir.ui.view" id="survey_page_search" >
- <field name="name">Search view for survey page</field>
- <field name="model">survey.page</field>
- <field name="arch" type="xml">
- <search string="Search Page">
- <field name="title" string="Page"/>
- <field name="survey_id" string="Survey"/>
- <group expand="0" string="Group By">
- <filter name="group_by_survey" string="Survey" domain="[]" context="{'group_by':'survey_id'}"/>
- </group>
- </search>
- </field>
- </record>
-
- <record model="ir.actions.act_window" id="action_survey_page_form">
- <field name="name">Pages</field>
- <field name="res_model">survey.page</field>
- <field name="view_type">form</field>
- <field name="view_mode">tree,form</field>
- <field name="search_view_id" ref="survey_page_search"/>
- <field name="context">{'search_default_group_by_survey': True}</field>
- <field name="help" type="html">
- <p class="o_view_nocontent_empty_folder">
- No pages found
- </p>
- </field>
- </record>
- <act_window context="{'search_default_page_id': active_id, 'default_page_id': active_id}" id="act_survey_page_question" name="Questions" res_model="survey.question" src_model="survey.page"/>
-
- <template id="assets_backend" name="survey assets" inherit_id="web.assets_backend">
- <xpath expr="." position="inside">
- <link rel="stylesheet" href="/survey/static/src/css/survey_result.css"/>
- </xpath>
- </template>
-
- <!-- QUESTIONS -->
- <record model="ir.ui.view" id="survey_question_form">
- <field name="name">Form view for survey question</field>
- <field name="model">survey.question</field>
- <field name="arch" type="xml">
- <form string="Survey Question" create="false">
- <sheet>
- <div class="oe_title" style="width: 100%;">
- <label for="question" string="Question name" class="oe_edit_only"/>
- <h1><field name="question" colspan="4" placeholder="Question Name"/></h1>
- <separator />
- </div>
- <group>
- <group>
- <field name="question_type" widget="radio" />
- </group>
- <group>
- <div class="col-lg-6 offset-lg-3 o_preview_questions">
- <!-- Multiple Lines Text Zone -->
- <div attrs="{'invisible': [('question_type', '!=', 'free_text')]}">
- <i class="fa fa-align-justify fa-4x" role="img" aria-label="Multiple lines" title="Multiple Lines"/>
- </div>
- <!-- Single Line Text Zone -->
- <div attrs="{'invisible': [('question_type', '!=', 'textbox')]}">
- <i class="fa fa-minus fa-4x" role="img" aria-label="Single Line" title="Single Line"/>
- </div>
- <!-- Numerical Value -->
- <div attrs="{'invisible': [('question_type', '!=', 'numerical_box')]}">
- <i class="fa fa-2x" role="img" aria-label="Numeric" title="Numeric">123..</i>
- </div>
- <!-- Date and Time -->
- <div attrs="{'invisible': [('question_type', '!=', 'date')]}">
- <p class="o_datetime">YYYY-MM-DD
- <i class="fa fa-calendar fa-2x" role="img" aria-label="Calendar" title="Calendar"/>
- </p>
- </div>
- <!-- Multiple choice: only one answer -->
- <div attrs="{'invisible': [('question_type', '!=', 'simple_choice')]}" role="img" aria-label="Multiple choice with one answer" title="Multiple choice with one answer">
- <div class="row"><i class="fa fa-circle-o fa-lg"/> answer</div>
- <div class="row"><i class="fa fa-dot-circle-o fa-lg"/> answer</div>
- <div class="row"><i class="fa fa-circle-o fa-lg"/> answer</div>
- </div>
- <!-- Multiple choice: multiple answers allowed -->
- <div attrs="{'invisible': [('question_type', '!=', 'multiple_choice')]}" role="img" aria-label="Multiple choice with multiple answers" title="Multiple choice with multiple answers">
- <div class="row"><i class="fa fa-square-o fa-lg"/> answer</div>
- <div class="row"><i class="fa fa-check-square-o fa-lg"/> answer</div>
- <div class="row"><i class="fa fa-square-o fa-lg"/> answer</div>
- </div>
- <!-- Matrix -->
- <div attrs="{'invisible': [('question_type', '!=', 'matrix')]}">
- <div class="row o_matrix_head">
- <div class="col-lg-3"></div>
- <div class="col-lg-3">ans</div>
- <div class="col-lg-3">ans</div>
- <div class="col-lg-3">ans</div>
- </div>
- <div class="row o_matrix_row">
- <div class="col-lg-3">Row1</div>
- <div class="col-lg-3"><i class="fa fa-circle-o fa-lg" role="img" aria-label="Not checked" title="Not checked"/></div>
- <div class="col-lg-3"><i class="fa fa-dot-circle-o fa-lg" role="img" aria-label="Checked" title="Checked"/></div>
- <div class="col-lg-3"><i class="fa fa-circle-o fa-lg" role="img" aria-label="Not checked" title="Not checked"/></div>
- </div>
- <div class="row o_matrix_row">
- <div class="col-lg-3">Row2</div>
- <div class="col-lg-3"><i class="fa fa-circle-o fa-lg" role="img" aria-label="Not checked" title="Not checked"/></div>
- <div class="col-lg-3"><i class="fa fa-circle-o fa-lg" role="img" aria-label="Not checked" title="Not checked"/></div>
- <div class="col-lg-3"><i class="fa fa-dot-circle-o fa-lg" role="img" aria-label="Checked" title="Checked"/></div>
- </div>
- <div class="row o_matrix_row">
- <div class="col-lg-3">Row3</div>
- <div class="col-lg-3"><i class="fa fa-dot-circle-o fa-lg" role="img" aria-label="Checked" title="Checked"/></div>
- <div class="col-lg-3"><i class="fa fa-circle-o fa-lg" role="img" aria-label="Not checked" title="Not checked"/></div>
- <div class="col-lg-3"><i class="fa fa-circle-o fa-lg" role="img" aria-label="Not checked" title="Not checked"/></div>
- </div>
- </div>
- </div>
- </group>
- </group>
- <notebook>
- <page string="Answers">
- <field name="validation_email" attrs="{'invisible': [('question_type', '!=', 'textbox')]}"/>
- <label for="validation_email" attrs="{'invisible': [('question_type', '!=', 'textbox')]}"/>
- <separator />
- <field name="labels_ids" string="Type of answers" context="{'default_question_id': active_id}" attrs="{'invisible': [('question_type', 'not in', ['simple_choice', 'multiple_choice', 'matrix'])]}">
- <tree editable="bottom">
- <field name="sequence" widget="handle"/>
- <field name="value" string="Choices"/>
- <field name="quizz_mark" groups="base.group_no_one"/>
- </tree>
- </field>
- <separator />
- <label for="labels_ids_2" attrs="{'invisible': [('question_type', '!=', 'matrix')]}" />
- <field name="labels_ids_2" context="{'default_question_id_2': active_id}" attrs="{'invisible': [('question_type', '!=', 'matrix')]}">
- <tree editable="bottom">
- <field name="sequence" widget="handle"/>
- <field name="value" string="Rows"/>
- </tree>
- </field>
- </page>
- <page string="Options">
- <group string="Constraints">
- <group colspan="2" col="4">
- <field name="constr_mandatory" string="Mandatory Answer"/>
- <field name="constr_error_msg" attrs="{'invisible': [('constr_mandatory', '=', False)]}"/>
- </group>
- <div colspan="2" attrs="{'invisible': [('question_type', 'not in', ['textbox', 'numerical_box', 'date'])]}">
- <group>
- <field name="validation_required" attrs="{'invisible': [('validation_email', '=', True), ('question_type', '=', 'textbox')]}"/>
- </group>
- <group col="4" attrs="{'invisible': [('validation_required', '=', False)]}">
- <field name="validation_length_min" attrs="{'invisible': [('question_type', '!=', 'textbox')]}"/>
- <field name="validation_length_max" attrs="{'invisible': [('question_type', '!=', 'textbox')]}"/>
- <field name="validation_min_float_value" attrs="{'invisible': [('question_type', '!=', 'numerical_box')]}"/>
- <field name="validation_max_float_value" attrs="{'invisible': [('question_type', '!=', 'numerical_box')]}"/>
- <field name="validation_min_date" attrs="{'invisible': [('question_type', '!=', 'date')]}"/>
- <field name="validation_max_date" attrs="{'invisible': [('question_type', '!=', 'date')]}"/>
- <field name="validation_error_msg" colspan="4"/>
- </group>
- </div>
- <group>
- <field name="matrix_subtype" attrs="{'invisible':[('question_type','not in',['matrix'])]}"/>
- </group>
- </group>
- <group string="Display mode" attrs="{'invisible':[('question_type','not in',['simple_choice', 'multiple_choice'])]}">
- <field name="display_mode" string="Format" attrs="{'invisible':[('question_type','not in',['simple_choice'])],'required':[('question_type','=','simple_choice')]}"/>
- <field name="column_nb" string="Number of columns" attrs="{'invisible':[('display_mode','=','dropdown'), ('question_type','=','simple_choice')]}"/>
- </group>
- <group string="Allow Comments" attrs="{'invisible':[('question_type','not in',['simple_choice','multiple_choice', 'matrix'])]}">
- <field name='comments_allowed' />
- <field name='comments_message' attrs="{'invisible': [('comments_allowed', '=', False)]}"/>
- <field name='comment_count_as_answer' attrs="{'invisible': ['|', ('comments_allowed', '=', False), ('question_type', 'in', ['matrix'])]}" />
- </group>
- </page>
- </notebook>
- </sheet>
- </form>
- </field>
- </record>
- <record model="ir.ui.view" id="survey_question_tree">
- <field name="name">Tree view for survey question</field>
- <field name="model">survey.question</field>
- <field name="arch" type="xml">
- <tree string="Survey Question" create="false">
- <field name="sequence" widget="handle"/>
- <field name="question"/>
- <field name="page_id"/>
- <field name="survey_id"/>
- <field name="question_type"/>
- </tree>
- </field>
- </record>
- <record model="ir.ui.view" id="survey_question_search">
- <field name="name">Search view for survey question</field>
- <field name="model">survey.question</field>
- <field name="arch" type="xml">
- <search string="Search Question">
- <field name="question" string="Question"/>
- <field name="survey_id" string="Survey"/>
- <field name="page_id" string="Page"/>
- <field name="question_type" string="Type"/>
- <group expand="1" string="Group By">
- <filter name="group_by_page" string="Page" domain="[]" context="{'group_by':'page_id'}"/>
- <filter name="group_by_type" string="Type" domain="[]" context="{'group_by':'question_type'}"/>
- </group>
- </search>
- </field>
- </record>
-
- <record model="ir.actions.act_window" id="action_survey_question_form">
- <field name="name">Questions</field>
- <field name="res_model">survey.question</field>
- <field name="view_type">form</field>
- <field name="view_mode">tree,form</field>
- <field name="search_view_id" ref="survey_question_search"/>
- <field name="context">{'search_default_group_by_page': True}</field>
- <field name="help" type="html">
- <p class="o_view_nocontent_empty_folder">
- No questions found
- </p>
- </field>
- </record>
-
- <!-- LABELS -->
- <record model="ir.ui.view" id="survey_label_tree">
- <field name="name">survey_label_tree</field>
- <field name="model">survey.label</field>
- <field name="arch" type="xml">
- <tree string="Survey Label" create="false">
- <field name="sequence" widget="handle"/>
- <field name="question_id"/>
- <field name="question_id_2"/>
- <field name="value"/>
- <field name="quizz_mark" groups="base.group_no_one"/>
- </tree>
- </field>
- </record>
- <record id="survey_label_search" model="ir.ui.view">
- <field name="name">survey_label_search</field>
- <field name="model">survey.label</field>
- <field name="arch" type="xml">
- <search string="Search Label">
- <field name="question_id" string="Question"/>
- <filter name="group_by_question" string="Question" domain="[]" context="{'group_by':'question_id'}"/>
- </search>
- </field>
- </record>
-
- <record model="ir.actions.act_window" id="action_survey_label_form">
- <field name="name">Labels</field>
- <field name="res_model">survey.label</field>
- <field name="view_type">form</field>
- <field name="view_mode">tree,form</field>
- <field name="search_view_id" ref="survey_label_search"/>
- <field name="context">{'search_default_group_by_question': True}</field>
- <field name="help" type="html">
- <p class="o_view_nocontent_empty_folder">
- No survey labels found
- </p>
- </field>
- </record>
-
- <!-- USER INPUT LINES
- .. note:: these views are useful mainly for technical users/administrators -->
- <record model="ir.ui.view" id="survey_user_input_line_form">
- <field name="name">survey_user_input_line_form</field>
- <field name="model">survey.user_input_line</field>
- <field name="arch" type="xml">
- <form string="User input line details" create="false">
- <sheet>
- <group col="4">
- <field name="question_id"/>
- <field name="date_create"/>
- <field name="answer_type"/>
- <field name="skipped" />
- <field name="quizz_mark" groups="base.group_no_one"/>
- </group>
- <group>
- <field name="value_text" colspan='2' attrs="{'invisible': [('answer_type','!=','text')]}"/>
- <field name="value_number" colspan='2' attrs="{'invisible': [('answer_type','!=','number')]}"/>
- <field name="value_date" colspan='2' attrs="{'invisible': [('answer_type','!=','date')]}"/>
- <field name="value_free_text" colspan='2' attrs="{'invisible': [('answer_type','!=','free_text')]}"/>
- <field name="value_suggested_row" colspan='2' />
- <field name="value_suggested" colspan='2' attrs="{'invisible': [('answer_type','!=','suggestion')]}"/>
- </group>
- </sheet>
- </form>
- </field>
- </record>
- <record model="ir.ui.view" id="survey_response_line_tree">
- <field name="name">survey_response_line_tree</field>
- <field name="model">survey.user_input_line</field>
- <field name="arch" type="xml">
- <tree string="Survey Answer Line" create="false">
- <field name="survey_id"/>
- <field name="user_input_id"/>
- <field name="page_id"/>
- <field name="question_id"/>
- <field name="date_create"/>
- <field name="answer_type"/>
- <field name="skipped"/>
- <field name="quizz_mark" groups="base.group_no_one"/>
- </tree>
- </field>
- </record>
- <record id="survey_response_line_search" model="ir.ui.view">
- <field name="name">survey_response_line_search</field>
- <field name="model">survey.user_input_line</field>
- <field name="arch" type="xml">
- <search string="Search User input lines">
- <field name="user_input_id"/>
- <field name="survey_id"/>
- <group expand="1" string="Group By">
- <filter name="group_by_survey" string="Survey" domain="[]" context="{'group_by':'survey_id'}"/>
- <filter name="group_by_user_input" string="User Input" domain="[]" context="{'group_by':'user_input_id'}"/>
- </group>
- </search>
- </field>
- </record>
-
- <record model="ir.actions.act_window" id="action_survey_user_input_line">
- <field name="name">Survey User Input lines</field>
- <field name="res_model">survey.user_input_line</field>
- <field name="view_mode">tree,form</field>
- <field name="search_view_id" ref="survey_response_line_search"/>
- <field name="context">{'search_default_group_by_survey': True, 'search_default_group_by_user_input': True}</field>
- <field name="help" type="html">
- <p class="o_view_nocontent_empty_folder">
- No user input lines found
- </p>
- </field>
- </record>
-
-
- <record model="ir.ui.view" id="survey_stage_form">
- <field name="name">Form view for survey stage</field>
- <field name="model">survey.stage</field>
- <field name="arch" type="xml">
- <form string="Stage">
- <group col='4' colspan='4'>
- <field name="name" />
- <field name="closed" />
- <field name="sequence" />
- <field name="fold" />
- </group>
- </form>
- </field>
- </record>
-
- <record id="survey_action_server_clean_test_answers" model="ir.actions.server">
- <field name="name">Survey: Clean test answers</field>
- <field name="type">ir.actions.server</field>
- <field name="model_id" ref="model_survey_survey" />
- <field name="binding_model_id" ref="model_survey_survey" />
- <field name="state">code</field>
- <field name="code">
-if records:
- env['survey.user_input'].search([('survey_id', 'in', records.ids), ('test_entry', '=', 'True')]).unlink()
- </field>
- </record>
-
- <!-- MENU ELEMENTS -->
-
- <!-- Left menu categories-->
- <menuitem name="Surveys" id="menu_surveys" sequence="70" groups="group_survey_user" web_icon="survey,static/description/icon.png"/>
-
- <!-- Left menu elements: Survey -->
- <menuitem name="Surveys" id="menu_survey_form" action="action_survey_form" parent="menu_surveys" sequence="1"/>
- <menuitem name="Answers" id="menu_survey_type_form1" action="action_survey_user_input" parent="menu_surveys" sequence="1"/>
-
-
- <!-- Left menu elements: Technical Configuration -->
- <menuitem name="Configuration" id="menu_surveys_configuration" parent="menu_surveys" groups="base.group_no_one" sequence="100" />
- <menuitem name="Pages" id="menu_survey_page_form1" action="action_survey_page_form" parent="menu_surveys_configuration" sequence="1"/>
- <menuitem name="Questions" id="menu_survey_question_form1" action="action_survey_question_form" parent="menu_surveys_configuration" sequence="2"/>
- <menuitem name="Labels" id="menu_survey_label_form1" action="action_survey_label_form" parent="menu_surveys_configuration" sequence="3"/>
- <menuitem name="User Input Lines" id="menu_survey_response_line_form" action="action_survey_user_input_line" parent="menu_surveys_configuration" sequence="4"/>
- </data>
-</odoo>
diff --git a/addons/survey/wizard/__init__.py b/addons/survey/wizard/__init__.py
index 4b354293447..09c05c0941a 100644
--- a/addons/survey/wizard/__init__.py
+++ b/addons/survey/wizard/__init__.py
@@ -1,4 +1,4 @@
# -*- encoding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from . import survey_email_compose_message
+from . import survey_mail_compose_message
diff --git a/addons/survey/wizard/survey_email_compose_message.py b/addons/survey/wizard/survey_mail_compose_message.py
index 1f1ab22a77e..1f1ab22a77e 100644
--- a/addons/survey/wizard/survey_email_compose_message.py
+++ b/addons/survey/wizard/survey_mail_compose_message.py
diff --git a/addons/survey/wizard/survey_email_compose_message.xml b/addons/survey/wizard/survey_mail_compose_message_views.xml
index 167464896ba..167464896ba 100644
--- a/addons/survey/wizard/survey_email_compose_message.xml
+++ b/addons/survey/wizard/survey_mail_compose_message_views.xml