diff --git a/addons/survey/__manifest__.py b/addons/survey/__manifest__.py
index c67a1123fd5..66e79a12ce8 100644
--- a/addons/survey/__manifest__.py
+++ b/addons/survey/__manifest__.py
@@ -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': [
diff --git a/addons/survey/models/__init__.py b/addons/survey/models/__init__.py
index 982f7817af9..b51646132df 100644
--- a/addons/survey/models/__init__.py
+++ b/addons/survey/models/__init__.py
@@ -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
diff --git a/addons/survey/models/badge.py b/addons/survey/models/badge.py
new file mode 100644
index 00000000000..7ed12731711
--- /dev/null
+++ b/addons/survey/models/badge.py
@@ -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
diff --git a/addons/survey/models/challenge.py b/addons/survey/models/challenge.py
new file mode 100644
index 00000000000..e1677d46fc9
--- /dev/null
+++ b/addons/survey/models/challenge.py
@@ -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')])
diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py
index acffbab2382..896430e078c 100644
--- a/addons/survey/models/survey_survey.py
+++ b/addons/survey/models/survey_survey.py
@@ -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
diff --git a/addons/survey/security/ir.model.access.csv b/addons/survey/security/ir.model.access.csv
index 907823200d4..2bf75531ba4 100644
--- a/addons/survey/security/ir.model.access.csv
+++ b/addons/survey/security/ir.model.access.csv
@@ -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
diff --git a/addons/survey/tests/__init__.py b/addons/survey/tests/__init__.py
index 7259a6d6673..feae4e21546 100644
--- a/addons/survey/tests/__init__.py
+++ b/addons/survey/tests/__init__.py
@@ -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
diff --git a/addons/survey/tests/test_certification_badge.py b/addons/survey/tests/test_certification_badge.py
new file mode 100644
index 00000000000..05c97ce6679
--- /dev/null
+++ b/addons/survey/tests/test_certification_badge.py
@@ -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")
diff --git a/addons/survey/views/gamification_badge_views.xml b/addons/survey/views/gamification_badge_views.xml
new file mode 100644
index 00000000000..e9f03f85936
--- /dev/null
+++ b/addons/survey/views/gamification_badge_views.xml
@@ -0,0 +1,28 @@
+
+
+
+ gamification.badge.form.view.simplified
+ gamification.badge
+ 100
+
+
+
+
+
diff --git a/addons/survey/views/survey_survey_views.xml b/addons/survey/views/survey_survey_views.xml
index e1317a4b57f..d96c68a649a 100644
--- a/addons/survey/views/survey_survey_views.xml
+++ b/addons/survey/views/survey_survey_views.xml
@@ -98,6 +98,18 @@
+
+
+