diff options
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; Invite</a></t> + <t t-if="! record.designed.raw_value"><a style="color: #aaaaaa;">Share &amp; Invite</a></t> + </li> + <li> + <t t-if="record.tot_start_survey.raw_value > 0"><a name="action_result_survey" type="object">Analyze Answers</a> <span t-if="record.tot_start_survey.raw_value > 0">(<field name="tot_start_survey" />)</span></t> + <t t-if="record.tot_start_survey.raw_value <= 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', '<', 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', '>', 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; Invite</a></t> - <t t-if="! record.designed.raw_value"><a style="color: #aaaaaa;">Share &amp; Invite</a></t> - </li> - <li> - <t t-if="record.tot_start_survey.raw_value > 0"><a name="action_result_survey" type="object">Analyze Answers</a> <span t-if="record.tot_start_survey.raw_value > 0">(<field name="tot_start_survey" />)</span></t> - <t t-if="record.tot_start_survey.raw_value <= 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', '<', 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', '>', 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 |
