mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-22 01:32:00 +02:00
19.0 vanilla
This commit is contained in:
parent
d1963a3c3a
commit
2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions
|
|
@ -19,41 +19,18 @@ pip install odoo-bringout-oca-ocb-survey
|
|||
|
||||
## Dependencies
|
||||
|
||||
This addon depends on:
|
||||
- auth_signup
|
||||
- http_routing
|
||||
- mail
|
||||
- web_tour
|
||||
- gamification
|
||||
|
||||
## Manifest Information
|
||||
|
||||
- **Name**: Surveys
|
||||
- **Version**: 3.5
|
||||
- **Category**: Marketing/Surveys
|
||||
- **License**: LGPL-3
|
||||
- **Installable**: True
|
||||
|
||||
## Source
|
||||
|
||||
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `survey`.
|
||||
- Repository: https://github.com/OCA/OCB
|
||||
- Branch: 19.0
|
||||
- Path: addons/survey
|
||||
|
||||
## License
|
||||
|
||||
This package maintains the original LGPL-3 license from the upstream Odoo project.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Overview: doc/OVERVIEW.md
|
||||
- Architecture: doc/ARCHITECTURE.md
|
||||
- Models: doc/MODELS.md
|
||||
- Controllers: doc/CONTROLLERS.md
|
||||
- Wizards: doc/WIZARDS.md
|
||||
- Reports: doc/REPORTS.md
|
||||
- Security: doc/SECURITY.md
|
||||
- Install: doc/INSTALL.md
|
||||
- Usage: doc/USAGE.md
|
||||
- Configuration: doc/CONFIGURATION.md
|
||||
- Dependencies: doc/DEPENDENCIES.md
|
||||
- Troubleshooting: doc/TROUBLESHOOTING.md
|
||||
- FAQ: doc/FAQ.md
|
||||
This package preserves the original LGPL-3 license.
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
[project]
|
||||
name = "odoo-bringout-oca-ocb-survey"
|
||||
version = "16.0.0"
|
||||
description = "Surveys - Send your surveys or share them live."
|
||||
description = "Surveys -
|
||||
Send your surveys or share them live.
|
||||
"
|
||||
authors = [
|
||||
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
|
||||
]
|
||||
dependencies = [
|
||||
"odoo-bringout-oca-ocb-auth_signup>=16.0.0",
|
||||
"odoo-bringout-oca-ocb-http_routing>=16.0.0",
|
||||
"odoo-bringout-oca-ocb-mail>=16.0.0",
|
||||
"odoo-bringout-oca-ocb-web_tour>=16.0.0",
|
||||
"odoo-bringout-oca-ocb-gamification>=16.0.0",
|
||||
"odoo-bringout-oca-ocb-auth_signup>=19.0.0",
|
||||
"odoo-bringout-oca-ocb-http_routing>=19.0.0",
|
||||
"odoo-bringout-oca-ocb-mail>=19.0.0",
|
||||
"odoo-bringout-oca-ocb-web_tour>=19.0.0",
|
||||
"odoo-bringout-oca-ocb-gamification>=19.0.0",
|
||||
"requests>=2.25.1"
|
||||
]
|
||||
readme = "README.md"
|
||||
|
|
@ -20,7 +22,7 @@ classifiers = [
|
|||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Office/Business",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -3,4 +3,5 @@
|
|||
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import report
|
||||
from . import wizard
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
{
|
||||
'name': 'Surveys',
|
||||
'version': '3.5',
|
||||
'version': '3.7',
|
||||
'category': 'Marketing/Surveys',
|
||||
'description': """
|
||||
Create beautiful surveys and visualize answers
|
||||
|
|
@ -23,9 +23,12 @@ sent mails with personal token for the invitation of the survey.
|
|||
'web_tour',
|
||||
'gamification'],
|
||||
'data': [
|
||||
'views/survey_report_templates.xml',
|
||||
'views/survey_reports.xml',
|
||||
'report/survey_templates.xml',
|
||||
'report/survey_reports.xml',
|
||||
'data/ir_actions_server_data.xml',
|
||||
'data/mail_message_subtype_data.xml',
|
||||
'data/mail_template_data.xml',
|
||||
'data/survey_tour.xml',
|
||||
'security/survey_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/survey_menus.xml',
|
||||
|
|
@ -50,9 +53,6 @@ sent mails with personal token for the invitation of the survey.
|
|||
'data/survey_demo_certification.xml',
|
||||
'data/survey_demo_certification_user_input.xml',
|
||||
'data/survey_demo_certification_user_input_line.xml',
|
||||
'data/survey_demo_quiz.xml',
|
||||
'data/survey_demo_quiz_user_input.xml',
|
||||
'data/survey_demo_quiz_user_input_line.xml',
|
||||
'data/survey_demo_conditional.xml',
|
||||
],
|
||||
'installable': True,
|
||||
|
|
@ -60,31 +60,28 @@ sent mails with personal token for the invitation of the survey.
|
|||
'sequence': 220,
|
||||
'assets': {
|
||||
'survey.survey_assets': [
|
||||
'web/static/lib/Chart/Chart.js',
|
||||
'survey/static/src/js/survey_image_zoomer.js',
|
||||
'/survey/static/src/xml/survey_image_zoomer_templates.xml',
|
||||
'survey/static/src/js/survey_quick_access.js',
|
||||
'survey/static/src/js/survey_timer.js',
|
||||
'survey/static/src/js/survey_breadcrumb.js',
|
||||
'survey/static/src/js/survey_form.js',
|
||||
('include', "web.chartjs_lib"),
|
||||
'survey/static/src/utils.js',
|
||||
'/survey/static/src/interactions/survey_image_zoomer_templates.xml',
|
||||
'survey/static/src/js/survey_preload_image_mixin.js',
|
||||
'survey/static/src/js/survey_print.js',
|
||||
'survey/static/src/js/survey_result.js',
|
||||
('include', 'web._assets_helpers'),
|
||||
('include', 'web._assets_frontend_helpers'),
|
||||
'web/static/src/scss/pre_variables.scss',
|
||||
'web/static/lib/bootstrap/scss/_variables.scss',
|
||||
'web/static/lib/bootstrap/scss/_variables-dark.scss',
|
||||
'web/static/lib/bootstrap/scss/_maps.scss',
|
||||
'survey/static/src/scss/survey_templates_form.scss',
|
||||
'survey/static/src/scss/survey_templates_results.scss',
|
||||
'survey/static/src/xml/survey_breadcrumb_templates.xml',
|
||||
'survey/static/src/interactions/survey_breadcrumb_templates.xml',
|
||||
'survey/static/src/xml/survey_paginated_results_rows_template.xml',
|
||||
'survey/static/src/interactions/*',
|
||||
],
|
||||
'survey.survey_user_input_session_assets': [
|
||||
'survey/static/src/js/libs/chartjs-plugin-datalabels.min.js',
|
||||
'survey/static/src/js/survey_session_colors.js',
|
||||
'survey/static/src/js/survey_session_chart.js',
|
||||
'survey/static/src/js/survey_session_text_answers.js',
|
||||
'survey/static/src/js/survey_session_leaderboard.js',
|
||||
'survey/static/src/js/survey_session_manage.js',
|
||||
'survey/static/src/interactions/survey_session_colors.js',
|
||||
'survey/static/src/interactions/survey_session_chart.js',
|
||||
'survey/static/src/interactions/survey_session_text_answers.js',
|
||||
'survey/static/src/interactions/survey_session_leaderboard.js',
|
||||
'survey/static/src/interactions/survey_session_manage.js',
|
||||
'survey/static/src/xml/survey_session_text_answer_template.xml',
|
||||
],
|
||||
'web.report_assets_common': [
|
||||
|
|
@ -92,27 +89,26 @@ sent mails with personal token for the invitation of the survey.
|
|||
],
|
||||
'web.assets_backend': [
|
||||
'survey/static/src/question_page/*',
|
||||
'survey/static/src/js/fields_section_one2many.js',
|
||||
'survey/static/src/js/fields_form_page_description.js',
|
||||
'survey/static/src/views/*.js',
|
||||
'survey/static/src/views/**/*.js',
|
||||
'survey/static/src/views/**/*.xml',
|
||||
'survey/static/src/scss/survey_survey_views.scss',
|
||||
'survey/static/src/scss/survey_question_views.scss',
|
||||
'survey/static/src/js/tours/survey_tour.js',
|
||||
],
|
||||
"web.dark_mode_assets_backend": [
|
||||
"web.assets_web_dark": [
|
||||
'survey/static/src/scss/*.dark.scss',
|
||||
],
|
||||
'web.assets_tests': [
|
||||
'survey/static/tests/tours/*.js',
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
'survey/static/tests/components/*.js',
|
||||
],
|
||||
'web.assets_common': [
|
||||
'survey/static/src/js/tours/survey_tour.js',
|
||||
'web.assets_unit_tests': [
|
||||
'survey/static/tests/components/*.test.js',
|
||||
'survey/static/tests/fields/*.test.js',
|
||||
],
|
||||
'web.assets_frontend': [
|
||||
'survey/static/src/js/tours/survey_tour.js',
|
||||
],
|
||||
},
|
||||
'author': 'Odoo S.A.',
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import werkzeug
|
||||
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import fields, http, SUPERUSER_ID, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.fields import Domain
|
||||
from odoo.http import request, content_disposition
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import format_datetime, format_date, is_html_empty
|
||||
from odoo.addons.base.models.ir_qweb import keep_query
|
||||
|
||||
|
|
@ -28,18 +28,22 @@ class Survey(http.Controller):
|
|||
""" Check that given token matches an answer from the given survey_id.
|
||||
Returns a sudo-ed browse record of survey in order to avoid access rights
|
||||
issues now that access is granted through token. """
|
||||
survey_sudo = request.env['survey.survey'].with_context(active_test=False).sudo().search([('access_token', '=', survey_token)])
|
||||
if not answer_token:
|
||||
answer_sudo = request.env['survey.user_input'].sudo()
|
||||
else:
|
||||
answer_sudo = request.env['survey.user_input'].sudo().search([
|
||||
('survey_id', '=', survey_sudo.id),
|
||||
('access_token', '=', answer_token)
|
||||
], limit=1)
|
||||
return survey_sudo, answer_sudo
|
||||
SurveySudo, UserInputSudo = request.env['survey.survey'].sudo(), request.env['survey.user_input'].sudo()
|
||||
if not survey_token:
|
||||
return SurveySudo, UserInputSudo
|
||||
if answer_token:
|
||||
answer_sudo = UserInputSudo.search(
|
||||
Domain('survey_id', 'any',
|
||||
Domain('access_token', '=', survey_token)
|
||||
& Domain('active', 'in', (True, False)) # keeping active test for UserInput
|
||||
) & Domain('access_token', '=', answer_token), limit=1)
|
||||
if answer_sudo:
|
||||
return answer_sudo.survey_id, answer_sudo
|
||||
|
||||
def _check_validity(self, survey_token, answer_token, ensure_token=True, check_partner=True):
|
||||
""" Check survey is open and can be taken. This does not checks for
|
||||
return SurveySudo.with_context(active_test=False).search([('access_token', '=', survey_token)]), UserInputSudo
|
||||
|
||||
def _check_validity(self, survey_sudo, answer_sudo, answer_token, ensure_token=True, check_partner=True):
|
||||
""" Check survey is open and can be taken. This does not check for
|
||||
security rules, only functional / business rules. It returns a string key
|
||||
allowing further manipulation of validity issues
|
||||
|
||||
|
|
@ -48,8 +52,7 @@ class Survey(http.Controller):
|
|||
* survey_closed: survey is closed and does not accept input anymore;
|
||||
* survey_void: survey is void and should not be taken;
|
||||
* token_wrong: given token not recognized;
|
||||
* token_required: no token given although it is necessary to access the
|
||||
survey;
|
||||
* token_required: no token given, but it is required to access the survey;
|
||||
* answer_deadline: token linked to an expired answer;
|
||||
|
||||
:param ensure_token: whether user input existence based on given access token
|
||||
|
|
@ -59,17 +62,15 @@ class Survey(http.Controller):
|
|||
:param check_partner: Whether we must check that the partner associated to the target
|
||||
answer corresponds to the active user.
|
||||
"""
|
||||
survey_sudo, answer_sudo = self._fetch_from_access_token(survey_token, answer_token)
|
||||
|
||||
if not survey_sudo.exists():
|
||||
if not survey_sudo:
|
||||
return 'survey_wrong'
|
||||
|
||||
if answer_token and not answer_sudo:
|
||||
return 'token_wrong'
|
||||
|
||||
if not answer_sudo and ensure_token is True:
|
||||
if not answer_sudo and ensure_token:
|
||||
return 'token_required'
|
||||
if not answer_sudo and ensure_token != 'survey_only' and survey_sudo.access_mode == 'token':
|
||||
if not answer_sudo and survey_sudo.access_mode == 'token':
|
||||
return 'token_required'
|
||||
|
||||
if survey_sudo.users_login_required and request.env.user._is_public():
|
||||
|
|
@ -81,6 +82,9 @@ class Survey(http.Controller):
|
|||
if (not survey_sudo.page_ids and survey_sudo.questions_layout == 'page_per_section') or not survey_sudo.question_ids:
|
||||
return 'survey_void'
|
||||
|
||||
if answer_sudo and answer_sudo.deadline and answer_sudo.deadline < datetime.now():
|
||||
return 'answer_deadline'
|
||||
|
||||
if answer_sudo and check_partner:
|
||||
if request.env.user._is_public() and answer_sudo.partner_id and not answer_token:
|
||||
# answers from public user should not have any partner_id; this indicates probably a cookie issue
|
||||
|
|
@ -89,9 +93,6 @@ class Survey(http.Controller):
|
|||
# partner mismatch, probably a cookie issue
|
||||
return 'answer_wrong_user'
|
||||
|
||||
if answer_sudo and answer_sudo.deadline and answer_sudo.deadline < datetime.now():
|
||||
return 'answer_deadline'
|
||||
|
||||
return True
|
||||
|
||||
def _get_access_data(self, survey_token, answer_token, ensure_token=True, check_partner=True):
|
||||
|
|
@ -101,24 +102,16 @@ class Survey(http.Controller):
|
|||
: param ensure_token: whether user input existence should be enforced or not(see ``_check_validity``)
|
||||
: param check_partner: whether the partner of the target answer should be checked (see ``_check_validity``)
|
||||
"""
|
||||
survey_sudo, answer_sudo = request.env['survey.survey'].sudo(), request.env['survey.user_input'].sudo()
|
||||
survey_sudo, answer_sudo = self._fetch_from_access_token(survey_token, answer_token)
|
||||
has_survey_access, can_answer = False, False
|
||||
|
||||
validity_code = self._check_validity(survey_token, answer_token, ensure_token=ensure_token, check_partner=check_partner)
|
||||
validity_code = self._check_validity(
|
||||
survey_sudo, answer_sudo, answer_token, ensure_token=ensure_token, check_partner=check_partner)
|
||||
if validity_code != 'survey_wrong':
|
||||
survey_sudo, answer_sudo = self._fetch_from_access_token(survey_token, answer_token)
|
||||
try:
|
||||
survey_user = survey_sudo.with_user(request.env.user)
|
||||
survey_user.check_access_rights('read', raise_exception=True)
|
||||
survey_user.check_access_rule('read')
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
has_survey_access = True
|
||||
has_survey_access = survey_sudo.with_user(request.env.user).has_access('read')
|
||||
can_answer = bool(answer_sudo)
|
||||
if not can_answer:
|
||||
can_answer = survey_sudo.access_mode == 'public' or (
|
||||
has_survey_access and ensure_token == 'survey_only')
|
||||
can_answer = survey_sudo.access_mode == 'public'
|
||||
|
||||
return {
|
||||
'survey_sudo': survey_sudo,
|
||||
|
|
@ -144,13 +137,15 @@ class Survey(http.Controller):
|
|||
if answer_sudo.partner_id.user_ids:
|
||||
answer_sudo.partner_id.signup_cancel()
|
||||
else:
|
||||
answer_sudo.partner_id.signup_prepare(expiration=fields.Datetime.now() + relativedelta(days=1))
|
||||
answer_sudo.partner_id.signup_prepare()
|
||||
redirect_url = answer_sudo.partner_id._get_signup_url_for_action(url='/survey/start/%s?answer_token=%s' % (survey_sudo.access_token, answer_sudo.access_token))[answer_sudo.partner_id.id]
|
||||
else:
|
||||
redirect_url = '/web/login?redirect=%s' % ('/survey/start/%s?answer_token=%s' % (survey_sudo.access_token, answer_sudo.access_token))
|
||||
return request.render("survey.survey_auth_required", {'survey': survey_sudo, 'redirect_url': redirect_url})
|
||||
elif error_key == 'answer_deadline' and answer_sudo.access_token:
|
||||
return request.render("survey.survey_closed_expired", {'survey': survey_sudo})
|
||||
elif error_key in ['answer_wrong_user', 'token_wrong']:
|
||||
return request.render("survey.survey_access_error", {'survey': survey_sudo})
|
||||
|
||||
return request.redirect("/")
|
||||
|
||||
|
|
@ -198,14 +193,13 @@ class Survey(http.Controller):
|
|||
def _prepare_retry_additional_values(self, answer):
|
||||
return {
|
||||
'deadline': answer.deadline,
|
||||
'nickname': answer.nickname,
|
||||
}
|
||||
|
||||
def _prepare_survey_finished_values(self, survey, answer, token=False):
|
||||
values = {'survey': survey, 'answer': answer}
|
||||
if token:
|
||||
values['token'] = token
|
||||
if survey.scoring_type != 'no_scoring':
|
||||
values['graph_data'] = json.dumps(answer._prepare_statistics()[answer])
|
||||
return values
|
||||
|
||||
# ------------------------------------------------------------
|
||||
|
|
@ -221,7 +215,7 @@ class Survey(http.Controller):
|
|||
# Get the current answer token from cookie
|
||||
answer_from_cookie = False
|
||||
if not answer_token:
|
||||
answer_token = request.httprequest.cookies.get('survey_%s' % survey_token)
|
||||
answer_token = request.cookies.get('survey_%s' % survey_token)
|
||||
answer_from_cookie = bool(answer_token)
|
||||
|
||||
access_data = self._get_access_data(survey_token, answer_token, ensure_token=False)
|
||||
|
|
@ -244,24 +238,29 @@ class Survey(http.Controller):
|
|||
|
||||
if not answer_sudo:
|
||||
try:
|
||||
survey_sudo.with_user(request.env.user).check_access_rights('read')
|
||||
survey_sudo.with_user(request.env.user).check_access_rule('read')
|
||||
survey_sudo.with_user(request.env.user).check_access('read')
|
||||
except:
|
||||
return request.redirect("/")
|
||||
else:
|
||||
return request.render("survey.survey_403_page", {'survey': survey_sudo})
|
||||
|
||||
return request.redirect('/survey/%s/%s' % (survey_sudo.access_token, answer_sudo.access_token))
|
||||
# When resuming survey, restore language + always enforce that the language is supported by the survey
|
||||
lang = self._get_lang_with_fallback(answer_sudo.sudo(False))
|
||||
response = request.redirect(self.env['ir.http']._url_for(f'/survey/{survey_sudo.access_token}', lang.code))
|
||||
response.set_cookie(f'survey_{survey_sudo.access_token}', answer_sudo.access_token, max_age=60 * 60 * 24)
|
||||
return response
|
||||
|
||||
def _prepare_survey_data(self, survey_sudo, answer_sudo, **post):
|
||||
""" This method prepares all the data needed for template rendering, in function of the survey user input state.
|
||||
:param post:
|
||||
- previous_page_id : come from the breadcrumb or the back button and force the next questions to load
|
||||
to be the previous ones. """
|
||||
to be the previous ones.
|
||||
- next_skipped_page : force the display of next skipped question or page if any."""
|
||||
data = {
|
||||
'is_html_empty': is_html_empty,
|
||||
'survey': survey_sudo,
|
||||
'answer': answer_sudo,
|
||||
'skipped_questions': answer_sudo._get_skipped_questions(),
|
||||
'breadcrumb_pages': [{
|
||||
'id': page.id,
|
||||
'title': page.title,
|
||||
|
|
@ -269,16 +268,22 @@ class Survey(http.Controller):
|
|||
'format_datetime': lambda dt: format_datetime(request.env, dt, dt_format=False),
|
||||
'format_date': lambda date: format_date(request.env, date)
|
||||
}
|
||||
if answer_sudo.state == 'new':
|
||||
# Data for the language selector
|
||||
supported_lang_codes = survey_sudo._get_supported_lang_codes()
|
||||
data['languages'] = [(lang_code, self.env['res.lang']._get_data(code=lang_code)['name'])
|
||||
for lang_code in supported_lang_codes]
|
||||
data['lang_code'] = self._get_lang_with_fallback(answer_sudo.sudo(False)).code
|
||||
triggering_answers_by_question, triggered_questions_by_answer, selected_answers = answer_sudo._get_conditional_values()
|
||||
if survey_sudo.questions_layout != 'page_per_question':
|
||||
triggering_answer_by_question, triggered_questions_by_answer, selected_answers = answer_sudo._get_conditional_values()
|
||||
data.update({
|
||||
'triggering_answer_by_question': {
|
||||
question.id: triggering_answer_by_question[question].id for question in triggering_answer_by_question.keys()
|
||||
if triggering_answer_by_question[question]
|
||||
'triggering_answers_by_question': {
|
||||
question.id: triggering_answers.ids
|
||||
for question, triggering_answers in triggering_answers_by_question.items() if triggering_answers
|
||||
},
|
||||
'triggered_questions_by_answer': {
|
||||
answer.id: triggered_questions_by_answer[answer].ids
|
||||
for answer in triggered_questions_by_answer.keys()
|
||||
answer.id: triggered_questions.ids
|
||||
for answer, triggered_questions in triggered_questions_by_answer.items()
|
||||
},
|
||||
'selected_answers': selected_answers.ids
|
||||
})
|
||||
|
|
@ -306,17 +311,41 @@ class Survey(http.Controller):
|
|||
return data
|
||||
|
||||
if answer_sudo.state == 'in_progress':
|
||||
next_page_or_question = None
|
||||
if answer_sudo.is_session_answer:
|
||||
next_page_or_question = survey_sudo.session_question_id
|
||||
else:
|
||||
next_page_or_question = survey_sudo._get_next_page_or_question(
|
||||
answer_sudo,
|
||||
answer_sudo.last_displayed_page_id.id if answer_sudo.last_displayed_page_id else 0)
|
||||
if 'next_skipped_page' in post:
|
||||
next_page_or_question = answer_sudo._get_next_skipped_page_or_question()
|
||||
if not next_page_or_question:
|
||||
next_page_or_question = survey_sudo._get_next_page_or_question(
|
||||
answer_sudo,
|
||||
answer_sudo.last_displayed_page_id.id if answer_sudo.last_displayed_page_id else 0)
|
||||
# fallback to skipped page so that there is a next_page_or_question otherwise this should be a submit
|
||||
if not next_page_or_question:
|
||||
next_page_or_question = answer_sudo._get_next_skipped_page_or_question()
|
||||
|
||||
if next_page_or_question:
|
||||
data.update({
|
||||
'survey_last': survey_sudo._is_last_page_or_question(answer_sudo, next_page_or_question)
|
||||
})
|
||||
if answer_sudo.survey_first_submitted:
|
||||
survey_last = answer_sudo._is_last_skipped_page_or_question(next_page_or_question)
|
||||
else:
|
||||
survey_last = survey_sudo._is_last_page_or_question(answer_sudo, next_page_or_question)
|
||||
values = {'survey_last': survey_last}
|
||||
# On the last survey page, get the suggested answers which are triggering questions on the following pages
|
||||
# to dynamically update the survey button to "submit" or "continue" depending on the selected answers.
|
||||
# NB: Not in the skipped questions flow as conditionals aren't handled.
|
||||
if not answer_sudo.survey_first_submitted and survey_last and survey_sudo.questions_layout != 'one_page':
|
||||
pages_or_questions = survey_sudo._get_pages_or_questions(answer_sudo)
|
||||
following_questions = pages_or_questions.filtered(lambda page_or_question: page_or_question.sequence > next_page_or_question.sequence)
|
||||
next_page_questions_suggested_answers = next_page_or_question.suggested_answer_ids
|
||||
if survey_sudo.questions_layout == 'page_per_section':
|
||||
following_questions = following_questions.question_ids
|
||||
next_page_questions_suggested_answers = next_page_or_question.question_ids.suggested_answer_ids
|
||||
values['survey_last_triggering_answers'] = [
|
||||
answer.id for answer in triggered_questions_by_answer
|
||||
if answer in next_page_questions_suggested_answers and any(q in following_questions for q in triggered_questions_by_answer[answer])
|
||||
]
|
||||
data.update(values)
|
||||
|
||||
if answer_sudo.is_session_answer and next_page_or_question.is_time_limited:
|
||||
data.update({
|
||||
|
|
@ -346,16 +375,19 @@ class Survey(http.Controller):
|
|||
object at frontend side."""
|
||||
survey_data = self._prepare_survey_data(survey_sudo, answer_sudo, **post)
|
||||
|
||||
IrQweb = request.env['ir.qweb'].with_context(
|
||||
lang=self.env['res.lang']._get_data(id=answer_sudo.lang_id.id).code
|
||||
or self._get_lang_with_fallback(answer_sudo.sudo(False)).code)
|
||||
if answer_sudo.state == 'done':
|
||||
survey_content = request.env['ir.qweb']._render('survey.survey_fill_form_done', survey_data)
|
||||
survey_content = IrQweb._render('survey.survey_fill_form_done', survey_data)
|
||||
else:
|
||||
survey_content = request.env['ir.qweb']._render('survey.survey_fill_form_in_progress', survey_data)
|
||||
survey_content = IrQweb._render('survey.survey_fill_form_in_progress', survey_data)
|
||||
|
||||
survey_progress = False
|
||||
if answer_sudo.state == 'in_progress' and not survey_data.get('question', request.env['survey.question']).is_page:
|
||||
if survey_sudo.questions_layout == 'page_per_section':
|
||||
page_ids = survey_sudo.page_ids.ids
|
||||
survey_progress = request.env['ir.qweb']._render('survey.survey_progression', {
|
||||
survey_progress = IrQweb._render('survey.survey_progression', {
|
||||
'survey': survey_sudo,
|
||||
'page_ids': page_ids,
|
||||
'page_number': page_ids.index(survey_data['page'].id) + (1 if survey_sudo.progression_mode == 'number' else 0)
|
||||
|
|
@ -364,7 +396,7 @@ class Survey(http.Controller):
|
|||
page_ids = (answer_sudo.predefined_question_ids.ids
|
||||
if not answer_sudo.is_session_answer and survey_sudo.questions_selection == 'random'
|
||||
else survey_sudo.question_ids.ids)
|
||||
survey_progress = request.env['ir.qweb']._render('survey.survey_progression', {
|
||||
survey_progress = IrQweb._render('survey.survey_progression', {
|
||||
'survey': survey_sudo,
|
||||
'page_ids': page_ids,
|
||||
'page_number': page_ids.index(survey_data['question'].id)
|
||||
|
|
@ -377,14 +409,20 @@ class Survey(http.Controller):
|
|||
background_image_url = survey_data['page'].background_image_url
|
||||
|
||||
return {
|
||||
'has_skipped_questions': any(answer_sudo._get_skipped_questions()),
|
||||
'survey_content': survey_content,
|
||||
'survey_progress': survey_progress,
|
||||
'survey_navigation': request.env['ir.qweb']._render('survey.survey_navigation', survey_data),
|
||||
'survey_navigation': IrQweb._render('survey.survey_navigation', survey_data),
|
||||
'background_image_url': background_image_url
|
||||
}
|
||||
|
||||
@http.route('/survey/<string:survey_token>/<string:answer_token>', type='http', auth='public', website=True)
|
||||
def survey_display_page(self, survey_token, answer_token, **post):
|
||||
@http.route([
|
||||
'/survey/<string:survey_token>',
|
||||
'/survey/<string:survey_token>/<string:answer_token>',
|
||||
], type='http', auth='public', website=True)
|
||||
def survey_display_page(self, survey_token, answer_token=None, **post):
|
||||
if not answer_token:
|
||||
answer_token = request.httprequest.cookies.get('survey_%s' % survey_token)
|
||||
access_data = self._get_access_data(survey_token, answer_token, ensure_token=True)
|
||||
if access_data['validity_code'] is not True:
|
||||
return self._redirect_with_error(access_data, access_data['validity_code'])
|
||||
|
|
@ -449,48 +487,52 @@ class Survey(http.Controller):
|
|||
# JSON ROUTES to begin / continue survey (ajax navigation) + Tools
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
@http.route('/survey/begin/<string:survey_token>/<string:answer_token>', type='json', auth='public', website=True)
|
||||
@http.route('/survey/begin/<string:survey_token>/<string:answer_token>', type='jsonrpc', auth='public', website=True)
|
||||
def survey_begin(self, survey_token, answer_token, **post):
|
||||
""" Route used to start the survey user input and display the first survey page. """
|
||||
""" Route used to start the survey user input and display the first survey page.
|
||||
Returns an empty dict for the correct answers and the first page html. """
|
||||
access_data = self._get_access_data(survey_token, answer_token, ensure_token=True)
|
||||
if access_data['validity_code'] is not True:
|
||||
return {'error': access_data['validity_code']}
|
||||
return {}, {'error': access_data['validity_code']}
|
||||
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo']
|
||||
|
||||
if answer_sudo.state != "new":
|
||||
return {'error': _("The survey has already started.")}
|
||||
return {}, {'error': _("The survey has already started.")}
|
||||
|
||||
if 'lang_code' in post:
|
||||
answer_sudo.lang_id = self.env['res.lang']._lang_get(post['lang_code'])
|
||||
answer_sudo._mark_in_progress()
|
||||
return self._prepare_question_html(survey_sudo, answer_sudo, **post)
|
||||
return {}, self._prepare_question_html(survey_sudo, answer_sudo, **post)
|
||||
|
||||
@http.route('/survey/next_question/<string:survey_token>/<string:answer_token>', type='json', auth='public', website=True)
|
||||
@http.route('/survey/next_question/<string:survey_token>/<string:answer_token>', type='jsonrpc', auth='public', website=True)
|
||||
def survey_next_question(self, survey_token, answer_token, **post):
|
||||
""" Method used to display the next survey question in an ongoing session.
|
||||
Triggered on all attendees screens when the host goes to the next question. """
|
||||
access_data = self._get_access_data(survey_token, answer_token, ensure_token=True)
|
||||
if access_data['validity_code'] is not True:
|
||||
return {'error': access_data['validity_code']}
|
||||
return {}, {'error': access_data['validity_code']}
|
||||
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo']
|
||||
|
||||
if answer_sudo.state == 'new' and answer_sudo.is_session_answer:
|
||||
answer_sudo._mark_in_progress()
|
||||
|
||||
return self._prepare_question_html(survey_sudo, answer_sudo, **post)
|
||||
return {}, self._prepare_question_html(survey_sudo, answer_sudo, **post)
|
||||
|
||||
@http.route('/survey/submit/<string:survey_token>/<string:answer_token>', type='json', auth='public', website=True)
|
||||
@http.route('/survey/submit/<string:survey_token>/<string:answer_token>', type='jsonrpc', auth='public', website=True)
|
||||
def survey_submit(self, survey_token, answer_token, **post):
|
||||
""" Submit a page from the survey.
|
||||
This will take into account the validation errors and store the answers to the questions.
|
||||
If the time limit is reached, errors will be skipped, answers will be ignored and
|
||||
survey state will be forced to 'done'"""
|
||||
survey state will be forced to 'done'.
|
||||
Also returns the correct answers if the scoring type is 'scoring_with_answers_after_page'."""
|
||||
# Survey Validation
|
||||
access_data = self._get_access_data(survey_token, answer_token, ensure_token=True)
|
||||
if access_data['validity_code'] is not True:
|
||||
return {'error': access_data['validity_code']}
|
||||
return {}, {'error': access_data['validity_code']}
|
||||
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo']
|
||||
|
||||
if answer_sudo.state == 'done':
|
||||
return {'error': 'unauthorized'}
|
||||
return {}, {'error': 'unauthorized'}
|
||||
|
||||
questions, page_or_question_id = survey_sudo._get_survey_questions(answer=answer_sudo,
|
||||
page_id=post.get('page_id'),
|
||||
|
|
@ -498,7 +540,7 @@ class Survey(http.Controller):
|
|||
|
||||
if not answer_sudo.test_entry and not survey_sudo._has_attempts_left(answer_sudo.partner_id, answer_sudo.email, answer_sudo.invite_token):
|
||||
# prevent cheating with users creating multiple 'user_input' before their last attempt
|
||||
return {'error': 'unauthorized'}
|
||||
return {}, {'error': 'unauthorized'}
|
||||
|
||||
if answer_sudo.survey_time_limit_reached or answer_sudo.question_time_limit_reached:
|
||||
if answer_sudo.question_time_limit_reached:
|
||||
|
|
@ -511,7 +553,7 @@ class Survey(http.Controller):
|
|||
time_limit += timedelta(seconds=10)
|
||||
if fields.Datetime.now() > time_limit:
|
||||
# prevent cheating with users blocking the JS timer and taking all their time to answer
|
||||
return {'error': 'unauthorized'}
|
||||
return {}, {'error': 'unauthorized'}
|
||||
|
||||
errors = {}
|
||||
# Prepare answers / comment by question, validate and save answers
|
||||
|
|
@ -522,30 +564,51 @@ class Survey(http.Controller):
|
|||
answer, comment = self._extract_comment_from_answers(question, post.get(str(question.id)))
|
||||
errors.update(question.validate_question(answer, comment))
|
||||
if not errors.get(question.id):
|
||||
answer_sudo.save_lines(question, answer, comment)
|
||||
answer_sudo._save_lines(question, answer, comment, overwrite_existing=survey_sudo.users_can_go_back or question.save_as_nickname or question.save_as_email)
|
||||
|
||||
if errors and not (answer_sudo.survey_time_limit_reached or answer_sudo.question_time_limit_reached):
|
||||
return {'error': 'validation', 'fields': errors}
|
||||
return {}, {'error': 'validation', 'fields': errors}
|
||||
|
||||
if not answer_sudo.is_session_answer:
|
||||
answer_sudo._clear_inactive_conditional_answers()
|
||||
|
||||
# Get the page questions correct answers if scoring type is scoring after page
|
||||
correct_answers = {}
|
||||
if survey_sudo.scoring_type == 'scoring_with_answers_after_page':
|
||||
scorable_questions = (questions - answer_sudo._get_inactive_conditional_questions()).filtered('is_scored_question')
|
||||
correct_answers = scorable_questions._get_correct_answers()
|
||||
|
||||
if answer_sudo.survey_time_limit_reached or survey_sudo.questions_layout == 'one_page':
|
||||
answer_sudo._mark_done()
|
||||
elif 'previous_page_id' in post:
|
||||
# when going back, save the last displayed to reload the survey where the user left it.
|
||||
answer_sudo.write({'last_displayed_page_id': post['previous_page_id']})
|
||||
answer_sudo.last_displayed_page_id = post['previous_page_id']
|
||||
# Go back to specific page using the breadcrumb. Lines are saved and survey continues
|
||||
return self._prepare_question_html(survey_sudo, answer_sudo, **post)
|
||||
return correct_answers, self._prepare_question_html(survey_sudo, answer_sudo, **post)
|
||||
elif 'next_skipped_page_or_question' in post:
|
||||
answer_sudo.last_displayed_page_id = page_or_question_id
|
||||
return correct_answers, self._prepare_question_html(survey_sudo, answer_sudo, next_skipped_page=True)
|
||||
else:
|
||||
if not answer_sudo.is_session_answer:
|
||||
next_page = survey_sudo._get_next_page_or_question(answer_sudo, page_or_question_id)
|
||||
page_or_question = request.env['survey.question'].sudo().browse(page_or_question_id)
|
||||
if answer_sudo.survey_first_submitted and answer_sudo._is_last_skipped_page_or_question(page_or_question):
|
||||
next_page = request.env['survey.question']
|
||||
else:
|
||||
next_page = survey_sudo._get_next_page_or_question(answer_sudo, page_or_question_id)
|
||||
if not next_page:
|
||||
answer_sudo._mark_done()
|
||||
if survey_sudo.users_can_go_back and answer_sudo.user_input_line_ids.filtered(
|
||||
lambda a: a.skipped and a.question_id.constr_mandatory):
|
||||
answer_sudo.write({
|
||||
'last_displayed_page_id': page_or_question_id,
|
||||
'survey_first_submitted': True,
|
||||
})
|
||||
return correct_answers, self._prepare_question_html(survey_sudo, answer_sudo, next_skipped_page=True)
|
||||
else:
|
||||
answer_sudo._mark_done()
|
||||
|
||||
answer_sudo.write({'last_displayed_page_id': page_or_question_id})
|
||||
answer_sudo.last_displayed_page_id = page_or_question_id
|
||||
|
||||
return self._prepare_question_html(survey_sudo, answer_sudo)
|
||||
return correct_answers, self._prepare_question_html(survey_sudo, answer_sudo)
|
||||
|
||||
def _extract_comment_from_answers(self, question, answers):
|
||||
""" Answers is a custom structure depending of the question type
|
||||
|
|
@ -600,9 +663,9 @@ class Survey(http.Controller):
|
|||
def survey_print(self, survey_token, review=False, answer_token=None, **post):
|
||||
'''Display an survey in printable view; if <answer_token> is set, it will
|
||||
grab the answers of the user_input_id that has <answer_token>.'''
|
||||
access_data = self._get_access_data(survey_token, answer_token, ensure_token='survey_only', check_partner=False)
|
||||
access_data = self._get_access_data(survey_token, answer_token, ensure_token=False, check_partner=False)
|
||||
if access_data['validity_code'] is not True and (
|
||||
access_data['has_survey_access'] or
|
||||
not access_data['has_survey_access'] or
|
||||
access_data['validity_code'] not in ['token_required', 'survey_closed', 'survey_void', 'answer_deadline']):
|
||||
return self._redirect_with_error(access_data, access_data['validity_code'])
|
||||
|
||||
|
|
@ -613,9 +676,11 @@ class Survey(http.Controller):
|
|||
'survey': survey_sudo,
|
||||
'answer': answer_sudo if survey_sudo.scoring_type != 'scoring_without_answers' else answer_sudo.browse(),
|
||||
'questions_to_display': answer_sudo._get_print_questions(),
|
||||
'scoring_display_correction': survey_sudo.scoring_type == 'scoring_with_answers' and answer_sudo,
|
||||
'scoring_display_correction': survey_sudo.scoring_type in ['scoring_with_answers', 'scoring_with_answers_after_page'] and answer_sudo,
|
||||
'format_datetime': lambda dt: format_datetime(request.env, dt, dt_format=False),
|
||||
'format_date': lambda date: format_date(request.env, date),
|
||||
'graph_data': json.dumps(answer_sudo._prepare_statistics()[answer_sudo])
|
||||
if answer_sudo and survey_sudo.scoring_type in ['scoring_with_answers', 'scoring_with_answers_after_page'] else False,
|
||||
})
|
||||
|
||||
@http.route('/survey/<model("survey.survey"):survey>/certification_preview', type="http", auth="user", website=True)
|
||||
|
|
@ -713,55 +778,153 @@ class Survey(http.Controller):
|
|||
('Content-Disposition', report_content_disposition),
|
||||
])
|
||||
|
||||
def _get_user_input_domain(self, survey, line_filter_domain, **post):
|
||||
user_input_domain = ['&', ('test_entry', '=', False), ('survey_id', '=', survey.id)]
|
||||
if line_filter_domain:
|
||||
matching_line_ids = request.env['survey.user_input.line'].sudo().search(line_filter_domain).ids
|
||||
user_input_domain = expression.AND([
|
||||
[('user_input_line_ids', 'in', matching_line_ids)],
|
||||
user_input_domain
|
||||
])
|
||||
def _get_results_page_user_input_domain(self, survey, **post):
|
||||
user_input_domains = []
|
||||
if post.get('finished'):
|
||||
user_input_domain = expression.AND([[('state', '=', 'done')], user_input_domain])
|
||||
user_input_domains.append(Domain('state', '=', 'done'))
|
||||
else:
|
||||
user_input_domain = expression.AND([[('state', '!=', 'new')], user_input_domain])
|
||||
user_input_domains.append(Domain('state', '!=', 'new'))
|
||||
if post.get('failed'):
|
||||
user_input_domain = expression.AND([[('scoring_success', '=', False)], user_input_domain])
|
||||
user_input_domains.append(Domain('scoring_success', '=', False))
|
||||
elif post.get('passed'):
|
||||
user_input_domain = expression.AND([[('scoring_success', '=', True)], user_input_domain])
|
||||
user_input_domains.append(Domain('scoring_success', '=', True))
|
||||
|
||||
return user_input_domain
|
||||
user_input_domains.extend((Domain('test_entry', '=', False), Domain('survey_id', '=', survey.id)))
|
||||
return Domain.AND(user_input_domains)
|
||||
|
||||
def _extract_filters_data(self, survey, post):
|
||||
search_filters = []
|
||||
line_filter_domain, line_choices = [], []
|
||||
for data in post.get('filters', '').split('|'):
|
||||
try:
|
||||
row_id, answer_id = (int(item) for item in data.split(','))
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
if row_id and answer_id:
|
||||
line_filter_domain = expression.AND([
|
||||
['&', ('matrix_row_id', '=', row_id), ('suggested_answer_id', '=', answer_id)],
|
||||
line_filter_domain
|
||||
])
|
||||
answers = request.env['survey.question.answer'].browse([row_id, answer_id])
|
||||
elif answer_id:
|
||||
line_choices.append(answer_id)
|
||||
answers = request.env['survey.question.answer'].browse([answer_id])
|
||||
if answer_id:
|
||||
question_id = answers[0].matrix_question_id or answers[0].question_id
|
||||
search_filters.append({
|
||||
'row_id': row_id,
|
||||
'answer_id': answer_id,
|
||||
'question': question_id.title,
|
||||
'answers': '%s%s' % (answers[0].value, ': %s' % answers[1].value if len(answers) > 1 else '')
|
||||
})
|
||||
if line_choices:
|
||||
line_filter_domain = expression.AND([[('suggested_answer_id', 'in', line_choices)], line_filter_domain])
|
||||
""" Extracts the filters from the URL to returns the related user_input_lines and
|
||||
the parameters used to render/remove the filters on the results page (search_filters).
|
||||
|
||||
user_input_domain = self._get_user_input_domain(survey, line_filter_domain, **post)
|
||||
user_input_lines = request.env['survey.user_input'].sudo().search(user_input_domain).mapped('user_input_line_ids')
|
||||
The matching user_input_lines are all the lines tied to the user inputs which respect
|
||||
the survey base domain and which have lines matching all the filters.
|
||||
For example, with the filter 'Where do you live?|Brussels', we need to display ALL the lines
|
||||
of the survey user inputs which have answered 'Brussels' to this question.
|
||||
|
||||
:return (recordset, List[dict]): all matching user input lines, each search filter data
|
||||
"""
|
||||
user_input_line_subdomains = []
|
||||
search_filters = []
|
||||
|
||||
answer_by_column, user_input_lines_ids = self._get_filters_from_post(post)
|
||||
|
||||
# Matrix, Multiple choice, Simple choice filters
|
||||
if answer_by_column:
|
||||
answer_ids, row_ids = [], []
|
||||
for answer_column_id, answer_row_ids in answer_by_column.items():
|
||||
answer_ids.append(answer_column_id)
|
||||
row_ids += answer_row_ids
|
||||
|
||||
answers_and_rows = request.env['survey.question.answer'].browse(answer_ids+row_ids)
|
||||
# For performance, accessing 'a.matrix_question_id' caches all useful fields of the
|
||||
# answers and rows records, avoiding unnecessary queries.
|
||||
answers = answers_and_rows.filtered(lambda a: not a.matrix_question_id)
|
||||
|
||||
for answer in answers:
|
||||
if not answer_by_column[answer.id]:
|
||||
# Simple/Multiple choice
|
||||
user_input_line_subdomains.append(answer._get_answer_matching_domain())
|
||||
search_filters.append(self._prepare_search_filter_answer(answer))
|
||||
else:
|
||||
# Matrix
|
||||
for row_id in answer_by_column[answer.id]:
|
||||
row = answers_and_rows.filtered(lambda answer_or_row: answer_or_row.id == row_id)
|
||||
user_input_line_subdomains.append(answer._get_answer_matching_domain(row_id))
|
||||
search_filters.append(self._prepare_search_filter_answer(answer, row))
|
||||
|
||||
# Char_box, Text_box, Numerical_box, Date, Datetime filters
|
||||
if user_input_lines_ids:
|
||||
user_input_lines = request.env['survey.user_input.line'].browse(user_input_lines_ids)
|
||||
for input_line in user_input_lines:
|
||||
user_input_line_subdomains.append(input_line._get_answer_matching_domain())
|
||||
search_filters.append(self._prepare_search_filter_input_line(input_line))
|
||||
|
||||
# Compute base domain
|
||||
user_input_domain = self._get_results_page_user_input_domain(survey, **post)
|
||||
|
||||
# Add filters domain to the base domain
|
||||
if user_input_line_subdomains:
|
||||
all_required_lines_domains = [
|
||||
[('user_input_line_ids', 'in', request.env['survey.user_input.line'].sudo()._search(subdomain))]
|
||||
for subdomain in user_input_line_subdomains
|
||||
]
|
||||
user_input_domain = Domain.AND([user_input_domain, *all_required_lines_domains])
|
||||
|
||||
# Get the matching user input lines
|
||||
user_inputs_query = request.env['survey.user_input'].sudo()._search(user_input_domain)
|
||||
user_input_lines = request.env['survey.user_input.line'].search([('user_input_id', 'in', user_inputs_query)])
|
||||
|
||||
return user_input_lines, search_filters
|
||||
|
||||
def _get_filters_from_post(self, post):
|
||||
""" Extract the filters from post depending on the model that needs to be called to retrieve the filtered answer data.
|
||||
Simple choice and multiple choice question types are mapped onto empty row_id.
|
||||
Input/output example with respectively matrix, simple_choice and char_box filters:
|
||||
input: 'A,1,24|A,0,13|L,0,36'
|
||||
output:
|
||||
answer_by_column: {24: [1], 13: []}
|
||||
user_input_lines_ids: [36]
|
||||
|
||||
* Model short key = 'A' : Match a `survey.question.answer` record (simple_choice, multiple_choice, matrix)
|
||||
* Model short key = 'L' : Match a `survey.user_input.line` record (char_box, text_box, numerical_box, date, datetime)
|
||||
:rtype: (collections.defaultdict[int, list[int]], list[int])
|
||||
"""
|
||||
answer_by_column = defaultdict(list)
|
||||
user_input_lines_ids = []
|
||||
|
||||
for data in post.get('filters', '').split('|'):
|
||||
if not data:
|
||||
break
|
||||
model_short_key, row_id, answer_id = data.split(',')
|
||||
row_id, answer_id = int(row_id), int(answer_id)
|
||||
if model_short_key == 'A':
|
||||
if row_id:
|
||||
answer_by_column[answer_id].append(row_id)
|
||||
else:
|
||||
answer_by_column[answer_id] = []
|
||||
elif model_short_key == 'L' and not row_id:
|
||||
user_input_lines_ids.append(answer_id)
|
||||
|
||||
return answer_by_column, user_input_lines_ids
|
||||
|
||||
def _prepare_search_filter_answer(self, answer, row=False):
|
||||
""" Format parameters used to render/remove this filter on the results page."""
|
||||
return {
|
||||
'question_id': answer.question_id.id,
|
||||
'question': answer.question_id.title,
|
||||
'row_id': row.id if row else 0,
|
||||
'answer': '%s : %s' % (row.value, answer.value) if row else answer.value,
|
||||
'model_short_key': 'A',
|
||||
'record_id': answer.id,
|
||||
}
|
||||
|
||||
def _prepare_search_filter_input_line(self, user_input_line):
|
||||
""" Format parameters used to render/remove this filter on the results page."""
|
||||
return {
|
||||
'question_id': user_input_line.question_id.id,
|
||||
'question': user_input_line.question_id.title,
|
||||
'row_id': 0,
|
||||
'answer': user_input_line._get_answer_value(),
|
||||
'model_short_key': 'L',
|
||||
'record_id': user_input_line.id,
|
||||
}
|
||||
|
||||
def _get_lang_with_fallback(self, user_input):
|
||||
""" :return: the most suitable language for the user that is supported by the survey. """
|
||||
user_input.ensure_one()
|
||||
user_input_sudo = user_input.sudo()
|
||||
if user_input_sudo.lang_id:
|
||||
return user_input_sudo.lang_id.sudo(False)
|
||||
lang_code = self.env.context.get('lang') or self.env['ir.http']._get_default_lang().code
|
||||
ResLang = self.env['res.lang']
|
||||
supported_lang_codes = user_input_sudo.survey_id._get_supported_lang_codes()
|
||||
supported_lang_codes_set = set(supported_lang_codes)
|
||||
if lang_code in supported_lang_codes_set:
|
||||
return ResLang._lang_get(lang_code)
|
||||
# Take the first frontend language supported by the survey and if there are none, the first survey language
|
||||
return ResLang._lang_get(
|
||||
next((
|
||||
lang.code
|
||||
for lang in self.env['res.lang']._get_frontend().values()
|
||||
if lang['code'] in supported_lang_codes_set
|
||||
), supported_lang_codes[0]))
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ class UserInputSession(http.Controller):
|
|||
# Note that at this stage survey.session_state can be False meaning that the survey has ended (session closed)
|
||||
return request.render('survey.user_input_session_manage', self._prepare_manage_session_values(survey))
|
||||
|
||||
@http.route('/survey/session/next_question/<string:survey_token>', type='json', auth='user', website=True)
|
||||
@http.route('/survey/session/next_question/<string:survey_token>', type='jsonrpc', auth='user', website=True)
|
||||
def survey_session_next_question(self, survey_token, go_back=False, **kwargs):
|
||||
""" This route is called when the host goes to the next question of the session.
|
||||
|
||||
|
|
@ -120,7 +120,7 @@ class UserInputSession(http.Controller):
|
|||
else:
|
||||
return {}
|
||||
|
||||
@http.route('/survey/session/results/<string:survey_token>', type='json', auth='user', website=True)
|
||||
@http.route('/survey/session/results/<string:survey_token>', type='jsonrpc', auth='user', website=True)
|
||||
def survey_session_results(self, survey_token, **kwargs):
|
||||
""" This route is called when the host shows the current question's results.
|
||||
|
||||
|
|
@ -141,7 +141,7 @@ class UserInputSession(http.Controller):
|
|||
|
||||
return self._prepare_question_results_values(survey, user_input_lines)
|
||||
|
||||
@http.route('/survey/session/leaderboard/<string:survey_token>', type='json', auth='user', website=True)
|
||||
@http.route('/survey/session/leaderboard/<string:survey_token>', type='jsonrpc', auth='user', website=True)
|
||||
def survey_session_leaderboard(self, survey_token, **kwargs):
|
||||
""" This route is called when the host shows the current question's attendees leaderboard.
|
||||
|
||||
|
|
@ -182,7 +182,7 @@ class UserInputSession(http.Controller):
|
|||
dict(**survey_error, session_code=session_code))
|
||||
return request.redirect(survey.get_start_url())
|
||||
|
||||
@http.route('/survey/check_session_code/<string:session_code>', type='json', auth='public', website=True)
|
||||
@http.route('/survey/check_session_code/<string:session_code>', type='jsonrpc', auth='public', website=True)
|
||||
def survey_check_session_code(self, session_code):
|
||||
""" Checks if the given code is matching a survey session_code.
|
||||
If yes, redirect to /s/code route.
|
||||
|
|
@ -206,6 +206,14 @@ class UserInputSession(http.Controller):
|
|||
'is_session_closed': not survey.session_state,
|
||||
}
|
||||
|
||||
if is_last_question:
|
||||
_, triggered_questions_by_answer = survey._get_conditional_maps()
|
||||
next_question = survey.session_question_id
|
||||
values['survey_last_triggering_answers'] = [
|
||||
answer.id for answer in triggered_questions_by_answer
|
||||
if answer in next_question.suggested_answer_ids and any(q.sequence > next_question.sequence for q in triggered_questions_by_answer[answer])
|
||||
]
|
||||
|
||||
values.update(self._prepare_question_results_values(survey, request.env['survey.user_input.line']))
|
||||
|
||||
return values
|
||||
|
|
@ -224,9 +232,14 @@ class UserInputSession(http.Controller):
|
|||
(and not the id or anything else we can identify).
|
||||
In other words, we need to know if the answer at index 2 is correct or not.
|
||||
- answer_count
|
||||
The number of answers to the current question. """
|
||||
The number of answers to the current question.
|
||||
- selected_answers
|
||||
The current question selected answers.
|
||||
"""
|
||||
|
||||
question = survey.session_question_id
|
||||
if not question:
|
||||
return {}
|
||||
answers_validity = []
|
||||
if (any(answer.is_correct for answer in question.suggested_answer_ids)):
|
||||
answers_validity = [answer.is_correct for answer in question.suggested_answer_ids]
|
||||
|
|
@ -248,4 +261,5 @@ class UserInputSession(http.Controller):
|
|||
'answers_validity': json.dumps(answers_validity),
|
||||
'answer_count': survey.session_question_answer_count,
|
||||
'attendees_count': survey.session_answer_count,
|
||||
'selected_answers': user_input_lines.suggested_answer_id.ids,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="action_survey_print" model="ir.actions.server">
|
||||
<field name="name">Print Survey</field>
|
||||
<field name="model_id" ref="survey.model_survey_survey"/>
|
||||
<field name="binding_model_id" ref="survey.model_survey_survey" />
|
||||
<field name="binding_view_types">form</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
if record:
|
||||
action = record.action_print_survey()
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="mt_survey_user_input_completed" model="mail.message.subtype">
|
||||
<field name="name">Participation completed</field>
|
||||
<field name="res_model">survey.user_input</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="description">Participation completed.</field>
|
||||
<field name="hidden" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="mt_survey_survey_user_input_completed" model="mail.message.subtype">
|
||||
<field name="name">Participation completed</field>
|
||||
<field name="res_model">survey.survey</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="hidden" eval="False"/>
|
||||
<field name="description">New participation completed.</field>
|
||||
<field name="parent_id" ref="mt_survey_user_input_completed"/>
|
||||
<field name="relation_field">survey_id</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -6,7 +6,8 @@
|
|||
<field name="model_id" ref="model_survey_user_input" />
|
||||
<field name="subject">Participate to {{ object.survey_id.display_name }} survey</field>
|
||||
<field name="email_from">{{ user.email_formatted }}</field>
|
||||
<field name="email_to">{{ (object.partner_id.email_formatted or object.email) }}</field>
|
||||
<field name="email_to" eval="False"/>
|
||||
<field name="use_default_to" eval="True"/>
|
||||
<field name="description">Sent to participant when you share a survey</field>
|
||||
<field name="body_html" type="html">
|
||||
<div style="margin: 0px; padding: 0px; font-size: 13px;">
|
||||
|
|
@ -20,7 +21,7 @@
|
|||
</t>
|
||||
<div style="margin: 16px 0px 16px 0px;">
|
||||
<a t-att-href="(object.get_start_url())"
|
||||
style="background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">
|
||||
t-attf-style="background-color: {{user.company_id.email_secondary_color or '#875A7B'}}; padding: 8px 16px 8px 16px; text-decoration: none; color: {{user.company_id.email_primary_color or '#FFFFFF'}}; border-radius: 5px; font-size:13px;">
|
||||
<t t-if="object.survey_id.certification">
|
||||
Start Certification
|
||||
</t>
|
||||
|
|
@ -41,7 +42,6 @@
|
|||
</p>
|
||||
</div>
|
||||
</field>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
|
|
@ -51,7 +51,8 @@
|
|||
<field name="model_id" ref="survey.model_survey_user_input"/>
|
||||
<field name="subject">Certification: {{ object.survey_id.display_name }}</field>
|
||||
<field name="email_from">{{ (object.survey_id.create_uid.email_formatted or user.email_formatted or user.company_id.catchall_formatted) }}</field>
|
||||
<field name="email_to">{{ (object.partner_id.email_formatted or object.email) }}</field>
|
||||
<field name="email_to" eval="False"/>
|
||||
<field name="use_default_to" eval="True"/>
|
||||
<field name="description">Sent to participant if they succeeded the certification</field>
|
||||
<field name="body_html" type="html">
|
||||
<div style="background:#F0F0F0;color:#515166;padding:10px 0px;font-family:Arial,Helvetica,sans-serif;font-size:14px;">
|
||||
|
|
@ -59,7 +60,9 @@
|
|||
<tbody>
|
||||
<tr><td>
|
||||
<!-- We use the logo of the company that created the survey (to handle multi company cases) -->
|
||||
<a href="/"><img t-attf-src="/logo.png?company={{ object.survey_id.create_uid.company_id.id }}" style="vertical-align:baseline;max-width:100px;" /></a>
|
||||
<a href="/"><img t-if="not object.survey_id.create_uid.company_id.uses_default_logo"
|
||||
t-attf-src="/logo.png?company={{ object.survey_id.create_uid.company_id.id }}"
|
||||
style="vertical-align:baseline;max-width:100px;" /></a>
|
||||
</td><td style="text-align:right;vertical-align:middle;">
|
||||
Certification: <t t-out="object.survey_id.display_name or ''">Feedback Form</t>
|
||||
</td></tr>
|
||||
|
|
@ -74,15 +77,13 @@
|
|||
<strong t-out="object.survey_id.display_name or ''">Furniture Creation</strong>
|
||||
certification
|
||||
</p>
|
||||
<p>Congratulations for passing the test with a score of <strong t-out="object.scoring_percentage"/>% !</p>
|
||||
<p>Congratulations for passing the test with a score of <strong t-out="object.scoring_percentage"/>%!</p>
|
||||
</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</field>
|
||||
<field name="report_template" ref="certification_report"/>
|
||||
<field name="report_name">Certification Document</field>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="report_template_ids" eval="[(4, ref('survey.certification_report'))]"/>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
</data>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Grant survey permissions to demo user -->
|
||||
<record id="base.user_demo" model="res.users">
|
||||
<field eval="[(4, ref('group_survey_manager'))]" name="groups_id"/>
|
||||
<field name="group_ids" eval="[(4, ref('survey.group_survey_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="base.default_user_group" model="res.groups">
|
||||
<field name="implied_ids" eval="[(4, ref('survey.group_survey_user'))]"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
<field name="certification_give_badge">True</field>
|
||||
<field name="certification_badge_id" ref="vendor_certification_badge"/>
|
||||
<field name="background_image" type="base64" file="survey/static/src/img/survey_background_2.jpg"/>
|
||||
<field name="lang_ids" eval="False"/>
|
||||
</record>
|
||||
<!-- Page 1 -->
|
||||
<record model="survey.question" id="vendor_certification_page_1">
|
||||
|
|
@ -336,7 +337,7 @@
|
|||
<field name="is_scored_question" eval="True" />
|
||||
<field name="answer_datetime">2021-01-07 00:00:01</field>
|
||||
<field name="answer_score">1.0</field>
|
||||
<field name="question_placeholder">Beware of leap years !</field>
|
||||
<field name="question_placeholder">Beware of leap years!</field>
|
||||
</record>
|
||||
<!-- Question and predefined answer 12 -->
|
||||
<record model="survey.question" id="vendor_certification_page_3_question_4">
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@
|
|||
<field name="is_time_limited" >limited</field>
|
||||
<field name="time_limit" >10.0</field>
|
||||
<field name="questions_layout">page_per_question</field>
|
||||
<field name="lang_ids" eval="False"/>
|
||||
<field name="description" type="html">
|
||||
<p>Choose your favourite subject and show how good you are. Ready ?</p></field>
|
||||
<field name="background_image" type="base64" file="survey/static/src/img/burger_quiz_background.jpg"/>
|
||||
<p>Choose your favourite subject and show how good you are. Ready?</p></field>
|
||||
<field name="background_image" type="base64" file="survey/static/src/img/burger_quiz_background.webp"/>
|
||||
</record>
|
||||
|
||||
<!-- Page 1: Start -->
|
||||
|
|
@ -67,9 +68,7 @@
|
|||
<field name="title">How long is the White Nile river?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_conditional" eval="True"/>
|
||||
<field name="triggering_question_id" ref="survey_demo_burger_quiz_p1_q1"/>
|
||||
<field name="triggering_answer_id" ref="survey_demo_burger_quiz_p1_q1_sug1"/>
|
||||
<field name="triggering_answer_ids" eval="[Command.link(ref('survey.survey_demo_burger_quiz_p1_q1_sug1'))]"/>
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p2_q1_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p2_q1"/>
|
||||
|
|
@ -93,12 +92,10 @@
|
|||
<record id="survey_demo_burger_quiz_p2_q2" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_burger_quiz"/>
|
||||
<field name="sequence">120</field>
|
||||
<field name="title">What is the biggest city in the world ?</field>
|
||||
<field name="title">What is the biggest city in the world?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_conditional" eval="True"/>
|
||||
<field name="triggering_question_id" ref="survey_demo_burger_quiz_p1_q1"/>
|
||||
<field name="triggering_answer_id" ref="survey_demo_burger_quiz_p1_q1_sug1"/>
|
||||
<field name="triggering_answer_ids" eval="[Command.link(ref('survey.survey_demo_burger_quiz_p1_q1_sug1'))]"/>
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p2_q2_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p2_q2"/>
|
||||
|
|
@ -126,12 +123,10 @@
|
|||
<record id="survey_demo_burger_quiz_p2_q3" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_burger_quiz"/>
|
||||
<field name="sequence">130</field>
|
||||
<field name="title">Which is the highest volcano in Europe ?</field>
|
||||
<field name="title">Which is the highest volcano in Europe?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_conditional" eval="True"/>
|
||||
<field name="triggering_question_id" ref="survey_demo_burger_quiz_p1_q1"/>
|
||||
<field name="triggering_answer_id" ref="survey_demo_burger_quiz_p1_q1_sug1"/>
|
||||
<field name="triggering_answer_ids" eval="[Command.link(ref('survey.survey_demo_burger_quiz_p1_q1_sug1'))]"/>
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p2_q3_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p2_q3"/>
|
||||
|
|
@ -167,12 +162,10 @@
|
|||
<record id="survey_demo_burger_quiz_p3_q1" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_burger_quiz"/>
|
||||
<field name="sequence">210</field>
|
||||
<field name="title">When did Genghis Khan die ?</field>
|
||||
<field name="title">When did Genghis Khan die?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_conditional" eval="True"/>
|
||||
<field name="triggering_question_id" ref="survey_demo_burger_quiz_p1_q1"/>
|
||||
<field name="triggering_answer_id" ref="survey_demo_burger_quiz_p1_q1_sug2"/>
|
||||
<field name="triggering_answer_ids" eval="[Command.link(ref('survey.survey_demo_burger_quiz_p1_q1_sug2'))]"/>
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p3_q1_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p3_q1"/>
|
||||
|
|
@ -195,12 +188,10 @@
|
|||
<record id="survey_demo_burger_quiz_p3_q2" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_burger_quiz"/>
|
||||
<field name="sequence">220</field>
|
||||
<field name="title">Who is the architect of the Great Pyramid of Giza ?</field>
|
||||
<field name="title">Who is the architect of the Great Pyramid of Giza?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_conditional" eval="True"/>
|
||||
<field name="triggering_question_id" ref="survey_demo_burger_quiz_p1_q1"/>
|
||||
<field name="triggering_answer_id" ref="survey_demo_burger_quiz_p1_q1_sug2"/>
|
||||
<field name="triggering_answer_ids" eval="[Command.link(ref('survey.survey_demo_burger_quiz_p1_q1_sug2'))]"/>
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p3_q2_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p3_q2"/>
|
||||
|
|
@ -228,12 +219,10 @@
|
|||
<record id="survey_demo_burger_quiz_p3_q3" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_burger_quiz"/>
|
||||
<field name="sequence">230</field>
|
||||
<field name="title">How many years did the 100 years war last ?</field>
|
||||
<field name="title">How many years did the 100 years war last?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_conditional" eval="True"/>
|
||||
<field name="triggering_question_id" ref="survey_demo_burger_quiz_p1_q1"/>
|
||||
<field name="triggering_answer_id" ref="survey_demo_burger_quiz_p1_q1_sug2"/>
|
||||
<field name="triggering_answer_ids" eval="[Command.link(ref('survey.survey_demo_burger_quiz_p1_q1_sug2'))]"/>
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p3_q3_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p3_q3"/>
|
||||
|
|
@ -269,12 +258,10 @@
|
|||
<record id="survey_demo_burger_quiz_p4_q1" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_burger_quiz"/>
|
||||
<field name="sequence">310</field>
|
||||
<field name="title">Who received a Nobel prize in Physics for the discovery of neutrino oscillations, which shows that neutrinos have mass ?</field>
|
||||
<field name="title">Who received a Nobel prize in Physics for the discovery of neutrino oscillations, which shows that neutrinos have mass?</field>
|
||||
<field name="question_type">multiple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_conditional" eval="True"/>
|
||||
<field name="triggering_question_id" ref="survey_demo_burger_quiz_p1_q1"/>
|
||||
<field name="triggering_answer_id" ref="survey_demo_burger_quiz_p1_q1_sug3"/>
|
||||
<field name="triggering_answer_ids" eval="[Command.link(ref('survey.survey_demo_burger_quiz_p1_q1_sug3'))]"/>
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p4_q1_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p4_q1"/>
|
||||
|
|
@ -304,12 +291,10 @@
|
|||
<record id="survey_demo_burger_quiz_p4_q2" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_burger_quiz"/>
|
||||
<field name="sequence">320</field>
|
||||
<field name="title">What is, approximately, the critical mass of plutonium-239 ?</field>
|
||||
<field name="title">What is, approximately, the critical mass of plutonium-239?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_conditional" eval="True"/>
|
||||
<field name="triggering_question_id" ref="survey_demo_burger_quiz_p1_q1"/>
|
||||
<field name="triggering_answer_id" ref="survey_demo_burger_quiz_p1_q1_sug3"/>
|
||||
<field name="triggering_answer_ids" eval="[Command.link(ref('survey.survey_demo_burger_quiz_p1_q1_sug3'))]"/>
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p4_q2_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p4_q2"/>
|
||||
|
|
@ -337,12 +322,10 @@
|
|||
<record id="survey_demo_burger_quiz_p4_q3" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_burger_quiz"/>
|
||||
<field name="sequence">330</field>
|
||||
<field name="title">Can Humans ever directly see a photon ?</field>
|
||||
<field name="title">Can Humans ever directly see a photon?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_conditional" eval="True"/>
|
||||
<field name="triggering_question_id" ref="survey_demo_burger_quiz_p1_q1"/>
|
||||
<field name="triggering_answer_id" ref="survey_demo_burger_quiz_p1_q1_sug3"/>
|
||||
<field name="triggering_answer_ids" eval="[Command.link(ref('survey.survey_demo_burger_quiz_p1_q1_sug3'))]"/>
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p4_q3_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p4_q3"/>
|
||||
|
|
@ -354,7 +337,7 @@
|
|||
<record id="survey_demo_burger_quiz_p4_q3_sug2" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p4_q3"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">No, it's to small for the human eye.</field>
|
||||
<field name="value">No, it's too small for the human eye.</field>
|
||||
</record>
|
||||
|
||||
<!-- Page 5 : Art & Culture -->
|
||||
|
|
@ -368,12 +351,10 @@
|
|||
<record id="survey_demo_burger_quiz_p5_q1" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_burger_quiz"/>
|
||||
<field name="sequence">410</field>
|
||||
<field name="title">Which Musician is not in the 27th Club ?</field>
|
||||
<field name="title">Which Musician is not in the 27th Club?</field>
|
||||
<field name="question_type">multiple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_conditional" eval="True"/>
|
||||
<field name="triggering_question_id" ref="survey_demo_burger_quiz_p1_q1"/>
|
||||
<field name="triggering_answer_id" ref="survey_demo_burger_quiz_p1_q1_sug4"/>
|
||||
<field name="triggering_answer_ids" eval="[Command.link(ref('survey.survey_demo_burger_quiz_p1_q1_sug4'))]"/>
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p5_q1_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p5_q1"/>
|
||||
|
|
@ -403,12 +384,10 @@
|
|||
<record id="survey_demo_burger_quiz_p5_q2" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_burger_quiz"/>
|
||||
<field name="sequence">420</field>
|
||||
<field name="title">Which painting/drawing was not made by Pablo Picasso ?</field>
|
||||
<field name="title">Which painting/drawing was not made by Pablo Picasso?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_conditional" eval="True"/>
|
||||
<field name="triggering_question_id" ref="survey_demo_burger_quiz_p1_q1"/>
|
||||
<field name="triggering_answer_id" ref="survey_demo_burger_quiz_p1_q1_sug4"/>
|
||||
<field name="triggering_answer_ids" eval="[Command.link(ref('survey.survey_demo_burger_quiz_p1_q1_sug4'))]"/>
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p5_q2_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p5_q2"/>
|
||||
|
|
@ -443,9 +422,7 @@
|
|||
<field name="title">Which quote is from Jean-Claude Van Damme</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_conditional" eval="True"/>
|
||||
<field name="triggering_question_id" ref="survey_demo_burger_quiz_p1_q1"/>
|
||||
<field name="triggering_answer_id" ref="survey_demo_burger_quiz_p1_q1_sug4"/>
|
||||
<field name="triggering_answer_ids" eval="[Command.link(ref('survey.survey_demo_burger_quiz_p1_q1_sug4'))]"/>
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p5_q3_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p5_q3"/>
|
||||
|
|
@ -462,7 +439,7 @@
|
|||
<record id="survey_demo_burger_quiz_p5_q3_sug3" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p5_q3"/>
|
||||
<field name="sequence">3</field>
|
||||
<field name="value">I've been noticing gravity since I was very young !</field> <!-- Cameron Diaz -->
|
||||
<field name="value">I've been noticing gravity since I was very young!</field> <!-- Cameron Diaz -->
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p5_q3_sug4" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p5_q3"/>
|
||||
|
|
@ -470,4 +447,111 @@
|
|||
<field name="value">I actually don't like thinking. I think people think I like to think a lot. And I don't. I do not like to think at all.</field> <!-- Kanye West -->
|
||||
</record>
|
||||
|
||||
<!-- Multiple triggers -->
|
||||
<record id="survey_demo_food_preferences" model="survey.survey">
|
||||
<field name="title">Food Preferences</field>
|
||||
<field name="survey_type">survey</field>
|
||||
<field name="access_token">foodpref-eren-ces1-abcd-344ca2tgb31e</field>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="access_mode">public</field>
|
||||
<field name="questions_layout">one_page</field>
|
||||
<field name="lang_ids" eval="False"/>
|
||||
<field name="description" type="html">
|
||||
<p>Please give us your preferences for this event's dinner!</p>
|
||||
</field>
|
||||
<field name="description_done" type="html">
|
||||
<p>Got it!</p>
|
||||
<p>See you soon!</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="survey_demo_food_preferences_q1" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_food_preferences"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="title">Are you vegetarian?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
</record>
|
||||
<record id="survey_demo_food_preferences_q1_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_food_preferences_q1"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="value">Yes</field>
|
||||
</record>
|
||||
<record id="survey_demo_food_preferences_q1_sug2" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_food_preferences_q1"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">No</field>
|
||||
</record>
|
||||
<record id="survey_demo_food_preferences_q1_sug3" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_food_preferences_q1"/>
|
||||
<field name="sequence">3</field>
|
||||
<field name="value">It depends</field>
|
||||
</record>
|
||||
|
||||
<record id="survey_demo_food_preferences_q2" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_food_preferences"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="title">Would you prefer a veggie meal if possible?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="triggering_answer_ids" eval="[
|
||||
Command.link(ref('survey.survey_demo_food_preferences_q1_sug3')),
|
||||
]"/>
|
||||
|
||||
</record>
|
||||
<record id="survey_demo_food_preferences_q2_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_food_preferences_q2"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="value">Yes</field>
|
||||
</record>
|
||||
<record id="survey_demo_food_preferences_q2_sug2" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_food_preferences_q2"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">No</field>
|
||||
</record>
|
||||
|
||||
<record id="survey_demo_food_preferences_q3" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_food_preferences"/>
|
||||
<field name="sequence">3</field>
|
||||
<field name="title">Choose your green meal</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="triggering_answer_ids" eval="[
|
||||
Command.link(ref('survey.survey_demo_food_preferences_q1_sug1')),
|
||||
Command.link(ref('survey.survey_demo_food_preferences_q2_sug1')),
|
||||
]"/>
|
||||
</record>
|
||||
<record id="survey_demo_food_preferences_q3_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_food_preferences_q3"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="value">Vegetarian pizza</field>
|
||||
</record>
|
||||
<record id="survey_demo_food_preferences_q3_sug2" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_food_preferences_q3"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">Vegetarian burger</field>
|
||||
</record>
|
||||
|
||||
<record id="survey_demo_food_preferences_q4" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_food_preferences"/>
|
||||
<field name="sequence">4</field>
|
||||
<field name="title">Choose your meal</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="triggering_answer_ids" eval="[
|
||||
Command.link(ref('survey.survey_demo_food_preferences_q1_sug2')),
|
||||
Command.link(ref('survey.survey_demo_food_preferences_q2_sug2')),
|
||||
]"/>
|
||||
</record>
|
||||
<record id="survey_demo_food_preferences_q4_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_food_preferences_q4"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="value">Steak with french fries</field>
|
||||
</record>
|
||||
<record id="survey_demo_food_preferences_q4_sug2" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_food_preferences_q4"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">Fish</field>
|
||||
</record>
|
||||
|
||||
</data></odoo>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
<field name="access_mode">public</field>
|
||||
<field name="users_can_go_back" eval="True" />
|
||||
<field name="questions_layout">page_per_section</field>
|
||||
<field name="lang_ids" eval="False"/>
|
||||
<field name="description" type="html">
|
||||
<p>This survey allows you to give a feedback about your experience with our products.
|
||||
Filling it helps us improving your experience.</p></field>
|
||||
|
|
@ -26,7 +27,7 @@
|
|||
<record model="survey.question" id="survey_feedback_p1_q1">
|
||||
<field name="survey_id" ref="survey_feedback" />
|
||||
<field name="sequence">2</field>
|
||||
<field name="title">Where do you live ?</field>
|
||||
<field name="title">Where do you live?</field>
|
||||
<field name="question_type">char_box</field>
|
||||
<field name="question_placeholder">Brussels</field>
|
||||
<field name="constr_mandatory" eval="False"/>
|
||||
|
|
@ -34,14 +35,14 @@
|
|||
<record model="survey.question" id="survey_feedback_p1_q2">
|
||||
<field name="survey_id" ref="survey_feedback" />
|
||||
<field name="sequence">3</field>
|
||||
<field name="title">When is your date of birth ?</field>
|
||||
<field name="title">When is your date of birth?</field>
|
||||
<field name="question_type">date</field>
|
||||
<field name="constr_mandatory" eval="False"/>
|
||||
</record>
|
||||
<record model="survey.question" id="survey_feedback_p1_q3">
|
||||
<field name="survey_id" ref="survey_feedback" />
|
||||
<field name="sequence">4</field>
|
||||
<field name="title">How frequently do you buy products online ?</field>
|
||||
<field name="title">How frequently do you buy products online?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="comments_allowed" eval="True"/>
|
||||
<field name="comment_count_as_answer" eval="True"/>
|
||||
|
|
@ -70,7 +71,7 @@
|
|||
<record model="survey.question" id="survey_feedback_p1_q4">
|
||||
<field name="survey_id" ref="survey_feedback" />
|
||||
<field name="sequence">5</field>
|
||||
<field name="title">How many times did you order products on our website ?</field>
|
||||
<field name="title">How many times did you order products on our website?</field>
|
||||
<field name="question_type">numerical_box</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
</record>
|
||||
|
|
@ -88,7 +89,7 @@
|
|||
<record model="survey.question" id="survey_feedback_p2_q1">
|
||||
<field name="survey_id" ref="survey_feedback" />
|
||||
<field name="sequence">7</field>
|
||||
<field name="title">Which of the following words would you use to describe our products ?</field>
|
||||
<field name="title">Which of the following words would you use to describe our products?</field>
|
||||
<field name="question_type">multiple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="comments_allowed" eval="True"/>
|
||||
|
|
@ -137,7 +138,7 @@
|
|||
<record model="survey.question" id="survey_feedback_p2_q2">
|
||||
<field name="survey_id" ref="survey_feedback" />
|
||||
<field name="sequence">8</field>
|
||||
<field name="title">What do you think about our new eCommerce ?</field>
|
||||
<field name="title">What do you think about our new eCommerce?</field>
|
||||
<field name="question_type">matrix</field>
|
||||
<field name="matrix_subtype">multiple</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
|
|
@ -190,7 +191,7 @@
|
|||
<record model="survey.question" id="survey_feedback_p2_q3">
|
||||
<field name="survey_id" ref="survey_feedback" />
|
||||
<field name="sequence">9</field>
|
||||
<field name="title">Do you have any other comments, questions, or concerns ?</field>
|
||||
<field name="title">Do you have any other comments, questions, or concerns?</field>
|
||||
<field name="question_type">text_box</field>
|
||||
<field name="constr_mandatory" eval="False"/>
|
||||
</record>
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@
|
|||
<field name="user_input_id" ref="survey_answer_3"/>
|
||||
<field name="question_id" ref="survey_feedback_p2_q3"/>
|
||||
<field name="answer_type">text_box</field>
|
||||
<field name="value_text_box">The customizable desk received is not the one I ordered on your website and the quality is very poor ! Really disappointed.</field>
|
||||
<field name="value_text_box">The customizable desk received is not the one I ordered on your website and the quality is very poor! Really disappointed.</field>
|
||||
</record>
|
||||
|
||||
</data></odoo>
|
||||
|
|
|
|||
|
|
@ -1,576 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="0">
|
||||
|
||||
<record id="survey_demo_quiz" model="survey.survey">
|
||||
<field name="title">Quiz about our Company</field>
|
||||
<field name="access_token">b137640d-9876-1234-abcd-344ca256531e</field>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="access_mode">public</field>
|
||||
<field name="users_can_go_back" eval="False"/>
|
||||
<field name="scoring_type">scoring_with_answers</field>
|
||||
<field name="scoring_success_min">55</field>
|
||||
<field name="questions_layout">page_per_question</field>
|
||||
<field name="description" type="html">
|
||||
<p>This small quiz will test your knowledge about our Company. Be prepared !</p></field>
|
||||
<field name="background_image" type="base64" file="survey/static/src/img/survey_background.jpg"/>
|
||||
</record>
|
||||
|
||||
<!-- Page 1: general informations -->
|
||||
<record id="survey_demo_quiz_p1" model="survey.question">
|
||||
<field name="title">Who are you ?</field>
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="question_type" eval="False"/>
|
||||
<field name="is_page" eval="True"/>
|
||||
<field name="description" type="html">
|
||||
<p>Some general information about you. It will be used internally for statistics only.</p></field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p1_q1" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="title">What is your email ?</field>
|
||||
<field name="question_type">char_box</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="validation_email" eval="True"/>
|
||||
<field name="save_as_email" eval="True"/>
|
||||
<field name="question_placeholder">ex@mple.com</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p1_q2" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">3</field>
|
||||
<field name="title">What is your nickname ?</field>
|
||||
<field name="question_type">char_box</field>
|
||||
<field name="question_placeholder">Don't be shy, be wild!</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="save_as_nickname" eval="True"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p1_q3" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">4</field>
|
||||
<field name="title">Where are you from ?</field>
|
||||
<field name="question_type">char_box</field>
|
||||
<field name="question_placeholder">Brussels, Belgium</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p1_q4" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">5</field>
|
||||
<field name="title">How old are you ?</field>
|
||||
<field name="description" type="html"><p>Just to categorize your answers, don't worry.</p></field>
|
||||
<field name="question_type">numerical_box</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
</record>
|
||||
|
||||
<!-- Page 2: quiz about company -->
|
||||
<record id="survey_demo_quiz_p2" model="survey.question">
|
||||
<field name="title">Our Company in a few questions ...</field>
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">10</field>
|
||||
<field name="question_type" eval="False"/>
|
||||
<field name="is_page" eval="True"/>
|
||||
<field name="description" type="html">
|
||||
<p>Some questions about our company. Do you really know us?</p></field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p2_q1" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">11</field>
|
||||
<field name="title">When is Mitchell Admin born ?</field>
|
||||
<field name="description" type="html"><span>Our famous Leader !</span></field>
|
||||
<field name="question_type">date</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p2_q2" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">12</field>
|
||||
<field name="title">When did precisely Marc Demo crop its first apple tree ?</field>
|
||||
<field name="question_type">datetime</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p2_q3" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">13</field>
|
||||
<field name="title">Give the list of all types of wood we sell.</field>
|
||||
<field name="question_type">text_box</field>
|
||||
<field name="constr_mandatory" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Page 3: quiz about fruits and vegetables -->
|
||||
<record id="survey_demo_quiz_p3" model="survey.question">
|
||||
<field name="title">Fruits and vegetables</field>
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">20</field>
|
||||
<field name="question_type" eval="False"/>
|
||||
<field name="is_page" eval="True"/>
|
||||
<field name="description" type="html">
|
||||
<p>An apple a day keeps the doctor away.</p></field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q1" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">21</field>
|
||||
<field name="title">Which category does a tomato belong to</field>
|
||||
<field name="description" type="html"><span>"Red" is not a category, I know what you are trying to do ;)</span></field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="comments_allowed" eval="True"/>
|
||||
<field name="comment_count_as_answer" eval="True"/>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q1_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q1"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="value">Fruits</field>
|
||||
<field name="is_correct" eval="True"/>
|
||||
<field name="answer_score">20</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q1_sug2" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q1"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">Vegetables</field>
|
||||
<field name="is_correct" eval="True"/>
|
||||
<field name="answer_score">10</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q1_sug3" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q1"/>
|
||||
<field name="sequence">3</field>
|
||||
<field name="value">Space stations</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q2" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">22</field>
|
||||
<field name="title">Which of the following would you use to pollinate</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="comments_allowed" eval="True"/>
|
||||
<field name="comment_count_as_answer" eval="False"/>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_time_limited" eval="True"/>
|
||||
<field name="time_limit">15</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q2_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q2"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="value">Bees</field>
|
||||
<field name="is_correct" eval="True"/>
|
||||
<field name="answer_score">20</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q2_sug2" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q2"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">Dogs</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q2_sug3" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q2"/>
|
||||
<field name="sequence">3</field>
|
||||
<field name="value">Mooses</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q3" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">23</field>
|
||||
<field name="title">Select trees that made more than 20K sales this year</field>
|
||||
<field name="description" type="html"><span>Our sales people have an advantage, but you can do it !</span></field>
|
||||
<field name="question_type">multiple_choice</field>
|
||||
<field name="constr_mandatory" eval="False"/>
|
||||
<field name="comments_allowed" eval="True"/>
|
||||
<field name="comment_count_as_answer" eval="True"/>
|
||||
<field name="is_time_limited" eval="True"/>
|
||||
<field name="time_limit">20</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q3_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q3"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="value">Apple Trees</field>
|
||||
<field name="is_correct" eval="True"/>
|
||||
<field name="answer_score">20</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q3_sug2" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q3"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">Lemon Trees</field>
|
||||
<field name="is_correct" eval="True"/>
|
||||
<field name="answer_score">10</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q3_sug3" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q3"/>
|
||||
<field name="sequence">3</field>
|
||||
<field name="value">Baobab Trees</field>
|
||||
<field name="answer_score">-10</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q3_sug4" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q3"/>
|
||||
<field name="sequence">4</field>
|
||||
<field name="value">Cookies</field>
|
||||
<field name="answer_score">-10</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q4" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">24</field>
|
||||
<field name="title">A "Citrus" could give you ...</field>
|
||||
<field name="question_type">multiple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="comments_allowed" eval="True"/>
|
||||
<field name="comment_count_as_answer" eval="False"/>
|
||||
<field name="is_time_limited" eval="True"/>
|
||||
<field name="time_limit">20</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q4_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q4"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="value">Pomelos</field>
|
||||
<field name="is_correct" eval="True"/>
|
||||
<field name="answer_score">20</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q4_sug2" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q4"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">Grapefruits</field>
|
||||
<field name="is_correct" eval="True"/>
|
||||
<field name="answer_score">20</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q4_sug3" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q4"/>
|
||||
<field name="sequence">3</field>
|
||||
<field name="value">Cosmic rays</field>
|
||||
<field name="answer_score">-10</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q4_sug4" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q4"/>
|
||||
<field name="sequence">4</field>
|
||||
<field name="value">Bricks</field>
|
||||
<field name="answer_score">-10</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q5" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">25</field>
|
||||
<field name="title">How often should you water those plants</field>
|
||||
<field name="question_type">matrix</field>
|
||||
<field name="matrix_subtype">simple</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="comments_allowed" eval="True"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q5_row1" model="survey.question.answer">
|
||||
<field name="matrix_question_id" ref="survey_demo_quiz_p3_q5"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="value">Cactus</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q5_row2" model="survey.question.answer">
|
||||
<field name="matrix_question_id" ref="survey_demo_quiz_p3_q5"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">Ficus</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q5_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q5"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="value">Once a month</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q5_sug2" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q5"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">Once a week</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q6" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">26</field>
|
||||
<field name="title">When do you harvest those fruits</field>
|
||||
<field name="description" type="html"><span>Best time to do it, is the right time to do it.</span></field>
|
||||
<field name="question_type">matrix</field>
|
||||
<field name="matrix_subtype">multiple</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="comments_allowed" eval="False"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q6_row1" model="survey.question.answer">
|
||||
<field name="matrix_question_id" ref="survey_demo_quiz_p3_q6"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="value">Apples</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q6_row2" model="survey.question.answer">
|
||||
<field name="matrix_question_id" ref="survey_demo_quiz_p3_q6"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">Strawberries</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q6_row3" model="survey.question.answer">
|
||||
<field name="matrix_question_id" ref="survey_demo_quiz_p3_q6"/>
|
||||
<field name="sequence">3</field>
|
||||
<field name="value">Clementine</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q6_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q6"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="value">Spring</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q6_sug2" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q6"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">Summer</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q6_sug3" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q6"/>
|
||||
<field name="sequence">3</field>
|
||||
<field name="value">Autumn</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p3_q6_sug4" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q6"/>
|
||||
<field name="sequence">4</field>
|
||||
<field name="value">Winter</field>
|
||||
</record>
|
||||
|
||||
<!-- Page 4: Trees -->
|
||||
<record id="survey_demo_quiz_p4" model="survey.question">
|
||||
<field name="title">Trees</field>
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">30</field>
|
||||
<field name="is_page" eval="True"/>
|
||||
<field name="question_type" eval="False"/>
|
||||
<field name="description" type="html">
|
||||
<p>
|
||||
We like to say that the apple doesn't fall far from the tree, so here are trees.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="survey_demo_quiz_p4_q1" model="survey.question">
|
||||
<field name="title">Dogwood is from which family of trees ?</field>
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">31</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p4_q1_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q1"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="value">Pinaceae</field>
|
||||
<field name="value_image" type="base64" file="survey/static/img/pinaceae.jpg"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p4_q1_sug2" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q1"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">Ulmaceae</field>
|
||||
<field name="value_image" type="base64" file="survey/static/img/ulmaceae.jpg"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p4_q1_sug3" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q1"/>
|
||||
<field name="sequence">3</field>
|
||||
<field name="value">Cornaceae</field>
|
||||
<field name="value_image" type="base64" file="survey/static/img/cornaceae.jpg"/>
|
||||
<field name="is_correct" eval="True"/>
|
||||
<field name="answer_score">20</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p4_q1_sug4" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q1"/>
|
||||
<field name="sequence">4</field>
|
||||
<field name="value">Salicaceae</field>
|
||||
<field name="value_image" type="base64" file="survey/static/img/salicaceae.jpg"/>
|
||||
</record>
|
||||
|
||||
<record id="survey_demo_quiz_p4_q2" model="survey.question">
|
||||
<field name="title">In which country did the bonsai technique develop ?</field>
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">32</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p4_q2_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q2"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="value">Japan</field>
|
||||
<field name="value_image" type="base64" file="survey/static/img/japan.jpg"/>
|
||||
<field name="is_correct" eval="True"/>
|
||||
<field name="answer_score">20</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p4_q2_sug2" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q2"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">China</field>
|
||||
<field name="value_image" type="base64" file="survey/static/img/china.jpg"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p4_q2_sug3" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q2"/>
|
||||
<field name="sequence">3</field>
|
||||
<field name="value">Vietnam</field>
|
||||
<field name="value_image" type="base64" file="survey/static/img/vietnam.jpg"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p4_q2_sug4" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q2"/>
|
||||
<field name="sequence">4</field>
|
||||
<field name="value">South Korea</field>
|
||||
<field name="value_image" type="base64" file="survey/static/img/south_korea.jpg"/>
|
||||
</record>
|
||||
|
||||
<record id="survey_demo_quiz_p4_q3" model="survey.question">
|
||||
<field name="title">Is the wood of a coniferous hard or soft ?</field>
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">33</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="description" type="html">
|
||||
<p>
|
||||
<img class="img-fluid o_we_custom_image d-block mx-auto"
|
||||
src="/survey/static/img/coniferous.jpg"/><br/>
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p4_q3_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q3"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="value">Hard</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p4_q3_sug2" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q3"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">Soft</field>
|
||||
<field name="is_correct" eval="True"/>
|
||||
<field name="answer_score">10</field>
|
||||
</record>
|
||||
|
||||
<record id="survey_demo_quiz_p4_q4" model="survey.question">
|
||||
<field name="title">From which continent is native the Scots pine (pinus sylvestris) ?</field>
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">34</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="description" type="html">
|
||||
<p>
|
||||
<img class="img-fluid o_we_custom_image d-block mx-auto"
|
||||
src="/survey/static/img/pinus_sylvestris.jpg" style="width: 100%;"/><br/>
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p4_q4_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q4"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="value">Africa</field>
|
||||
<field name="value_image" type="base64" file="survey/static/img/africa.png"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p4_q4_sug2" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q4"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">Asia</field>
|
||||
<field name="is_correct" eval="True"/>
|
||||
<field name="answer_score">20</field>
|
||||
<field name="value_image" type="base64" file="survey/static/img/asia.png"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p4_q4_sug3" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q4"/>
|
||||
<field name="sequence">3</field>
|
||||
<field name="value">Europe</field>
|
||||
<field name="value_image" type="base64" file="survey/static/img/europe.png"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p4_q4_sug4" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q4"/>
|
||||
<field name="sequence">4</field>
|
||||
<field name="value">South America</field>
|
||||
<field name="value_image" type="base64" file="survey/static/img/south_america.png"/>
|
||||
</record>
|
||||
|
||||
<record id="survey_demo_quiz_p4_q5" model="survey.question">
|
||||
<field name="title">In the list below, select all the coniferous.</field>
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">35</field>
|
||||
<field name="question_type">multiple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p4_q5_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q5"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="value">Douglas Fir</field>
|
||||
<field name="value_image" type="base64" file="survey/static/img/douglas_fir.jpg"/>
|
||||
<field name="is_correct" eval="True"/>
|
||||
<field name="answer_score">5</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p4_q5_sug2" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q5"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">Norway Spruce</field>
|
||||
<field name="value_image" type="base64" file="survey/static/img/norway_spruce.jpg"/>
|
||||
<field name="is_correct" eval="True"/>
|
||||
<field name="answer_score">5</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p4_q5_sug3" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q5"/>
|
||||
<field name="sequence">3</field>
|
||||
<field name="value">European Yew</field>
|
||||
<field name="value_image" type="base64" file="survey/static/img/european_yew.jpg"/>
|
||||
<field name="is_correct" eval="True"/>
|
||||
<field name="answer_score">5</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p4_q5_sug4" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q5"/>
|
||||
<field name="sequence">4</field>
|
||||
<field name="value">Mountain Pine</field>
|
||||
<field name="value_image" type="base64" file="survey/static/img/mountain_pine.jpg"/>
|
||||
<field name="is_correct" eval="True"/>
|
||||
<field name="answer_score">5</field>
|
||||
</record>
|
||||
|
||||
<record id="survey_demo_quiz_p4_q6" model="survey.question">
|
||||
<field name="title">After watching this video, will you swear that you are not going to procrastinate to trim your hedge this year ?</field>
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">36</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="description" type="html">
|
||||
<div class="text-center">
|
||||
<div class="media_iframe_video" data-oe-expression="//www.youtube.com/embed/7y4T6yv5L1k?autoplay=0&rel=0" style="width: 50%;">
|
||||
<div class="css_editable_mode_display"/>
|
||||
<div class="media_iframe_video_size" contenteditable="false"/>
|
||||
<iframe src="//www.youtube.com/embed/7y4T6yv5L1k?autoplay=0&rel=0" frameborder="0" contenteditable="false"></iframe>
|
||||
</div><br/>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p4_q6_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q6"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="value">Yes</field>
|
||||
<field name="is_correct" eval="True"/>
|
||||
<field name="answer_score">10</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p4_q6_sug2" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q6"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">No</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p4_q6_sug3" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q6"/>
|
||||
<field name="sequence">3</field>
|
||||
<field name="value">Perhaps</field>
|
||||
<field name="answer_score">-10</field>
|
||||
</record>
|
||||
|
||||
<!-- Page 5: Feedback - non scored question -->
|
||||
<record id="survey_demo_quiz_p5" model="survey.question">
|
||||
<field name="title">Your feeling</field>
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">40</field>
|
||||
<field name="question_type" eval="False"/>
|
||||
<field name="is_page" eval="True"/>
|
||||
<field name="description" type="html">
|
||||
<p>We may be interested by your input.</p></field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p5_q1" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_quiz"/>
|
||||
<field name="sequence">41</field>
|
||||
<field name="title">What do you think about this survey ?</field>
|
||||
<field name="description" type="html"><span>If you don't like us, please try to be as objective as possible.</span></field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="comments_allowed" eval="True"/>
|
||||
<field name="comment_count_as_answer" eval="True"/>
|
||||
<field name="constr_mandatory" eval="False"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p5_q1_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p5_q1"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="value">Good</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p5_q1_sug2" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p5_q1"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">Not Good, Not Bad</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p5_q1_sug3" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p5_q1"/>
|
||||
<field name="sequence">3</field>
|
||||
<field name="value">Iznogoud</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_p5_q1_sug4" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_quiz_p5_q1"/>
|
||||
<field name="sequence">4</field>
|
||||
<field name="value">I have no idea, I'm a dog!</field>
|
||||
</record>
|
||||
</data></odoo>
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="0">
|
||||
|
||||
<record id="survey_demo_quiz_answer_1" model="survey.user_input">
|
||||
<field name="survey_id" ref="survey.survey_demo_quiz"/>
|
||||
<field name="partner_id" ref="base.partner_demo"/>
|
||||
<field name="email">mark.brown23@example.com</field>
|
||||
<field name="end_datetime" eval="datetime.now() - timedelta(days=1, hours=3, minutes=30)"/>
|
||||
<field name="start_datetime" eval="datetime.now() - timedelta(days=1, hours=4, minutes=50)"/>
|
||||
<field name="state">done</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_2" model="survey.user_input">
|
||||
<field name="survey_id" ref="survey.survey_demo_quiz"/>
|
||||
<field name="partner_id" ref="base.partner_admin"/>
|
||||
<field name="email">admin@yourcompany.example.com</field>
|
||||
<field name="end_datetime" eval="datetime.now() - timedelta(days=1, hours=2, minutes=50)"/>
|
||||
<field name="start_datetime" eval="datetime.now() - timedelta(days=1, hours=3, minutes=50)"/>
|
||||
<field name="state">done</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3" model="survey.user_input">
|
||||
<field name="survey_id" ref="survey.survey_demo_quiz"/>
|
||||
<field name="partner_id" ref="base.partner_demo_portal"/>
|
||||
<field name="email">joel.willis63@example.com</field>
|
||||
<field name="end_datetime" eval="datetime.now() - timedelta(days=1, hours=2, minutes=10)"/>
|
||||
<field name="start_datetime" eval="datetime.now() - timedelta(days=1, hours=2, minutes=50)"/>
|
||||
<field name="state">done</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_4" model="survey.user_input">
|
||||
<field name="survey_id" ref="survey.survey_demo_quiz"/>
|
||||
<field name="partner_id" ref="base.res_partner_address_28"/>
|
||||
<field name="email">colleen.diaz83@example.com</field>
|
||||
<field name="start_datetime" eval="datetime.now() - timedelta(days=1, hours=0, minutes=50)"/>
|
||||
<field name="state">in_progress</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_5" model="survey.user_input">
|
||||
<field name="survey_id" ref="survey.survey_demo_quiz"/>
|
||||
<field name="partner_id" ref="base.res_partner_address_34"/>
|
||||
<field name="email">travis.mendoza24@example.com</field>
|
||||
<field name="state">new</field>
|
||||
</record>
|
||||
|
||||
</data></odoo>
|
||||
|
|
@ -1,560 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="0">
|
||||
<!-- Page 1: general informations -->
|
||||
<record id="survey_demo_quiz_answer_1_p1_q1_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p1_q1"/>
|
||||
<field name="answer_type">char_box</field>
|
||||
<field name="value_char_box">mark.brown23@example.com</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_1_p1_q2_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p1_q2"/>
|
||||
<field name="answer_type">char_box</field>
|
||||
<field name="value_char_box">Mark Brown</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_1_p1_q3_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p1_q3"/>
|
||||
<field name="answer_type">char_box</field>
|
||||
<field name="value_char_box">Brussels</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_1_p1_q4_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p1_q4"/>
|
||||
<field name="answer_type">numerical_box</field>
|
||||
<field name="value_numerical_box">36</field>
|
||||
</record>
|
||||
<!-- Page 2: quiz about company -->
|
||||
<record id="survey_demo_quiz_answer_1_p2_q1_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p2_q1"/>
|
||||
<field name="answer_type">date</field>
|
||||
<field name="value_date" eval="DateTime.today() - relativedelta(years=36)"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_1_p2_q2_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p2_q2"/>
|
||||
<field name="answer_type">datetime</field>
|
||||
<field name="value_datetime" eval="DateTime.now().replace(year=2017, month=10, day=2, hour=2, minute=27, second=0)"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_1_p2_q3_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p2_q3"/>
|
||||
<field name="answer_type">text_box</field>
|
||||
<field name="value_text_box">Oak, ash, pine</field>
|
||||
</record>
|
||||
<!-- Page 3: quiz about fruits and vegetables -->
|
||||
<record id="survey_demo_quiz_answer_1_p3_q1_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q1"/>
|
||||
<field name="answer_is_correct" eval="True"/>
|
||||
<field name="answer_score">20</field>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q1_sug1"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_1_p3_q2_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q2"/>
|
||||
<field name="answer_is_correct" eval="True"/>
|
||||
<field name="answer_score">20</field>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q2_sug1"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_1_p3_q2_l2" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q2"/>
|
||||
<field name="answer_type">char_box</field>
|
||||
<field name="value_char_box">Mooses ?? Really ?</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_1_p3_q3_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q3"/>
|
||||
<field name="answer_is_correct" eval="True"/>
|
||||
<field name="answer_score">10</field>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q3_sug2"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_1_p3_q3_l2" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q3"/>
|
||||
<field name="answer_score">-10</field>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q3_sug3"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_1_p3_q4_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q4"/>
|
||||
<field name="answer_is_correct" eval="True"/>
|
||||
<field name="answer_score">20</field>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q4_sug1"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_1_p3_q4_l2" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q4"/>
|
||||
<field name="answer_score">-10</field>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q4_sug3"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_1_p3_q5_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q5"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q5_sug1"/>
|
||||
<field name="matrix_row_id" ref="survey_demo_quiz_p3_q5_row1"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_1_p3_q5_l2" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q5"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q5_sug1"/>
|
||||
<field name="matrix_row_id" ref="survey_demo_quiz_p3_q5_row2"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_1_p3_q6_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q6"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q6_sug1"/>
|
||||
<field name="matrix_row_id" ref="survey_demo_quiz_p3_q6_row1"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_1_p3_q6_l2" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q6"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q6_sug1"/>
|
||||
<field name="matrix_row_id" ref="survey_demo_quiz_p3_q6_row2"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_1_p3_q6_l3" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q6"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q6_sug1"/>
|
||||
<field name="matrix_row_id" ref="survey_demo_quiz_p3_q6_row3"/>
|
||||
</record>
|
||||
<!-- Page 4: trees -->
|
||||
<record id="survey_demo_quiz_answer_1_p4_q1_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q1"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p4_q1_sug1"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_1_p4_q2_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q2"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p4_q2_sug1"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_1_p4_q3_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q3"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p4_q3_sug1"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_1_p4_q4_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q4"/>
|
||||
<field name="answer_is_correct" eval="True"/>
|
||||
<field name="answer_score">20</field>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p4_q4_sug2"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_1_p4_q5_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q5"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p4_q5_sug1"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_1_p4_q5_l2" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q5"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p4_q5_sug2"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_1_p4_q6_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_1"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q6"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p4_q6_sug1"/>
|
||||
</record>
|
||||
|
||||
|
||||
<!-- Page 1: general informations -->
|
||||
<record id="survey_demo_quiz_answer_2_p1_q1_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p1_q1"/>
|
||||
<field name="answer_type">char_box</field>
|
||||
<field name="value_char_box">admin@yourcompany.example.com</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_2_p1_q2_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p1_q2"/>
|
||||
<field name="answer_type">char_box</field>
|
||||
<field name="value_char_box">Mitchell Admin</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_2_p1_q3_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p1_q3"/>
|
||||
<field name="answer_type">char_box</field>
|
||||
<field name="value_char_box">Ottawa</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_2_p1_q4_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p1_q4"/>
|
||||
<field name="answer_type">numerical_box</field>
|
||||
<field name="value_numerical_box">48</field>
|
||||
</record>
|
||||
<!-- Page 2: quiz about company -->
|
||||
<record id="survey_demo_quiz_answer_2_p2_q1_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p2_q1"/>
|
||||
<field name="answer_type">date</field>
|
||||
<field name="value_date" eval="DateTime.today() + relativedelta(years=24)"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_2_p2_q2_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p2_q2"/>
|
||||
<field name="answer_type">datetime</field>
|
||||
<field name="value_datetime" eval="DateTime.now().replace(year=2011, month=8, day=21, hour=15, minute=34, second=0)"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_2_p2_q3_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p2_q3"/>
|
||||
<field name="skipped" eval="True"/>
|
||||
</record>
|
||||
<!-- Page 3: quiz about fruits and vegetables -->
|
||||
<record id="survey_demo_quiz_answer_2_p3_q1_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q1"/>
|
||||
<field name="answer_is_correct" eval="True"/>
|
||||
<field name="answer_score">10</field>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q1_sug2"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_2_p3_q2_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q2"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q2_sug3"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_2_p3_q2_l2" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q2"/>
|
||||
<field name="answer_type">char_box</field>
|
||||
<field name="value_char_box">Mooses are best pollinators of the world !</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_2_p3_q3_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q3"/>
|
||||
<field name="answer_is_correct" eval="True"/>
|
||||
<field name="answer_score">20</field>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q3_sug1"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_2_p3_q3_l2" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q3"/>
|
||||
<field name="answer_type">char_box</field>
|
||||
<field name="value_char_box">I sold a 30K raspberry tree once.</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_2_p3_q4_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q4"/>
|
||||
<field name="answer_is_correct" eval="True"/>
|
||||
<field name="answer_score">20</field>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q4_sug1"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_2_p3_q5_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q5"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q5_sug1"/>
|
||||
<field name="matrix_row_id" ref="survey_demo_quiz_p3_q5_row1"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_2_p3_q5_l2" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q5"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q5_sug2"/>
|
||||
<field name="matrix_row_id" ref="survey_demo_quiz_p3_q5_row2"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_2_p3_q6_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q6"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q6_sug1"/>
|
||||
<field name="matrix_row_id" ref="survey_demo_quiz_p3_q6_row1"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_2_p3_q6_l2" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q6"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q6_sug2"/>
|
||||
<field name="matrix_row_id" ref="survey_demo_quiz_p3_q6_row2"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_2_p3_q6_l3" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q6"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q6_sug3"/>
|
||||
<field name="matrix_row_id" ref="survey_demo_quiz_p3_q6_row3"/>
|
||||
</record>
|
||||
<!-- Page 4: trees -->
|
||||
<record id="survey_demo_quiz_answer_2_p4_q1_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q1"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p4_q1_sug1"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_2_p4_q2_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q2"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p4_q2_sug1"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_2_p4_q3_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q3"/>
|
||||
<field name="answer_is_correct" eval="True"/>
|
||||
<field name="answer_score">10</field>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p4_q3_sug2"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_2_p4_q4_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q4"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p4_q4_sug1"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_2_p4_q5_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q5"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p4_q5_sug1"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_2_p4_q5_l2" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q5"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p4_q5_sug3"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_2_p4_q6_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_2"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q6"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p4_q6_sug1"/>
|
||||
</record>
|
||||
|
||||
|
||||
<!-- Page 1: general informations -->
|
||||
<record id="survey_demo_quiz_answer_3_p1_q1_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p1_q1"/>
|
||||
<field name="answer_type">char_box</field>
|
||||
<field name="value_char_box">joel.willis63@example.com</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p1_q2_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p1_q2"/>
|
||||
<field name="answer_type">char_box</field>
|
||||
<field name="value_char_box">Joël Willis</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p1_q3_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p1_q3"/>
|
||||
<field name="answer_type">char_box</field>
|
||||
<field name="value_char_box">Brussels</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p1_q4_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p1_q4"/>
|
||||
<field name="answer_type">numerical_box</field>
|
||||
<field name="value_numerical_box">28</field>
|
||||
</record>
|
||||
<!-- Page 2: quiz about company -->
|
||||
<record id="survey_demo_quiz_answer_3_p2_q1_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p2_q1"/>
|
||||
<field name="answer_type">date</field>
|
||||
<field name="value_date" eval="DateTime.today() - relativedelta(years=38)"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p2_q2_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p2_q2"/>
|
||||
<field name="answer_type">datetime</field>
|
||||
<field name="value_datetime" eval="DateTime.now().replace(year=2005, month=4, day=18, hour=10, minute=0, second=0)"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p2_q3_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p2_q3"/>
|
||||
<field name="answer_type">text_box</field>
|
||||
<field name="value_text_box">Oak, fur, pine, red pine</field>
|
||||
</record>
|
||||
<!-- Page 3: quiz about fruits and vegetables -->
|
||||
<record id="survey_demo_quiz_answer_3_p3_q1_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q1"/>
|
||||
<field name="answer_type">char_box</field>
|
||||
<field name="value_char_box">Both fruit (seeds, par of the plant) and vegetable (culinary use), obviously.</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p3_q2_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q2"/>
|
||||
<field name="answer_is_correct" eval="True"/>
|
||||
<field name="answer_score">20</field>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q2_sug1"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p3_q3_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q3"/>
|
||||
<field name="answer_is_correct" eval="True"/>
|
||||
<field name="answer_score">20</field>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q3_sug1"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p3_q3_l2" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q3"/>
|
||||
<field name="answer_is_correct" eval="True"/>
|
||||
<field name="answer_score">10</field>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q3_sug2"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p3_q3_l3" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q3"/>
|
||||
<field name="answer_type">char_box</field>
|
||||
<field name="value_char_box">You forgot the strawberry tree.</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p3_q4_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q4"/>
|
||||
<field name="answer_is_correct" eval="True"/>
|
||||
<field name="answer_score">20</field>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q4_sug1"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p3_q4_l2" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q4"/>
|
||||
<field name="answer_is_correct" eval="True"/>
|
||||
<field name="answer_score">20</field>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q4_sug2"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p3_q4_l3" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q4"/>
|
||||
<field name="answer_score">-10</field>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q4_sug3"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p3_q4_l4" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q4"/>
|
||||
<field name="answer_type">char_box</field>
|
||||
<field name="value_char_box">Gives the beeest cosmics rays man. So juicy.</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p3_q5_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q5"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q5_sug1"/>
|
||||
<field name="matrix_row_id" ref="survey_demo_quiz_p3_q5_row1"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p3_q5_l2" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q5"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q5_sug2"/>
|
||||
<field name="matrix_row_id" ref="survey_demo_quiz_p3_q5_row2"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p3_q5_l3" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q5"/>
|
||||
<field name="answer_type">char_box</field>
|
||||
<field name="value_char_box">Well sometimes I forget them, they survived. Almost.</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p3_q6_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q6"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q6_sug3"/>
|
||||
<field name="matrix_row_id" ref="survey_demo_quiz_p3_q6_row1"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p3_q6_l2" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q6"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q6_sug4"/>
|
||||
<field name="matrix_row_id" ref="survey_demo_quiz_p3_q6_row1"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p3_q6_l3" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q6"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q6_sug1"/>
|
||||
<field name="matrix_row_id" ref="survey_demo_quiz_p3_q6_row2"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p3_q6_l4" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q6"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q6_sug2"/>
|
||||
<field name="matrix_row_id" ref="survey_demo_quiz_p3_q6_row2"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p3_q6_l5" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p3_q6"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p3_q6_sug4"/>
|
||||
<field name="matrix_row_id" ref="survey_demo_quiz_p3_q6_row3"/>
|
||||
</record>
|
||||
<!-- Page 4: trees -->
|
||||
<record id="survey_demo_quiz_answer_3_p4_q1_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q1"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p4_q1_sug3"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p4_q2_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q2"/>
|
||||
<field name="answer_score">20</field>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p4_q2_sug2"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p4_q3_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q3"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p4_q3_sug1"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p4_q4_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q4"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p4_q4_sug3"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p4_q5_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q5"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p4_q5_sug2"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p4_q5_l2" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q5"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p4_q5_sug3"/>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_3_p4_q6_l1" model="survey.user_input.line">
|
||||
<field name="user_input_id" ref="survey_demo_quiz_answer_3"/>
|
||||
<field name="question_id" ref="survey_demo_quiz_p4_q6"/>
|
||||
<field name="answer_type">suggestion</field>
|
||||
<field name="suggested_answer_id" ref="survey_demo_quiz_p4_q6_sug1"/>
|
||||
</record>
|
||||
|
||||
</data></odoo>
|
||||
10
odoo-bringout-oca-ocb-survey/survey/data/survey_tour.xml
Normal file
10
odoo-bringout-oca-ocb-survey/survey/data/survey_tour.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="survey_tour" model="web_tour.tour">
|
||||
<field name="name">survey_tour</field>
|
||||
<field name="sequence">225</field>
|
||||
<field name="rainbow_man_message"><![CDATA[
|
||||
Congratulations! You are now ready to collect feedback like a pro :-)
|
||||
]]></field>
|
||||
</record>
|
||||
</odoo>
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,11 +1,12 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import survey_survey
|
||||
from . import survey_survey_template
|
||||
from . import survey_question
|
||||
from . import survey_user_input
|
||||
from . import badge
|
||||
from . import challenge
|
||||
from . import res_partner
|
||||
from . import ir_http
|
||||
from . import res_lang
|
||||
from . import res_partner
|
||||
from . import survey_question
|
||||
from . import survey_survey
|
||||
from . import survey_user_input
|
||||
from . import templates
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
from odoo import models, fields
|
||||
|
||||
|
||||
class Challenge(models.Model):
|
||||
class GamificationChallenge(models.Model):
|
||||
_inherit = 'gamification.challenge'
|
||||
|
||||
challenge_category = fields.Selection(selection_add=[
|
||||
|
|
|
|||
|
|
@ -1,12 +1,37 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import re
|
||||
|
||||
from odoo import models
|
||||
from odoo import api, models
|
||||
from odoo.http import request
|
||||
|
||||
SURVEY_URL_PREFIX_REGEX = re.compile(r"""
|
||||
^
|
||||
( # Optional locale part of the URL
|
||||
/[a-z]{2,3} # Language (only 2- or 3-letter ISO 639 code)
|
||||
# e.g. fr, kab
|
||||
(_([A-Z]{2}|[0-9]{3}))? # [Optional] Region (2-letter ISO 3166-1 code or 3-digit UN M.49 code)
|
||||
# e.g. fr_BE, es_419
|
||||
(@[a-zA-Z]+)? # [Optional] Script (ISO 15924 code)
|
||||
# e.g. sr@Cyrl
|
||||
)?
|
||||
/survey/
|
||||
""", re.VERBOSE)
|
||||
|
||||
|
||||
class IrHttp(models.AbstractModel):
|
||||
_inherit = ["ir.http"]
|
||||
_inherit = 'ir.http'
|
||||
|
||||
@api.model
|
||||
def get_nearest_lang(self, lang_code):
|
||||
if request and self._is_survey_frontend(request.httprequest.path):
|
||||
return super(IrHttp, self.with_context(web_force_installed_langs=True)).get_nearest_lang(lang_code)
|
||||
return super().get_nearest_lang(lang_code)
|
||||
|
||||
@classmethod
|
||||
def _get_translation_frontend_modules_name(cls):
|
||||
modules = super()._get_translation_frontend_modules_name()
|
||||
return modules + ["survey"]
|
||||
mods = super()._get_translation_frontend_modules_name()
|
||||
return mods + ['survey']
|
||||
|
||||
@api.model
|
||||
def _is_survey_frontend(self, path):
|
||||
return bool(SURVEY_URL_PREFIX_REGEX.match(path))
|
||||
|
|
|
|||
27
odoo-bringout-oca-ocb-survey/survey/models/res_lang.py
Normal file
27
odoo-bringout-oca-ocb-survey/survey/models/res_lang.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import Command, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ResLang(models.Model):
|
||||
_inherit = "res.lang"
|
||||
|
||||
def write(self, vals):
|
||||
""" When languages are disabled, clear corresponding survey languages. """
|
||||
if 'active' in vals and not vals['active']:
|
||||
self.env['survey.user_input'].sudo().search([('lang_id', 'in', self.ids)]).lang_id = False
|
||||
surveys_sudo = self.env['survey.survey'].sudo().search([('lang_ids', 'in', self.ids)])
|
||||
if will_be_all_lang_survey_sudo := surveys_sudo.filtered(lambda survey: survey.lang_ids <= self):
|
||||
if len(self) > 1:
|
||||
error = _("Cannot deactivate languages currently used by survey(s) only supporting those languages.")
|
||||
else:
|
||||
error = _("Cannot deactivate a language currently used by survey(s) only supporting that language.")
|
||||
if self.env['survey.survey'].search(
|
||||
[('id', 'in', will_be_all_lang_survey_sudo.ids)]) == will_be_all_lang_survey_sudo:
|
||||
error += '\n'
|
||||
error += _("Survey(s): %(surveys_list)s",
|
||||
surveys_list=', '.join(f'"{survey.title}"' for survey in will_be_all_lang_survey_sudo))
|
||||
raise UserError(error)
|
||||
surveys_sudo.write({'lang_ids': [Command.unlink(lang.id) for lang in self]})
|
||||
return super().write(vals)
|
||||
|
|
@ -14,9 +14,9 @@ class ResPartner(models.Model):
|
|||
def _compute_certifications_count(self):
|
||||
read_group_res = self.env['survey.user_input'].sudo()._read_group(
|
||||
[('partner_id', 'in', self.ids), ('scoring_success', '=', True)],
|
||||
['partner_id'], 'partner_id'
|
||||
['partner_id'], ['__count']
|
||||
)
|
||||
data = dict((res['partner_id'][0], res['partner_id_count']) for res in read_group_res)
|
||||
data = {partner.id: count for partner, count in read_group_res}
|
||||
for partner in self:
|
||||
partner.certifications_count = data.get(partner.id, 0)
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ class ResPartner(models.Model):
|
|||
|
||||
def action_view_certifications(self):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("survey.res_partner_action_certifications")
|
||||
action['view_mode'] = 'tree'
|
||||
action['view_mode'] = 'list'
|
||||
action['domain'] = ['|', ('partner_id', 'in', self.ids), ('partner_id', 'in', self.child_ids.ids)]
|
||||
|
||||
return action
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@
|
|||
|
||||
import collections
|
||||
import contextlib
|
||||
import json
|
||||
import itertools
|
||||
import json
|
||||
import operator
|
||||
from textwrap import shorten
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
|
@ -30,7 +31,7 @@ class SurveyQuestion(models.Model):
|
|||
items around.
|
||||
|
||||
It also removes on level of encoding by directly having 'Add a page' and 'Add a question'
|
||||
links on the tree view of questions, enabling a faster encoding.
|
||||
links on the list view of questions, enabling a faster encoding.
|
||||
|
||||
However, this has the downside of making the code reading a little bit more complicated.
|
||||
Efforts were made at the model level to create computed fields so that the use of these models
|
||||
|
|
@ -46,6 +47,17 @@ class SurveyQuestion(models.Model):
|
|||
_rec_name = 'title'
|
||||
_order = 'sequence,id'
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super().default_get(fields)
|
||||
if default_survey_id := self.env.context.get('default_survey_id'):
|
||||
survey = self.env['survey.survey'].browse(default_survey_id)
|
||||
if 'is_time_limited' in fields and 'is_time_limited' not in res:
|
||||
res['is_time_limited'] = survey.session_speed_rating
|
||||
if 'time_limit' in fields and 'time_limit' not in res:
|
||||
res['time_limit'] = survey.session_speed_rating_time_limit
|
||||
return res
|
||||
|
||||
# question generic data
|
||||
title = fields.Char('Title', required=True, translate=True)
|
||||
description = fields.Html(
|
||||
|
|
@ -54,9 +66,13 @@ class SurveyQuestion(models.Model):
|
|||
question_placeholder = fields.Char("Placeholder", translate=True, compute="_compute_question_placeholder", store=True, readonly=False)
|
||||
background_image = fields.Image("Background Image", compute="_compute_background_image", store=True, readonly=False)
|
||||
background_image_url = fields.Char("Background Url", compute="_compute_background_image_url")
|
||||
survey_id = fields.Many2one('survey.survey', string='Survey', ondelete='cascade')
|
||||
survey_id = fields.Many2one('survey.survey', string='Survey', ondelete='cascade', index='btree_not_null')
|
||||
scoring_type = fields.Selection(related='survey_id.scoring_type', string='Scoring Type', readonly=True)
|
||||
sequence = fields.Integer('Sequence', default=10)
|
||||
session_available = fields.Boolean(related='survey_id.session_available', string='Live Session available', readonly=True)
|
||||
survey_session_speed_rating = fields.Boolean(related="survey_id.session_speed_rating")
|
||||
survey_session_speed_rating_time_limit = fields.Integer(related="survey_id.session_speed_rating_time_limit", string="General Time limit (seconds)")
|
||||
|
||||
# page specific
|
||||
is_page = fields.Boolean('Is a page?')
|
||||
question_ids = fields.One2many('survey.question', string='Questions', compute="_compute_question_ids")
|
||||
|
|
@ -74,6 +90,7 @@ class SurveyQuestion(models.Model):
|
|||
('text_box', 'Multiple Lines Text Box'),
|
||||
('char_box', 'Single Line Text Box'),
|
||||
('numerical_box', 'Numerical Value'),
|
||||
('scale', 'Scale'),
|
||||
('date', 'Date'),
|
||||
('datetime', 'Datetime'),
|
||||
('matrix', 'Matrix')], string='Question Type',
|
||||
|
|
@ -82,6 +99,8 @@ class SurveyQuestion(models.Model):
|
|||
'Scored', compute='_compute_is_scored_question',
|
||||
readonly=False, store=True, copy=True,
|
||||
help="Include this question as part of quiz scoring. Requires an answer and answer score to be taken into account.")
|
||||
has_image_only_suggested_answer = fields.Boolean(
|
||||
"Has image only suggested answer", compute='_compute_has_image_only_suggested_answer')
|
||||
# -- scoreable/answerable simple answer_types: numerical_box / date / datetime
|
||||
answer_numerical_box = fields.Float('Correct numerical answer', help="Correct number answer for this question.")
|
||||
answer_date = fields.Date('Correct date answer', help="Correct date answer for this question.")
|
||||
|
|
@ -105,9 +124,16 @@ class SurveyQuestion(models.Model):
|
|||
matrix_row_ids = fields.One2many(
|
||||
'survey.question.answer', 'matrix_question_id', string='Matrix Rows', copy=True,
|
||||
help='Labels used for proposed choices: rows of matrix')
|
||||
# -- scale
|
||||
scale_min = fields.Integer("Scale Minimum Value", default=0)
|
||||
scale_max = fields.Integer("Scale Maximum Value", default=10)
|
||||
scale_min_label = fields.Char("Scale Minimum Label", translate=True)
|
||||
scale_mid_label = fields.Char("Scale Middle Label", translate=True)
|
||||
scale_max_label = fields.Char("Scale Maximum Label", translate=True)
|
||||
# -- display & timing options
|
||||
is_time_limited = fields.Boolean("The question is limited in time",
|
||||
help="Currently only supported for live sessions.")
|
||||
is_time_customized = fields.Boolean("Customized speed rewards")
|
||||
time_limit = fields.Integer("Time limit (seconds)")
|
||||
# -- comments (simple choice, multiple choice, matrix (without count as an answer))
|
||||
comments_allowed = fields.Boolean('Show Comments Field')
|
||||
|
|
@ -124,7 +150,7 @@ class SurveyQuestion(models.Model):
|
|||
validation_max_date = fields.Date('Maximum Date')
|
||||
validation_min_datetime = fields.Datetime('Minimum Datetime')
|
||||
validation_max_datetime = fields.Datetime('Maximum Datetime')
|
||||
validation_error_msg = fields.Char('Validation Error message', translate=True)
|
||||
validation_error_msg = fields.Char('Validation Error', translate=True)
|
||||
constr_mandatory = fields.Boolean('Mandatory Answer')
|
||||
constr_error_msg = fields.Char('Error message', translate=True)
|
||||
# answers
|
||||
|
|
@ -132,36 +158,73 @@ class SurveyQuestion(models.Model):
|
|||
'survey.user_input.line', 'question_id', string='Answers',
|
||||
domain=[('skipped', '=', False)], groups='survey.group_survey_user')
|
||||
|
||||
# Conditional display
|
||||
is_conditional = fields.Boolean(
|
||||
string='Conditional Display', copy=True, help="""If checked, this question will be displayed only
|
||||
if the specified conditional answer have been selected in a previous question""")
|
||||
triggering_question_id = fields.Many2one(
|
||||
'survey.question', string="Triggering Question", copy=False, compute="_compute_triggering_question_id",
|
||||
store=True, readonly=False, help="Question containing the triggering answer to display the current question.",
|
||||
domain="[('survey_id', '=', survey_id), \
|
||||
'&', ('question_type', 'in', ['simple_choice', 'multiple_choice']), \
|
||||
'|', \
|
||||
('sequence', '<', sequence), \
|
||||
'&', ('sequence', '=', sequence), ('id', '<', id)]")
|
||||
triggering_answer_id = fields.Many2one(
|
||||
'survey.question.answer', string="Triggering Answer", copy=False, compute="_compute_triggering_answer_id",
|
||||
store=True, readonly=False, help="Answer that will trigger the display of the current question.",
|
||||
domain="[('question_id', '=', triggering_question_id)]")
|
||||
# Not stored, convenient for trigger display computation.
|
||||
triggering_question_ids = fields.Many2many(
|
||||
'survey.question', string="Triggering Questions", compute="_compute_triggering_question_ids",
|
||||
store=False, help="Questions containing the triggering answer(s) to display the current question.")
|
||||
|
||||
_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!'),
|
||||
('validation_datetime', 'CHECK (validation_min_datetime <= validation_max_datetime)', 'Max datetime cannot be smaller than min datetime!'),
|
||||
('positive_answer_score', 'CHECK (answer_score >= 0)', 'An answer score for a non-multiple choice question cannot be negative!'),
|
||||
('scored_datetime_have_answers', "CHECK (is_scored_question != True OR question_type != 'datetime' OR answer_datetime is not null)",
|
||||
'All "Is a scored question = True" and "Question Type: Datetime" questions need an answer'),
|
||||
('scored_date_have_answers', "CHECK (is_scored_question != True OR question_type != 'date' OR answer_date is not null)",
|
||||
'All "Is a scored question = True" and "Question Type: Date" questions need an answer')
|
||||
]
|
||||
allowed_triggering_question_ids = fields.Many2many(
|
||||
'survey.question', string="Allowed Triggering Questions", copy=False, compute="_compute_allowed_triggering_question_ids")
|
||||
is_placed_before_trigger = fields.Boolean(
|
||||
string='Is misplaced?', help="Is this question placed before any of its trigger questions?",
|
||||
compute="_compute_allowed_triggering_question_ids")
|
||||
triggering_answer_ids = fields.Many2many(
|
||||
'survey.question.answer', string="Triggering Answers", copy=False, store=True,
|
||||
readonly=False, help="Picking any of these answers will trigger this question.\n"
|
||||
"Leave the field empty if the question should always be displayed.",
|
||||
domain="""[
|
||||
('question_id.survey_id', '=', survey_id),
|
||||
'&', ('question_id.question_type', 'in', ['simple_choice', 'multiple_choice']),
|
||||
'|',
|
||||
('question_id.sequence', '<', sequence),
|
||||
'&', ('question_id.sequence', '=', sequence), ('question_id.id', '<', id)
|
||||
]"""
|
||||
)
|
||||
|
||||
_positive_len_min = models.Constraint(
|
||||
'CHECK (validation_length_min >= 0)',
|
||||
'A length must be positive!',
|
||||
)
|
||||
_positive_len_max = models.Constraint(
|
||||
'CHECK (validation_length_max >= 0)',
|
||||
'A length must be positive!',
|
||||
)
|
||||
_validation_length = models.Constraint(
|
||||
'CHECK (validation_length_min <= validation_length_max)',
|
||||
'Max length cannot be smaller than min length!',
|
||||
)
|
||||
_validation_float = models.Constraint(
|
||||
'CHECK (validation_min_float_value <= validation_max_float_value)',
|
||||
'Max value cannot be smaller than min value!',
|
||||
)
|
||||
_validation_date = models.Constraint(
|
||||
'CHECK (validation_min_date <= validation_max_date)',
|
||||
'Max date cannot be smaller than min date!',
|
||||
)
|
||||
_validation_datetime = models.Constraint(
|
||||
'CHECK (validation_min_datetime <= validation_max_datetime)',
|
||||
'Max datetime cannot be smaller than min datetime!',
|
||||
)
|
||||
_positive_answer_score = models.Constraint(
|
||||
'CHECK (answer_score >= 0)',
|
||||
'An answer score for a non-multiple choice question cannot be negative!',
|
||||
)
|
||||
_scored_datetime_have_answers = models.Constraint(
|
||||
"CHECK (is_scored_question != True OR question_type != 'datetime' OR answer_datetime is not null)",
|
||||
'All "Is a scored question = True" and "Question Type: Datetime" questions need an answer',
|
||||
)
|
||||
_scored_date_have_answers = models.Constraint(
|
||||
"CHECK (is_scored_question != True OR question_type != 'date' OR answer_date is not null)",
|
||||
'All "Is a scored question = True" and "Question Type: Date" questions need an answer',
|
||||
)
|
||||
_scale = models.Constraint(
|
||||
"CHECK (question_type != 'scale' OR (scale_min >= 0 AND scale_max <= 10 AND scale_min < scale_max))",
|
||||
'The scale must be a growing non-empty range between 0 and 10 (inclusive)',
|
||||
)
|
||||
_is_time_limited_have_time_limit = models.Constraint(
|
||||
'CHECK (is_time_limited != TRUE OR time_limit IS NOT NULL AND time_limit > 0)',
|
||||
'All time-limited questions need a positive time limit',
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# CONSTRAINT METHODS
|
||||
|
|
@ -177,6 +240,13 @@ class SurveyQuestion(models.Model):
|
|||
# COMPUTE METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@api.depends('suggested_answer_ids', 'suggested_answer_ids.value')
|
||||
def _compute_has_image_only_suggested_answer(self):
|
||||
questions_with_image_only_answer = self.env['survey.question'].search(
|
||||
[('id', 'in', self.ids), ('suggested_answer_ids.value', 'in', [False, ''])])
|
||||
questions_with_image_only_answer.has_image_only_suggested_answer = True
|
||||
(self - questions_with_image_only_answer).has_image_only_suggested_answer = False
|
||||
|
||||
@api.depends('question_type')
|
||||
def _compute_question_placeholder(self):
|
||||
for question in self:
|
||||
|
|
@ -220,19 +290,10 @@ class SurveyQuestion(models.Model):
|
|||
|
||||
@api.depends('survey_id.question_and_page_ids.is_page', 'survey_id.question_and_page_ids.sequence')
|
||||
def _compute_question_ids(self):
|
||||
"""Will take all questions of the survey for which the index is higher than the index of this page
|
||||
and lower than the index of the next page."""
|
||||
for question in self:
|
||||
if question.is_page:
|
||||
next_page_index = False
|
||||
for page in question.survey_id.page_ids:
|
||||
if page._index() > question._index():
|
||||
next_page_index = page._index()
|
||||
break
|
||||
|
||||
question.question_ids = question.survey_id.question_ids.filtered(
|
||||
lambda q: q._index() > question._index() and (not next_page_index or q._index() < next_page_index)
|
||||
)
|
||||
question.question_ids = question.survey_id.question_ids\
|
||||
.filtered(lambda q: q.page_id == question).sorted(lambda q: q._index())
|
||||
else:
|
||||
question.question_ids = self.env['survey.question']
|
||||
|
||||
|
|
@ -269,33 +330,59 @@ class SurveyQuestion(models.Model):
|
|||
if not question.validation_required or question.question_type not in ['char_box', 'numerical_box', 'date', 'datetime']:
|
||||
question.validation_required = False
|
||||
|
||||
@api.depends('is_conditional')
|
||||
def _compute_triggering_question_id(self):
|
||||
""" Used as an 'onchange' : Reset the triggering question if user uncheck 'Conditional Display'
|
||||
Avoid CacheMiss : set the value to False if the value is not set yet."""
|
||||
for question in self:
|
||||
if not question.is_conditional or question.triggering_question_id is None:
|
||||
question.triggering_question_id = False
|
||||
@api.depends('survey_id', 'survey_id.question_ids', 'triggering_answer_ids')
|
||||
def _compute_allowed_triggering_question_ids(self):
|
||||
"""Although the question (and possible trigger questions) sequence
|
||||
is used here, we do not add these fields to the dependency list to
|
||||
avoid cascading rpc calls when reordering questions via the webclient.
|
||||
"""
|
||||
possible_trigger_questions = self.search([
|
||||
('is_page', '=', False),
|
||||
('question_type', 'in', ['simple_choice', 'multiple_choice']),
|
||||
('suggested_answer_ids', '!=', False),
|
||||
('survey_id', 'in', self.survey_id.ids)
|
||||
])
|
||||
# Using the sequence stored in db is necessary for existing questions that are passed as
|
||||
# NewIds because the sequence provided by the JS client can be incorrect.
|
||||
(self | possible_trigger_questions).flush_recordset()
|
||||
self.env.cr.execute(
|
||||
"SELECT id, sequence FROM survey_question WHERE id =ANY(%s)",
|
||||
[self.ids]
|
||||
)
|
||||
conditional_questions_sequences = dict(self.env.cr.fetchall()) # id: sequence mapping
|
||||
|
||||
@api.depends('triggering_question_id')
|
||||
def _compute_triggering_answer_id(self):
|
||||
""" Used as an 'onchange' : Reset the triggering answer if user unset or change the triggering question
|
||||
or uncheck 'Conditional Display'.
|
||||
Avoid CacheMiss : set the value to False if the value is not set yet."""
|
||||
for question in self:
|
||||
if not question.triggering_question_id \
|
||||
or question.triggering_question_id != question.triggering_answer_id.question_id\
|
||||
or question.triggering_answer_id is None:
|
||||
question.triggering_answer_id = False
|
||||
question_id = question._origin.id
|
||||
if not question_id: # New question
|
||||
question.allowed_triggering_question_ids = possible_trigger_questions.filtered(
|
||||
lambda q: q.survey_id.id == question.survey_id._origin.id)
|
||||
question.is_placed_before_trigger = False
|
||||
continue
|
||||
|
||||
@api.depends('question_type', 'scoring_type', 'answer_date', 'answer_datetime', 'answer_numerical_box')
|
||||
question_sequence = conditional_questions_sequences[question_id]
|
||||
|
||||
question.allowed_triggering_question_ids = possible_trigger_questions.filtered(
|
||||
lambda q: q.survey_id.id == question.survey_id._origin.id
|
||||
and (q.sequence < question_sequence or q.sequence == question_sequence and q.id < question_id)
|
||||
)
|
||||
question.is_placed_before_trigger = bool(
|
||||
set(question.triggering_answer_ids.question_id.ids)
|
||||
- set(question.allowed_triggering_question_ids.ids) # .ids necessary to match ids with newIds
|
||||
)
|
||||
|
||||
@api.depends('triggering_answer_ids')
|
||||
def _compute_triggering_question_ids(self):
|
||||
for question in self:
|
||||
question.triggering_question_ids = question.triggering_answer_ids.question_id
|
||||
|
||||
@api.depends('question_type', 'scoring_type', 'answer_date', 'answer_datetime', 'answer_numerical_box', 'suggested_answer_ids.is_correct')
|
||||
def _compute_is_scored_question(self):
|
||||
""" Computes whether a question "is scored" or not. Handles following cases:
|
||||
- inconsistent Boolean=None edge case that breaks tests => False
|
||||
- survey is not scored => False
|
||||
- 'date'/'datetime'/'numerical_box' question types w/correct answer => True
|
||||
(implied without user having to activate, except for numerical whose correct value is 0.0)
|
||||
- 'simple_choice / multiple_choice': set to True even if logic is a bit different (coming from answers)
|
||||
- 'simple_choice / multiple_choice': set to True if any of suggested answers are marked as correct
|
||||
- question_type isn't scoreable (note: choice questions scoring logic handled separately) => False
|
||||
"""
|
||||
for question in self:
|
||||
|
|
@ -308,14 +395,45 @@ class SurveyQuestion(models.Model):
|
|||
elif question.question_type == 'numerical_box' and question.answer_numerical_box:
|
||||
question.is_scored_question = True
|
||||
elif question.question_type in ['simple_choice', 'multiple_choice']:
|
||||
question.is_scored_question = True
|
||||
question.is_scored_question = any(question.suggested_answer_ids.mapped('is_correct'))
|
||||
else:
|
||||
question.is_scored_question = False
|
||||
|
||||
@api.onchange('question_type', 'validation_required')
|
||||
def _onchange_validation_parameters(self):
|
||||
"""Ensure no value stays set but not visible on form,
|
||||
preventing saving (+consistency with question type)."""
|
||||
self.validation_email = False
|
||||
self.validation_length_min = 0
|
||||
self.validation_length_max = 0
|
||||
self.validation_min_date = False
|
||||
self.validation_max_date = False
|
||||
self.validation_min_datetime = False
|
||||
self.validation_max_datetime = False
|
||||
self.validation_min_float_value = 0
|
||||
self.validation_max_float_value = 0
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# CRUD
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def copy(self, default=None):
|
||||
new_questions = super().copy(default)
|
||||
for old_question, new_question in zip(self, new_questions):
|
||||
if old_question.triggering_answer_ids:
|
||||
new_question.triggering_answer_ids = old_question.triggering_answer_ids
|
||||
return new_questions
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
questions = super().create(vals_list)
|
||||
questions.filtered(
|
||||
lambda q: q.survey_id
|
||||
and (q.survey_id.session_speed_rating != q.is_time_limited
|
||||
or q.is_time_limited and q.survey_id.session_speed_rating_time_limit != q.time_limit)
|
||||
).is_time_customized = True
|
||||
return questions
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_live_sessions_in_progress(self):
|
||||
running_surveys = self.survey_id.filtered(lambda survey: survey.session_state == 'in_progress')
|
||||
|
|
@ -331,24 +449,26 @@ class SurveyQuestion(models.Model):
|
|||
|
||||
def validate_question(self, answer, comment=None):
|
||||
""" Validate question, depending on question type and parameters
|
||||
for simple choice, text, date and number, answer is simply the answer of the question.
|
||||
For other multiple choices questions, answer is a list of answers (the selected choices
|
||||
or a list of selected answers per question -for matrix type-):
|
||||
- Simple answer : answer = 'example' or 2 or question_answer_id or 2019/10/10
|
||||
- Multiple choice : answer = [question_answer_id1, question_answer_id2, question_answer_id3]
|
||||
- Matrix: answer = { 'rowId1' : [colId1, colId2,...], 'rowId2' : [colId1, colId3, ...] }
|
||||
for simple choice, text, date and number, answer is simply the answer of the question.
|
||||
For other multiple choices questions, answer is a list of answers (the selected choices
|
||||
or a list of selected answers per question -for matrix type-):
|
||||
|
||||
return dict {question.id (int): error (str)} -> empty dict if no validation error.
|
||||
"""
|
||||
- Simple answer : ``answer = 'example'`` or ``2`` or ``question_answer_id`` or ``2019/10/10``
|
||||
- Multiple choice : ``answer = [question_answer_id1, question_answer_id2, question_answer_id3]``
|
||||
- Matrix: ``answer = { 'rowId1' : [colId1, colId2,...], 'rowId2' : [colId1, colId3, ...] }``
|
||||
|
||||
:returns: A dict ``{question.id: error}``, or an empty dict if no validation error.
|
||||
:rtype: dict[int, str]
|
||||
"""
|
||||
self.ensure_one()
|
||||
if isinstance(answer, str):
|
||||
answer = answer.strip()
|
||||
# Empty answer to mandatory question
|
||||
if self.constr_mandatory and not answer and self.question_type not in ['simple_choice', 'multiple_choice']:
|
||||
return {self.id: self.constr_error_msg or _('This question requires an answer.')}
|
||||
|
||||
# because in choices question types, comment can count as answer
|
||||
if answer or self.question_type in ['simple_choice', 'multiple_choice']:
|
||||
if not answer and self.question_type not in ['simple_choice', 'multiple_choice']:
|
||||
if self.constr_mandatory and not self.survey_id.users_can_go_back:
|
||||
return {self.id: self.constr_error_msg or _('This question requires an answer.')}
|
||||
else:
|
||||
if self.question_type == 'char_box':
|
||||
return self._validate_char_box(answer)
|
||||
elif self.question_type == 'numerical_box':
|
||||
|
|
@ -359,6 +479,8 @@ class SurveyQuestion(models.Model):
|
|||
return self._validate_choice(answer, comment)
|
||||
elif self.question_type == 'matrix':
|
||||
return self._validate_matrix(answer)
|
||||
elif self.question_type == 'scale':
|
||||
return self._validate_scale(answer)
|
||||
return {}
|
||||
|
||||
def _validate_char_box(self, answer):
|
||||
|
|
@ -413,11 +535,22 @@ class SurveyQuestion(models.Model):
|
|||
return {}
|
||||
|
||||
def _validate_choice(self, answer, comment):
|
||||
# Empty comment
|
||||
if self.constr_mandatory \
|
||||
and not answer \
|
||||
and not (self.comments_allowed and self.comment_count_as_answer and comment):
|
||||
""" Validates choice-based questions.
|
||||
- Checks that mandatory questions have at least one answer.
|
||||
- For 'simple_choice', ensures that exactly one answer is provided.
|
||||
"""
|
||||
answers = answer if isinstance(answer, list) else ([answer] if answer else [])
|
||||
|
||||
valid_answers_count = len(answers)
|
||||
if comment and self.comment_count_as_answer:
|
||||
valid_answers_count += 1
|
||||
|
||||
if valid_answers_count == 0 and self.constr_mandatory and not self.survey_id.users_can_go_back:
|
||||
return {self.id: self.constr_error_msg or _('This question requires an answer.')}
|
||||
|
||||
if valid_answers_count > 1 and self.question_type == 'simple_choice':
|
||||
return {self.id: _('For this question, you can only select one answer.')}
|
||||
|
||||
return {}
|
||||
|
||||
def _validate_matrix(self, answers):
|
||||
|
|
@ -426,6 +559,13 @@ class SurveyQuestion(models.Model):
|
|||
return {self.id: self.constr_error_msg or _('This question requires an answer.')}
|
||||
return {}
|
||||
|
||||
def _validate_scale(self, answer):
|
||||
if not self.survey_id.users_can_go_back \
|
||||
and self.constr_mandatory \
|
||||
and not answer:
|
||||
return {self.id: self.constr_error_msg or _('This question requires an answer.')}
|
||||
return {}
|
||||
|
||||
def _index(self):
|
||||
"""We would normally just use the 'sequence' field of questions BUT, if the pages and questions are
|
||||
created without ever moving records around, the sequence field can be set to 0 for all the questions.
|
||||
|
|
@ -434,6 +574,36 @@ class SurveyQuestion(models.Model):
|
|||
self.ensure_one()
|
||||
return list(self.survey_id.question_and_page_ids).index(self)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# SPEED RATING
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _update_time_limit_from_survey(self, is_time_limited=None, time_limit=None):
|
||||
"""Update the speed rating values after a change in survey's speed rating configuration.
|
||||
|
||||
* Questions that were not customized will take the new default values from the survey
|
||||
* Questions that were customized will not change their values, but this method will check
|
||||
and update the `is_time_customized` flag if necessary (to `False`) such that the user
|
||||
won't need to "actively" do it to make the question sensitive to change in survey values.
|
||||
|
||||
This is not done with `_compute`s because `is_time_limited` (and `time_limit`) would depend
|
||||
on `is_time_customized` and vice versa.
|
||||
"""
|
||||
write_vals = {}
|
||||
if is_time_limited is not None:
|
||||
write_vals['is_time_limited'] = is_time_limited
|
||||
if time_limit is not None:
|
||||
write_vals['time_limit'] = time_limit
|
||||
non_time_customized_questions = self.filtered(lambda s: not s.is_time_customized)
|
||||
non_time_customized_questions.write(write_vals)
|
||||
|
||||
# Reset `is_time_customized` as necessary
|
||||
customized_questions = self - non_time_customized_questions
|
||||
back_to_default_questions = customized_questions.filtered(
|
||||
lambda q: q.is_time_limited == q.survey_id.session_speed_rating
|
||||
and (q.is_time_limited is False or q.time_limit == q.survey_id.session_speed_rating_time_limit))
|
||||
back_to_default_questions.is_time_customized = False
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# STATISTICS / REPORTING
|
||||
# ------------------------------------------------------------
|
||||
|
|
@ -466,7 +636,7 @@ class SurveyQuestion(models.Model):
|
|||
answer_line_ids=answer_lines,
|
||||
answer_line_done_ids=done_lines,
|
||||
answer_input_done_ids=done_lines.mapped('user_input_id'),
|
||||
answer_input_skipped_ids=skipped_lines.mapped('user_input_id'),
|
||||
answer_input_ids=answer_lines.mapped('user_input_id'),
|
||||
comment_line_ids=comment_line_ids)
|
||||
question_data.update(question._get_stats_summary_data(answer_lines))
|
||||
|
||||
|
|
@ -474,7 +644,12 @@ class SurveyQuestion(models.Model):
|
|||
table_data, graph_data = question._get_stats_data(answer_lines)
|
||||
question_data['table_data'] = table_data
|
||||
question_data['graph_data'] = json.dumps(graph_data)
|
||||
|
||||
if question.question_type in ["text_box", "char_box", "numerical_box", "date", "datetime"]:
|
||||
answers_data = [
|
||||
[input_line.id, input_line._get_answer_value(), input_line.user_input_id.get_print_url()]
|
||||
for input_line in table_data if not input_line.skipped
|
||||
]
|
||||
question_data["answers_data"] = json.dumps(answers_data, default=str)
|
||||
all_questions_data.append(question_data)
|
||||
return all_questions_data
|
||||
|
||||
|
|
@ -486,6 +661,9 @@ class SurveyQuestion(models.Model):
|
|||
return table_data, [{'key': self.title, 'values': graph_data}]
|
||||
elif self.question_type == 'matrix':
|
||||
return self._get_stats_graph_data_matrix(user_input_lines)
|
||||
elif self.question_type == 'scale':
|
||||
table_data, graph_data = self._get_stats_data_scale(user_input_lines)
|
||||
return table_data, [{'key': self.title, 'values': graph_data}]
|
||||
return [line for line in user_input_lines], []
|
||||
|
||||
def _get_stats_data_answers(self, user_input_lines):
|
||||
|
|
@ -504,17 +682,17 @@ class SurveyQuestion(models.Model):
|
|||
count_data[line.suggested_answer_id] += 1
|
||||
|
||||
table_data = [{
|
||||
'value': _('Other (see comments)') if not sug_answer else sug_answer.value,
|
||||
'suggested_answer': sug_answer,
|
||||
'count': count_data[sug_answer],
|
||||
'count_text': _("%s Votes", count_data[sug_answer]),
|
||||
'value': _('Other (see comments)') if not suggested_answer else suggested_answer.value_label,
|
||||
'suggested_answer': suggested_answer,
|
||||
'count': count_data[suggested_answer],
|
||||
'count_text': self.env._("%s Votes", count_data[suggested_answer]),
|
||||
}
|
||||
for sug_answer in suggested_answers]
|
||||
for suggested_answer in suggested_answers]
|
||||
graph_data = [{
|
||||
'text': _('Other (see comments)') if not sug_answer else sug_answer.value,
|
||||
'count': count_data[sug_answer]
|
||||
'text': self.env._('Other (see comments)') if not suggested_answer else suggested_answer.value_label,
|
||||
'count': count_data[suggested_answer]
|
||||
}
|
||||
for sug_answer in suggested_answers]
|
||||
for suggested_answer in suggested_answers]
|
||||
|
||||
return table_data, graph_data
|
||||
|
||||
|
|
@ -530,19 +708,42 @@ class SurveyQuestion(models.Model):
|
|||
table_data = [{
|
||||
'row': row,
|
||||
'columns': [{
|
||||
'suggested_answer': sug_answer,
|
||||
'count': count_data[(row, sug_answer)]
|
||||
} for sug_answer in suggested_answers],
|
||||
'suggested_answer': suggested_answer,
|
||||
'count': count_data[(row, suggested_answer)]
|
||||
} for suggested_answer in suggested_answers],
|
||||
} for row in matrix_rows]
|
||||
graph_data = [{
|
||||
'key': sug_answer.value,
|
||||
'key': suggested_answer.value,
|
||||
'values': [{
|
||||
'text': row.value,
|
||||
'count': count_data[(row, sug_answer)]
|
||||
'count': count_data[(row, suggested_answer)]
|
||||
}
|
||||
for row in matrix_rows
|
||||
]
|
||||
} for sug_answer in suggested_answers]
|
||||
} for suggested_answer in suggested_answers]
|
||||
|
||||
return table_data, graph_data
|
||||
|
||||
def _get_stats_data_scale(self, user_input_lines):
|
||||
suggested_answers = range(self.scale_min, self.scale_max + 1)
|
||||
# Scale doesn't support comment as answer, so no extra value added
|
||||
|
||||
count_data = dict.fromkeys(suggested_answers, 0)
|
||||
for line in user_input_lines:
|
||||
if not line.skipped and line.value_scale in count_data:
|
||||
count_data[line.value_scale] += 1
|
||||
|
||||
table_data = []
|
||||
graph_data = []
|
||||
for sug_answer in suggested_answers:
|
||||
table_data.append({'value': str(sug_answer),
|
||||
'suggested_answer': self.env['survey.question.answer'],
|
||||
'count': count_data[sug_answer],
|
||||
'count_text': _("%s Votes", count_data[sug_answer]),
|
||||
})
|
||||
graph_data.append({'text': str(sug_answer),
|
||||
'count': count_data[sug_answer]
|
||||
})
|
||||
|
||||
return table_data, graph_data
|
||||
|
||||
|
|
@ -552,8 +753,10 @@ class SurveyQuestion(models.Model):
|
|||
stats.update(self._get_stats_summary_data_choice(user_input_lines))
|
||||
elif self.question_type == 'numerical_box':
|
||||
stats.update(self._get_stats_summary_data_numerical(user_input_lines))
|
||||
elif self.question_type == 'scale':
|
||||
stats.update(self._get_stats_summary_data_numerical(user_input_lines, 'value_scale'))
|
||||
|
||||
if self.question_type in ['numerical_box', 'date', 'datetime']:
|
||||
if self.question_type in ['numerical_box', 'date', 'datetime', 'scale']:
|
||||
stats.update(self._get_stats_summary_data_scored(user_input_lines))
|
||||
return stats
|
||||
|
||||
|
|
@ -575,8 +778,8 @@ class SurveyQuestion(models.Model):
|
|||
'partial_inputs_count': len(partial_inputs),
|
||||
}
|
||||
|
||||
def _get_stats_summary_data_numerical(self, user_input_lines):
|
||||
all_values = user_input_lines.filtered(lambda line: not line.skipped).mapped('value_numerical_box')
|
||||
def _get_stats_summary_data_numerical(self, user_input_lines, fname='value_numerical_box'):
|
||||
all_values = user_input_lines.filtered(lambda line: not line.skipped).mapped(fname)
|
||||
lines_sum = sum(all_values)
|
||||
return {
|
||||
'numerical_max': max(all_values, default=0),
|
||||
|
|
@ -592,6 +795,42 @@ class SurveyQuestion(models.Model):
|
|||
'right_inputs_count': len(user_input_lines.filtered(lambda line: line.answer_is_correct).mapped('user_input_id'))
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# OTHERS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _get_correct_answers(self):
|
||||
""" Return a dictionary linking the scorable question ids to their correct answers.
|
||||
The questions without correct answers are not considered.
|
||||
"""
|
||||
correct_answers = {}
|
||||
|
||||
# Simple and multiple choice
|
||||
choices_questions = self.filtered(lambda q: q.question_type in ['simple_choice', 'multiple_choice'])
|
||||
if choices_questions:
|
||||
suggested_answers_data = self.env['survey.question.answer'].search_read(
|
||||
[('question_id', 'in', choices_questions.ids), ('is_correct', '=', True)],
|
||||
['question_id', 'id'],
|
||||
load='', # prevent computing display_names
|
||||
)
|
||||
for data in suggested_answers_data:
|
||||
if not data.get('id'):
|
||||
continue
|
||||
correct_answers.setdefault(data['question_id'], []).append(data['id'])
|
||||
|
||||
# Numerical box, date, datetime
|
||||
for question in self - choices_questions:
|
||||
if question.question_type not in ['numerical_box', 'date', 'datetime']:
|
||||
continue
|
||||
answer = question[f'answer_{question.question_type}']
|
||||
if question.question_type == 'date':
|
||||
answer = tools.format_date(self.env, answer)
|
||||
elif question.question_type == 'datetime':
|
||||
answer = tools.format_datetime(self.env, answer, tz='UTC', dt_format=False)
|
||||
correct_answers[question.id] = answer
|
||||
|
||||
return correct_answers
|
||||
|
||||
|
||||
class SurveyQuestionAnswer(models.Model):
|
||||
""" A preconfigured answer for a question. This model stores values used
|
||||
|
|
@ -604,25 +843,79 @@ class SurveyQuestionAnswer(models.Model):
|
|||
"""
|
||||
_name = 'survey.question.answer'
|
||||
_rec_name = 'value'
|
||||
_order = 'sequence, id'
|
||||
_rec_names_search = ['question_id.title', 'value']
|
||||
_order = 'question_id, sequence, id'
|
||||
_description = 'Survey Label'
|
||||
|
||||
MAX_ANSWER_NAME_LENGTH = 90 # empirically tested in client dropdown
|
||||
|
||||
# question and question related fields
|
||||
question_id = fields.Many2one('survey.question', string='Question', ondelete='cascade')
|
||||
matrix_question_id = fields.Many2one('survey.question', string='Question (as matrix row)', ondelete='cascade')
|
||||
question_id = fields.Many2one('survey.question', string='Question', ondelete='cascade', index='btree_not_null')
|
||||
matrix_question_id = fields.Many2one('survey.question', string='Question (as matrix row)', ondelete='cascade', index='btree_not_null')
|
||||
question_type = fields.Selection(related='question_id.question_type')
|
||||
sequence = fields.Integer('Label Sequence order', default=10)
|
||||
scoring_type = fields.Selection(related='question_id.scoring_type')
|
||||
# answer related fields
|
||||
value = fields.Char('Suggested value', translate=True, required=True)
|
||||
value = fields.Char('Suggested value', translate=True)
|
||||
value_image = fields.Image('Image', max_width=1024, max_height=1024)
|
||||
value_image_filename = fields.Char('Image Filename')
|
||||
value_label = fields.Char('Value Label', compute='_compute_value_label',
|
||||
help="Answer label as either the value itself if not empty "
|
||||
"or a letter representing the index of the answer otherwise.")
|
||||
is_correct = fields.Boolean('Correct')
|
||||
answer_score = fields.Float('Score', help="A positive score indicates a correct choice; a negative or null score indicates a wrong answer")
|
||||
|
||||
_value_not_empty = models.Constraint(
|
||||
'CHECK (value IS NOT NULL OR value_image_filename IS NOT NULL)',
|
||||
'Suggested answer value must not be empty (a text and/or an image must be provided).',
|
||||
)
|
||||
|
||||
@api.depends('value_label', 'question_id.question_type', 'question_id.title', 'matrix_question_id')
|
||||
def _compute_display_name(self):
|
||||
"""Render an answer name as "Question title : Answer label value", making sure it is not too long.
|
||||
|
||||
Unless the answer is part of a matrix-type question, this implementation makes sure we have
|
||||
at least 30 characters for the question title, then we elide it, leaving the rest of the
|
||||
space for the answer.
|
||||
"""
|
||||
for answer in self:
|
||||
answer_label = answer.value_label
|
||||
if not answer.question_id or answer.question_id.question_type == 'matrix':
|
||||
answer.display_name = answer_label
|
||||
continue
|
||||
title = answer.question_id.title or _("[Question Title]")
|
||||
n_extra_characters = len(title) + len(answer_label) + 3 - self.MAX_ANSWER_NAME_LENGTH # 3 for `" : "`
|
||||
if n_extra_characters <= 0:
|
||||
answer.display_name = f'{title} : {answer_label}'
|
||||
else:
|
||||
answer.display_name = shorten(
|
||||
f'{shorten(title, max(30, len(title) - n_extra_characters), placeholder="...")} : {answer_label}',
|
||||
self.MAX_ANSWER_NAME_LENGTH,
|
||||
placeholder="..."
|
||||
)
|
||||
|
||||
@api.depends('question_id.suggested_answer_ids', 'sequence', 'value')
|
||||
def _compute_value_label(self):
|
||||
""" Compute the label as the value if not empty or a letter representing the index of the answer otherwise. """
|
||||
for answer in self:
|
||||
# using image -> use a letter to represent the value
|
||||
if not answer.value and answer.question_id and answer.id:
|
||||
answer_idx = answer.question_id.suggested_answer_ids.ids.index(answer.id)
|
||||
answer.value_label = chr(65 + answer_idx) if answer_idx < 27 else ''
|
||||
else:
|
||||
answer.value_label = answer.value or ''
|
||||
|
||||
@api.constrains('question_id', 'matrix_question_id')
|
||||
def _check_question_not_empty(self):
|
||||
"""Ensure that field question_id XOR field matrix_question_id is not null"""
|
||||
for label in self:
|
||||
if not bool(label.question_id) != bool(label.matrix_question_id):
|
||||
raise ValidationError(_("A label must be attached to only one question."))
|
||||
|
||||
def _get_answer_matching_domain(self, row_id=False):
|
||||
self.ensure_one()
|
||||
if self.question_type == "matrix":
|
||||
return ['&', '&', ('question_id', '=', self.question_id.id), ('matrix_row_id', '=', row_id), ('suggested_answer_id', '=', self.id)]
|
||||
elif self.question_type in ('multiple_choice', 'simple_choice'):
|
||||
return ['&', ('question_id', '=', self.question_id.id), ('suggested_answer_id', '=', self.id)]
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
import random
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
|
||||
import werkzeug
|
||||
|
||||
from odoo import api, exceptions, fields, models, _
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import clean_context, is_html_empty
|
||||
from odoo.exceptions import AccessError, UserError, ValidationError
|
||||
from odoo.fields import Domain
|
||||
from odoo.tools import is_html_empty
|
||||
from odoo.tools.urls import urljoin as url_join
|
||||
|
||||
|
||||
class Survey(models.Model):
|
||||
class SurveySurvey(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'
|
||||
|
|
@ -21,40 +22,35 @@ class Survey(models.Model):
|
|||
_rec_name = 'title'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
|
||||
@api.model
|
||||
def _get_default_access_token(self):
|
||||
return str(uuid.uuid4())
|
||||
|
||||
def _get_default_session_code(self):
|
||||
""" Attempt to generate a session code for our survey.
|
||||
The method will first try to generate 20 codes with 4 digits each and check if any are colliding.
|
||||
If we have at least one non-colliding code, we use it.
|
||||
If all 20 generated codes are colliding, we try with 20 codes of 5 digits,
|
||||
then 6, ... up to 10 digits. """
|
||||
|
||||
for digits_count in range(4, 10):
|
||||
range_lower_bound = 1 * (10 ** (digits_count - 1))
|
||||
range_upper_bound = (range_lower_bound * 10) - 1
|
||||
code_candidates = set([str(random.randint(range_lower_bound, range_upper_bound)) for i in range(20)])
|
||||
colliding_codes = self.sudo().search_read(
|
||||
[('session_code', 'in', list(code_candidates))],
|
||||
['session_code']
|
||||
)
|
||||
code_candidates -= set([colliding_code['session_code'] for colliding_code in colliding_codes])
|
||||
if code_candidates:
|
||||
return list(code_candidates)[0]
|
||||
|
||||
return False # could not generate a code
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
result = super().default_get(fields_list)
|
||||
def default_get(self, fields):
|
||||
result = super().default_get(fields)
|
||||
# allows to propagate the text one write in a many2one widget after
|
||||
# clicking on 'Create and Edit...' to the popup form.
|
||||
if 'title' in fields_list and not result.get('title') and self.env.context.get('default_name'):
|
||||
if 'title' in fields and not result.get('title') and self.env.context.get('default_name'):
|
||||
result['title'] = self.env.context.get('default_name')
|
||||
return result
|
||||
|
||||
# description
|
||||
survey_type = fields.Selection([
|
||||
('survey', 'Survey'),
|
||||
('live_session', 'Live session'),
|
||||
('assessment', 'Assessment'),
|
||||
('custom', 'Custom'),
|
||||
],
|
||||
string='Survey Type', required=True, default='custom')
|
||||
lang_ids = fields.Many2many(
|
||||
'res.lang', string='Languages',
|
||||
default=lambda self: self.env['res.lang']._lang_get(
|
||||
self.env.context.get('lang') or self.env['res.lang'].get_installed()[0][0]),
|
||||
domain=lambda self: [('id', 'in', [lang.id for lang in self.env['res.lang']._get_active_by('code').values()])],
|
||||
help="Leave the field empty to support all installed languages."
|
||||
)
|
||||
allowed_survey_types = fields.Json(string='Allowed survey types', compute="_compute_allowed_survey_types")
|
||||
title = fields.Char('Survey Title', required=True, translate=True)
|
||||
color = fields.Integer('Color Index', default=0)
|
||||
description = fields.Html(
|
||||
|
|
@ -68,8 +64,9 @@ class Survey(models.Model):
|
|||
active = fields.Boolean("Active", default=True)
|
||||
user_id = fields.Many2one(
|
||||
'res.users', string='Responsible',
|
||||
domain=[('share', '=', False)], tracking=True,
|
||||
domain=[('share', '=', False)], tracking=1,
|
||||
default=lambda self: self.env.user)
|
||||
restrict_user_ids = fields.Many2many('res.users', string='Restricted to', domain=[('share', '=', False)], tracking=2)
|
||||
# questions
|
||||
question_and_page_ids = fields.One2many('survey.question', 'survey_id', string='Sections and Questions', copy=True)
|
||||
page_ids = fields.One2many('survey.question', string='Pages', compute="_compute_page_and_question_ids")
|
||||
|
|
@ -90,7 +87,7 @@ class Survey(models.Model):
|
|||
('number', 'Number')], string='Display Progress as', default='percent',
|
||||
help="If Number is selected, it will display the number of questions answered on the total number of question to answer.")
|
||||
# attendees
|
||||
user_input_ids = fields.One2many('survey.user_input', 'survey_id', string='User responses', readonly=True, groups='survey.group_survey_user')
|
||||
user_input_ids = fields.One2many('survey.user_input', 'survey_id', string='User responses', readonly=True)
|
||||
# security / access
|
||||
access_mode = fields.Selection([
|
||||
('public', 'Anyone with the link'),
|
||||
|
|
@ -110,10 +107,12 @@ class Survey(models.Model):
|
|||
# scoring
|
||||
scoring_type = fields.Selection([
|
||||
('no_scoring', 'No scoring'),
|
||||
('scoring_with_answers_after_page', 'Scoring with answers after each page'),
|
||||
('scoring_with_answers', 'Scoring with answers at the end'),
|
||||
('scoring_without_answers', 'Scoring without answers at the end')],
|
||||
string="Scoring", required=True, store=True, readonly=False, compute="_compute_scoring_type", precompute=True)
|
||||
('scoring_without_answers', 'Scoring without answers')],
|
||||
string='Scoring', required=True, store=True, readonly=False, compute='_compute_scoring_type', precompute=True)
|
||||
scoring_success_min = fields.Float('Required Score (%)', default=80.0)
|
||||
scoring_max_obtainable = fields.Float('Maximum obtainable score', compute='_compute_scoring_max_obtainable')
|
||||
# attendees context: attempts and time limitation
|
||||
is_attempts_limited = fields.Boolean('Limited number of attempts', help="Check this option if you want to limit the number of attempts per user",
|
||||
compute="_compute_is_attempts_limited", store=True, readonly=False)
|
||||
|
|
@ -142,14 +141,16 @@ class Survey(models.Model):
|
|||
# So it can be edited but not removed or replaced.
|
||||
certification_give_badge = fields.Boolean('Give Badge', compute='_compute_certification_give_badge',
|
||||
readonly=False, store=True, copy=False)
|
||||
certification_badge_id = fields.Many2one('gamification.badge', 'Certification Badge', copy=False)
|
||||
certification_badge_id = fields.Many2one('gamification.badge', 'Certification Badge', copy=False, index='btree_not_null')
|
||||
certification_badge_id_dummy = fields.Many2one(related='certification_badge_id', string='Certification Badge ')
|
||||
# live sessions
|
||||
session_available = fields.Boolean('Live session available', compute='_compute_session_available')
|
||||
session_state = fields.Selection([
|
||||
('ready', 'Ready'),
|
||||
('in_progress', 'In Progress'),
|
||||
], string="Session State", copy=False)
|
||||
session_code = fields.Char('Session Code', default=lambda self: self._get_default_session_code(), copy=False,
|
||||
session_code = fields.Char('Session Code', copy=False, compute="_compute_session_code",
|
||||
precompute=True, store=True, readonly=False,
|
||||
help="This code will be used by your attendees to reach your session. Feel free to customize it however you like!")
|
||||
session_link = fields.Char('Session Link', compute='_compute_session_link')
|
||||
# live sessions - current question fields
|
||||
|
|
@ -164,29 +165,63 @@ class Survey(models.Model):
|
|||
session_show_leaderboard = fields.Boolean("Show Session Leaderboard", compute='_compute_session_show_leaderboard',
|
||||
help="Whether or not we want to show the attendees leaderboard for this survey.")
|
||||
session_speed_rating = fields.Boolean("Reward quick answers", help="Attendees get more points if they answer quickly")
|
||||
session_speed_rating_time_limit = fields.Integer(
|
||||
"Time limit (seconds)", help="Default time given to receive additional points for right answers")
|
||||
# conditional questions management
|
||||
has_conditional_questions = fields.Boolean("Contains conditional questions", compute="_compute_has_conditional_questions")
|
||||
|
||||
_sql_constraints = [
|
||||
('access_token_unique', 'unique(access_token)', 'Access token should be unique'),
|
||||
('session_code_unique', 'unique(session_code)', 'Session code should be unique'),
|
||||
('certification_check', "CHECK( scoring_type!='no_scoring' OR certification=False )",
|
||||
'You can only create certifications for surveys that have a scoring mechanism.'),
|
||||
('scoring_success_min_check', "CHECK( scoring_success_min IS NULL OR (scoring_success_min>=0 AND scoring_success_min<=100) )",
|
||||
'The percentage of success has to be defined between 0 and 100.'),
|
||||
('time_limit_check', "CHECK( (is_time_limited=False) OR (time_limit is not null AND time_limit > 0) )",
|
||||
'The time limit needs to be a positive number if the survey is time limited.'),
|
||||
('attempts_limit_check', "CHECK( (is_attempts_limited=False) OR (attempts_limit is not null AND attempts_limit > 0) )",
|
||||
'The attempts limit needs to be a positive number if the survey has a limited number of attempts.'),
|
||||
('badge_uniq', 'unique (certification_badge_id)', "The badge for each survey should be unique!"),
|
||||
]
|
||||
_access_token_unique = models.Constraint(
|
||||
'unique(access_token)',
|
||||
'Access token should be unique',
|
||||
)
|
||||
_session_code_unique = models.Constraint(
|
||||
'unique(session_code)',
|
||||
'Session code should be unique',
|
||||
)
|
||||
_certification_check = models.Constraint(
|
||||
"CHECK( scoring_type!='no_scoring' OR certification=False )",
|
||||
'You can only create certifications for surveys that have a scoring mechanism.',
|
||||
)
|
||||
_scoring_success_min_check = models.Constraint(
|
||||
'CHECK( scoring_success_min IS NULL OR (scoring_success_min>=0 AND scoring_success_min<=100) )',
|
||||
'The percentage of success has to be defined between 0 and 100.',
|
||||
)
|
||||
_time_limit_check = models.Constraint(
|
||||
'CHECK( (is_time_limited=False) OR (time_limit is not null AND time_limit > 0) )',
|
||||
'The time limit needs to be a positive number if the survey is time limited.',
|
||||
)
|
||||
_attempts_limit_check = models.Constraint(
|
||||
'CHECK( (is_attempts_limited=False) OR (attempts_limit is not null AND attempts_limit > 0) )',
|
||||
'The attempts limit needs to be a positive number if the survey has a limited number of attempts.',
|
||||
)
|
||||
_badge_uniq = models.Constraint(
|
||||
'unique (certification_badge_id)',
|
||||
'The badge for each survey should be unique!',
|
||||
)
|
||||
_session_speed_rating_has_time_limit = models.Constraint(
|
||||
'CHECK (session_speed_rating != TRUE OR session_speed_rating_time_limit IS NOT NULL AND session_speed_rating_time_limit > 0)',
|
||||
'A positive default time limit is required when the session rewards quick answers.',
|
||||
)
|
||||
|
||||
@api.depends('background_image', 'access_token')
|
||||
def _compute_background_image_url(self):
|
||||
self.background_image_url = False
|
||||
for survey in self.filtered(lambda survey: survey.background_image and survey.access_token):
|
||||
for survey in self.filtered(lambda s: s.background_image and s.access_token):
|
||||
survey.background_image_url = "/survey/%s/get_background_image" % survey.access_token
|
||||
|
||||
@api.depends(
|
||||
'question_and_page_ids',
|
||||
'question_and_page_ids.suggested_answer_ids',
|
||||
'question_and_page_ids.suggested_answer_ids.answer_score',
|
||||
)
|
||||
def _compute_scoring_max_obtainable(self):
|
||||
for survey in self:
|
||||
survey.scoring_max_obtainable = sum(
|
||||
question.answer_score
|
||||
or sum(answer.answer_score for answer in question.suggested_answer_ids if answer.answer_score > 0)
|
||||
for question in survey.question_ids
|
||||
)
|
||||
|
||||
def _compute_users_can_signup(self):
|
||||
signup_allowed = self.env['res.users'].sudo()._get_signup_invitation_scope() == 'b2c'
|
||||
for survey in self:
|
||||
|
|
@ -202,14 +237,14 @@ class Survey(models.Model):
|
|||
UserInput = self.env['survey.user_input']
|
||||
base_domain = [('survey_id', 'in', self.ids)]
|
||||
|
||||
read_group_res = UserInput._read_group(base_domain, ['survey_id', 'state'], ['survey_id', 'state', 'scoring_percentage', 'scoring_success'], lazy=False)
|
||||
for item in read_group_res:
|
||||
stat[item['survey_id'][0]]['answer_count'] += item['__count']
|
||||
stat[item['survey_id'][0]]['answer_score_avg_total'] += item['scoring_percentage']
|
||||
if item['state'] == 'done':
|
||||
stat[item['survey_id'][0]]['answer_done_count'] += item['__count']
|
||||
if item['scoring_success']:
|
||||
stat[item['survey_id'][0]]['success_count'] += item['__count']
|
||||
read_group_res = UserInput._read_group(base_domain, ['survey_id', 'state', 'scoring_percentage', 'scoring_success'], ['__count'])
|
||||
for survey, state, scoring_percentage, scoring_success, count in read_group_res:
|
||||
stat[survey.id]['answer_count'] += count
|
||||
stat[survey.id]['answer_score_avg_total'] += scoring_percentage
|
||||
if state == 'done':
|
||||
stat[survey.id]['answer_done_count'] += count
|
||||
if scoring_success:
|
||||
stat[survey.id]['success_count'] += count
|
||||
|
||||
for survey_stats in stat.values():
|
||||
avg_total = survey_stats.pop('answer_score_avg_total')
|
||||
|
|
@ -239,7 +274,6 @@ class Survey(models.Model):
|
|||
# as avg returns None if nothing found, set 0 if it's the case.
|
||||
survey.answer_duration_avg = (result_per_survey_id.get(survey.id) or 0) / 3600
|
||||
|
||||
|
||||
@api.depends('question_and_page_ids')
|
||||
def _compute_page_and_question_ids(self):
|
||||
for survey in self:
|
||||
|
|
@ -247,11 +281,12 @@ class Survey(models.Model):
|
|||
survey.question_ids = survey.question_and_page_ids - survey.page_ids
|
||||
survey.question_count = len(survey.question_ids)
|
||||
|
||||
@api.depends('users_login_required', 'access_mode')
|
||||
@api.depends('question_and_page_ids.triggering_answer_ids', 'users_login_required', 'access_mode')
|
||||
def _compute_is_attempts_limited(self):
|
||||
for survey in self:
|
||||
if not survey.is_attempts_limited or \
|
||||
(survey.access_mode == 'public' and not survey.users_login_required):
|
||||
(survey.access_mode == 'public' and not survey.users_login_required) or \
|
||||
any(question.triggering_answer_ids for question in survey.question_and_page_ids):
|
||||
survey.is_attempts_limited = False
|
||||
|
||||
@api.depends('session_start_time', 'user_input_ids')
|
||||
|
|
@ -261,18 +296,13 @@ class Survey(models.Model):
|
|||
context of sessions, so it should not matter too much. """
|
||||
|
||||
for survey in self:
|
||||
answer_count = 0
|
||||
input_count = self.env['survey.user_input']._read_group(
|
||||
[answer_count] = self.env['survey.user_input']._read_group(
|
||||
[('survey_id', '=', survey.id),
|
||||
('is_session_answer', '=', True),
|
||||
('state', '!=', 'done'),
|
||||
('create_date', '>=', survey.session_start_time)],
|
||||
['create_uid:count'],
|
||||
['survey_id'],
|
||||
)
|
||||
if input_count:
|
||||
answer_count = input_count[0].get('create_uid', 0)
|
||||
|
||||
aggregates=['create_uid:count'],
|
||||
)[0]
|
||||
survey.session_answer_count = answer_count
|
||||
|
||||
@api.depends('session_question_id', 'session_start_time', 'user_input_ids.user_input_line_ids')
|
||||
|
|
@ -282,28 +312,33 @@ class Survey(models.Model):
|
|||
This field is currently used to display the count about a single survey, in the
|
||||
context of sessions, so it should not matter too much. """
|
||||
for survey in self:
|
||||
answer_count = 0
|
||||
input_line_count = self.env['survey.user_input.line']._read_group(
|
||||
[answer_count] = self.env['survey.user_input.line']._read_group(
|
||||
[('question_id', '=', survey.session_question_id.id),
|
||||
('survey_id', '=', survey.id),
|
||||
('create_date', '>=', survey.session_start_time)],
|
||||
['user_input_id:count_distinct'],
|
||||
['question_id'],
|
||||
)
|
||||
if input_line_count:
|
||||
answer_count = input_line_count[0].get('user_input_id', 0)
|
||||
|
||||
aggregates=['user_input_id:count_distinct'],
|
||||
)[0]
|
||||
survey.session_question_answer_count = answer_count
|
||||
|
||||
@api.depends('access_token')
|
||||
def _compute_session_code(self):
|
||||
survey_without_session_code = self.filtered(lambda survey: not survey.session_code)
|
||||
session_codes = self._generate_session_codes(
|
||||
code_count=len(survey_without_session_code),
|
||||
excluded_codes=set((self - survey_without_session_code).mapped('session_code'))
|
||||
)
|
||||
for survey, session_code in zip(survey_without_session_code, session_codes):
|
||||
survey.session_code = session_code
|
||||
|
||||
@api.depends('session_code')
|
||||
def _compute_session_link(self):
|
||||
for survey in self:
|
||||
if survey.session_code:
|
||||
survey.session_link = werkzeug.urls.url_join(
|
||||
survey.session_link = url_join(
|
||||
survey.get_base_url(),
|
||||
'/s/%s' % survey.session_code)
|
||||
else:
|
||||
survey.session_link = werkzeug.urls.url_join(
|
||||
survey.session_link = url_join(
|
||||
survey.get_base_url(),
|
||||
survey.get_start_url())
|
||||
|
||||
|
|
@ -313,10 +348,10 @@ class Survey(models.Model):
|
|||
survey.session_show_leaderboard = survey.scoring_type != 'no_scoring' and \
|
||||
any(question.save_as_nickname for question in survey.question_and_page_ids)
|
||||
|
||||
@api.depends('question_and_page_ids.is_conditional')
|
||||
@api.depends('question_and_page_ids.triggering_answer_ids')
|
||||
def _compute_has_conditional_questions(self):
|
||||
for survey in self:
|
||||
survey.has_conditional_questions = any(question.is_conditional for question in survey.question_and_page_ids)
|
||||
survey.has_conditional_questions = any(question.triggering_answer_ids for question in survey.question_and_page_ids)
|
||||
|
||||
@api.depends('scoring_type')
|
||||
def _compute_certification(self):
|
||||
|
|
@ -335,72 +370,163 @@ class Survey(models.Model):
|
|||
@api.depends('certification')
|
||||
def _compute_scoring_type(self):
|
||||
for survey in self:
|
||||
if survey.certification and survey.scoring_type not in ['scoring_without_answers', 'scoring_with_answers']:
|
||||
if survey.certification and survey.scoring_type in {False, 'no_scoring'}:
|
||||
survey.scoring_type = 'scoring_without_answers'
|
||||
elif not survey.scoring_type:
|
||||
survey.scoring_type = 'no_scoring'
|
||||
|
||||
@api.depends('survey_type', 'certification')
|
||||
def _compute_session_available(self):
|
||||
for survey in self:
|
||||
survey.session_available = survey.survey_type in {'live_session', 'custom'} and not survey.certification
|
||||
|
||||
@api.depends_context('uid')
|
||||
def _compute_allowed_survey_types(self):
|
||||
self.allowed_survey_types = [
|
||||
'survey',
|
||||
'live_session',
|
||||
'assessment',
|
||||
'custom',
|
||||
] if self.env.user.has_group('survey.group_survey_user') else False
|
||||
|
||||
@api.onchange('survey_type')
|
||||
def _onchange_survey_type(self):
|
||||
if self.survey_type == 'survey':
|
||||
self.write({
|
||||
'certification': False,
|
||||
'is_time_limited': False,
|
||||
'scoring_type': 'no_scoring',
|
||||
})
|
||||
elif self.survey_type == 'live_session':
|
||||
self.write({
|
||||
'access_mode': 'public',
|
||||
'is_attempts_limited': False,
|
||||
'is_time_limited': False,
|
||||
'progression_mode': 'percent',
|
||||
'questions_layout': 'page_per_question',
|
||||
'questions_selection': 'all',
|
||||
'scoring_type': 'scoring_with_answers',
|
||||
'users_can_go_back': False,
|
||||
})
|
||||
elif self.survey_type == 'assessment':
|
||||
self.write({
|
||||
'access_mode': 'token',
|
||||
'scoring_type': 'scoring_with_answers',
|
||||
})
|
||||
|
||||
@api.onchange('session_speed_rating', 'session_speed_rating_time_limit')
|
||||
def _onchange_session_speed_rating(self):
|
||||
"""Show impact on questions in the form view (before survey is saved)."""
|
||||
for survey in self.filtered('question_ids'):
|
||||
survey.question_ids._update_time_limit_from_survey(
|
||||
is_time_limited=survey.session_speed_rating, time_limit=survey.session_speed_rating_time_limit)
|
||||
|
||||
@api.onchange('restrict_user_ids', 'user_id')
|
||||
def _onchange_restrict_user_ids(self):
|
||||
"""
|
||||
Add survey user_id to restrict_user_ids when:
|
||||
- restrict_user_ids is not False
|
||||
- user_id is not part of restrict_user_ids
|
||||
- user_id is not a survey manager
|
||||
"""
|
||||
surveys_to_check = self.filtered(lambda s: s.restrict_user_ids and bool(s.user_id - s.restrict_user_ids))
|
||||
users_are_managers = surveys_to_check.user_id.filtered(lambda user: user.has_group('survey.group_survey_manager'))
|
||||
for survey in surveys_to_check.filtered(lambda s: s.user_id not in users_are_managers):
|
||||
survey.restrict_user_ids += survey.user_id
|
||||
|
||||
@api.constrains('scoring_type', 'users_can_go_back')
|
||||
def _check_scoring_after_page_availability(self):
|
||||
failing = self.filtered(lambda survey: survey.scoring_type == 'scoring_with_answers_after_page' and survey.users_can_go_back)
|
||||
if failing:
|
||||
raise ValidationError(
|
||||
_('Combining roaming and "Scoring with answers after each page" is not possible; please update the following surveys:\n- %(survey_names)s',
|
||||
survey_names="\n- ".join(failing.mapped('title')))
|
||||
)
|
||||
|
||||
@api.constrains('user_id', 'restrict_user_ids')
|
||||
def _check_survey_responsible_access(self):
|
||||
""" When:
|
||||
- a survey access is restricted to a list of users
|
||||
- and there is a survey responsible,
|
||||
- and this responsible is not survey manager (just survey officer),
|
||||
check the responsible is part of the list."""
|
||||
for user_id, surveys in self.filtered(lambda s: bool(s.user_id - s.restrict_user_ids)).grouped('user_id').items():
|
||||
accessible = surveys.with_user(user_id)._filtered_access("write")
|
||||
if len(accessible) < len(surveys):
|
||||
failing_surveys_sudo = (self - accessible).sudo()
|
||||
raise ValidationError(
|
||||
_('The access of the following surveys is restricted. Make sure their responsible still has access to it: \n%(survey_names)s\n',
|
||||
survey_names='\n'.join(f'- {survey.title}: {survey.user_id.name}' for survey in failing_surveys_sudo)))
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# CRUD
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
surveys = super(Survey, self).create(vals_list)
|
||||
surveys = super().create(vals_list)
|
||||
for survey_sudo in surveys.filtered(lambda survey: survey.certification_give_badge).sudo():
|
||||
survey_sudo._create_certification_badge_trigger()
|
||||
return surveys
|
||||
|
||||
def write(self, vals):
|
||||
result = super(Survey, self).write(vals)
|
||||
speed_rating, speed_limit = vals.get('session_speed_rating'), vals.get('session_speed_rating_time_limit')
|
||||
|
||||
surveys_to_update = self.filtered(lambda s: (
|
||||
speed_rating is not None and s.session_speed_rating != speed_rating
|
||||
or speed_limit is not None and s.session_speed_rating_time_limit != speed_limit
|
||||
))
|
||||
|
||||
result = super().write(vals)
|
||||
if 'certification_give_badge' in vals:
|
||||
return self.sudo().with_context(clean_context(self._context))._handle_certification_badges(vals)
|
||||
return self.sudo()._handle_certification_badges(vals)
|
||||
|
||||
if questions_to_update := surveys_to_update.question_ids:
|
||||
questions_to_update._update_time_limit_from_survey(is_time_limited=speed_rating, time_limit=speed_limit)
|
||||
|
||||
return result
|
||||
|
||||
@api.returns('self', lambda value: value.id)
|
||||
def copy(self, default=None):
|
||||
""" Correctly copy the 'triggering_question_id' and 'triggering_answer_id' fields from the original
|
||||
to the clone.
|
||||
This needs to be done in post-processing to make sure we get references to the newly created
|
||||
answers/questions from the copy instead of references to the answers/questions of the original.
|
||||
This implementation assumes that the order of created questions/answers will be kept between
|
||||
"""Correctly copy the 'triggering_answer_ids' field from the original to the clone.
|
||||
|
||||
This needs to be done in post-processing to make sure we get references to the newly
|
||||
created answers from the copy instead of references to the answers of the original.
|
||||
This implementation assumes that the order of created answers will be kept between
|
||||
the original and the clone, using 'zip()' to match the records between the two.
|
||||
|
||||
Note that when question_ids is provided in the default parameter, it falls back to the standard copy.
|
||||
Note that when `question_ids` is provided in the default parameter, it falls back to the
|
||||
standard copy, meaning that triggering logic will not be maintained.
|
||||
"""
|
||||
self.ensure_one()
|
||||
clone = super(Survey, self).copy(default)
|
||||
new_surveys = super().copy(default)
|
||||
if default and 'question_ids' in default:
|
||||
return clone
|
||||
return new_surveys
|
||||
|
||||
src_questions = self.question_ids
|
||||
dst_questions = clone.question_ids.sorted()
|
||||
for old_survey, new_survey in zip(self, new_surveys):
|
||||
cloned_question_ids = new_survey.question_ids.sorted()
|
||||
|
||||
questions_map = {src.id: dst.id for src, dst in zip(src_questions, dst_questions)}
|
||||
answers_map = {
|
||||
src_answer.id: dst_answer.id
|
||||
for src, dst
|
||||
in zip(src_questions, dst_questions)
|
||||
for src_answer, dst_answer
|
||||
in zip(src.suggested_answer_ids, dst.suggested_answer_ids.sorted())
|
||||
}
|
||||
|
||||
for src, dst in zip(src_questions, dst_questions):
|
||||
if src.is_conditional:
|
||||
dst.triggering_question_id = questions_map.get(src.triggering_question_id.id)
|
||||
dst.triggering_answer_id = answers_map.get(src.triggering_answer_id.id)
|
||||
return clone
|
||||
answers_map = {
|
||||
src_answer.id: dst_answer.id
|
||||
for src, dst
|
||||
in zip(old_survey.question_ids, cloned_question_ids)
|
||||
for src_answer, dst_answer
|
||||
in zip(src.suggested_answer_ids, dst.suggested_answer_ids.sorted())
|
||||
}
|
||||
for src, dst in zip(old_survey.question_ids, cloned_question_ids):
|
||||
if src.triggering_answer_ids:
|
||||
dst.triggering_answer_ids = [answers_map[src_answer_id.id] for src_answer_id in src.triggering_answer_ids]
|
||||
return new_surveys
|
||||
|
||||
def copy_data(self, default=None):
|
||||
new_defaults = {'title': _("%s (copy)") % (self.title)}
|
||||
default = dict(new_defaults, **(default or {}))
|
||||
return super(Survey, self).copy_data(default)
|
||||
vals_list = super().copy_data(default=default)
|
||||
return [dict(vals, title=self.env._("%s (copy)", survey.title)) for survey, vals in zip(self, vals_list)]
|
||||
|
||||
def toggle_active(self):
|
||||
super(Survey, self).toggle_active()
|
||||
activated = self.filtered(lambda survey: survey.active)
|
||||
activated.mapped('certification_badge_id').action_unarchive()
|
||||
(self - activated).mapped('certification_badge_id').action_archive()
|
||||
def action_archive(self):
|
||||
super().action_archive()
|
||||
self.certification_badge_id.action_archive()
|
||||
|
||||
def action_unarchive(self):
|
||||
super().action_unarchive()
|
||||
self.certification_badge_id.action_unarchive()
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# ANSWER MANAGEMENT
|
||||
|
|
@ -408,15 +534,13 @@ class Survey(models.Model):
|
|||
|
||||
def _create_answer(self, user=False, partner=False, email=False, test_entry=False, check_attempts=True, **additional_vals):
|
||||
""" Main entry point to get a token back or create a new one. This method
|
||||
does check for current user access in order to explicitely validate
|
||||
security.
|
||||
does check for current user access in order to explicitly validate security.
|
||||
|
||||
:param user: target user asking for a token; it might be void or a
|
||||
public user in which case an email is welcomed;
|
||||
:param email: email of the person asking the token is no user exists;
|
||||
"""
|
||||
self.check_access_rights('read')
|
||||
self.check_access_rule('read')
|
||||
self.check_access('read')
|
||||
|
||||
user_inputs = self.env['survey.user_input']
|
||||
for survey in self:
|
||||
|
|
@ -424,6 +548,7 @@ class Survey(models.Model):
|
|||
user = partner.user_ids[0]
|
||||
|
||||
invite_token = additional_vals.pop('invite_token', False)
|
||||
nickname = additional_vals.pop('nickname', False)
|
||||
survey._check_answer_creation(user, partner, email, test_entry=test_entry, check_attempts=check_attempts, invite_token=invite_token)
|
||||
answer_vals = {
|
||||
'survey_id': survey.id,
|
||||
|
|
@ -439,14 +564,14 @@ class Survey(models.Model):
|
|||
if user and not user._is_public():
|
||||
answer_vals['partner_id'] = user.partner_id.id
|
||||
answer_vals['email'] = user.email
|
||||
answer_vals['nickname'] = user.name
|
||||
answer_vals['nickname'] = nickname or user.name
|
||||
elif partner:
|
||||
answer_vals['partner_id'] = partner.id
|
||||
answer_vals['email'] = partner.email
|
||||
answer_vals['nickname'] = partner.name
|
||||
answer_vals['nickname'] = nickname or partner.name
|
||||
else:
|
||||
answer_vals['email'] = email
|
||||
answer_vals['nickname'] = email
|
||||
answer_vals['nickname'] = nickname
|
||||
|
||||
if invite_token:
|
||||
answer_vals['invite_token'] = invite_token
|
||||
|
|
@ -463,9 +588,9 @@ class Survey(models.Model):
|
|||
lambda q: q.question_type == 'char_box' and (q.save_as_email or q.save_as_nickname)):
|
||||
for user_input in user_inputs:
|
||||
if question.save_as_email and user_input.email:
|
||||
user_input.save_lines(question, user_input.email)
|
||||
user_input._save_lines(question, user_input.email)
|
||||
if question.save_as_nickname and user_input.nickname:
|
||||
user_input.save_lines(question, user_input.nickname)
|
||||
user_input._save_lines(question, user_input.nickname)
|
||||
|
||||
return user_inputs
|
||||
|
||||
|
|
@ -473,10 +598,12 @@ class Survey(models.Model):
|
|||
""" Ensure conditions to create new tokens are met. """
|
||||
self.ensure_one()
|
||||
if test_entry:
|
||||
# the current user must have the access rights to survey
|
||||
if not user.has_group('survey.group_survey_user'):
|
||||
try:
|
||||
self.with_user(user).check_access('read')
|
||||
except AccessError:
|
||||
raise exceptions.UserError(_('Creating test token is not allowed for you.'))
|
||||
else:
|
||||
|
||||
if not test_entry:
|
||||
if not self.active:
|
||||
raise exceptions.UserError(_('Creating token for closed/archived surveys is not allowed.'))
|
||||
if self.access_mode == 'authentication':
|
||||
|
|
@ -510,7 +637,7 @@ class Survey(models.Model):
|
|||
if self.questions_selection == 'all':
|
||||
questions |= page.question_ids
|
||||
else:
|
||||
if page.random_questions_count > 0 and len(page.question_ids) > page.random_questions_count:
|
||||
if 0 < page.random_questions_count < len(page.question_ids):
|
||||
questions = questions.concat(*random.sample(page.question_ids, page.random_questions_count))
|
||||
else:
|
||||
questions |= page.question_ids
|
||||
|
|
@ -529,17 +656,13 @@ class Survey(models.Model):
|
|||
(pages are displayed in 'page_per_question' layout when they have a description, see PR#44271)
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
if self.users_can_go_back and answer.state == 'in_progress':
|
||||
if self.questions_layout == 'page_per_section' and page_or_question != self.page_ids[0]:
|
||||
return True
|
||||
elif self.questions_layout == 'page_per_question' and \
|
||||
not answer.is_session_answer and \
|
||||
page_or_question != answer.predefined_question_ids[0] \
|
||||
and (not self.page_ids or page_or_question != self.page_ids[0]):
|
||||
return True
|
||||
|
||||
return False
|
||||
if self.questions_layout == "one_page" or not self.users_can_go_back:
|
||||
return False
|
||||
if answer.state != 'in_progress' or answer.is_session_answer:
|
||||
return False
|
||||
if self.page_ids and page_or_question == self.page_ids[0]:
|
||||
return False
|
||||
return self.questions_layout == 'page_per_section' or page_or_question != answer.predefined_question_ids[0]
|
||||
|
||||
def _has_attempts_left(self, partner, email, invite_token):
|
||||
self.ensure_one()
|
||||
|
|
@ -553,19 +676,19 @@ class Survey(models.Model):
|
|||
""" Returns the number of attempts left. """
|
||||
self.ensure_one()
|
||||
|
||||
domain = [
|
||||
domain = Domain([
|
||||
('survey_id', '=', self.id),
|
||||
('test_entry', '=', False),
|
||||
('state', '=', 'done')
|
||||
]
|
||||
])
|
||||
|
||||
if partner:
|
||||
domain = expression.AND([domain, [('partner_id', '=', partner.id)]])
|
||||
domain &= Domain('partner_id', '=', partner.id)
|
||||
else:
|
||||
domain = expression.AND([domain, [('email', '=', email)]])
|
||||
domain &= Domain('email', '=', email)
|
||||
|
||||
if invite_token:
|
||||
domain = expression.AND([domain, [('invite_token', '=', invite_token)]])
|
||||
domain &= Domain('invite_token', '=', invite_token)
|
||||
|
||||
return self.attempts_limit - self.env['survey.user_input'].search_count(domain)
|
||||
|
||||
|
|
@ -591,22 +714,31 @@ class Survey(models.Model):
|
|||
return result
|
||||
|
||||
def _get_pages_and_questions_to_show(self):
|
||||
"""
|
||||
:return: survey.question recordset excluding invalid conditional questions and pages without description
|
||||
"""
|
||||
"""Filter question_and_pages_ids to include only valid pages and questions.
|
||||
|
||||
Pages are invalid if they have no description. Questions are invalid if
|
||||
they are conditional and all their triggers are invalid.
|
||||
Triggers are invalid if they:
|
||||
- Are a page (not a question)
|
||||
- Have the wrong question type (`simple_choice` and `multiple_choice` are supported)
|
||||
- Are misplaced (positioned after the conditional question)
|
||||
- They are themselves conditional and were found invalid
|
||||
"""
|
||||
self.ensure_one()
|
||||
invalid_questions = self.env['survey.question']
|
||||
questions_and_valid_pages = self.question_and_page_ids.filtered(
|
||||
lambda question: not question.is_page or not is_html_empty(question.description))
|
||||
for question in questions_and_valid_pages.filtered(lambda q: q.is_conditional).sorted():
|
||||
trigger = question.triggering_question_id
|
||||
if (trigger in invalid_questions
|
||||
or trigger.is_page
|
||||
or trigger.question_type not in ['simple_choice', 'multiple_choice']
|
||||
or not trigger.suggested_answer_ids
|
||||
or trigger.sequence > question.sequence
|
||||
or (trigger.sequence == question.sequence and trigger.id > question.id)):
|
||||
|
||||
for question in questions_and_valid_pages.filtered('triggering_answer_ids').sorted():
|
||||
for trigger in question.triggering_question_ids:
|
||||
if (trigger not in invalid_questions
|
||||
and not trigger.is_page
|
||||
and trigger.question_type in ['simple_choice', 'multiple_choice']
|
||||
and (trigger.sequence < question.sequence
|
||||
or (trigger.sequence == question.sequence and trigger.id < question.id))):
|
||||
break
|
||||
else:
|
||||
# No valid trigger found
|
||||
invalid_questions |= question
|
||||
return questions_and_valid_pages - invalid_questions
|
||||
|
||||
|
|
@ -650,7 +782,6 @@ class Survey(models.Model):
|
|||
return Question
|
||||
|
||||
# Conditional Questions Management
|
||||
triggering_answer_by_question, triggered_questions_by_answer, selected_answers = user_input._get_conditional_values()
|
||||
inactive_questions = user_input._get_inactive_conditional_questions()
|
||||
if survey.questions_layout == 'page_per_question':
|
||||
question_candidates = pages_or_questions[0:current_page_index] if go_back \
|
||||
|
|
@ -663,8 +794,7 @@ class Survey(models.Model):
|
|||
if contains_active_question or is_description_section:
|
||||
return question
|
||||
else:
|
||||
triggering_answer = triggering_answer_by_question.get(question)
|
||||
if not triggering_answer or triggering_answer in selected_answers:
|
||||
if question not in inactive_questions:
|
||||
# question is visible because not conditioned or conditioned by a selected answer
|
||||
return question
|
||||
elif survey.questions_layout == 'page_per_section':
|
||||
|
|
@ -681,7 +811,7 @@ class Survey(models.Model):
|
|||
""" This method checks if the given question or page is the first one to display.
|
||||
If the first section of the survey as a description, this will be the first screen to display.
|
||||
else, the first question will be the first screen to be displayed.
|
||||
This methods is used for survey session management where the host should not be able to go back on the
|
||||
This method is used for survey session management where the host should not be able to go back on the
|
||||
first page or question."""
|
||||
first_section_has_description = self.page_ids and not is_html_empty(self.page_ids[0].description)
|
||||
is_first_page_or_question = (first_section_has_description and page_or_question == self.page_ids[0]) or \
|
||||
|
|
@ -689,38 +819,30 @@ class Survey(models.Model):
|
|||
return is_first_page_or_question
|
||||
|
||||
def _is_last_page_or_question(self, user_input, page_or_question):
|
||||
""" This method checks if the given question or page is the last one.
|
||||
This includes conditional questions configuration. If the given question is normally not the last one but
|
||||
every following questions are inactive due to conditional questions configurations (and user choices),
|
||||
the given question will be the last one, except if the given question is conditioning at least
|
||||
one of the following questions.
|
||||
For section, we check in each following section if there is an active question.
|
||||
If yes, the given page is not the last one.
|
||||
""" Check if the given question or page is the last one, accounting for conditional questions.
|
||||
|
||||
A question/page will be determined as the last one if any of the following is true:
|
||||
- The survey layout is "one_page",
|
||||
- There are no more questions/page after `page_or_question` in `user_input`,
|
||||
- All the following questions are conditional AND were not triggered by previous answers.
|
||||
Not accounting for the question/page own conditionals.
|
||||
"""
|
||||
if self.questions_layout == "one_page":
|
||||
return True
|
||||
pages_or_questions = self._get_pages_or_questions(user_input)
|
||||
current_page_index = pages_or_questions.ids.index(page_or_question.id)
|
||||
next_page_or_question_candidates = pages_or_questions[current_page_index + 1:]
|
||||
if next_page_or_question_candidates:
|
||||
inactive_questions = user_input._get_inactive_conditional_questions()
|
||||
triggering_answer_by_question, triggered_questions_by_answer, selected_answers = user_input._get_conditional_values()
|
||||
if self.questions_layout == 'page_per_question':
|
||||
next_active_question = any(next_question not in inactive_questions for next_question in next_page_or_question_candidates)
|
||||
is_triggering_question = any(triggering_answer in triggered_questions_by_answer.keys() for triggering_answer in page_or_question.suggested_answer_ids)
|
||||
return not(next_active_question or is_triggering_question)
|
||||
elif self.questions_layout == 'page_per_section':
|
||||
is_triggering_section = False
|
||||
for question in page_or_question.question_ids:
|
||||
if any(triggering_answer in triggered_questions_by_answer.keys() for triggering_answer in
|
||||
question.suggested_answer_ids):
|
||||
is_triggering_section = True
|
||||
break
|
||||
next_active_question = False
|
||||
for section in next_page_or_question_candidates:
|
||||
next_active_question = any(next_question not in inactive_questions for next_question in section.question_ids)
|
||||
if next_active_question:
|
||||
break
|
||||
return not(next_active_question or is_triggering_section)
|
||||
|
||||
if not next_page_or_question_candidates:
|
||||
return True
|
||||
inactive_questions = user_input._get_inactive_conditional_questions()
|
||||
if self.questions_layout == 'page_per_question':
|
||||
return not (
|
||||
any(next_question not in inactive_questions for next_question in next_page_or_question_candidates)
|
||||
)
|
||||
elif self.questions_layout == 'page_per_section':
|
||||
for section in next_page_or_question_candidates:
|
||||
if any(next_question not in inactive_questions for next_question in section.question_ids):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _get_survey_questions(self, answer=None, page_id=None, question_id=None):
|
||||
|
|
@ -739,24 +861,21 @@ class Survey(models.Model):
|
|||
|
||||
In addition, we cross the returned questions with the answer.predefined_question_ids,
|
||||
that allows to handle the randomization of questions. """
|
||||
|
||||
questions, page_or_question_id = None, None
|
||||
|
||||
if answer and answer.is_session_answer:
|
||||
return self.session_question_id, self.session_question_id.id
|
||||
if self.questions_layout == 'page_per_section':
|
||||
if not page_id:
|
||||
raise ValueError("Page id is needed for question layout 'page_per_section'")
|
||||
page_id = int(page_id)
|
||||
questions = self.env['survey.question'].sudo().search([('survey_id', '=', self.id), ('page_id', '=', page_id)])
|
||||
page_or_question_id = page_id
|
||||
page_or_question_id = int(page_id)
|
||||
questions = self.env['survey.question'].sudo().search(
|
||||
Domain('survey_id', '=', self.id) & Domain('page_id', '=', page_or_question_id))
|
||||
elif self.questions_layout == 'page_per_question':
|
||||
if not question_id:
|
||||
raise ValueError("Question id is needed for question layout 'page_per_question'")
|
||||
question_id = int(question_id)
|
||||
questions = self.env['survey.question'].sudo().browse(question_id)
|
||||
page_or_question_id = question_id
|
||||
page_or_question_id = int(question_id)
|
||||
questions = self.env['survey.question'].sudo().browse(page_or_question_id)
|
||||
else:
|
||||
page_or_question_id = None
|
||||
questions = self.question_ids
|
||||
|
||||
# we need the intersection of the questions of this page AND the questions prepared for that user_input
|
||||
|
|
@ -770,17 +889,15 @@ class Survey(models.Model):
|
|||
# ------------------------------------------------------------
|
||||
|
||||
def _get_conditional_maps(self):
|
||||
triggering_answer_by_question = {}
|
||||
triggered_questions_by_answer = {}
|
||||
triggering_answers_by_question = defaultdict(lambda: self.env['survey.question.answer'])
|
||||
triggered_questions_by_answer = defaultdict(lambda: self.env['survey.question'])
|
||||
for question in self.question_ids:
|
||||
triggering_answer_by_question[question] = question.is_conditional and question.triggering_answer_id
|
||||
triggering_answers_by_question[question] |= question.triggering_answer_ids
|
||||
|
||||
if question.is_conditional:
|
||||
if question.triggering_answer_id in triggered_questions_by_answer:
|
||||
triggered_questions_by_answer[question.triggering_answer_id] |= question
|
||||
else:
|
||||
triggered_questions_by_answer[question.triggering_answer_id] = question
|
||||
return triggering_answer_by_question, triggered_questions_by_answer
|
||||
for triggering_answer_id in question.triggering_answer_ids:
|
||||
triggered_questions_by_answer[triggering_answer_id] |= question
|
||||
|
||||
return triggering_answers_by_question, triggered_questions_by_answer
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# SESSIONS MANAGEMENT
|
||||
|
|
@ -812,8 +929,8 @@ class Survey(models.Model):
|
|||
many users, we need to extract the most chosen answers, to determine the next questions to display. """
|
||||
|
||||
# get user_inputs from current session
|
||||
current_user_inputs = self.user_input_ids.filtered(lambda input: input.create_date > self.session_start_time)
|
||||
current_user_input_lines = current_user_inputs.mapped('user_input_line_ids').filtered(lambda answer: answer.suggested_answer_id)
|
||||
current_user_inputs = self.user_input_ids.filtered(lambda ui: ui.create_date > self.session_start_time)
|
||||
current_user_input_lines = current_user_inputs.user_input_line_ids.filtered('suggested_answer_id')
|
||||
|
||||
# count the number of vote per answer
|
||||
votes_by_answer = dict.fromkeys(current_user_input_lines.mapped('suggested_answer_id'), 0)
|
||||
|
|
@ -848,7 +965,7 @@ class Survey(models.Model):
|
|||
return fake_user_input
|
||||
|
||||
def _prepare_leaderboard_values(self):
|
||||
"""" The leaderboard is descending and takes the total of the attendee points minus the
|
||||
""" The leaderboard is descending and takes the total of the attendee points minus the
|
||||
current question score.
|
||||
We need both the total and the current question points to be able to show the attendees
|
||||
leaderboard and shift their position based on the score they have on the current question.
|
||||
|
|
@ -900,17 +1017,20 @@ class Survey(models.Model):
|
|||
|
||||
return leaderboard
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# ACTIONS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def action_send_survey(self):
|
||||
""" Open a window to compose an email, pre-filled with the survey message """
|
||||
def check_validity(self):
|
||||
# Ensure that this survey has at least one question.
|
||||
if not self.question_ids:
|
||||
raise UserError(_('You cannot send an invitation for a survey that has no questions.'))
|
||||
|
||||
# Ensure scored survey have a positive total score obtainable.
|
||||
if self.scoring_type != 'no_scoring' and self.scoring_max_obtainable <= 0:
|
||||
raise UserError(_("A scored survey needs at least one question that gives points.\n"
|
||||
"Please check answers and their scores."))
|
||||
|
||||
# Ensure that this survey has at least one section with question(s), if question layout is 'One page per section'.
|
||||
if self.questions_layout == 'page_per_section':
|
||||
if not self.page_ids:
|
||||
|
|
@ -921,14 +1041,18 @@ class Survey(models.Model):
|
|||
if not self.active:
|
||||
raise exceptions.UserError(_("You cannot send invitations for closed surveys."))
|
||||
|
||||
def action_send_survey(self):
|
||||
""" Open a window to compose an email, pre-filled with the survey message """
|
||||
self.check_validity()
|
||||
|
||||
template = self.env.ref('survey.mail_template_user_input_invite', raise_if_not_found=False)
|
||||
|
||||
local_context = dict(
|
||||
self.env.context,
|
||||
default_survey_id=self.id,
|
||||
default_use_template=bool(template),
|
||||
default_template_id=template and template.id or False,
|
||||
default_email_layout_xmlid='mail.mail_notification_light',
|
||||
default_send_email=(self.access_mode != 'public'),
|
||||
)
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
|
|
@ -957,7 +1081,7 @@ class Survey(models.Model):
|
|||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'name': "Print Survey",
|
||||
'target': 'self',
|
||||
'target': 'new',
|
||||
'url': url
|
||||
}
|
||||
|
||||
|
|
@ -1009,7 +1133,7 @@ class Survey(models.Model):
|
|||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'target': 'new',
|
||||
'url': '/survey/%s/certification_preview' % (self.id)
|
||||
'url': f'/survey/{self.id}/certification_preview'
|
||||
}
|
||||
|
||||
def action_start_session(self):
|
||||
|
|
@ -1076,31 +1200,20 @@ class Survey(models.Model):
|
|||
('state', '=', 'done'),
|
||||
('test_entry', '=', False)
|
||||
]
|
||||
count_data_success = self.env['survey.user_input'].sudo()._read_group(user_input_domain, ['scoring_success', 'id:count_distinct'], ['scoring_success'])
|
||||
count_data_success = self.env['survey.user_input'].sudo()._read_group(user_input_domain, ['scoring_success'], ['__count'])
|
||||
completed_count = self.env['survey.user_input'].sudo().search_count(user_input_domain + [('state', "=", "done")])
|
||||
|
||||
scoring_success_count = 0
|
||||
scoring_failed_count = 0
|
||||
for count_data_item in count_data_success:
|
||||
if count_data_item['scoring_success']:
|
||||
scoring_success_count += count_data_item['scoring_success_count']
|
||||
for scoring_success, count in count_data_success:
|
||||
if scoring_success:
|
||||
scoring_success_count += count
|
||||
else:
|
||||
scoring_failed_count += count_data_item['scoring_success_count']
|
||||
|
||||
success_graph = json.dumps([{
|
||||
'text': _('Passed'),
|
||||
'count': scoring_success_count,
|
||||
'color': '#2E7D32'
|
||||
}, {
|
||||
'text': _('Failed'),
|
||||
'count': scoring_failed_count,
|
||||
'color': '#C62828'
|
||||
}])
|
||||
scoring_failed_count += count
|
||||
|
||||
total = scoring_success_count + scoring_failed_count
|
||||
return {
|
||||
'global_success_rate': round((scoring_success_count / total) * 100, 1) if total > 0 else 0,
|
||||
'global_success_graph': success_graph,
|
||||
'count_all': total,
|
||||
'count_finished': completed_count,
|
||||
'count_failed': scoring_failed_count,
|
||||
|
|
@ -1150,9 +1263,7 @@ class Survey(models.Model):
|
|||
|
||||
def _handle_certification_badges(self, vals):
|
||||
if vals.get('certification_give_badge'):
|
||||
# If badge already set on records, reactivate the ones that are not active.
|
||||
surveys_with_badge = self.filtered(lambda survey: survey.certification_badge_id and not survey.certification_badge_id.active)
|
||||
surveys_with_badge.mapped('certification_badge_id').action_unarchive()
|
||||
self.certification_badge_id.action_unarchive()
|
||||
# (re-)create challenge and goal
|
||||
for survey in self:
|
||||
survey._create_certification_badge_trigger()
|
||||
|
|
@ -1165,3 +1276,38 @@ class Survey(models.Model):
|
|||
# delete all challenges and goals because not needed anymore (challenge lines are deleted in cascade)
|
||||
challenges_to_delete.unlink()
|
||||
goals_to_delete.unlink()
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# TOOLING / MISC
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _generate_session_codes(self, code_count=1, excluded_codes=False):
|
||||
""" Generate {code_count} session codes for surveys.
|
||||
|
||||
We try to generate 4 digits code first and see if we have {code_count} unique ones.
|
||||
Then we raise up to 5 digits if we need more, etc until up to 10 digits.
|
||||
(We generate an extra 20 codes per loop to try to mitigate back luck collisions). """
|
||||
|
||||
self.flush_model(['session_code'])
|
||||
|
||||
session_codes = set()
|
||||
excluded_codes = excluded_codes or set()
|
||||
existing_codes = self.sudo().search_read(
|
||||
[('session_code', '!=', False)],
|
||||
['session_code']
|
||||
)
|
||||
unavailable_codes = excluded_codes | {existing_code['session_code'] for existing_code in existing_codes}
|
||||
for digits_count in range(4, 10):
|
||||
range_lower_bound = 10 ** (digits_count - 1)
|
||||
range_upper_bound = (range_lower_bound * 10) - 1
|
||||
code_candidates = {str(random.randint(range_lower_bound, range_upper_bound)) for _ in range(code_count + 20)}
|
||||
session_codes |= code_candidates - unavailable_codes
|
||||
if len(session_codes) >= code_count:
|
||||
return list(session_codes)[:code_count]
|
||||
|
||||
# could not generate enough codes, fill with False for remainder
|
||||
return session_codes + [False] * (code_count - len(session_codes))
|
||||
|
||||
def _get_supported_lang_codes(self):
|
||||
self.ensure_one()
|
||||
return self.lang_ids.mapped('code') or [lg[0] for lg in self.env['res.lang'].get_installed()]
|
||||
|
|
|
|||
|
|
@ -8,28 +8,29 @@ import uuid
|
|||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
from odoo.tools import float_is_zero
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SurveyUserInput(models.Model):
|
||||
class SurveyUser_Input(models.Model):
|
||||
""" Metadata for a set of one user's answers to a particular survey """
|
||||
_name = "survey.user_input"
|
||||
_name = 'survey.user_input'
|
||||
_description = "Survey User Input"
|
||||
_rec_name = "survey_id"
|
||||
_order = "create_date desc"
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
|
||||
# answer description
|
||||
survey_id = fields.Many2one('survey.survey', string='Survey', required=True, readonly=True, ondelete='cascade')
|
||||
survey_id = fields.Many2one('survey.survey', string='Survey', required=True, readonly=True, index=True, ondelete='cascade')
|
||||
scoring_type = fields.Selection(string="Scoring", related="survey_id.scoring_type")
|
||||
start_datetime = fields.Datetime('Start date and time', readonly=True)
|
||||
end_datetime = fields.Datetime('End date and time', readonly=True)
|
||||
deadline = fields.Datetime('Deadline', help="Datetime until customer can open the survey and submit answers")
|
||||
lang_id = fields.Many2one('res.lang', string='Language')
|
||||
state = fields.Selection([
|
||||
('new', 'Not started yet'),
|
||||
('new', 'New'),
|
||||
('in_progress', 'In Progress'),
|
||||
('done', 'Completed')], string='Status', default='new', readonly=True)
|
||||
test_entry = fields.Boolean(readonly=True)
|
||||
|
|
@ -43,22 +44,24 @@ class SurveyUserInput(models.Model):
|
|||
# identification / access
|
||||
access_token = fields.Char('Identification token', default=lambda self: str(uuid.uuid4()), readonly=True, required=True, copy=False)
|
||||
invite_token = fields.Char('Invite token', readonly=True, copy=False) # no unique constraint, as it identifies a pool of attempts
|
||||
partner_id = fields.Many2one('res.partner', string='Contact', readonly=True)
|
||||
partner_id = fields.Many2one('res.partner', string='Contact', readonly=True, index='btree_not_null')
|
||||
email = fields.Char('Email', readonly=True)
|
||||
nickname = fields.Char('Nickname', help="Attendee nickname, mainly used to identify them in the survey session leaderboard.")
|
||||
# questions / answers
|
||||
user_input_line_ids = fields.One2many('survey.user_input.line', 'user_input_id', string='Answers', copy=True)
|
||||
predefined_question_ids = fields.Many2many('survey.question', string='Predefined Questions', readonly=True)
|
||||
scoring_percentage = fields.Float("Score (%)", compute="_compute_scoring_values", store=True, compute_sudo=True) # stored for perf reasons
|
||||
scoring_total = fields.Float("Total Score", compute="_compute_scoring_values", store=True, compute_sudo=True) # stored for perf reasons
|
||||
scoring_success = fields.Boolean('Quizz Passed', compute='_compute_scoring_success', store=True, compute_sudo=True) # stored for perf reasons
|
||||
scoring_total = fields.Float("Total Score", compute="_compute_scoring_values", store=True, compute_sudo=True, digits=(10, 2)) # stored for perf reasons
|
||||
scoring_success = fields.Boolean('Quiz Passed', compute='_compute_scoring_success', store=True, compute_sudo=True) # stored for perf reasons
|
||||
survey_first_submitted = fields.Boolean(string='Survey First Submitted')
|
||||
# live sessions
|
||||
is_session_answer = fields.Boolean('Is in a Session', help="Is that user input part of a survey session or not.")
|
||||
question_time_limit_reached = fields.Boolean("Question Time Limit Reached", compute='_compute_question_time_limit_reached')
|
||||
|
||||
_sql_constraints = [
|
||||
('unique_token', 'UNIQUE (access_token)', 'An access token must be unique!'),
|
||||
]
|
||||
_unique_token = models.Constraint(
|
||||
'UNIQUE (access_token)',
|
||||
'An access token must be unique!',
|
||||
)
|
||||
|
||||
@api.depends('user_input_line_ids.answer_score', 'user_input_line_ids.question_id', 'predefined_question_ids.answer_score')
|
||||
def _compute_scoring_values(self):
|
||||
|
|
@ -168,7 +171,7 @@ class SurveyUserInput(models.Model):
|
|||
suvey_id = vals.get('survey_id', self.env.context.get('default_survey_id'))
|
||||
survey = self.env['survey.survey'].browse(suvey_id)
|
||||
vals['predefined_question_ids'] = [(6, 0, survey._prepare_user_input_predefined_questions().ids)]
|
||||
return super(SurveyUserInput, self).create(vals_list)
|
||||
return super().create(vals_list)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# ACTIONS / BUSINESS
|
||||
|
|
@ -192,11 +195,14 @@ class SurveyUserInput(models.Model):
|
|||
def action_print_answers(self):
|
||||
""" Open the website page with the survey form """
|
||||
self.ensure_one()
|
||||
url = self.env['ir.http']._url_for(
|
||||
'/survey/print/%s?answer_token=%s' % (self.survey_id.access_token, self.access_token),
|
||||
self.lang_id.code or None)
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'name': "View Answers",
|
||||
'target': 'self',
|
||||
'url': '/survey/print/%s?answer_token=%s' % (self.survey_id.access_token, self.access_token)
|
||||
'url': url,
|
||||
}
|
||||
|
||||
def action_redirect_to_attempts(self):
|
||||
|
|
@ -234,14 +240,16 @@ class SurveyUserInput(models.Model):
|
|||
- The survey is a certification
|
||||
- It has a certification_mail_template_id set
|
||||
- The user succeeded the test
|
||||
3. Notify survey subtype subscribers of the newly completed input
|
||||
Will also run challenge Cron to give the certification badge if any."""
|
||||
self.write({
|
||||
'end_datetime': fields.Datetime.now(),
|
||||
'state': 'done',
|
||||
})
|
||||
|
||||
Challenge = self.env['gamification.challenge'].sudo()
|
||||
Challenge_sudo = self.env['gamification.challenge'].sudo()
|
||||
badge_ids = []
|
||||
self._notify_new_participation_subscribers()
|
||||
for user_input in self:
|
||||
if user_input.survey_id.certification and user_input.scoring_success:
|
||||
if user_input.survey_id.certification_mail_template_id and not user_input.test_entry:
|
||||
|
|
@ -253,9 +261,9 @@ class SurveyUserInput(models.Model):
|
|||
user_input.predefined_question_ids -= user_input._get_inactive_conditional_questions()
|
||||
|
||||
if badge_ids:
|
||||
challenges = Challenge.search([('reward_id', 'in', badge_ids)])
|
||||
challenges = Challenge_sudo.search([('reward_id', 'in', badge_ids)])
|
||||
if challenges:
|
||||
Challenge._cron_update(ids=challenges.ids, commit=False)
|
||||
Challenge_sudo._cron_update(ids=challenges.ids, commit=False)
|
||||
|
||||
def get_start_url(self):
|
||||
self.ensure_one()
|
||||
|
|
@ -269,18 +277,21 @@ class SurveyUserInput(models.Model):
|
|||
# CREATE / UPDATE LINES FROM SURVEY FRONTEND INPUT
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def save_lines(self, question, answer, comment=None):
|
||||
""" Save answers to questions, depending on question type
|
||||
def _save_lines(self, question, answer, comment=None, overwrite_existing=True):
|
||||
""" Save answers to questions, depending on question type.
|
||||
|
||||
If an answer already exists for question and user_input_id, it will be
|
||||
overwritten (or deleted for 'choice' questions) (in order to maintain data consistency).
|
||||
:param bool overwrite_existing: if an answer already exists for question and user_input_id
|
||||
it will be overwritten (or deleted for 'choice' questions) in order to maintain data consistency.
|
||||
:raises UserError: if line exists and overwrite_existing is False
|
||||
"""
|
||||
old_answers = self.env['survey.user_input.line'].search([
|
||||
('user_input_id', '=', self.id),
|
||||
('question_id', '=', question.id)
|
||||
])
|
||||
if old_answers and not overwrite_existing:
|
||||
raise UserError(_("This answer cannot be overwritten."))
|
||||
|
||||
if question.question_type in ['char_box', 'text_box', 'numerical_box', 'date', 'datetime']:
|
||||
if question.question_type in ['char_box', 'text_box', 'scale', 'numerical_box', 'date', 'datetime']:
|
||||
self._save_line_simple_answer(question, old_answers, answer)
|
||||
if question.save_as_email and answer:
|
||||
self.write({'email': answer})
|
||||
|
|
@ -306,18 +317,12 @@ class SurveyUserInput(models.Model):
|
|||
if not (isinstance(answers, list)):
|
||||
answers = [answers]
|
||||
|
||||
if not answers:
|
||||
if not answers and not (comment and question.comment_count_as_answer):
|
||||
# add a False answer to force saving a skipped line
|
||||
# this will make this question correctly considered as skipped in statistics
|
||||
answers = [False]
|
||||
|
||||
vals_list = []
|
||||
|
||||
if question.question_type == 'simple_choice':
|
||||
if not question.comment_count_as_answer or not question.comments_allowed or not comment:
|
||||
vals_list = [self._get_line_answer_values(question, answer, 'suggestion') for answer in answers]
|
||||
elif question.question_type == 'multiple_choice':
|
||||
vals_list = [self._get_line_answer_values(question, answer, 'suggestion') for answer in answers]
|
||||
vals_list = [self._get_line_answer_values(question, answer, 'suggestion') for answer in answers]
|
||||
|
||||
if comment:
|
||||
vals_list.append(self._get_line_comment_values(question, comment))
|
||||
|
|
@ -361,6 +366,8 @@ class SurveyUserInput(models.Model):
|
|||
vals['suggested_answer_id'] = int(answer)
|
||||
elif answer_type == 'numerical_box':
|
||||
vals['value_numerical_box'] = float(answer)
|
||||
elif answer_type == 'scale':
|
||||
vals['value_scale'] = int(answer)
|
||||
else:
|
||||
vals['value_%s' % answer_type] = answer
|
||||
return vals
|
||||
|
|
@ -430,14 +437,20 @@ class SurveyUserInput(models.Model):
|
|||
scored_questions = self.mapped('predefined_question_ids').filtered(lambda question: question.is_scored_question)
|
||||
|
||||
for question in scored_questions:
|
||||
if question.question_type == 'simple_choice':
|
||||
question_incorrect_scored_answers = question.suggested_answer_ids.filtered(lambda answer: not answer.is_correct and answer.answer_score > 0)
|
||||
|
||||
if question.question_type in ['simple_choice', 'multiple_choice']:
|
||||
question_correct_suggested_answers = question.suggested_answer_ids.filtered(lambda answer: answer.is_correct)
|
||||
|
||||
question_section = question.page_id.title or _('Uncategorized')
|
||||
for user_input in self:
|
||||
user_input_lines = user_input.user_input_line_ids.filtered(lambda line: line.question_id == question)
|
||||
if question.question_type in ['simple_choice', 'multiple_choice']:
|
||||
answer_result_key = self._choice_question_answer_result(user_input_lines, question_correct_suggested_answers)
|
||||
user_input_lines = user_input.user_input_line_ids.filtered(lambda line:
|
||||
line.question_id == question and (line.answer_type != 'char_box' or question.comment_count_as_answer))
|
||||
if question.question_type == 'simple_choice':
|
||||
answer_result_key = self._simple_choice_question_answer_result(user_input_lines, question_correct_suggested_answers, question_incorrect_scored_answers)
|
||||
elif question.question_type == 'multiple_choice':
|
||||
answer_result_key = self._multiple_choice_question_answer_result(user_input_lines, question_correct_suggested_answers)
|
||||
else:
|
||||
answer_result_key = self._simple_question_answer_result(user_input_lines)
|
||||
|
||||
|
|
@ -474,7 +487,7 @@ class SurveyUserInput(models.Model):
|
|||
|
||||
return res
|
||||
|
||||
def _choice_question_answer_result(self, user_input_lines, question_correct_suggested_answers):
|
||||
def _multiple_choice_question_answer_result(self, user_input_lines, question_correct_suggested_answers):
|
||||
correct_user_input_lines = user_input_lines.filtered(lambda line: line.answer_is_correct and not line.skipped).mapped('suggested_answer_id')
|
||||
incorrect_user_input_lines = user_input_lines.filtered(lambda line: not line.answer_is_correct and not line.skipped)
|
||||
if question_correct_suggested_answers and correct_user_input_lines == question_correct_suggested_answers:
|
||||
|
|
@ -486,6 +499,17 @@ class SurveyUserInput(models.Model):
|
|||
else:
|
||||
return 'skipped'
|
||||
|
||||
def _simple_choice_question_answer_result(self, user_input_line, question_correct_suggested_answers, question_incorrect_scored_answers):
|
||||
user_answer = user_input_line.suggested_answer_id if not user_input_line.skipped else self.env['survey.question.answer']
|
||||
if user_answer in question_correct_suggested_answers:
|
||||
return 'correct'
|
||||
elif user_answer in question_incorrect_scored_answers:
|
||||
return 'partial'
|
||||
elif user_answer:
|
||||
return 'incorrect'
|
||||
else:
|
||||
return 'skipped'
|
||||
|
||||
def _simple_question_answer_result(self, user_input_line):
|
||||
if user_input_line.skipped:
|
||||
return 'skipped'
|
||||
|
|
@ -519,20 +543,21 @@ class SurveyUserInput(models.Model):
|
|||
that is the next in sequence and that is either not triggered by another question's answer, or that
|
||||
is triggered by an already selected answer.
|
||||
To do all this, we need to return:
|
||||
- list of all selected answers: [answer_id1, answer_id2, ...] (for survey reloading, otherwise, this list is
|
||||
updated at client side)
|
||||
- triggering_answers_by_question: dict -> for a given question, the answers that triggers it
|
||||
Used mainly to ease template rendering
|
||||
- triggered_questions_by_answer: dict -> for a given answer, list of questions triggered by this answer;
|
||||
Used mainly for dynamic show/hide behaviour at client side
|
||||
- triggering_answer_by_question: dict -> for a given question, the answer that triggers it
|
||||
Used mainly to ease template rendering
|
||||
- list of all selected answers: [answer_id1, answer_id2, ...] (for survey reloading, otherwise, this list is
|
||||
updated at client side)
|
||||
"""
|
||||
triggering_answer_by_question, triggered_questions_by_answer = {}, {}
|
||||
triggering_answers_by_question = {}
|
||||
triggered_questions_by_answer = {}
|
||||
# Ignore conditional configuration if randomised questions selection
|
||||
if self.survey_id.questions_selection != 'random':
|
||||
triggering_answer_by_question, triggered_questions_by_answer = self.survey_id._get_conditional_maps()
|
||||
triggering_answers_by_question, triggered_questions_by_answer = self.survey_id._get_conditional_maps()
|
||||
selected_answers = self._get_selected_suggested_answers()
|
||||
|
||||
return triggering_answer_by_question, triggered_questions_by_answer, selected_answers
|
||||
return triggering_answers_by_question, triggered_questions_by_answer, selected_answers
|
||||
|
||||
def _get_selected_suggested_answers(self):
|
||||
"""
|
||||
|
|
@ -568,14 +593,13 @@ class SurveyUserInput(models.Model):
|
|||
answers_to_delete.unlink()
|
||||
|
||||
def _get_inactive_conditional_questions(self):
|
||||
triggering_answer_by_question, triggered_questions_by_answer, selected_answers = self._get_conditional_values()
|
||||
triggering_answers_by_question, _, selected_answers = self._get_conditional_values()
|
||||
|
||||
# get questions that should not be answered
|
||||
inactive_questions = self.env['survey.question']
|
||||
for answer in triggered_questions_by_answer.keys():
|
||||
if answer not in selected_answers:
|
||||
for question in triggered_questions_by_answer[answer]:
|
||||
inactive_questions |= question
|
||||
for question, triggering_answers in triggering_answers_by_question.items():
|
||||
if triggering_answers and not triggering_answers & selected_answers:
|
||||
inactive_questions |= question
|
||||
return inactive_questions
|
||||
|
||||
def _get_print_questions(self):
|
||||
|
|
@ -591,23 +615,82 @@ class SurveyUserInput(models.Model):
|
|||
inactive_questions = self._get_inactive_conditional_questions()
|
||||
return survey.question_ids - inactive_questions
|
||||
|
||||
def _get_next_skipped_page_or_question(self):
|
||||
"""Get next skipped question or page in case the option 'can_go_back' is set on the survey
|
||||
It loops to the first skipped question or page if 'last_displayed_page_id' is the last
|
||||
skipped question or page."""
|
||||
self.ensure_one()
|
||||
skipped_mandatory_answer_ids = self.user_input_line_ids.filtered(
|
||||
lambda answer: answer.skipped and answer.question_id.constr_mandatory)
|
||||
|
||||
if not skipped_mandatory_answer_ids:
|
||||
return self.env['survey.question']
|
||||
|
||||
page_or_question_key = 'page_id' if self.survey_id.questions_layout == 'page_per_section' else 'question_id'
|
||||
page_or_question_ids = skipped_mandatory_answer_ids.mapped(page_or_question_key).sorted()
|
||||
|
||||
if self.last_displayed_page_id not in page_or_question_ids\
|
||||
or self.last_displayed_page_id == page_or_question_ids[-1]:
|
||||
return page_or_question_ids[0]
|
||||
|
||||
current_page_index = page_or_question_ids.ids.index(self.last_displayed_page_id.id)
|
||||
return page_or_question_ids[current_page_index + 1]
|
||||
|
||||
def _get_skipped_questions(self):
|
||||
self.ensure_one()
|
||||
|
||||
return self.user_input_line_ids.filtered(
|
||||
lambda answer: answer.skipped and answer.question_id.constr_mandatory).question_id
|
||||
|
||||
def _is_last_skipped_page_or_question(self, page_or_question):
|
||||
"""In case of a submitted survey tells if the question or page is the last
|
||||
skipped page or question.
|
||||
|
||||
This is used to :
|
||||
|
||||
- Display a Submit button if the actual question is the last skipped question.
|
||||
- Avoid displaying a Submit button on the last survey question if there are
|
||||
still skipped questions before.
|
||||
- Avoid displaying the next page if submitting the latest skipped question.
|
||||
|
||||
:param page_or_question: page if survey's layout is page_per_section, question if page_per_question.
|
||||
"""
|
||||
if self.survey_id.questions_layout == 'one_page':
|
||||
return True
|
||||
skipped = self._get_skipped_questions()
|
||||
if not skipped:
|
||||
return True
|
||||
if self.survey_id.questions_layout == 'page_per_section':
|
||||
skipped = skipped.page_id
|
||||
return skipped == page_or_question
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# MESSAGING
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _message_get_suggested_recipients(self):
|
||||
recipients = super()._message_get_suggested_recipients()
|
||||
for user_input in self:
|
||||
def _notify_new_participation_subscribers(self):
|
||||
subtype_id = self.env.ref('survey.mt_survey_survey_user_input_completed', raise_if_not_found=False)
|
||||
if not self.ids or not subtype_id:
|
||||
return
|
||||
author_id = self.env.ref('base.partner_root').id if self.env.user.is_public else self.env.user.partner_id.id
|
||||
# Only post if there are any followers
|
||||
recipients_data = self.env['mail.followers']._get_recipient_data(self.survey_id, 'notification', subtype_id.id)
|
||||
followed_survey_ids = [survey_id for survey_id, followers in recipients_data.items() if followers]
|
||||
for user_input in self.filtered(lambda user_input_: user_input_.survey_id.id in followed_survey_ids):
|
||||
survey_title = user_input.survey_id.title
|
||||
if user_input.partner_id:
|
||||
user_input._message_add_suggested_recipient(
|
||||
recipients,
|
||||
partner=user_input.partner_id,
|
||||
reason=_('Survey Participant')
|
||||
body = _(
|
||||
'%(participant)s just participated in "%(survey_title)s".',
|
||||
participant=user_input.partner_id.display_name,
|
||||
survey_title=survey_title,
|
||||
)
|
||||
return recipients
|
||||
else:
|
||||
body = _('Someone just participated in "%(survey_title)s".', survey_title=survey_title)
|
||||
|
||||
user_input.message_post(author_id=author_id, body=body, subtype_xmlid='survey.mt_survey_user_input_completed')
|
||||
|
||||
|
||||
class SurveyUserInputLine(models.Model):
|
||||
class SurveyUser_InputLine(models.Model):
|
||||
_name = 'survey.user_input.line'
|
||||
_description = 'Survey User Input Line'
|
||||
_rec_name = 'user_input_id'
|
||||
|
|
@ -616,30 +699,37 @@ class SurveyUserInputLine(models.Model):
|
|||
# survey data
|
||||
user_input_id = fields.Many2one('survey.user_input', string='User Input', ondelete='cascade', required=True, index=True)
|
||||
survey_id = fields.Many2one(related='user_input_id.survey_id', string='Survey', store=True, readonly=False)
|
||||
question_id = fields.Many2one('survey.question', string='Question', ondelete='cascade', required=True)
|
||||
question_id = fields.Many2one('survey.question', string='Question', ondelete='cascade', required=True, index=True)
|
||||
page_id = fields.Many2one(related='question_id.page_id', string="Section", readonly=False)
|
||||
question_sequence = fields.Integer('Sequence', related='question_id.sequence', store=True)
|
||||
lang_id = fields.Many2one('res.lang', related="user_input_id.lang_id")
|
||||
# answer
|
||||
skipped = fields.Boolean('Skipped')
|
||||
answer_type = fields.Selection([
|
||||
('text_box', 'Free Text'),
|
||||
('char_box', 'Text'),
|
||||
('numerical_box', 'Number'),
|
||||
('scale', 'Number'),
|
||||
('date', 'Date'),
|
||||
('datetime', 'Datetime'),
|
||||
('suggestion', 'Suggestion')], string='Answer Type')
|
||||
value_char_box = fields.Char('Text answer')
|
||||
value_numerical_box = fields.Float('Numerical answer')
|
||||
value_scale = fields.Integer('Scale value')
|
||||
value_date = fields.Date('Date answer')
|
||||
value_datetime = fields.Datetime('Datetime answer')
|
||||
value_text_box = fields.Text('Free Text answer')
|
||||
suggested_answer_id = fields.Many2one('survey.question.answer', string="Suggested answer")
|
||||
matrix_row_id = fields.Many2one('survey.question.answer', string="Row answer")
|
||||
# scoring
|
||||
answer_score = fields.Float('Score')
|
||||
answer_is_correct = fields.Boolean('Correct')
|
||||
answer_score = fields.Float('Score', compute='_compute_answer_score', precompute=True, store=True)
|
||||
answer_is_correct = fields.Boolean('Correct', compute='_compute_answer_score', precompute=True, store=True)
|
||||
|
||||
@api.depends('answer_type')
|
||||
@api.depends(
|
||||
'answer_type', 'value_text_box', 'value_numerical_box',
|
||||
'value_char_box', 'value_date', 'value_datetime',
|
||||
'suggested_answer_id.value', 'matrix_row_id.value',
|
||||
)
|
||||
def _compute_display_name(self):
|
||||
for line in self:
|
||||
if line.answer_type == 'char_box':
|
||||
|
|
@ -651,69 +741,23 @@ class SurveyUserInputLine(models.Model):
|
|||
elif line.answer_type == 'date':
|
||||
line.display_name = fields.Date.to_string(line.value_date)
|
||||
elif line.answer_type == 'datetime':
|
||||
line.display_name = fields.Datetime.to_string(line.value_datetime)
|
||||
line.display_name = fields.Datetime.to_string(fields.Datetime.context_timestamp(self.env.user, line.value_datetime))
|
||||
elif line.answer_type == 'scale':
|
||||
line.display_name = line.value_scale
|
||||
elif line.answer_type == 'suggestion':
|
||||
if line.matrix_row_id:
|
||||
line.display_name = '%s: %s' % (
|
||||
line.suggested_answer_id.value,
|
||||
line.matrix_row_id.value)
|
||||
line.display_name = f'{line.suggested_answer_id.value}: {line.matrix_row_id.value}'
|
||||
else:
|
||||
line.display_name = line.suggested_answer_id.value
|
||||
|
||||
if not line.display_name:
|
||||
line.display_name = _('Skipped')
|
||||
|
||||
@api.constrains('skipped', 'answer_type')
|
||||
def _check_answer_type_skipped(self):
|
||||
for line in self:
|
||||
if (line.skipped == bool(line.answer_type)):
|
||||
raise ValidationError(_('A question can either be skipped or answered, not both.'))
|
||||
|
||||
# allow 0 for numerical box
|
||||
if line.answer_type == 'numerical_box' and float_is_zero(line['value_numerical_box'], precision_digits=6):
|
||||
continue
|
||||
if line.answer_type == 'suggestion':
|
||||
field_name = 'suggested_answer_id'
|
||||
elif line.answer_type:
|
||||
field_name = 'value_%s' % line.answer_type
|
||||
else: # skipped
|
||||
field_name = False
|
||||
|
||||
if field_name and not line[field_name]:
|
||||
raise ValidationError(_('The answer must be in the right type'))
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if not vals.get('answer_score'):
|
||||
score_vals = self._get_answer_score_values(vals)
|
||||
vals.update(score_vals)
|
||||
return super(SurveyUserInputLine, self).create(vals_list)
|
||||
|
||||
def write(self, vals):
|
||||
res = True
|
||||
for line in self:
|
||||
vals_copy = {**vals}
|
||||
getter_params = {
|
||||
'user_input_id': line.user_input_id.id,
|
||||
'answer_type': line.answer_type,
|
||||
'question_id': line.question_id.id,
|
||||
**vals_copy
|
||||
}
|
||||
if not vals_copy.get('answer_score'):
|
||||
score_vals = self._get_answer_score_values(getter_params, compute_speed_score=False)
|
||||
vals_copy.update(score_vals)
|
||||
res = super(SurveyUserInputLine, line).write(vals_copy) and res
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _get_answer_score_values(self, vals, compute_speed_score=True):
|
||||
@api.depends('answer_type', 'value_text_box', 'value_numerical_box', 'value_date', 'value_datetime',
|
||||
'suggested_answer_id', 'user_input_id')
|
||||
def _compute_answer_score(self):
|
||||
""" Get values for: answer_is_correct and associated answer_score.
|
||||
|
||||
Requires vals to contain 'answer_type', 'question_id', and 'user_input_id'.
|
||||
Depending on 'answer_type' additional value of 'suggested_answer_id' may also be
|
||||
required.
|
||||
|
||||
Calculates whether an answer_is_correct and its score based on 'answer_type' and
|
||||
corresponding question. Handles choice (answer_type == 'suggestion') questions
|
||||
separately from other question types. Each selected choice answer is handled as an
|
||||
|
|
@ -725,60 +769,111 @@ class SurveyUserInputLine(models.Model):
|
|||
50% scaled by the time limit and time taken to answer [i.e. a minimum of 50% of the
|
||||
possible points is given to all correct answers]
|
||||
|
||||
Example of returned values:
|
||||
Example of updated values:
|
||||
* {'answer_is_correct': False, 'answer_score': 0} (default)
|
||||
* {'answer_is_correct': True, 'answer_score': 2.0}
|
||||
"""
|
||||
user_input_id = vals.get('user_input_id')
|
||||
answer_type = vals.get('answer_type')
|
||||
question_id = vals.get('question_id')
|
||||
if not question_id:
|
||||
raise ValueError(_('Computing score requires a question in arguments.'))
|
||||
question = self.env['survey.question'].browse(int(question_id))
|
||||
for line in self:
|
||||
answer_is_correct, answer_score = False, 0
|
||||
if line.answer_type:
|
||||
# record selected suggested choice answer_score (can be: pos, neg, or 0)
|
||||
if line.question_id.question_type in ['simple_choice', 'multiple_choice']:
|
||||
if line.answer_type == 'suggestion' and line.suggested_answer_id:
|
||||
answer_score = line.suggested_answer_id.answer_score
|
||||
answer_is_correct = line.suggested_answer_id.is_correct
|
||||
# for all other scored question cases, record question answer_score (can be: pos or 0)
|
||||
elif line.question_id.question_type in ['date', 'datetime', 'numerical_box']:
|
||||
answer = line[f'value_{line.answer_type}']
|
||||
if line.answer_type == 'numerical_box':
|
||||
answer = float(answer)
|
||||
elif line.answer_type == 'date':
|
||||
answer = fields.Date.from_string(answer)
|
||||
elif line.answer_type == 'datetime':
|
||||
answer = fields.Datetime.from_string(answer)
|
||||
if answer and answer == line.question_id[f'answer_{line.answer_type}']:
|
||||
answer_is_correct = True
|
||||
answer_score = line.question_id.answer_score
|
||||
|
||||
# default and non-scored questions
|
||||
answer_is_correct = False
|
||||
answer_score = 0
|
||||
|
||||
# record selected suggested choice answer_score (can be: pos, neg, or 0)
|
||||
if question.question_type in ['simple_choice', 'multiple_choice']:
|
||||
if answer_type == 'suggestion':
|
||||
suggested_answer_id = vals.get('suggested_answer_id')
|
||||
if suggested_answer_id:
|
||||
question_answer = self.env['survey.question.answer'].browse(int(suggested_answer_id))
|
||||
answer_score = question_answer.answer_score
|
||||
answer_is_correct = question_answer.is_correct
|
||||
# for all other scored question cases, record question answer_score (can be: pos or 0)
|
||||
elif question.question_type in ['date', 'datetime', 'numerical_box']:
|
||||
answer = vals.get('value_%s' % answer_type)
|
||||
if answer_type == 'numerical_box':
|
||||
answer = float(answer)
|
||||
elif answer_type == 'date':
|
||||
answer = fields.Date.from_string(answer)
|
||||
elif answer_type == 'datetime':
|
||||
answer = fields.Datetime.from_string(answer)
|
||||
if answer and answer == question['answer_%s' % answer_type]:
|
||||
answer_is_correct = True
|
||||
answer_score = question.answer_score
|
||||
|
||||
if compute_speed_score and answer_score > 0:
|
||||
user_input = self.env['survey.user_input'].browse(user_input_id)
|
||||
session_speed_rating = user_input.exists() and user_input.is_session_answer and user_input.survey_id.session_speed_rating
|
||||
if session_speed_rating:
|
||||
# Session speed rating
|
||||
if (
|
||||
answer_score > 0
|
||||
and line.user_input_id.survey_id.session_speed_rating
|
||||
and line.user_input_id.is_session_answer
|
||||
and line.question_id.is_time_limited
|
||||
):
|
||||
max_score_delay = 2
|
||||
time_limit = question.time_limit
|
||||
time_limit = line.question_id.time_limit
|
||||
now = fields.Datetime.now()
|
||||
seconds_to_answer = (now - user_input.survey_id.session_question_start_time).total_seconds()
|
||||
seconds_to_answer = (now - line.user_input_id.survey_id.session_question_start_time).total_seconds()
|
||||
question_remaining_time = time_limit - seconds_to_answer
|
||||
# if answered within the max_score_delay => leave score as is
|
||||
if question_remaining_time < 0: # if no time left
|
||||
if question_remaining_time < 0 or line.question_id != line.user_input_id.survey_id.session_question_id:
|
||||
answer_score /= 2
|
||||
elif seconds_to_answer > max_score_delay:
|
||||
time_limit -= max_score_delay # we remove the max_score_delay to have all possible values
|
||||
score_proportion = (time_limit - seconds_to_answer) / time_limit
|
||||
elif seconds_to_answer > max_score_delay: # linear decrease in score after 2 sec
|
||||
score_proportion = (time_limit - seconds_to_answer) / (time_limit - max_score_delay)
|
||||
answer_score = (answer_score / 2) * (1 + score_proportion)
|
||||
|
||||
return {
|
||||
'answer_is_correct': answer_is_correct,
|
||||
'answer_score': answer_score
|
||||
}
|
||||
line.answer_is_correct = answer_is_correct
|
||||
line.answer_score = answer_score
|
||||
|
||||
@api.constrains('skipped', 'answer_type')
|
||||
def _check_answer_type_skipped(self):
|
||||
for line in self:
|
||||
if (line.skipped == bool(line.answer_type)):
|
||||
raise ValidationError(_('A question can either be skipped or answered, not both.'))
|
||||
|
||||
# allow 0 for numerical box and scale
|
||||
if line.answer_type == 'numerical_box' and float_is_zero(line['value_numerical_box'], precision_digits=6):
|
||||
continue
|
||||
if line.answer_type == 'scale' and line['value_scale'] == 0:
|
||||
continue
|
||||
|
||||
if line.answer_type == 'suggestion':
|
||||
field_name = 'suggested_answer_id'
|
||||
elif line.answer_type:
|
||||
field_name = 'value_%s' % line.answer_type
|
||||
else: # skipped
|
||||
field_name = False
|
||||
|
||||
if field_name and not line[field_name]:
|
||||
raise ValidationError(_('The answer must be in the right type'))
|
||||
|
||||
def _get_answer_matching_domain(self):
|
||||
self.ensure_one()
|
||||
if self.answer_type in ('char_box', 'text_box', 'numerical_box', 'scale', 'date', 'datetime'):
|
||||
value_field = {
|
||||
'char_box': 'value_char_box',
|
||||
'text_box': 'value_text_box',
|
||||
'numerical_box': 'value_numerical_box',
|
||||
'scale': 'value_scale',
|
||||
'date': 'value_date',
|
||||
'datetime': 'value_datetime',
|
||||
}
|
||||
operators = {
|
||||
'char_box': 'ilike',
|
||||
'text_box': 'ilike',
|
||||
'numerical_box': '=',
|
||||
'scale': '=',
|
||||
'date': '=',
|
||||
'datetime': '=',
|
||||
}
|
||||
return ['&', ('question_id', '=', self.question_id.id), (value_field[self.answer_type], operators[self.answer_type], self._get_answer_value())]
|
||||
elif self.answer_type == 'suggestion':
|
||||
return self.suggested_answer_id._get_answer_matching_domain(self.matrix_row_id.id if self.matrix_row_id else False)
|
||||
|
||||
def _get_answer_value(self):
|
||||
self.ensure_one()
|
||||
if self.answer_type == 'char_box':
|
||||
return self.value_char_box
|
||||
elif self.answer_type == 'text_box':
|
||||
return self.value_text_box
|
||||
elif self.answer_type == 'numerical_box':
|
||||
return self.value_numerical_box
|
||||
elif self.answer_type == 'scale':
|
||||
return self.value_scale
|
||||
elif self.answer_type == 'date':
|
||||
return self.value_date
|
||||
elif self.answer_type == 'datetime':
|
||||
return self.value_datetime
|
||||
elif self.answer_type == 'suggestion':
|
||||
return self.suggested_answer_id.value
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
from . import survey_survey
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue