summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/website_slides/models/slide_channel.py27
-rw-r--r--addons/website_slides_survey/__manifest__.py1
-rw-r--r--addons/website_slides_survey/controllers/survey.py2
-rw-r--r--addons/website_slides_survey/data/mail_template_data.xml29
-rw-r--r--addons/website_slides_survey/models/slide_slide.py6
-rw-r--r--addons/website_slides_survey/models/survey_user.py54
-rw-r--r--addons/website_slides_survey/tests/__init__.py4
-rw-r--r--addons/website_slides_survey/tests/test_course_certification_failure.py122
-rw-r--r--addons/website_slides_survey/views/survey_templates.xml4
9 files changed, 244 insertions, 5 deletions
diff --git a/addons/website_slides/models/slide_channel.py b/addons/website_slides/models/slide_channel.py
index dee622ab7f2..4ea64a1a297 100644
--- a/addons/website_slides/models/slide_channel.py
+++ b/addons/website_slides/models/slide_channel.py
@@ -425,6 +425,33 @@ class Channel(models.Model):
for channel in self:
channel._action_add_members(channel.mapped('enroll_group_ids.users.partner_id'))
+ def _remove_membership(self, partner_ids):
+ """ Unlink (!!!) the relationships between the passed partner_ids
+ and the channels and their slides. """
+ if not partner_ids:
+ raise ValueError("Do not use this method with an empty partner_id recordset")
+
+ removed_slide_partner_domain = []
+ removed_channel_partner_domain = []
+ for channel in self:
+ removed_slide_partner_domain = expression.OR([
+ removed_slide_partner_domain,
+ [('partner_id', 'in', partner_ids.ids),
+ ('slide_id', 'in', channel.slide_ids.ids)]
+ ])
+
+ removed_channel_partner_domain = expression.OR([
+ removed_channel_partner_domain,
+ [('partner_id', 'in', partner_ids.ids),
+ ('channel_id', '=', channel.id)]
+ ])
+
+ if removed_slide_partner_domain:
+ self.env['slide.slide.partner'].sudo().search(removed_slide_partner_domain).unlink()
+
+ if removed_channel_partner_domain:
+ self.env['slide.channel.partner'].sudo().search(removed_channel_partner_domain).unlink()
+
# ---------------------------------------------------------
# Rating Mixin API
# ---------------------------------------------------------
diff --git a/addons/website_slides_survey/__manifest__.py b/addons/website_slides_survey/__manifest__.py
index 3ccd9168ece..c4406a1ae4c 100644
--- a/addons/website_slides_survey/__manifest__.py
+++ b/addons/website_slides_survey/__manifest__.py
@@ -19,6 +19,7 @@
'views/website_slides_templates_homepage.xml',
'views/survey_templates.xml',
'views/website_profile.xml',
+ 'data/mail_template_data.xml',
'data/gamification_data.xml',
],
'demo': [
diff --git a/addons/website_slides_survey/controllers/survey.py b/addons/website_slides_survey/controllers/survey.py
index ce83f61bc8c..85a8700156a 100644
--- a/addons/website_slides_survey/controllers/survey.py
+++ b/addons/website_slides_survey/controllers/survey.py
@@ -8,7 +8,7 @@ class Survey(Survey):
def _prepare_survey_finished_values(self, survey, answer, token=False):
result = super(Survey, self)._prepare_survey_finished_values(survey, answer, token)
if answer.slide_id:
- result['slide_url'] = answer.slide_id.website_url
+ result['channel_url'] = answer.slide_id.channel_id.website_url
return result
diff --git a/addons/website_slides_survey/data/mail_template_data.xml b/addons/website_slides_survey/data/mail_template_data.xml
new file mode 100644
index 00000000000..d9c2c90b870
--- /dev/null
+++ b/addons/website_slides_survey/data/mail_template_data.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data noupdate="1">
+ <record id="mail_template_user_input_certification_failed" model="mail.template">
+ <field name="name">Certification failed email</field>
+ <field name="model_id" ref="model_survey_user_input" />
+ <field name="partner_to">${object.partner_id.id}</field>
+ <field name="subject">You have failed the course: ${object.slide_partner_id.channel_id.name}</field>
+ <field name="body_html" type="html">
+<div style="margin: 0px; padding: 0px; font-size: 13px;">
+ <p style="margin: 0px; padding: 0px; font-size: 13px;">
+ Dear ${object.partner_id.name or 'participant'}<br/><br/>
+ Unfortunately, you have failed the certification and are no longer a member of the course: ${object.slide_partner_id.channel_id.name}.<br/><br/>
+ Don't hesitate to enroll again!
+ <div style="margin: 16px 0px 16px 0px;">
+ <a href="${(object.slide_partner_id.channel_id.website_url) | safe}"
+ style="background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">
+ Enroll now
+ </a>
+ </div>
+ Thank you for your participation.
+ </p>
+</div>
+ </field>
+ <field name="auto_delete" eval="True"/>
+ <field name="user_signature" eval="False"/>
+ </record>
+ </data>
+</odoo>
diff --git a/addons/website_slides_survey/models/slide_slide.py b/addons/website_slides_survey/models/slide_slide.py
index da0598addc2..3b477364dcd 100644
--- a/addons/website_slides_survey/models/slide_slide.py
+++ b/addons/website_slides_survey/models/slide_slide.py
@@ -52,6 +52,9 @@ class Slide(models.Model):
2/ create a new user_input for member
3/ for no member, a test user_input is created and the url is returned
Note: the slide.slides.partner should already exist
+
+ We have to generate a new invite_token to differentiate pools of attempts since the
+ course can be enrolled multiple times.
"""
certification_urls = {}
for slide in self.filtered(lambda slide: slide.slide_type == 'certification' and slide.survey_id):
@@ -70,7 +73,8 @@ class Slide(models.Model):
**{
'slide_id': slide.id,
'slide_partner_id': user_membership_id_sudo.id
- }
+ },
+ invite_token=self.env['survey.user_input']._generate_invite_token()
)
certification_urls[slide.id] = user_input._get_survey_url()
else:
diff --git a/addons/website_slides_survey/models/survey_user.py b/addons/website_slides_survey/models/survey_user.py
index 427e5196fcb..73ecef89129 100644
--- a/addons/website_slides_survey/models/survey_user.py
+++ b/addons/website_slides_survey/models/survey_user.py
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import fields, models
+from odoo import fields, models, api
+from odoo.osv import expression
class SurveyUserInput(models.Model):
@@ -11,3 +12,54 @@ class SurveyUserInput(models.Model):
help="The related course slide when there is no membership information")
slide_partner_id = fields.Many2one('slide.slide.partner', 'Subscriber information',
help="Slide membership information for the logged in user")
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ records = super(SurveyUserInput, self).create(vals_list)
+ records._check_for_failed_attempt()
+ return records
+
+ @api.multi
+ def write(self, vals):
+ res = super(SurveyUserInput, self).write(vals)
+ if 'state' in vals:
+ self._check_for_failed_attempt()
+ return res
+
+ def _check_for_failed_attempt(self):
+ """ If the user fails his last attempt at a course certification,
+ we remove him from the members of the course (and he has to enroll again).
+ He receives an email in the process notifying him of his failure and suggesting
+ he enrolls to the course again.
+
+ The purpose is to have a 'certification flow' where the user can re-purchase the
+ certification when they have failed it."""
+
+ if self:
+ user_inputs = self.search([
+ ('id', 'in', self.ids),
+ ('state', '=', 'done'),
+ ('quizz_passed', '=', False),
+ ('slide_partner_id', '!=', False)
+ ])
+
+ if user_inputs:
+ for user_input in user_inputs:
+ removed_memberships_per_partner = {}
+ if user_input.survey_id._has_attempts_left(user_input.partner_id, user_input.email, user_input.invite_token):
+ # skip if user still has attempts left
+ continue
+
+ self.env.ref('website_slides_survey.mail_template_user_input_certification_failed').send_mail(
+ user_input.id, notif_layout="mail.mail_notification_light"
+ )
+
+ removed_memberships = removed_memberships_per_partner.get(
+ user_input.partner_id,
+ self.env['slide.channel']
+ )
+ removed_memberships |= user_input.slide_partner_id.channel_id
+ removed_memberships_per_partner[user_input.partner_id] = removed_memberships
+
+ for partner_id, removed_memberships in removed_memberships_per_partner.items():
+ removed_memberships._remove_membership(partner_id)
diff --git a/addons/website_slides_survey/tests/__init__.py b/addons/website_slides_survey/tests/__init__.py
new file mode 100644
index 00000000000..c2f581dd98c
--- /dev/null
+++ b/addons/website_slides_survey/tests/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import test_course_certification_failure
diff --git a/addons/website_slides_survey/tests/test_course_certification_failure.py b/addons/website_slides_survey/tests/test_course_certification_failure.py
new file mode 100644
index 00000000000..bdb72b5d294
--- /dev/null
+++ b/addons/website_slides_survey/tests/test_course_certification_failure.py
@@ -0,0 +1,122 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.addons.survey.tests.common import SurveyCase
+
+
+class TestCourseCertificationFailureFlow(SurveyCase):
+ def test_course_certification_failure_flow(self):
+ # Step 1: create a simple certification
+ # --------------------------------------------------
+ with self.sudo(self.survey_user):
+ certification = self.env['survey.survey'].create({
+ 'title': 'Small course certification',
+ 'access_mode': 'public',
+ 'users_login_required': True,
+ 'scoring_type': 'scoring_with_answers',
+ 'certificate': True,
+ 'is_attempts_limited': True,
+ 'passing_score': 100.0,
+ 'attempts_limit': 2,
+ 'stage_id': self.env['survey.stage'].search([('closed', '=', False)]).id
+ })
+
+ self._add_question(
+ None, 'Question 1', 'simple_choice',
+ sequence=1,
+ survey_id=certification.id,
+ labels=[
+ {'value': 'Wrong answer'},
+ {'value': 'Correct answer', 'is_correct': True, 'answer_score': 1.0}
+ ])
+
+ self._add_question(
+ None, 'Question 2', 'simple_choice',
+ sequence=2,
+ survey_id=certification.id,
+ labels=[
+ {'value': 'Wrong answer'},
+ {'value': 'Correct answer', 'is_correct': True, 'answer_score': 1.0}
+ ])
+
+ # Step 1.1: create a simple channel
+ self.channel = self.env['slide.channel'].sudo().create({
+ 'name': 'Test Channel',
+ 'channel_type': 'training',
+ 'enroll': 'public',
+ 'visibility': 'public',
+ 'website_published': True,
+ })
+
+ # Step 2: link the certification to a slide of type 'certification'
+ self.slide_certification = self.env['slide.slide'].sudo().create({
+ 'name': 'Certification slide',
+ 'channel_id': self.channel.id,
+ 'slide_type': 'certification',
+ 'survey_id': certification.id,
+ 'website_published': True,
+ })
+ # Step 3: add public user as member of the channel
+ self.channel._action_add_members(self.user_public.partner_id)
+ # forces recompute of partner_ids as we create directly in relation
+ self.channel.invalidate_cache()
+ slide_partner = self.slide_certification._action_set_viewed(self.user_public.partner_id)
+ self.slide_certification.sudo(self.user_public)._generate_certification_url()
+
+ self.assertEqual(1, len(slide_partner.user_input_ids), 'A user input should have been automatically created upon slide view')
+
+ # Step 4: fill in the created user_input with wrong answers
+ self.fill_in_answer(slide_partner.user_input_ids[0], certification.question_ids)
+
+ self.assertFalse(slide_partner.survey_quizz_passed, 'Quizz should not be marked as passed with wrong answers')
+ # forces recompute of partner_ids as we delete directly in relation
+ self.channel.invalidate_cache()
+ self.assertIn(self.user_public.partner_id, self.channel.partner_ids, 'Public user should still be a member of the course because he still has attempts left')
+
+ # Step 5: simulate a 'retry'
+ retry_user_input = self.slide_certification.survey_id.sudo()._create_answer(
+ partner=self.user_public.partner_id,
+ **{
+ 'slide_id': self.slide_certification.id,
+ 'slide_partner_id': slide_partner.id
+ },
+ invite_token=slide_partner.user_input_ids[0].invite_token
+ )
+ # Step 6: fill in the new user_input with wrong answers again
+ self.fill_in_answer(retry_user_input, certification.question_ids)
+ # forces recompute of partner_ids as we delete directly in relation
+ self.channel.invalidate_cache()
+ self.assertNotIn(self.user_public.partner_id, self.channel.partner_ids, 'Public user should have been kicked out of the course because he failed his last attempt')
+
+ # Step 7: add public user as member of the channel once again
+ self.channel._action_add_members(self.user_public.partner_id)
+ # forces recompute of partner_ids as we create directly in relation
+ self.channel.invalidate_cache()
+
+ self.assertIn(self.user_public.partner_id, self.channel.partner_ids, 'Public user should be a member of the course once again')
+ new_slide_partner = self.slide_certification._action_set_viewed(self.user_public.partner_id)
+ self.slide_certification.sudo(self.user_public)._generate_certification_url()
+ self.assertEqual(1, len(new_slide_partner.user_input_ids.filtered(lambda user_input: user_input.state != 'done')), 'A new user input should have been automatically created upon slide view')
+
+ # Step 8: fill in the created user_input with correct answers this time
+ self.fill_in_answer(new_slide_partner.user_input_ids.filtered(lambda user_input: user_input.state != 'done')[0], certification.question_ids, good_answers=True)
+ self.assertTrue(new_slide_partner.survey_quizz_passed, 'Quizz should be marked as passed with correct answers')
+ # forces recompute of partner_ids as we delete directly in relation
+ self.channel.invalidate_cache()
+ self.assertIn(self.user_public.partner_id, self.channel.partner_ids, 'Public user should still be a member of the course')
+
+ def fill_in_answer(self, answer, questions, good_answers=False):
+ """ Fills in the user_input with answers for all given questions.
+ You can control whether the answer will be correct or not with the 'good_answers' param.
+ (It's assumed that wrong answers are at index 0 of question.labels_ids and good answers at index 1) """
+ answer.write({
+ 'state': 'done',
+ 'user_input_line_ids': [
+ (0, 0, {
+ 'question_id': question.id,
+ 'answer_type': 'suggestion',
+ 'answer_score': 1 if good_answers else 0,
+ 'value_suggested': question.labels_ids[1 if good_answers else 0].id
+ }) for question in questions
+ ]
+ })
diff --git a/addons/website_slides_survey/views/survey_templates.xml b/addons/website_slides_survey/views/survey_templates.xml
index c6816dde39b..deea71a9c99 100644
--- a/addons/website_slides_survey/views/survey_templates.xml
+++ b/addons/website_slides_survey/views/survey_templates.xml
@@ -3,10 +3,10 @@
<data>
<template id="survey_templates_inherit_website_slides" inherit_id="survey.sfinished">
<xpath expr="//div[hasclass('jumbotron')]/div[last()]" position="after">
- <div t-if="slide_url" class="mt16">
+ <div t-if="channel_url" class="mt16">
<a role="button"
class="btn btn-primary btn-lg"
- t-att-href="slide_url">
+ t-att-href="channel_url">
Go back to course
</a>
</div>