Files
edx-platform/common/djangoapps/student/tests/test_models.py
2020-05-01 19:42:15 +05:00

406 lines
18 KiB
Python

import datetime
import hashlib
import ddt
import factory
import pytz
from django.contrib.auth.models import AnonymousUser
from django.core.cache import cache
from django.db.models import signals
from django.db.models.functions import Lower
from django.test import TestCase
from opaque_keys.edx.keys import CourseKey
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from lms.djangoapps.courseware.models import DynamicUpgradeDeadlineConfiguration
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.schedules.models import Schedule
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
from openedx.core.djangolib.testing.utils import skip_unless_lms
from student.models import (
ALLOWEDTOENROLL_TO_ENROLLED,
AccountRecovery,
CourseEnrollment,
CourseEnrollmentAllowed,
ManualEnrollmentAudit,
PendingEmailChange,
PendingNameChange
)
from student.tests.factories import AccountRecoveryFactory, CourseEnrollmentFactory, UserFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
@ddt.ddt
class CourseEnrollmentTests(SharedModuleStoreTestCase):
@classmethod
def setUpClass(cls):
super(CourseEnrollmentTests, cls).setUpClass()
cls.course = CourseFactory()
def setUp(self):
super(CourseEnrollmentTests, self).setUp()
self.user = UserFactory()
self.user_2 = UserFactory()
def test_enrollment_status_hash_cache_key(self):
username = 'test-user'
user = UserFactory(username=username)
expected = 'enrollment_status_hash_' + username
self.assertEqual(CourseEnrollment.enrollment_status_hash_cache_key(user), expected)
def assert_enrollment_status_hash_cached(self, user, expected_value):
self.assertEqual(cache.get(CourseEnrollment.enrollment_status_hash_cache_key(user)), expected_value)
def test_generate_enrollment_status_hash(self):
""" Verify the method returns a hash of a user's current enrollments. """
# Return None for anonymous users
self.assertIsNone(CourseEnrollment.generate_enrollment_status_hash(AnonymousUser()))
# No enrollments
expected = hashlib.md5(self.user.username.encode('utf-8')).hexdigest()
self.assertEqual(CourseEnrollment.generate_enrollment_status_hash(self.user), expected)
self.assert_enrollment_status_hash_cached(self.user, expected)
# No active enrollments
enrollment_mode = 'verified'
course_id = self.course.id # pylint: disable=no-member
enrollment = CourseEnrollmentFactory.create(user=self.user, course_id=course_id, mode=enrollment_mode,
is_active=False)
self.assertEqual(CourseEnrollment.generate_enrollment_status_hash(self.user), expected)
self.assert_enrollment_status_hash_cached(self.user, expected)
# One active enrollment
enrollment.is_active = True
enrollment.save()
expected = '{username}&{course_id}={mode}'.format(
username=self.user.username, course_id=str(course_id).lower(), mode=enrollment_mode.lower()
)
expected = hashlib.md5(expected.encode('utf-8')).hexdigest()
self.assertEqual(CourseEnrollment.generate_enrollment_status_hash(self.user), expected)
self.assert_enrollment_status_hash_cached(self.user, expected)
# Multiple enrollments
CourseEnrollmentFactory.create(user=self.user)
enrollments = CourseEnrollment.enrollments_for_user(self.user).order_by(Lower('course_id'))
hash_elements = [self.user.username]
hash_elements += [
'{course_id}={mode}'.format(course_id=str(enrollment.course_id).lower(), mode=enrollment.mode.lower()) for
enrollment in enrollments]
expected = hashlib.md5('&'.join(hash_elements).encode('utf-8')).hexdigest()
self.assertEqual(CourseEnrollment.generate_enrollment_status_hash(self.user), expected)
self.assert_enrollment_status_hash_cached(self.user, expected)
def test_save_deletes_cached_enrollment_status_hash(self):
""" Verify the method deletes the cached enrollment status hash for the user. """
# There should be no cached value for a new user with no enrollments.
self.assertIsNone(cache.get(CourseEnrollment.enrollment_status_hash_cache_key(self.user)))
# Generating a status hash should cache the generated value.
status_hash = CourseEnrollment.generate_enrollment_status_hash(self.user)
self.assert_enrollment_status_hash_cached(self.user, status_hash)
# Modifying enrollments should delete the cached value.
CourseEnrollmentFactory.create(user=self.user)
self.assertIsNone(cache.get(CourseEnrollment.enrollment_status_hash_cache_key(self.user)))
def test_users_enrolled_in_active_only(self):
"""CourseEnrollment.users_enrolled_in should return only Users with active enrollments when
`include_inactive` has its default value (False)."""
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id, is_active=True)
CourseEnrollmentFactory.create(user=self.user_2, course_id=self.course.id, is_active=False)
active_enrolled_users = list(CourseEnrollment.objects.users_enrolled_in(self.course.id))
self.assertEqual([self.user], active_enrolled_users)
def test_users_enrolled_in_all(self):
"""CourseEnrollment.users_enrolled_in should return active and inactive users when
`include_inactive` is True."""
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id, is_active=True)
CourseEnrollmentFactory.create(user=self.user_2, course_id=self.course.id, is_active=False)
all_enrolled_users = list(
CourseEnrollment.objects.users_enrolled_in(self.course.id, include_inactive=True)
)
self.assertListEqual([self.user, self.user_2], all_enrolled_users)
@skip_unless_lms
# NOTE: We mute the post_save signal to prevent Schedules from being created for new enrollments
@factory.django.mute_signals(signals.post_save)
def test_upgrade_deadline(self):
""" The property should use either the CourseMode or related Schedule to determine the deadline. """
course = CourseFactory(self_paced=True)
course_mode = CourseModeFactory(
course_id=course.id,
mode_slug=CourseMode.VERIFIED,
# This must be in the future to ensure it is returned by downstream code.
expiration_datetime=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=1)
)
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
self.assertEqual(Schedule.objects.all().count(), 0)
self.assertEqual(enrollment.upgrade_deadline, course_mode.expiration_datetime)
@skip_unless_lms
# NOTE: We mute the post_save signal to prevent Schedules from being created for new enrollments
@factory.django.mute_signals(signals.post_save)
def test_upgrade_deadline_with_schedule(self):
""" The property should use either the CourseMode or related Schedule to determine the deadline. """
course = CourseFactory(self_paced=True)
CourseModeFactory(
course_id=course.id,
mode_slug=CourseMode.VERIFIED,
# This must be in the future to ensure it is returned by downstream code.
expiration_datetime=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=30),
)
course_overview = CourseOverview.load_from_module_store(course.id)
enrollment = CourseEnrollmentFactory(
course_id=course.id,
mode=CourseMode.AUDIT,
course=course_overview,
)
# The schedule's upgrade deadline should be used if a schedule exists
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
schedule = ScheduleFactory(enrollment=enrollment)
self.assertEqual(enrollment.upgrade_deadline, schedule.upgrade_deadline)
@skip_unless_lms
@ddt.data(*(set(CourseMode.ALL_MODES) - set(CourseMode.AUDIT_MODES)))
def test_upgrade_deadline_for_non_upgradeable_enrollment(self, mode):
""" The property should return None if an upgrade cannot be upgraded. """
enrollment = CourseEnrollmentFactory(course_id=self.course.id, mode=mode)
self.assertIsNone(enrollment.upgrade_deadline)
@skip_unless_lms
def test_upgrade_deadline_instructor_paced(self):
course = CourseFactory(self_paced=False)
course_upgrade_deadline = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=1)
CourseModeFactory(
course_id=course.id,
mode_slug=CourseMode.VERIFIED,
# This must be in the future to ensure it is returned by downstream code.
expiration_datetime=course_upgrade_deadline
)
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
ScheduleFactory(enrollment=enrollment)
self.assertIsNotNone(enrollment.schedule)
self.assertEqual(enrollment.upgrade_deadline, course_upgrade_deadline)
@skip_unless_lms
def test_upgrade_deadline_with_schedule_and_professional_mode(self):
"""
Deadline should be None for courses with professional mode.
Regression test for EDUCATOR-2419.
"""
course = CourseFactory(self_paced=True)
CourseModeFactory(
course_id=course.id,
mode_slug=CourseMode.PROFESSIONAL,
)
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
ScheduleFactory(enrollment=enrollment)
self.assertIsNotNone(enrollment.schedule)
self.assertIsNone(enrollment.upgrade_deadline)
@skip_unless_lms
# NOTE: We mute the post_save signal to prevent Schedules from being created for new enrollments
@factory.django.mute_signals(signals.post_save)
def test_enrollments_not_deleted(self):
""" Recreating a CourseOverview with an outdated version should not delete the associated enrollment. """
course = CourseFactory(self_paced=True)
CourseModeFactory(
course_id=course.id,
mode_slug=CourseMode.VERIFIED,
# This must be in the future to ensure it is returned by downstream code.
expiration_datetime=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=30),
)
# Create a CourseOverview with an outdated version
course_overview = CourseOverview.load_from_module_store(course.id)
course_overview.version = CourseOverview.VERSION - 1
course_overview.save()
# Create an inactive enrollment with this course overview
enrollment = CourseEnrollmentFactory(
user=self.user,
course_id=course.id,
mode=CourseMode.AUDIT,
course=course_overview,
)
# Re-fetch the CourseOverview record.
# As a side effect, this will recreate the record, and update the version.
course_overview_new = CourseOverview.get_from_id(course.id)
self.assertEqual(course_overview_new.version, CourseOverview.VERSION)
# Ensure that the enrollment record was unchanged during this re-creation
enrollment_refetched = CourseEnrollment.objects.filter(id=enrollment.id)
self.assertTrue(enrollment_refetched.exists())
self.assertEqual(enrollment_refetched.all()[0], enrollment)
class PendingNameChangeTests(SharedModuleStoreTestCase):
"""
Tests the deletion of PendingNameChange records
"""
@classmethod
def setUpClass(cls):
super(PendingNameChangeTests, cls).setUpClass()
cls.user = UserFactory()
cls.user2 = UserFactory()
def setUp(self):
self.name_change, _ = PendingNameChange.objects.get_or_create(
user=self.user,
new_name='New Name PII',
rationale='for testing!'
)
self.assertEqual(1, len(PendingNameChange.objects.all()))
def test_delete_by_user_removes_pending_name_change(self):
record_was_deleted = PendingNameChange.delete_by_user_value(self.user, field='user')
self.assertTrue(record_was_deleted)
self.assertEqual(0, len(PendingNameChange.objects.all()))
def test_delete_by_user_no_effect_for_user_with_no_name_change(self):
record_was_deleted = PendingNameChange.delete_by_user_value(self.user2, field='user')
self.assertFalse(record_was_deleted)
self.assertEqual(1, len(PendingNameChange.objects.all()))
class PendingEmailChangeTests(SharedModuleStoreTestCase):
"""
Tests the deletion of PendingEmailChange records.
"""
@classmethod
def setUpClass(cls):
super(PendingEmailChangeTests, cls).setUpClass()
cls.user = UserFactory()
cls.user2 = UserFactory()
def setUp(self):
self.email_change, _ = PendingEmailChange.objects.get_or_create(
user=self.user,
new_email='new@example.com',
activation_key='a' * 32
)
def test_delete_by_user_removes_pending_email_change(self):
record_was_deleted = PendingEmailChange.delete_by_user_value(self.user, field='user')
self.assertTrue(record_was_deleted)
self.assertEqual(0, len(PendingEmailChange.objects.all()))
def test_delete_by_user_no_effect_for_user_with_no_email_change(self):
record_was_deleted = PendingEmailChange.delete_by_user_value(self.user2, field='user')
self.assertFalse(record_was_deleted)
self.assertEqual(1, len(PendingEmailChange.objects.all()))
class TestCourseEnrollmentAllowed(TestCase):
def setUp(self):
super(TestCourseEnrollmentAllowed, self).setUp()
self.email = 'learner@example.com'
self.course_key = CourseKey.from_string("course-v1:edX+DemoX+Demo_Course")
self.user = UserFactory.create()
self.allowed_enrollment = CourseEnrollmentAllowed.objects.create(
email=self.email,
course_id=self.course_key,
user=self.user
)
def test_retiring_user_deletes_record(self):
is_successful = CourseEnrollmentAllowed.delete_by_user_value(
value=self.email,
field='email'
)
self.assertTrue(is_successful)
user_search_results = CourseEnrollmentAllowed.objects.filter(
email=self.email
)
self.assertFalse(user_search_results)
def test_retiring_nonexistent_user_doesnt_modify_records(self):
is_successful = CourseEnrollmentAllowed.delete_by_user_value(
value='nonexistentlearner@example.com',
field='email'
)
self.assertFalse(is_successful)
user_search_results = CourseEnrollmentAllowed.objects.filter(
email=self.email
)
self.assertTrue(user_search_results.exists())
class TestManualEnrollmentAudit(SharedModuleStoreTestCase):
"""
Tests for the ManualEnrollmentAudit model.
"""
@classmethod
def setUpClass(cls):
super(TestManualEnrollmentAudit, cls).setUpClass()
cls.course = CourseFactory()
cls.other_course = CourseFactory()
cls.user = UserFactory()
cls.instructor = UserFactory(username='staff', is_staff=True)
def test_retirement(self):
"""
Tests that calling the retirement method for a specific enrollment retires
the enrolled_email and reason columns of each row associated with that
enrollment.
"""
enrollment = CourseEnrollment.enroll(self.user, self.course.id)
other_enrollment = CourseEnrollment.enroll(self.user, self.other_course.id)
ManualEnrollmentAudit.create_manual_enrollment_audit(
self.instructor, self.user.email, ALLOWEDTOENROLL_TO_ENROLLED,
'manually enrolling unenrolled user', enrollment
)
ManualEnrollmentAudit.create_manual_enrollment_audit(
self.instructor, self.user.email, ALLOWEDTOENROLL_TO_ENROLLED,
'manually enrolling unenrolled user again', enrollment
)
ManualEnrollmentAudit.create_manual_enrollment_audit(
self.instructor, self.user.email, ALLOWEDTOENROLL_TO_ENROLLED,
'manually enrolling unenrolled user', other_enrollment
)
ManualEnrollmentAudit.create_manual_enrollment_audit(
self.instructor, self.user.email, ALLOWEDTOENROLL_TO_ENROLLED,
'manually enrolling unenrolled user again', other_enrollment
)
self.assertTrue(ManualEnrollmentAudit.objects.filter(enrollment=enrollment).exists())
# retire the ManualEnrollmentAudit objects associated with the above enrollments
ManualEnrollmentAudit.retire_manual_enrollments(user=self.user, retired_email="xxx")
self.assertTrue(ManualEnrollmentAudit.objects.filter(enrollment=enrollment).exists())
self.assertFalse(ManualEnrollmentAudit.objects.filter(enrollment=enrollment).exclude(
enrolled_email="xxx"
))
self.assertFalse(ManualEnrollmentAudit.objects.filter(enrollment=enrollment).exclude(
reason=""
))
class TestAccountRecovery(TestCase):
"""
Tests for the AccountRecovery Model
"""
def test_retire_recovery_email(self):
"""
Assert that Account Record for a given user is deleted when `retire_recovery_email` is called
"""
# Create user and associated recovery email record
user = UserFactory()
AccountRecoveryFactory(user=user)
assert len(AccountRecovery.objects.filter(user_id=user.id)) == 1
# Retire recovery email
AccountRecovery.retire_recovery_email(user_id=user.id)
# Assert that there is no longer an AccountRecovery record for this user
assert len(AccountRecovery.objects.filter(user_id=user.id)) == 0