mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-23 23: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
|
|
@ -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()]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue