website_slides/tests/test_attendee.py

513 lines
26 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.base.tests.common import HttpCaseWithUserPortal
from odoo.addons.http_routing.models.ir_http import slug
from odoo.addons.website_slides.tests import common
from odoo.tests import tagged, users
from werkzeug.urls import url_decode
@tagged('post_install', '-at_install')
class TestAttendee(common.SlidesCase):
@users('user_officer')
def test_attendee_course_completion_values(self):
""" Check that once completed, the member_status remains 'completed', except if
attendee leaves course and is reinvited / rejoins the course, it is then recomputed."""
def check_course_completion_values(member_status='completed'):
""" Check that the course completion is still accounted for, with given member_status """
self.assertEqual(user_portal_channel_partner.member_status, member_status)
self.assertEqual(user_portal_channel_partner.completion, 100)
self.assertTrue(self.channel.with_user(self.user_portal).completed)
user_portal_partner = self.user_portal.partner_id
user_portal_channel_partner = self.env['slide.channel.partner'].create({
'channel_id': self.channel.id,
'partner_id': user_portal_partner.id,
})
(self.slide | self.slide_2 | self.slide_3).with_user(self.user_portal)._action_mark_completed()
check_course_completion_values()
# A new slide should not update status / completion
self.env['slide.slide'].create({
'name': 'About completion',
'channel_id': self.channel.id,
'slide_category': 'document',
'is_published': True,
'completion_time': 2.0,
'sequence': 10,
})
check_course_completion_values()
# Unpublish a slide user has completed
self.slide.is_published = False
check_course_completion_values()
# Archive attendee
user_portal_channel_partner.action_archive()
check_course_completion_values()
# Invited attendee only gets status update
self.channel._action_add_members(self.user_portal.partner_id, member_status='invited')
check_course_completion_values(member_status='invited')
# Pulbishing a slide user has completed. This will update user values,
# but only on channel (those are used for diplay and not before joining)
self.slide.is_published = True
self.assertEqual(user_portal_channel_partner.member_status, 'invited')
self.assertEqual(user_portal_channel_partner.completion, 100)
self.assertEqual(self.channel.with_user(self.user_portal).completion, 75)
self.assertFalse(self.channel.with_user(self.user_portal).completed)
# Once they are enrolled (or join), values are now updated and completion is lost.
self.channel._action_add_members(self.user_portal.partner_id)
self.assertEqual(user_portal_channel_partner.member_status, 'ongoing')
self.assertEqual(user_portal_channel_partner.completion, 75)
self.assertEqual(self.channel.with_user(self.user_portal).completion, 75)
self.assertFalse(self.channel.with_user(self.user_portal).completed)
@users('user_officer')
def test_enroll_to_course(self):
user_portal_partner = self.user_portal.partner_id
self.assertFalse(user_portal_partner.id in self.channel.partner_ids.ids)
# Enroll partner to course
self.slide_channel_invite_wizard = self.env['slide.channel.invite'].create({
'channel_id': self.channel.id,
'partner_ids': [(6, 0, [self.user_portal.partner_id.id])],
'enroll_mode': True,
})
self.slide_channel_invite_wizard.action_invite()
# The partner should be in the attendees as 'joined'
user_portal_channel_partner = self.channel.channel_partner_all_ids.filtered(lambda p: p.partner_id.id == user_portal_partner.id)
self.assertTrue(user_portal_channel_partner)
self.assertFalse(self.channel.with_user(self.user_portal).is_member_invited)
self.assertTrue(user_portal_channel_partner.id in self.channel.channel_partner_ids.ids)
self.assertTrue(self.channel.with_user(self.user_portal).is_member)
self.assertEqual(user_portal_channel_partner.member_status, 'joined')
# Subscribe enrolled attendees to the chatter
self.assertTrue(user_portal_partner.id in self.channel.message_partner_ids.ids)
@users('user_officer')
def test_invite_to_course(self):
user_portal_partner = self.user_portal.partner_id
self.assertFalse(user_portal_partner.id in self.channel.partner_ids.ids)
# Invite partner to course
self.slide_channel_invite_wizard = self.env['slide.channel.invite'].create({
'channel_id': self.channel.id,
'partner_ids': [(6, 0, [self.user_portal.partner_id.id])],
'send_email': True,
})
self.slide_channel_invite_wizard.action_invite()
# The partner should be in the attendees as 'invited'
user_portal_channel_partner = self.channel.channel_partner_all_ids.filtered(lambda p: p.partner_id.id == user_portal_partner.id)
self.assertTrue(user_portal_channel_partner)
self.assertTrue(self.channel.with_user(self.user_portal).is_member_invited)
self.assertFalse(user_portal_channel_partner.id in self.channel.channel_partner_ids.ids)
self.assertFalse(self.channel.with_user(self.user_portal).is_member)
self.assertEqual(user_portal_channel_partner.member_status, 'invited')
# Do not subscribe invited members to the chatter
self.assertFalse(user_portal_partner.id in self.channel.message_partner_ids.ids)
@users('user_officer')
def test_invite_archived_attendees_to_course(self):
# Make user_portal have ongoing progress in the course
user_portal_partner = self.user_portal.partner_id
user_portal_channel_partner = self.env['slide.channel.partner'].create({
'channel_id': self.channel.id,
'partner_id': user_portal_partner.id,
})
self.slide.with_user(self.user_portal).sudo().action_mark_completed()
user_portal_channel_partner.action_archive()
self.assertEqual(user_portal_channel_partner.member_status, 'ongoing')
# Invite archived ongoing partner to course
self.slide_channel_invite_wizard = self.env['slide.channel.invite'].create({
'channel_id': self.channel.id,
'partner_ids': [(6, 0, [self.user_portal.partner_id.id])],
'send_email': True,
})
self.slide_channel_invite_wizard.action_invite()
# The partner should be reactivated in the attendees as 'invited'
self.assertTrue(user_portal_channel_partner.active)
self.assertTrue(user_portal_channel_partner.completion > 0)
self.assertEqual(user_portal_channel_partner.member_status, 'invited')
# Archive then enroll the attendee
user_portal_channel_partner.action_archive()
self.slide_channel_invite_wizard.enroll_mode = True
self.slide_channel_invite_wizard.flush_recordset()
self.slide_channel_invite_wizard.action_invite()
# The partner should be reactivated in the attendees as 'ongoing'
self.assertTrue(user_portal_channel_partner.active)
self.assertTrue(user_portal_channel_partner.completion > 0)
self.assertEqual(user_portal_channel_partner.member_status, 'ongoing')
@users('user_officer')
def test_join_invite_enroll_channel(self):
self.channel.enroll = 'invite'
user_portal_partner = self.user_portal.partner_id
# Uninvited partner cannot join the course
self.channel.with_user(self.user_portal)._action_add_members(user_portal_partner)
self.assertFalse(user_portal_partner.id in self.channel.partner_ids.ids)
user_portal_channel_partner = self.env['slide.channel.partner'].create({
'channel_id': self.channel.id,
'partner_id': user_portal_partner.id,
'member_status': 'invited'
})
self.assertTrue(user_portal_partner in self.channel.channel_partner_all_ids.partner_id)
self.assertFalse(user_portal_partner.id in self.channel.partner_ids.ids)
# Invited partner can join the course and enroll itself. Sudo is used in controller if invited.
self.assertTrue(self.channel.with_user(self.user_portal).is_member_invited)
self.channel.with_user(self.user_portal).sudo()._action_add_members(user_portal_partner)
self.assertEqual(user_portal_channel_partner.member_status, 'joined')
self.assertTrue(user_portal_partner.id in self.channel.partner_ids.ids)
self.assertTrue(self.user_portal.partner_id.id in self.channel.message_partner_ids.ids)
@users('user_officer')
def test_member_default_create(self):
slide_channel_partner = self.env['slide.channel.partner'].create({
'channel_id': self.channel.id,
'partner_id': self.user_portal.partner_id.id
})
# By default, partner is enrolled
self.assertFalse(self.channel.with_user(self.user_portal).is_member_invited)
self.assertTrue(self.channel.with_user(self.user_portal).is_member)
self.assertEqual(slide_channel_partner.member_status, 'joined')
@users('user_officer')
def test_partners_and_search_on_slide_channel(self):
''' Check that partner_ids contains (only) active enrolled partners '''
invited_cp, joined_cp = self.env['slide.channel.partner'].create([{
'channel_id': self.channel.id,
'partner_id': self.user_portal.partner_id.id,
'member_status': 'invited'
}, {
'channel_id': self.channel.id,
'partner_id': self.user_emp.partner_id.id,
'member_status': 'joined'
}])
# Search partner_ids on model
invited_cp_channel_ids = self.env['slide.channel'].search([('partner_ids', '=', invited_cp.partner_id.id)])
self.assertFalse(self.channel in invited_cp_channel_ids)
joined_cp_channel_ids = self.env['slide.channel'].search([('partner_ids', '=', joined_cp.partner_id.id)])
self.assertTrue(self.channel in joined_cp_channel_ids)
partner_ids = self.channel.partner_ids
self.assertFalse(invited_cp.partner_id in partner_ids)
self.assertTrue(joined_cp.partner_id in partner_ids)
partner_ids = self.channel.partner_ids
self.assertFalse(invited_cp.partner_id in partner_ids)
self.assertTrue(joined_cp.partner_id in partner_ids)
invited_cp.action_archive()
joined_cp.action_archive()
partner_ids = self.channel.partner_ids
self.assertFalse(invited_cp.partner_id in partner_ids)
self.assertFalse(joined_cp.partner_id in partner_ids)
invited_cp_channel_ids = self.env['slide.channel'].search([('partner_ids', '=', invited_cp.partner_id.id)])
self.assertFalse(self.channel in invited_cp_channel_ids)
joined_cp_channel_ids = self.env['slide.channel'].search([('partner_ids', '=', joined_cp.partner_id.id)])
self.assertFalse(self.channel in joined_cp_channel_ids)
def test_copy_partner_not_course_member(self):
""" To check members of the channel after duplication of contact """
# Adding member
self.channel._action_add_members(self.customer)
self.channel.invalidate_recordset()
# Member count before copy of contact
member_before = self.env['slide.channel.partner'].search_count([])
# Duplicating the contact
self.customer.copy()
# Member count after copy of contact
member_after = self.env['slide.channel.partner'].search_count([])
self.assertEqual(member_before, member_after, "Duplicating the contact should not create a new member")
@tagged('-at_install', 'post_install')
class TestAttendeeCase(HttpCaseWithUserPortal):
def setUp(self):
super(TestAttendeeCase, self).setUp()
self.user_admin = self.env.ref('base.user_admin')
self.user_emp = mail_new_test_user(
self.env,
email='employee@example.com',
groups='base.group_user',
login='user_emp',
name='Eglantine Employee',
notification_type='email',
)
self.channel = self.env['slide.channel'].with_user(self.user_admin).create({
'name': 'All about attendee status - Attendees only',
'channel_type': 'training',
'enroll': 'public',
'visibility': 'public',
'is_published': True,
})
self.slide = self.env['slide.slide'].with_user(self.user_admin).create({
'name': 'How to understand membership',
'channel_id': self.channel.id,
'slide_type': 'article',
'is_published': True,
'completion_time': 2.0,
'sequence': 1,
})
self.partner_no_user = self.env['res.partner'].create({
'country_id': self.env.ref('base.be').id,
'email': 'partner_no_user@example.com',
'name': 'Partner Without User',
})
# Enrolled by default
self.channel_partner_emp, self.channel_partner_no_user = self.env['slide.channel.partner'].create([{
'channel_id': self.channel.id,
'partner_id': self.user_emp.partner_id.id
}, {
'channel_id': self.channel.id,
'partner_id': self.partner_no_user.id
}])
def test_direct_enroll_link_redirection(self):
''' Check that the /invite route redirects properly when enrolled user clicks their invitation link.'''
invite_url_emp = self.channel_partner_emp.invitation_link
invite_url_no_user = self.channel_partner_no_user.invitation_link
# No user logged. Partner has a user. Redirects to login.
res = self.url_open(invite_url_emp)
self.assertEqual(res.status_code, 200)
self.assertTrue('/login' in res.url, "Should redirect to login page if not logged in.")
self.assertTrue('auth_login=user_emp' in res.url, "The login should correspond to the invited partner.")
self.assertTrue(f'redirect=/slides/{self.channel.id}' in res.url, "Login should redirect to the course.")
# No user logged. Partner has no user. Redirects to a prepared signup. Decode is used because of signup prepare.
res = self.url_open(invite_url_no_user)
decoded_url = url_decode(res.url)
self.assertEqual(res.status_code, 200)
self.assertTrue('/signup' in res.url, "Should redirect to signup page if not logged in and without user.")
self.assertEqual(self.partner_no_user.signup_token, decoded_url['token'], "Signup should correspond to the invited partner.")
self.assertEqual(f'/slides/{self.channel.id}', decoded_url['redirect'], "Signup should redirect to the course.")
# Logged user is an attendee of the course
self.authenticate("user_emp", "user_emp")
res = self.url_open(invite_url_emp)
self.assertEqual(res.status_code, 200)
self.assertTrue(f'slides/{slug(self.channel)}' in res.url, "Should redirect the logged attendee to the course page")
# Logged user is not an attendee of the course, and has no rights to see it.
self.channel_partner_emp.sudo().unlink()
self.channel.visibility = 'members'
res = self.url_open(invite_url_emp)
self.assertEqual(res.status_code, 200)
self.assertFalse(f'slides/{slug(self.channel)}' in res.url, "Should not redirect the logged attendee to the course page")
def test_direct_invite_link_members_visibility_as_archived(self):
''' Check that archived attendees are not given access to the course with the link, whatever their status.'''
self.channel.visibility = 'members'
self.channel_partner_emp.action_archive()
invite_url_emp = self.channel_partner_emp.invitation_link
# No user logged, 'joined' and archived
res = self.url_open(invite_url_emp)
self.assertEqual(res.status_code, 200)
self.assertTrue('/slides?invite_error=expired' in res.url, "Archived 'joined' attendees cannot access 'members only' courses")
# No user logged, 'invited' and archived
self.channel_partner_emp.member_status = 'invited'
self.channel_partner_emp.last_invitation_date = fields.Datetime.now()
res = self.url_open(invite_url_emp)
self.assertEqual(res.status_code, 200)
self.assertTrue('/slides?invite_error=expired' in res.url, "Archived 'invited' attendees cannot access 'members only' courses")
def test_direct_invite_link_public_visibility(self):
''' Check that 'invited' attendees will be redirected to the course with public visibility'''
self.channel_partner_emp.member_status = 'invited'
self.channel_partner_emp.last_invitation_date = fields.Datetime.now()
invite_url_emp = self.channel_partner_emp.invitation_link
# No user logged.
res = self.url_open(invite_url_emp)
self.assertEqual(res.status_code, 200)
self.assertTrue(f'/slides/{self.channel.id}' in res.url, "Invited partners should always see the public course page")
def test_direct_invite_link_not_public_visibility(self):
''' Check that 'invited' attendees are redirected to courses with 'members' and 'connected' visibilities.'''
self.channel_partner_emp.member_status = 'invited'
self.channel_partner_emp.last_invitation_date = fields.Datetime.now()
invite_url_emp = self.channel_partner_emp.invitation_link
# No user logged, but access granted via parameters in url.
self.channel.visibility = 'connected'
res = self.url_open(invite_url_emp)
self.assertEqual(res.status_code, 200)
self.assertTrue(f'/slides/{self.channel.id}' in res.url, "Partners being invited to the course can access the course page")
self.channel.visibility = 'members'
res = self.url_open(invite_url_emp)
self.assertEqual(res.status_code, 200)
self.assertTrue(f'/slides/{self.channel.id}' in res.url, "Partners being invited to the course can access the course page")
# Courses must still be published to access.
self.channel.sudo().is_published = False
res = self.url_open(invite_url_emp)
self.assertEqual(res.status_code, 200)
self.assertTrue('/slides?invite_error=no_rights' in res.url, "Invited partners cannot access non published courses")
# If removed from invited attendees, the link is not valid anymore.
self.channel.sudo().is_published = True
self.channel_partner_emp.sudo().unlink()
res = self.url_open(invite_url_emp)
self.assertEqual(res.status_code, 200)
self.assertTrue('/slides?invite_error=expired' in res.url, "Using an expired link should redirect to the main /slides page")
def test_generic_invite_link_members_visiblity_as_archived_connected(self):
''' Check that connected archived attendees are not given access to 'members' courses, whatever their status.'''
self.channel_partner_emp.action_archive()
self.channel.visibility = 'members'
# Connected, 'invited' and archived
self.authenticate("user_emp", "user_emp")
res = self.url_open(f'/slides/{self.channel.id}')
self.assertEqual(res.status_code, 200)
self.assertTrue('/slides?invite_error=no_rights' in res.url, "Archived 'invited' attendees cannot access 'members only' courses")
# Connected, 'joined' and archived
self.channel_partner_emp.member_status = 'joined'
res = self.url_open(f'/slides/{self.channel.id}')
self.assertEqual(res.status_code, 200)
self.assertTrue('/slides?invite_error=no_rights' in res.url, "Archived 'joined' attendees cannot access 'members only' courses")
def test_generic_invite_link_public_visibility(self):
''' Check that generic invite link for public course is accessible, even if not logged.'''
invite_url = f"/slides/{self.channel.id}"
res = self.url_open(invite_url)
self.assertEqual(res.status_code, 200)
self.assertTrue(invite_url in res.url, "Public course should be accessible from its invitation link.")
def test_generic_invite_link_not_public_visibility(self):
''' Check that generic route properly the (not) logged user for courses with 'members' and 'connected' visibilities.'''
invite_url = f"/slides/{self.channel.id}"
# No user logged
self.channel.visibility = 'connected'
res = self.url_open(invite_url)
self.assertEqual(res.status_code, 200)
self.assertTrue('/slides?invite_error=no_rights' in res.url, "The public user has no access to connected-only courses.")
# No user logged
self.channel.visibility = 'members'
res = self.url_open(invite_url)
self.assertEqual(res.status_code, 200)
self.assertTrue('/slides?invite_error=no_rights' in res.url, "The public user has no access to members-only courses.")
# User logged but not invited nor enrolled
self.authenticate("portal", "portal")
res = self.url_open(invite_url)
self.assertEqual(res.status_code, 200)
self.assertTrue('/slides?invite_error=no_rights' in res.url, "An external user has no access to members-only courses.")
# Logged user now has a pending invitation to the course
self.env['slide.channel.partner'].create({
'channel_id': self.channel.id,
'partner_id': self.user_portal.partner_id.id,
'member_status': 'invited'
})
# User logged and invited
res = self.url_open(invite_url)
self.assertEqual(res.status_code, 200)
self.assertTrue(invite_url in res.url, "Invited partner should be allowed the access to the course page")
def test_invite_route_errors_handling(self):
''' Check that the /invite route redirects properly when an error is encountered, and current user has no rights to the course.'''
invite_url_emp = self.channel_partner_emp.invitation_link
invite_url_no_user = self.channel_partner_no_user.invitation_link
self.channel.visibility = 'members'
# No such channel
invite_url = "/slides/-1"
res = self.url_open(invite_url)
self.assertEqual(res.status_code, 200)
self.assertTrue('/slides?invite_error=no_channel' in res.url, "This channel does not exist. Redirect to the main /slides page.")
# Hash is wrong
invite_url_false_hash = invite_url_emp + 'abc'
res = self.url_open(invite_url_false_hash)
self.assertEqual(res.status_code, 200)
self.assertTrue('/slides?invite_error=hash_fail' in res.url, "A wrong hash should redirect to the main /slides page")
# Link is for another user
self.authenticate("user_emp", "user_emp")
res = self.url_open(invite_url_no_user)
self.assertEqual(res.status_code, 200)
self.assertTrue('/slides?invite_error=partner_fail' in res.url, "Using an other user's invitation link should redirect to the course page")
# Expired Link. Redirects to the main slides page.
self.channel_partner_emp.sudo().unlink()
res = self.url_open(invite_url_emp)
self.assertEqual(res.status_code, 200)
self.assertTrue('/slides?invite_error=expired' in res.url, "Using an expired link should redirect to the main /slides page for non public courses")
def test_members_invitation_expiration(self):
''' Check invitations are expired after 3 months, and that garbage collector remove appropriate records.'''
# Let user_emp be completed
self.slide.with_user(self.user_emp).action_mark_completed()
self.assertEqual(self.channel_partner_emp.member_status, 'completed')
self.assertTrue(self.channel_partner_emp.completion > 0)
# Logged user_emp has been reinvited more than three months ago and link should be expired
self.channel_partner_emp.write({
'member_status': 'invited',
'last_invitation_date': fields.Datetime.subtract(fields.Datetime.now(), months=3, days=5)
})
self.channel.visibility = 'members'
res = self.url_open(self.channel_partner_emp.invitation_link)
self.assertEqual(res.status_code, 200)
self.assertTrue('/slides?invite_error=expired' in res.url, "Using an expired link should redirect to the main /slides page")
# Let user_portal be invited, with completion = 0, outdated
outdated_portal_membership_values = {
'channel_id': self.channel.id,
'partner_id': self.user_portal.partner_id.id,
'member_status': 'invited',
'last_invitation_date': fields.Datetime.subtract(fields.Datetime.now(), months=3, days=5)
}
channel_partner_portal = self.env['slide.channel.partner'].create(outdated_portal_membership_values)
# Clean expired records with no progress and 'invited'
self.env['slide.channel.partner']._gc_slide_channel_partner()
self.assertTrue(self.channel_partner_emp.exists(), 'Memberships with progress should never be removed.')
self.assertFalse(channel_partner_portal.exists(), 'Expired invitations with no progress should be removed by the GC.')
self.assertTrue(self.channel_partner_no_user.exists(), 'Joined members should never be removed.')
channel_partner_portal = self.env['slide.channel.partner'].create(outdated_portal_membership_values)
channel_partner_portal.action_archive()
self.channel_partner_emp.action_archive()
self.channel_partner_no_user.member_status = 'invited'
# Clean outdated archived records as well, and ones 'invited' with no last_invitation_date
self.env['slide.channel.partner']._gc_slide_channel_partner()
self.assertFalse(channel_partner_portal.exists(), 'Expired invitations should be removed, even if archived')
self.assertTrue(self.channel_partner_emp.exists(), 'Memberships with progress should never be removed, even archived.')
self.assertFalse(self.channel_partner_no_user.exists(), 'No Last Invitation Date is considered as expired for invited members')