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 @@ + + +