[IMP] gamification, survey : apply badge on certification survey

This commit allow gaining a badge at the end of a certification survey if succeeded.
The badge can be configured on the survey if the certification mode is activated.

The badge is linked directly to the survey and not the challenge,
because it makes more sense to configure directly the reward on the survey and
not the way to gain this reward. As the way is always the same.
Only one badge can be set on the certification survey.
Only the name, description, image and badge level can be configured.
The rest of badge configuration is automatically set to correspond to the use case.
When a badge is configured on a certification survey, the needed challange, goal
and challenge line are autogenerated.

The badge is available on the user's profile page if he gained it.

Once the badge is configured on the survey, he cannot be changed
(remove + create new one), only modified (badge attributes edition).
The only way to remove the badge is to uncheck 'certification_give_badge'.

When removing the badge from the certification survey, all the autogenerated
records (at badge creation) are deleted to avoid ghost records (as they have
no purpose outside of this context). If the badge is owned by someone, the badge
is only archived. If nobody owns the badge, the badge is deleted.

Survey users now have the right to create badges but not challenges or goals.
All the autogenerated records are handled in sudo, to allow the survey user
to configure a badge on their survey.

Note : To avoid having to rewrite the complete context in an xpath expression
only to display the certification badge on the user's profile page,
the default website_published value of the certification badge is defined
directly in the survey module even if survey does not depend of website.
This attribute will be ignored until website module is installed.

Task ID : 1935136
Closes PR #31486
This commit is contained in:
David Beguin
2019-07-15 08:16:43 +00:00
parent 3e5d2e7942
commit be9461bf8b
10 changed files with 373 additions and 4 deletions
+3 -1
View File
@@ -20,7 +20,8 @@ sent mails with personal token for the invitation of the survey.
'auth_signup',
'http_routing',
'mail',
'web_tour'],
'web_tour',
'gamification'],
'data': [
'views/survey_report_templates.xml',
'views/survey_reports.xml',
@@ -34,6 +35,7 @@ sent mails with personal token for the invitation of the survey.
'views/survey_user_views.xml',
'views/survey_question_views.xml',
'views/survey_templates.xml',
'views/gamification_badge_views.xml',
'wizard/survey_invite_views.xml',
],
'demo': [
+2
View File
@@ -5,3 +5,5 @@ from . import ir_autovacuum
from . import survey_survey
from . import survey_question
from . import survey_user
from . import badge
from . import challenge
+16
View File
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class GamificationBadge(models.Model):
_inherit = 'gamification.badge'
survey_ids = fields.One2many('survey.survey', 'certification_badge_id', 'Survey Ids')
survey_id = fields.Many2one('survey.survey', 'Survey', compute='_compute_survey_id', store=True)
@api.depends('survey_ids.certification_badge_id')
def _compute_survey_id(self):
for badge in self:
badge.survey_id = badge.survey_ids[0] if badge.survey_ids else None
+10
View File
@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields
class Challenge(models.Model):
_inherit = 'gamification.challenge'
category = fields.Selection(selection_add=[('certification', 'Certifications')])
+89 -3
View File
@@ -94,11 +94,26 @@ class Survey(models.Model):
domain="[('model', '=', 'survey.user_input')]",
help="Automated email sent to the user when he succeeds the certification, containing his certification document.")
# Certification badge
# certification_badge_id_dummy is used to have two different behaviours in the form view :
# - If the certification badge is not set, show certification_badge_id and only display create option in the m2o
# - If the certification badge is set, show certification_badge_id_dummy in 'no create' mode.
# So it can be edited but not removed or replaced.
certification_give_badge = fields.Boolean('Give Badge')
certification_badge_id = fields.Many2one('gamification.badge', 'Certification Badge')
certification_badge_id_dummy = fields.Many2one(related='certification_badge_id', string='Certification Badge ')
_sql_constraints = [
('access_token_unique', 'unique(access_token)', 'Access token should be unique'),
('certificate_check', "CHECK( scoring_type!='no_scoring' OR certificate=False )", 'You can only create certifications for surveys that have a scoring mechanism.'),
('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.')
('certificate_check', "CHECK( scoring_type!='no_scoring' OR certificate=False )",
'You can only create certifications for surveys that have a scoring mechanism.'),
('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!"),
('give_badge_check', "CHECK(certification_give_badge=False OR (certification_give_badge=True AND certification_badge_id is not null))",
'Certification badge must be configured if Give Badge is set.'),
]
@api.multi
@@ -181,6 +196,25 @@ class Survey(models.Model):
selection = self.env['survey.survey'].fields_get(allfields=['state'])['state']['selection']
return [s[0] for s in selection]
@api.onchange('users_login_required', 'certificate')
def _onchange_set_certification_give_badge(self):
if not self.users_login_required or not self.certificate:
self.certification_give_badge = False
# CRUD
@api.model
def create(self, vals):
survey = super(Survey, self).create(vals)
if vals.get('certification_give_badge'):
survey.sudo()._create_certification_badge_trigger()
return survey
def write(self, vals):
result = super(Survey, self).write(vals)
if 'certification_give_badge' in vals:
return self.sudo()._handle_certification_badges(vals)
return result
# Public methods #
def copy_data(self, default=None):
title = _("%s (copy)") % (self.title)
@@ -435,6 +469,58 @@ class Survey(models.Model):
return result
@api.multi
def _create_certification_badge_trigger(self):
self.ensure_one()
goal = self.env['gamification.goal.definition'].create({
'name': self.title,
'description': "%s certification passed" % self.title,
'domain': "['&', ('survey_id', '=', %s), ('quizz_passed', '=', True)]" % self.id,
'computation_mode': 'count',
'display_mode': 'boolean',
'model_id': self.env.ref('survey.model_survey_user_input').id,
'condition': 'higher',
'batch_mode': True,
'batch_distinctive_field': self.env.ref('survey.field_survey_user_input__partner_id').id,
'batch_user_expression': 'user.partner_id.id'
})
challenge = self.env['gamification.challenge'].create({
'name': _('%s challenge certificate' % self.title),
'reward_id': self.certification_badge_id.id,
'state': 'inprogress',
'period': 'once',
'category': 'certification',
'reward_realtime': True,
'report_message_frequency': 'never',
'user_domain': [('karma', '>', 0)],
'visibility_mode': 'personal'
})
self.env['gamification.challenge.line'].create({
'definition_id': goal.id,
'challenge_id': challenge.id,
'target_goal': 1
})
@api.multi
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').write({'active': True})
# (re-)create challenge and goal
for survey in self:
survey._create_certification_badge_trigger()
else:
# if badge with owner : archive them, else delete everything (badge, challenge, goal)
badges = self.mapped('certification_badge_id')
challenges_to_delete = self.env['gamification.challenge'].search([('reward_id', 'in', badges.ids)])
goals_to_delete = challenges_to_delete.mapped('line_ids').mapped('definition_id')
badges.write({'active': False})
# delete all challenges and goals because not needed anymore (challenge lines are deleted in cascade)
challenges_to_delete.unlink()
goals_to_delete.unlink()
# Actions
@api.multi
@@ -19,3 +19,4 @@ access_survey_user_input_line_all,survey.user_input_line.all,model_survey_user_i
access_survey_user_input_line_user,survey.user_input_line.user,model_survey_user_input_line,base.group_user,0,0,0,0
access_survey_user_input_line_survey_user,survey.user_input_line.survey.user,model_survey_user_input_line,group_survey_user,1,1,1,1
access_survey_user_input_line_survey_manager,survey.user_input_line.survey.manager,model_survey_user_input_line,group_survey_manager,1,1,1,1
access_gamification_badge_survey_user,gamification.badge.survey.user,model_gamification_badge,group_survey_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
19 access_survey_user_input_line_user survey.user_input_line.user model_survey_user_input_line base.group_user 0 0 0 0
20 access_survey_user_input_line_survey_user survey.user_input_line.survey.user model_survey_user_input_line group_survey_user 1 1 1 1
21 access_survey_user_input_line_survey_manager survey.user_input_line.survey.manager model_survey_user_input_line group_survey_manager 1 1 1 1
22 access_gamification_badge_survey_user gamification.badge.survey.user model_gamification_badge group_survey_user 1 1 1 1
+1
View File
@@ -10,3 +10,4 @@ from . import test_survey_security
from . import test_survey_randomize
from . import test_survey_ui
from . import test_survey_compute_pages_questions
from . import test_certification_badge
@@ -0,0 +1,211 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.survey.tests import common
from odoo.tests.common import HttpCase
from psycopg2 import IntegrityError
from odoo.exceptions import AccessError
from odoo.tools import mute_logger
class TestCertificationBadge(common.SurveyCase, HttpCase):
def setUp(self):
super(TestCertificationBadge, self).setUp()
self.certification_survey = self.env['survey.survey'].with_user(self.survey_manager).create({
'title': 'Certification Survey',
'access_mode': 'public',
'users_login_required': True,
'scoring_type': 'scoring_with_answers',
'certificate': True,
'state': 'open',
})
self.certification_survey_2 = self.env['survey.survey'].with_user(self.survey_manager).create({
'title': 'Another Certification Survey',
'access_mode': 'public',
'users_login_required': True,
'scoring_type': 'scoring_with_answers',
'certificate': True,
'state': 'open',
})
self.certification_badge = self.env['gamification.badge'].with_user(self.survey_manager).create({
'name': self.certification_survey.title,
'description': 'Congratulation, you succeeded this certification',
'rule_auth': 'nobody',
'level': None,
})
self.certification_badge_2 = self.env['gamification.badge'].with_user(self.survey_manager).create({
'name': self.certification_survey.title + ' 2',
'description': 'Congratulation, you succeeded this certification',
'rule_auth': 'nobody',
'level': None,
})
self.certification_badge_3 = self.env['gamification.badge'].with_user(self.survey_manager).create({
'name': self.certification_survey.title + ' 3',
'description': 'Congratulation, you succeeded this certification',
'rule_auth': 'nobody',
'level': None,
})
def test_give_badge_without_badge(self):
with mute_logger('odoo.sql_db'):
with self.assertRaises(IntegrityError):
self.certification_survey.write({'certification_give_badge': True})
def test_remove_badge_with_give_badge(self):
self.certification_survey.write({
'certification_give_badge': True,
'certification_badge_id': self.certification_badge.id
})
with mute_logger('odoo.sql_db'):
with self.assertRaises(IntegrityError):
self.certification_survey.write({'certification_badge_id': None})
def test_remove_badge_with_give_badge_multi(self):
self.certification_survey.write({
'certification_give_badge': True,
'certification_badge_id': self.certification_badge.id
})
self.certification_survey_2.write({
'certification_give_badge': True,
'certification_badge_id': self.certification_badge_2.id
})
surveys = self.env['survey.survey'].browse([
self.certification_survey.id,
self.certification_survey_2.id
])
with mute_logger('odoo.sql_db'):
with self.assertRaises(IntegrityError):
surveys.write({'certification_badge_id': None})
def test_set_same_badge_on_multiple_survey(self):
self.certification_survey.write({
'certification_give_badge': True,
'certification_badge_id': self.certification_badge.id
})
# set the same badge on another survey should fail:
with mute_logger('odoo.sql_db'):
with self.assertRaises(IntegrityError):
self.certification_survey_2.write({
'certification_give_badge': True,
'certification_badge_id': self.certification_badge.id
})
def test_badge_configuration(self):
# add a certification badge on a new survey
challenge = self.env['gamification.challenge'].search([('reward_id', '=', self.certification_badge.id)])
self.assertEquals(len(challenge), 0, """A challenge should not exist or be linked to the certification badge
if the certification badge have not been activated on a certification survey""")
self.certification_survey.write({
'certification_give_badge': True,
'certification_badge_id': self.certification_badge.id
})
challenge = self.env['gamification.challenge'].search([('reward_id', '=', self.certification_badge.id)])
self.assertEquals(len(challenge), 1,
"A challenge should be created if the certification badge is activated on a certification survey")
challenge_line = self.env['gamification.challenge.line'].search([('challenge_id', '=', challenge.id)])
self.assertEquals(len(challenge_line), 1,
"A challenge_line should be created if the certification badge is activated on a certification survey")
goal = challenge_line.definition_id
self.assertEquals(len(goal), 1,
"A goal should be created if the certification badge is activated on a certification survey")
# don't give badge anymore
self.certification_survey.write({'certification_give_badge': False})
self.assertEqual(self.certification_badge.id, self.certification_survey.certification_badge_id.id,
'The certification badge should still be set on certification survey even if give_badge is false.')
self.assertEqual(self.certification_badge.active, False,
'The certification badge should be inactive if give_badge is false.')
challenge = self.env['gamification.challenge'].search([('id', '=', challenge.id)])
self.assertEquals(len(challenge), 0,
"The challenge should be deleted if the certification badge is unset from the certification survey")
challenge_line = self.env['gamification.challenge.line'].search([('id', '=', challenge_line.id)])
self.assertEquals(len(challenge_line), 0,
"The challenge_line should be deleted if the certification badge is unset from the certification survey")
goal = self.env['gamification.goal'].search([('id', '=', goal.id)])
self.assertEquals(len(goal), 0,
"The goal should be deleted if the certification badge is unset from the certification survey")
# re active the badge in the survey
self.certification_survey.write({'certification_give_badge': True})
self.assertEqual(self.certification_badge.active, True,
'The certification badge should be active if give_badge is true.')
challenge = self.env['gamification.challenge'].search([('reward_id', '=', self.certification_badge.id)])
self.assertEquals(len(challenge), 1,
"A challenge should be created if the certification badge is activated on a certification survey")
challenge_line = self.env['gamification.challenge.line'].search([('challenge_id', '=', challenge.id)])
self.assertEquals(len(challenge_line), 1,
"A challenge_line should be created if the certification badge is activated on a certification survey")
goal = challenge_line.definition_id
self.assertEquals(len(goal), 1,
"A goal should be created if the certification badge is activated on a certification survey")
def test_certification_badge_access(self):
self.certification_badge.with_user(self.survey_manager).write(
{'description': "Spoiler alert: I'm Aegon Targaryen and I sleep with the Dragon Queen, who is my aunt by the way! So I can do whatever I want! Even if I know nothing!"})
self.certification_badge.with_user(self.survey_user).write({'description': "Youpie Yeay!"})
with self.assertRaises(AccessError):
self.certification_badge.with_user(self.user_emp).write({'description': "I'm a dude who think that has every right on the Iron Throne"})
with self.assertRaises(AccessError):
self.certification_badge.with_user(self.user_portal).write({'description': "Guy, you just can't do that !"})
with self.assertRaises(AccessError):
self.certification_badge.with_user(self.user_public).write({'description': "What did you expect ? Schwepps !"})
def test_badge_configuration_multi(self):
vals = {
'title': 'Certification Survey',
'access_mode': 'public',
'users_login_required': True,
'scoring_type': 'scoring_with_answers',
'certificate': True,
'certification_give_badge': True,
'certification_badge_id': self.certification_badge.id,
'state': 'open'
}
survey_1 = self.env['survey.survey'].create(vals.copy())
vals.update({'certification_badge_id': self.certification_badge_2.id})
survey_2 = self.env['survey.survey'].create(vals.copy())
vals.update({'certification_badge_id': self.certification_badge_3.id})
survey_3 = self.env['survey.survey'].create(vals)
certification_surveys = self.env['survey.survey'].browse([survey_1.id, survey_2.id, survey_3.id])
self.assertEqual(len(certification_surveys), 3, 'There should be 3 certification survey created')
challenges = self.env['gamification.challenge'].search([('reward_id', 'in', certification_surveys.mapped('certification_badge_id').ids)])
self.assertEquals(len(challenges), 3, "3 challenges should be created")
challenge_lines = self.env['gamification.challenge.line'].search([('challenge_id', 'in', challenges.ids)])
self.assertEquals(len(challenge_lines), 3, "3 challenge_lines should be created")
goals = challenge_lines.mapped('definition_id')
self.assertEquals(len(goals), 3, "3 goals should be created")
# Test write multi
certification_surveys.write({'certification_give_badge': False})
for survey in certification_surveys:
self.assertEqual(survey.certification_badge_id.active, False,
'Every badge should be inactive if the 3 survey does not give badge anymore')
challenges = self.env['gamification.challenge'].search([('id', 'in', challenges.ids)])
self.assertEquals(len(challenges), 0, "The 3 challenges should be deleted")
challenge_lines = self.env['gamification.challenge.line'].search([('id', 'in', challenge_lines.ids)])
self.assertEquals(len(challenge_lines), 0, "The 3 challenge_lines should be deleted")
goals = self.env['gamification.goal'].search([('id', 'in', goals.ids)])
self.assertEquals(len(goals), 0, "The 3 goals should be deleted")
certification_surveys.write({'certification_give_badge': True})
for survey in certification_surveys:
self.assertEqual(survey.certification_badge_id.active, True,
'Every badge should be reactivated if the 3 survey give badges again')
challenges = self.env['gamification.challenge'].search([('reward_id', 'in', certification_surveys.mapped('certification_badge_id').ids)])
self.assertEquals(len(challenges), 3, "3 challenges should be created")
challenge_lines = self.env['gamification.challenge.line'].search([('challenge_id', 'in', challenges.ids)])
self.assertEquals(len(challenge_lines), 3, "3 challenge_lines should be created")
goals = challenge_lines.mapped('definition_id')
self.assertEquals(len(goals), 3, "3 goals should be created")
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data>
<record id="gamification_badge_form_view_simplified" model="ir.ui.view">
<field name="name">gamification.badge.form.view.simplified</field>
<field name="model">gamification.badge</field>
<field name="priority">100</field>
<field name="arch" type="xml">
<form string="Badge">
<sheet>
<div class="oe_button_box" name="button_box"/>
<field name="image" widget='image' class="oe_avatar"/>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name" placeholder="Badge Name"/>
</h1>
</div>
<group>
<field name="description" nolabel="1" placeholder="Badge Description"/>
</group>
<group string="Rewards for challenges">
<field name="level"/>
</group>
</sheet>
</form>
</field>
</record>
</data></odoo>
@@ -98,6 +98,18 @@
<field name="passing_score" attrs="{'invisible': [('scoring_type', '=', 'no_scoring')]}" />
<field name="certificate" attrs="{'invisible': [('scoring_type', '=', 'no_scoring')]}" />
<field name="certification_mail_template_id" attrs="{'invisible': [('certificate', '=', False)]}" />
<field name="certification_give_badge" attrs="{'invisible': ['|', ('certificate', '=', False), ('users_login_required', '=', False)]}" />
<field name="certification_badge_id" attrs="{'invisible': ['|', ('certification_give_badge', '=', False), ('certification_badge_id', '!=', False)]}"
domain="[('survey_id', '=', active_id), ('survey_id', '!=', False)]"
context="{'default_name': title,
'default_description': 'Congratulation, you succeeded this certification',
'default_rule_auth': 'nobody',
'default_level': None,
'form_view_ref': 'survey.gamification_badge_form_view_simplified',
'default_website_published': True}"/>
<field name="certification_badge_id_dummy" attrs="{'invisible': ['|', ('certification_give_badge', '=', False), ('certification_badge_id', '=', False)]}"
options="{'no_create': True}"
context="{'form_view_ref': 'survey.gamification_badge_form_view_simplified'}"/>
</group>
</group>
</page>