Feat: Add Learner pathway progress update signal (#30547)
* feat: Add Learner pathway progress update signal
This commit is contained in:
@@ -57,9 +57,23 @@ class AllowlistGeneratedCertificatesTest(ModuleStoreTestCase):
|
||||
Tests for allowlisted student auto-certificate generation
|
||||
"""
|
||||
|
||||
def setup_patch(self, function_name, return_value):
|
||||
"""
|
||||
Patch a function with a given return value, and return the mock
|
||||
"""
|
||||
magic_mock = mock.MagicMock(return_value=return_value)
|
||||
new_patch = mock.patch(function_name, new=magic_mock)
|
||||
new_patch.start()
|
||||
self.addCleanup(new_patch.stop)
|
||||
return magic_mock
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = UserFactory.create()
|
||||
self.mock_pathways_with_course = self.setup_patch(
|
||||
'learner_pathway_progress.signals.get_learner_pathways_associated_with_course',
|
||||
None,
|
||||
)
|
||||
# Instructor paced course
|
||||
self.ip_course = CourseFactory.create(self_paced=False)
|
||||
CourseEnrollmentFactory(
|
||||
@@ -105,11 +119,29 @@ class PassingGradeCertsTest(ModuleStoreTestCase):
|
||||
Tests for certificate generation task firing on passing grade receipt
|
||||
"""
|
||||
|
||||
def setup_patch(self, function_name, return_value):
|
||||
"""
|
||||
Patch a function with a given return value, and return the mock
|
||||
"""
|
||||
magic_mock = mock.MagicMock(return_value=return_value)
|
||||
new_patch = mock.patch(function_name, new=magic_mock)
|
||||
new_patch.start()
|
||||
self.addCleanup(new_patch.stop)
|
||||
return magic_mock
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.course = CourseFactory.create(
|
||||
self_paced=True,
|
||||
)
|
||||
self.mock_pathways_with_course = self.setup_patch(
|
||||
'learner_pathway_progress.signals.get_learner_pathways_associated_with_course',
|
||||
None,
|
||||
)
|
||||
self.signal_mock_course_passed_pathway_progress = self.setup_patch(
|
||||
'learner_pathway_progress.signals.update_learner_pathway_progress',
|
||||
None,
|
||||
)
|
||||
self.course_key = self.course.id
|
||||
self.user = UserFactory.create()
|
||||
self.enrollment = CourseEnrollmentFactory(
|
||||
@@ -222,12 +254,26 @@ class FailingGradeCertsTest(ModuleStoreTestCase):
|
||||
status
|
||||
"""
|
||||
|
||||
def setup_patch(self, function_name, return_value):
|
||||
"""
|
||||
Patch a function with a given return value, and return the mock
|
||||
"""
|
||||
magic_mock = mock.MagicMock(return_value=return_value)
|
||||
new_patch = mock.patch(function_name, new=magic_mock)
|
||||
new_patch.start()
|
||||
self.addCleanup(new_patch.stop)
|
||||
return magic_mock
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.course = CourseFactory.create(
|
||||
self_paced=True,
|
||||
)
|
||||
self.user = UserFactory.create()
|
||||
self.mock_pathways_with_course = self.setup_patch(
|
||||
'learner_pathway_progress.signals.get_learner_pathways_associated_with_course',
|
||||
None,
|
||||
)
|
||||
self.enrollment = CourseEnrollmentFactory(
|
||||
user=self.user,
|
||||
course_id=self.course.id,
|
||||
@@ -304,10 +350,28 @@ class LearnerIdVerificationTest(ModuleStoreTestCase):
|
||||
Tests for certificate generation task firing on learner id verification
|
||||
"""
|
||||
|
||||
def setup_patch(self, function_name, return_value):
|
||||
"""
|
||||
Patch a function with a given return value, and return the mock
|
||||
"""
|
||||
magic_mock = mock.MagicMock(return_value=return_value)
|
||||
new_patch = mock.patch(function_name, new=magic_mock)
|
||||
new_patch.start()
|
||||
self.addCleanup(new_patch.stop)
|
||||
return magic_mock
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.course_one = CourseFactory.create(self_paced=True)
|
||||
self.user_one = UserFactory.create()
|
||||
self.mock_pathways_with_course = self.setup_patch(
|
||||
'learner_pathway_progress.signals.get_learner_pathways_associated_with_course',
|
||||
None,
|
||||
)
|
||||
self.signal_mock_course_passed_pathway_progress = self.setup_patch(
|
||||
'learner_pathway_progress.signals.update_learner_pathway_progress',
|
||||
None,
|
||||
)
|
||||
self.enrollment_one = CourseEnrollmentFactory(
|
||||
user=self.user_one,
|
||||
course_id=self.course_one.id,
|
||||
@@ -386,12 +450,31 @@ class EnrollmentModeChangeCertsTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for certificate generation task firing when the user's enrollment mode changes
|
||||
"""
|
||||
|
||||
def setup_patch(self, function_name, return_value):
|
||||
"""
|
||||
Patch a function with a given return value, and return the mock
|
||||
"""
|
||||
magic_mock = mock.MagicMock(return_value=return_value)
|
||||
new_patch = mock.patch(function_name, new=magic_mock)
|
||||
new_patch.start()
|
||||
self.addCleanup(new_patch.stop)
|
||||
return magic_mock
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = UserFactory.create()
|
||||
self.verified_course = CourseFactory.create(
|
||||
self_paced=True,
|
||||
)
|
||||
self.mock_update_pathway_progress = self.setup_patch(
|
||||
'learner_pathway_progress.signals.update_learner_pathway_progress',
|
||||
None,
|
||||
)
|
||||
self.signal_mock_course_passed_pathway_progress = self.setup_patch(
|
||||
'learner_pathway_progress.signals.listen_for_course_grade_upgrade_in_learner_pathway',
|
||||
None,
|
||||
)
|
||||
self.verified_course_key = self.verified_course.id # pylint: disable=no-member
|
||||
self.verified_enrollment = CourseEnrollmentFactory(
|
||||
user=self.user,
|
||||
|
||||
@@ -37,24 +37,31 @@ class TestEnterpriseCourseEnrollmentCreateOldOrder(TestCase):
|
||||
"""
|
||||
Creates `count` test enrollments plus 1 invalid and 1 Audit enrollment
|
||||
"""
|
||||
for _ in range(count):
|
||||
with patch(
|
||||
'learner_pathway_progress.signals.get_learner_pathways_associated_with_course',
|
||||
return_value=None
|
||||
):
|
||||
for _ in range(count):
|
||||
user = UserFactory()
|
||||
course_enrollment = CourseEnrollmentFactory(mode=CourseMode.VERIFIED, user=user)
|
||||
course = course_enrollment.course
|
||||
enterprise_customer_user = EnterpriseCustomerUserFactory(user_id=user.id)
|
||||
EnterpriseCourseEnrollmentFactory(
|
||||
enterprise_customer_user=enterprise_customer_user,
|
||||
course_id=course.id
|
||||
)
|
||||
|
||||
# creating audit enrollment
|
||||
user = UserFactory()
|
||||
course_enrollment = CourseEnrollmentFactory(mode=CourseMode.VERIFIED, user=user)
|
||||
course_enrollment = CourseEnrollmentFactory(mode=CourseMode.AUDIT, user=user)
|
||||
course = course_enrollment.course
|
||||
enterprise_customer_user = EnterpriseCustomerUserFactory(user_id=user.id)
|
||||
EnterpriseCourseEnrollmentFactory(enterprise_customer_user=enterprise_customer_user, course_id=course.id)
|
||||
|
||||
# creating audit enrollment
|
||||
user = UserFactory()
|
||||
course_enrollment = CourseEnrollmentFactory(mode=CourseMode.AUDIT, user=user)
|
||||
course = course_enrollment.course
|
||||
enterprise_customer_user = EnterpriseCustomerUserFactory(user_id=user.id)
|
||||
EnterpriseCourseEnrollmentFactory(enterprise_customer_user=enterprise_customer_user, course_id=course.id)
|
||||
|
||||
# creating invalid enrollment (with no CourseEnrollment)
|
||||
user = UserFactory()
|
||||
enterprise_customer_user = EnterpriseCustomerUserFactory(user_id=user.id)
|
||||
EnterpriseCourseEnrollmentFactory(enterprise_customer_user=enterprise_customer_user, course_id=course.id)
|
||||
# creating invalid enrollment (with no CourseEnrollment)
|
||||
user = UserFactory()
|
||||
enterprise_customer_user = EnterpriseCustomerUserFactory(user_id=user.id)
|
||||
EnterpriseCourseEnrollmentFactory(enterprise_customer_user=enterprise_customer_user, course_id=course.id)
|
||||
|
||||
@patch('lms.djangoapps.commerce.management.commands.create_orders_for_old_enterprise_course_enrollment'
|
||||
'.Command._create_manual_enrollment_orders')
|
||||
|
||||
@@ -9,7 +9,7 @@ import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from textwrap import dedent
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import ddt
|
||||
import pytz
|
||||
@@ -345,6 +345,27 @@ class TestCourseGrader(TestSubmittingProblems):
|
||||
"""
|
||||
Suite of tests for the course grader.
|
||||
"""
|
||||
def setup_patch(self, function_name, return_value):
|
||||
"""
|
||||
Patch a function with a given return value, and return the mock
|
||||
"""
|
||||
magic_mock = MagicMock(return_value=return_value)
|
||||
new_patch = patch(function_name, new=magic_mock)
|
||||
new_patch.start()
|
||||
self.addCleanup(new_patch.stop)
|
||||
return magic_mock
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.mock_pathways_with_course = self.setup_patch(
|
||||
'learner_pathway_progress.signals.get_learner_pathways_associated_with_course',
|
||||
None,
|
||||
)
|
||||
self.signal_mock_course_passed_pathway_progress = self.setup_patch(
|
||||
'learner_pathway_progress.signals.update_learner_pathway_progress',
|
||||
None,
|
||||
)
|
||||
|
||||
# Tell Django to clean out all databases, not just default
|
||||
databases = set(connections)
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@ from openedx.core.djangoapps.signals.signals import (
|
||||
COURSE_GRADE_NOW_PASSED
|
||||
)
|
||||
|
||||
from lms.djangoapps.grades.signals.signals import (
|
||||
COURSE_GRADE_PASSED_UPDATE_IN_LEARNER_PATHWAY,
|
||||
)
|
||||
|
||||
from .config import assume_zero_if_absent, should_persist_grades
|
||||
from .course_data import CourseData
|
||||
from .course_grade import CourseGrade, ZeroCourseGrade
|
||||
@@ -171,6 +175,7 @@ class CourseGradeFactory:
|
||||
COURSE_GRADE_CHANGED signal to listeners and
|
||||
COURSE_GRADE_NOW_PASSED if learner has passed course or
|
||||
COURSE_GRADE_NOW_FAILED if learner is now failing course
|
||||
COURSE_GRADE_PASSED_UPDATE_IN_LEARNER_PATHWAY if learner has passed course
|
||||
"""
|
||||
should_persist = should_persist_grades(course_data.course_key)
|
||||
if should_persist and force_update_subsections:
|
||||
@@ -211,6 +216,11 @@ class CourseGradeFactory:
|
||||
user=user,
|
||||
course_id=course_data.course_key,
|
||||
)
|
||||
COURSE_GRADE_PASSED_UPDATE_IN_LEARNER_PATHWAY.send(
|
||||
sender=CourseGradeFactory,
|
||||
user_id=user.id,
|
||||
course_id=course_data.course_key,
|
||||
)
|
||||
else:
|
||||
COURSE_GRADE_NOW_FAILED.send(
|
||||
sender=CourseGradeFactory,
|
||||
|
||||
@@ -27,7 +27,11 @@ from simple_history.models import HistoricalRecords
|
||||
from lms.djangoapps.courseware.fields import UnsignedBigIntAutoField
|
||||
from lms.djangoapps.grades import events # lint-amnesty, pylint: disable=unused-import
|
||||
from openedx.core.lib.cache_utils import get_cache
|
||||
from lms.djangoapps.grades.signals.signals import COURSE_GRADE_PASSED_FIRST_TIME
|
||||
from lms.djangoapps.grades.signals.signals import (
|
||||
COURSE_GRADE_PASSED_FIRST_TIME,
|
||||
COURSE_GRADE_PASSED_UPDATE_IN_LEARNER_PATHWAY
|
||||
)
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -650,6 +654,11 @@ class PersistentCourseGrade(TimeStampedModel):
|
||||
course_id=course_id,
|
||||
user_id=user_id
|
||||
)
|
||||
COURSE_GRADE_PASSED_UPDATE_IN_LEARNER_PATHWAY.send(
|
||||
sender=None,
|
||||
user_id=user_id,
|
||||
course_id=course_id,
|
||||
)
|
||||
grade.passed_timestamp = now()
|
||||
grade.save()
|
||||
|
||||
|
||||
@@ -111,3 +111,4 @@ SUBSECTION_OVERRIDE_CHANGED = Signal()
|
||||
# 'user_id', # User object id
|
||||
# ]
|
||||
COURSE_GRADE_PASSED_FIRST_TIME = Signal()
|
||||
COURSE_GRADE_PASSED_UPDATE_IN_LEARNER_PATHWAY = Signal()
|
||||
|
||||
@@ -13,6 +13,7 @@ from common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.certificates.config import AUTO_CERTIFICATE_GENERATION
|
||||
from lms.djangoapps.courseware.access import has_access
|
||||
from lms.djangoapps.grades.config.tests.utils import persistent_grades_feature_flags
|
||||
from lms.djangoapps.grades.signals.signals import COURSE_GRADE_PASSED_UPDATE_IN_LEARNER_PATHWAY
|
||||
from openedx.core.djangoapps.content.block_structure.factory import BlockStructureFactory
|
||||
from openedx.core.djangoapps.signals.signals import COURSE_GRADE_NOW_PASSED
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
|
||||
@@ -95,16 +96,25 @@ class TestCourseGradeFactory(GradeTestBase):
|
||||
course_id=self.course.id,
|
||||
enabled_for_course=False
|
||||
):
|
||||
with override_waffle_switch(AUTO_CERTIFICATE_GENERATION, active=True), mock_get_score(2, 2):
|
||||
COURSE_GRADE_NOW_PASSED.connect(handler)
|
||||
try:
|
||||
CourseGradeFactory().update(self.request.user, self.course)
|
||||
except RecursionError:
|
||||
pytest.fail("The COURSE_GRADE_NOW_PASSED signal fired recursively.")
|
||||
with patch(
|
||||
'lms.djangoapps.grades.signals.signals.COURSE_GRADE_PASSED_UPDATE_IN_LEARNER_PATHWAY.send',
|
||||
return_value=None
|
||||
) as mock_course_passed_pathway:
|
||||
with override_waffle_switch(AUTO_CERTIFICATE_GENERATION, active=True), mock_get_score(2, 2):
|
||||
COURSE_GRADE_PASSED_UPDATE_IN_LEARNER_PATHWAY.connect(handler)
|
||||
COURSE_GRADE_NOW_PASSED.connect(handler)
|
||||
try:
|
||||
CourseGradeFactory().update(self.request.user, self.course)
|
||||
except RecursionError:
|
||||
pytest.fail("The COURSE_GRADE_NOW_PASSED signal fired recursively.")
|
||||
|
||||
mock_course_passed_pathway.assert_called_once()
|
||||
self.mock_process_signal.assert_called_once()
|
||||
COURSE_GRADE_PASSED_UPDATE_IN_LEARNER_PATHWAY.disconnect(handler)
|
||||
COURSE_GRADE_NOW_PASSED.disconnect(handler)
|
||||
|
||||
@patch('lms.djangoapps.grades.signals.signals.COURSE_GRADE_PASSED_UPDATE_IN_LEARNER_PATHWAY.send',
|
||||
Mock(return_value=None))
|
||||
def test_read_and_update(self):
|
||||
grade_factory = CourseGradeFactory()
|
||||
|
||||
@@ -170,6 +180,8 @@ class TestCourseGradeFactory(GradeTestBase):
|
||||
else:
|
||||
assert course_grade is None
|
||||
|
||||
@patch('lms.djangoapps.grades.signals.signals.COURSE_GRADE_PASSED_UPDATE_IN_LEARNER_PATHWAY.send',
|
||||
Mock(return_value=None))
|
||||
def test_read_optimization(self):
|
||||
grade_factory = CourseGradeFactory()
|
||||
with patch('lms.djangoapps.grades.course_data.get_course_blocks') as mocked_course_blocks:
|
||||
@@ -188,6 +200,8 @@ class TestCourseGradeFactory(GradeTestBase):
|
||||
assert not mocked_course_blocks.called
|
||||
# no user-specific transformer calculation
|
||||
|
||||
@patch('lms.djangoapps.grades.signals.signals.COURSE_GRADE_PASSED_UPDATE_IN_LEARNER_PATHWAY.send',
|
||||
Mock(return_value=None))
|
||||
def test_subsection_grade(self):
|
||||
grade_factory = CourseGradeFactory()
|
||||
with mock_get_score(1, 2):
|
||||
|
||||
@@ -8,7 +8,7 @@ from base64 import b64encode
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
from hashlib import sha1
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import ddt
|
||||
import pytest
|
||||
@@ -373,6 +373,20 @@ class PersistentCourseGradesTest(GradesModelTestCase):
|
||||
"letter_grade": "Great job",
|
||||
"passed": True,
|
||||
}
|
||||
self.signal_mock_pathway_progress = self.setup_patch(
|
||||
'lms.djangoapps.grades.signals.signals.COURSE_GRADE_PASSED_UPDATE_IN_LEARNER_PATHWAY.send',
|
||||
None,
|
||||
)
|
||||
|
||||
def setup_patch(self, function_name, return_value):
|
||||
"""
|
||||
Patch a function with a given return value, and return the mock
|
||||
"""
|
||||
mock = MagicMock(return_value=return_value)
|
||||
new_patch = patch(function_name, new=mock)
|
||||
new_patch.start()
|
||||
self.addCleanup(new_patch.stop)
|
||||
return mock
|
||||
|
||||
def test_update(self):
|
||||
created_grade = PersistentCourseGrade.update_or_create(**self.params)
|
||||
@@ -425,12 +439,14 @@ class PersistentCourseGradesTest(GradesModelTestCase):
|
||||
assert grade.letter_grade == ''
|
||||
assert grade.passed_timestamp == passed_timestamp
|
||||
|
||||
@patch('lms.djangoapps.grades.signals.signals.COURSE_GRADE_PASSED_UPDATE_IN_LEARNER_PATHWAY.send')
|
||||
@patch('lms.djangoapps.grades.signals.signals.COURSE_GRADE_PASSED_FIRST_TIME.send')
|
||||
def test_passed_timestamp_is_now(self, mock):
|
||||
def test_passed_timestamp_is_now(self, mock, mock_grade_update_in_learner_pathway):
|
||||
with freeze_time(now()):
|
||||
grade = PersistentCourseGrade.update_or_create(**self.params)
|
||||
assert now() == grade.passed_timestamp
|
||||
self.assertEqual(mock.call_count, 1)
|
||||
self.assertEqual(mock_grade_update_in_learner_pathway.call_count, 1)
|
||||
|
||||
def test_create_and_read_grade(self):
|
||||
created_grade = PersistentCourseGrade.update_or_create(**self.params)
|
||||
|
||||
@@ -285,6 +285,10 @@ class CourseEventsSignalsTest(ModuleStoreTestCase):
|
||||
Configure mocks for all the dependencies of the render method
|
||||
"""
|
||||
super().setUp()
|
||||
self.signal_mock_pathway_progress = self.setup_patch(
|
||||
'lms.djangoapps.grades.signals.signals.COURSE_GRADE_PASSED_UPDATE_IN_LEARNER_PATHWAY.send',
|
||||
None,
|
||||
)
|
||||
self.user_mock = MagicMock()
|
||||
self.user_mock.id = 42
|
||||
self.get_user_mock = self.setup_patch(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""
|
||||
Tests of the instructor dashboard spoc gradebook
|
||||
"""
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from capa.tests.response_xml_factory import StringResponseXMLFactory
|
||||
@@ -21,6 +23,16 @@ class TestGradebook(SharedModuleStoreTestCase):
|
||||
"""
|
||||
grading_policy = None
|
||||
|
||||
def setup_patch(self, function_name, return_value):
|
||||
"""
|
||||
Patch a function with a given return value, and return the mock
|
||||
"""
|
||||
mock = MagicMock(return_value=return_value)
|
||||
new_patch = patch(function_name, new=mock)
|
||||
new_patch.start()
|
||||
self.addCleanup(new_patch.stop)
|
||||
return mock
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
@@ -58,7 +70,10 @@ class TestGradebook(SharedModuleStoreTestCase):
|
||||
instructor = AdminFactory.create()
|
||||
self.client.login(username=instructor.username, password='test')
|
||||
self.users = [UserFactory.create() for _ in range(USER_COUNT)]
|
||||
|
||||
self.signal_mock_pathway_progress = self.setup_patch(
|
||||
'lms.djangoapps.grades.signals.signals.COURSE_GRADE_PASSED_UPDATE_IN_LEARNER_PATHWAY.send',
|
||||
None,
|
||||
)
|
||||
for user in self.users:
|
||||
CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
|
||||
|
||||
|
||||
@@ -338,36 +338,40 @@ class TestInstructorGradeReport(InstructorGradeReportTestCase):
|
||||
audit_user = CourseEnrollment.enroll(UserFactory.create(), course.id)
|
||||
self._verify_cell_data_for_user(audit_user.username, course.id, 'Certificate Eligible', 'N', num_rows=1)
|
||||
grading_policy_hash = GradesTransformer.grading_policy_hash(course)
|
||||
PersistentCourseGrade.update_or_create(
|
||||
user_id=audit_user.user_id,
|
||||
course_id=course.id,
|
||||
passed=False,
|
||||
percent_grade=0.0,
|
||||
grading_policy_hash=grading_policy_hash,
|
||||
)
|
||||
self._verify_cell_data_for_user(audit_user.username, course.id, 'Certificate Eligible', 'N', num_rows=1)
|
||||
PersistentCourseGrade.update_or_create(
|
||||
user_id=audit_user.user_id,
|
||||
course_id=course.id,
|
||||
passed=True,
|
||||
percent_grade=0.8,
|
||||
letter_grade="pass",
|
||||
grading_policy_hash=grading_policy_hash,
|
||||
)
|
||||
# verifies that audit passing learner is not eligible for certificate
|
||||
self._verify_cell_data_for_user(audit_user.username, course.id, 'Certificate Eligible', 'N', num_rows=1)
|
||||
with patch(
|
||||
'learner_pathway_progress.signals.update_learner_pathway_progress',
|
||||
return_value=None
|
||||
):
|
||||
PersistentCourseGrade.update_or_create(
|
||||
user_id=audit_user.user_id,
|
||||
course_id=course.id,
|
||||
passed=False,
|
||||
percent_grade=0.0,
|
||||
grading_policy_hash=grading_policy_hash,
|
||||
)
|
||||
self._verify_cell_data_for_user(audit_user.username, course.id, 'Certificate Eligible', 'N', num_rows=1)
|
||||
PersistentCourseGrade.update_or_create(
|
||||
user_id=audit_user.user_id,
|
||||
course_id=course.id,
|
||||
passed=True,
|
||||
percent_grade=0.8,
|
||||
letter_grade="pass",
|
||||
grading_policy_hash=grading_policy_hash,
|
||||
)
|
||||
# verifies that audit passing learner is not eligible for certificate
|
||||
self._verify_cell_data_for_user(audit_user.username, course.id, 'Certificate Eligible', 'N', num_rows=1)
|
||||
|
||||
verified_user = CourseEnrollment.enroll(UserFactory.create(), course.id, 'verified')
|
||||
PersistentCourseGrade.update_or_create(
|
||||
user_id=verified_user.user_id,
|
||||
course_id=course.id,
|
||||
passed=True,
|
||||
percent_grade=0.8,
|
||||
letter_grade="pass",
|
||||
grading_policy_hash=grading_policy_hash,
|
||||
)
|
||||
# verifies that verified passing learner is eligible for certificate
|
||||
self._verify_cell_data_for_user(verified_user.username, course.id, 'Certificate Eligible', 'Y', num_rows=2)
|
||||
verified_user = CourseEnrollment.enroll(UserFactory.create(), course.id, 'verified')
|
||||
PersistentCourseGrade.update_or_create(
|
||||
user_id=verified_user.user_id,
|
||||
course_id=course.id,
|
||||
passed=True,
|
||||
percent_grade=0.8,
|
||||
letter_grade="pass",
|
||||
grading_policy_hash=grading_policy_hash,
|
||||
)
|
||||
# verifies that verified passing learner is eligible for certificate
|
||||
self._verify_cell_data_for_user(verified_user.username, course.id, 'Certificate Eligible', 'Y', num_rows=2)
|
||||
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.mongo, 4, 47),
|
||||
|
||||
@@ -155,10 +155,14 @@ class TestProgramsView(SharedModuleStoreTestCase, ProgramCacheMixin):
|
||||
course_id=modulestore_course.id,
|
||||
user=cls.user
|
||||
)
|
||||
EnterpriseCourseEnrollmentFactory(
|
||||
course_id=modulestore_course.id,
|
||||
enterprise_customer_user=enterprise_customer_user
|
||||
)
|
||||
with mock.patch(
|
||||
'learner_pathway_progress.signals.get_learner_pathways_associated_with_course',
|
||||
return_value=None
|
||||
):
|
||||
EnterpriseCourseEnrollmentFactory(
|
||||
course_id=modulestore_course.id,
|
||||
enterprise_customer_user=enterprise_customer_user
|
||||
)
|
||||
|
||||
cls.program = ProgramFactory(
|
||||
uuid=cls.program_uuid,
|
||||
|
||||
@@ -343,10 +343,14 @@ class SupportViewEnrollmentsTests(SharedModuleStoreTestCase, SupportViewTestCase
|
||||
enterprise_customer_user = EnterpriseCustomerUserFactory(
|
||||
user_id=self.student.id
|
||||
)
|
||||
enterprise_course_enrollment = EnterpriseCourseEnrollmentFactory(
|
||||
course_id=self.course.id,
|
||||
enterprise_customer_user=enterprise_customer_user
|
||||
)
|
||||
with patch(
|
||||
'learner_pathway_progress.signals.get_learner_pathways_associated_with_course',
|
||||
return_value=None
|
||||
):
|
||||
enterprise_course_enrollment = EnterpriseCourseEnrollmentFactory(
|
||||
course_id=self.course.id,
|
||||
enterprise_customer_user=enterprise_customer_user
|
||||
)
|
||||
data_sharing_consent = DataSharingConsent(
|
||||
course_id=self.course.id,
|
||||
enterprise_customer=enterprise_customer_user.enterprise_customer,
|
||||
|
||||
@@ -1310,6 +1310,10 @@ class TestAccountRetirementPost(RetirementTestCase):
|
||||
|
||||
self.cache_key = UserProfile.country_cache_key_name(self.test_user.id)
|
||||
cache.set(self.cache_key, 'Timor-leste')
|
||||
self.mock_pathways_with_course = self.setup_patch(
|
||||
'learner_pathway_progress.signals.get_learner_pathways_associated_with_course',
|
||||
None,
|
||||
)
|
||||
|
||||
# Enterprise model setup
|
||||
self.course_id = 'course-v1:edX+DemoX.1+2T2017'
|
||||
@@ -1371,6 +1375,16 @@ class TestAccountRetirementPost(RetirementTestCase):
|
||||
self.headers['content_type'] = "application/json"
|
||||
self.url = reverse('accounts_retire')
|
||||
|
||||
def setup_patch(self, function_name, return_value):
|
||||
"""
|
||||
Patch a function with a given return value, and return the mock
|
||||
"""
|
||||
magic_mock = mock.MagicMock(return_value=return_value)
|
||||
new_patch = mock.patch(function_name, new=magic_mock)
|
||||
new_patch.start()
|
||||
self.addCleanup(new_patch.stop)
|
||||
return magic_mock
|
||||
|
||||
def post_and_assert_status(self, data, expected_status=status.HTTP_204_NO_CONTENT):
|
||||
"""
|
||||
Helper function for making a request to the retire subscriptions endpoint, and asserting the status.
|
||||
|
||||
@@ -447,10 +447,14 @@ class TestEnterpriseApi(EnterpriseServiceMockMixin, CacheIsolationTestCase):
|
||||
if not is_enterprise_enabled:
|
||||
assert get_enterprise_course_enrollments(self.user) == []
|
||||
else:
|
||||
ece = EnterpriseCourseEnrollmentFactory(enterprise_customer_user=enterprise_customer_user)
|
||||
enterprise_course_enrollments = get_enterprise_course_enrollments(self.user)
|
||||
assert len(enterprise_course_enrollments) == 1
|
||||
assert enterprise_course_enrollments[0].id == ece.id
|
||||
with mock.patch(
|
||||
'learner_pathway_progress.signals.get_learner_pathways_associated_with_course',
|
||||
return_value=None
|
||||
):
|
||||
ece = EnterpriseCourseEnrollmentFactory(enterprise_customer_user=enterprise_customer_user)
|
||||
enterprise_course_enrollments = get_enterprise_course_enrollments(self.user)
|
||||
assert len(enterprise_course_enrollments) == 1
|
||||
assert enterprise_course_enrollments[0].id == ece.id
|
||||
|
||||
@httpretty.activate
|
||||
@mock.patch('openedx.features.enterprise_support.api.get_enterprise_learner_data_from_db')
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""
|
||||
Test the enterprise support APIs.
|
||||
"""
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.conf import settings
|
||||
from django.test.utils import override_settings
|
||||
|
||||
@@ -33,12 +35,16 @@ class TestEnterpriseContext(EnterpriseServiceMockMixin, CacheIsolationTestCase):
|
||||
super().setUpTestData()
|
||||
|
||||
def test_get_enterprise_event_context(self):
|
||||
course_enrollment = CourseEnrollmentFactory(user=self.user)
|
||||
course = course_enrollment.course
|
||||
enterprise_customer_user = EnterpriseCustomerUserFactory(user_id=self.user.id)
|
||||
EnterpriseCourseEnrollmentFactory(
|
||||
enterprise_customer_user=enterprise_customer_user,
|
||||
course_id=course.id
|
||||
)
|
||||
assert get_enterprise_event_context(course_id=course.id, user_id=self.user.id) == \
|
||||
{'enterprise_uuid': str(enterprise_customer_user.enterprise_customer_id)}
|
||||
with patch(
|
||||
'learner_pathway_progress.signals.get_learner_pathways_associated_with_course',
|
||||
return_value=None
|
||||
):
|
||||
course_enrollment = CourseEnrollmentFactory(user=self.user)
|
||||
course = course_enrollment.course
|
||||
enterprise_customer_user = EnterpriseCustomerUserFactory(user_id=self.user.id)
|
||||
EnterpriseCourseEnrollmentFactory(
|
||||
enterprise_customer_user=enterprise_customer_user,
|
||||
course_id=course.id
|
||||
)
|
||||
assert get_enterprise_event_context(course_id=course.id, user_id=self.user.id) == \
|
||||
{'enterprise_uuid': str(enterprise_customer_user.enterprise_customer_id)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Tests for custom enterprise_support Serializers.
|
||||
"""
|
||||
|
||||
from unittest.mock import patch, MagicMock
|
||||
from uuid import uuid4
|
||||
|
||||
from django.test import TestCase
|
||||
@@ -19,14 +19,29 @@ class EnterpriseCourseEnrollmentSerializerTests(TestCase):
|
||||
Tests for EnterpriseCourseEnrollmentSerializer.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls): # lint-amnesty, pylint: disable=super-method-not-called
|
||||
def setup_patch(self, function_name, return_value):
|
||||
"""
|
||||
Patch a function with a given return value, and return the mock
|
||||
"""
|
||||
mock = MagicMock(return_value=return_value)
|
||||
new_patch = patch(function_name, new=mock)
|
||||
new_patch.start()
|
||||
self.addCleanup(new_patch.stop)
|
||||
return mock
|
||||
|
||||
def setUp(self):
|
||||
self.mock_pathways_with_course = self.setup_patch(
|
||||
'learner_pathway_progress.signals.get_learner_pathways_associated_with_course',
|
||||
None,
|
||||
)
|
||||
enterprise_customer_user = EnterpriseCustomerUserFactory()
|
||||
enterprise_course_enrollment = EnterpriseCourseEnrollmentFactory(
|
||||
enterprise_customer_user=enterprise_customer_user
|
||||
)
|
||||
cls.enterprise_customer_user = enterprise_customer_user
|
||||
cls.enterprise_course_enrollment = enterprise_course_enrollment
|
||||
self.enterprise_customer_user = enterprise_customer_user
|
||||
self.enterprise_course_enrollment = enterprise_course_enrollment
|
||||
|
||||
super().setUp()
|
||||
|
||||
def test_data_with_license(self):
|
||||
""" Verify the correct fields are serialized when the enrollment is licensed. """
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import ddt
|
||||
from django.test.utils import override_settings
|
||||
@@ -46,12 +46,26 @@ class EnterpriseSupportSignals(SharedModuleStoreTestCase):
|
||||
Tests for the enterprise support signals.
|
||||
"""
|
||||
|
||||
def setup_patch(self, function_name, return_value):
|
||||
"""
|
||||
Patch a function with a given return value, and return the mock
|
||||
"""
|
||||
mock = MagicMock(return_value=return_value)
|
||||
new_patch = patch(function_name, new=mock)
|
||||
new_patch.start()
|
||||
self.addCleanup(new_patch.stop)
|
||||
return mock
|
||||
|
||||
def setUp(self):
|
||||
UserFactory.create(username=TEST_ECOMMERCE_WORKER)
|
||||
self.user = UserFactory.create(username='test', email=TEST_EMAIL)
|
||||
self.course_id = 'course-v1:edX+DemoX+Demo_Course'
|
||||
self.enterprise_customer = EnterpriseCustomerFactory()
|
||||
self.enterprise_customer_uuid = str(self.enterprise_customer.uuid)
|
||||
self.mock_pathways_with_course = self.setup_patch(
|
||||
'learner_pathway_progress.signals.get_learner_pathways_associated_with_course',
|
||||
None,
|
||||
)
|
||||
super().setUp()
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
# See BOM-2721 for more details.
|
||||
# Below is the copied and edited version of common_constraints
|
||||
|
||||
# This is a temporary solution to override the real common_constraints.txt
|
||||
# In edx-lint, until the pyjwt constraint in edx-lint has been removed.
|
||||
# See BOM-2721 for more details.
|
||||
# Below is the copied and edited version of common_constraints
|
||||
|
||||
# This is a temporary solution to override the real common_constraints.txt
|
||||
# In edx-lint, until the pyjwt constraint in edx-lint has been removed.
|
||||
# See BOM-2721 for more details.
|
||||
# Below is the copied and edited version of common_constraints
|
||||
|
||||
# A central location for most common version constraints
|
||||
# (across edx repos) for pip-installation.
|
||||
#
|
||||
|
||||
@@ -73,3 +73,4 @@ scipy<1.8.0
|
||||
# This will be fixed when sphinxcontrib-openapi depends on m2r2 instead of m2r
|
||||
# See issue: https://github.com/sphinx-contrib/openapi/issues/123
|
||||
mistune<2.0.0
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ matplotlib==3.3.4
|
||||
# -r requirements/edx-sandbox/py38.in
|
||||
mpmath==1.2.1
|
||||
# via sympy
|
||||
networkx==2.8.3
|
||||
networkx==2.8.4
|
||||
# via -r requirements/edx-sandbox/py38.in
|
||||
nltk==3.7
|
||||
# via
|
||||
|
||||
@@ -109,6 +109,7 @@ ipaddress # Ip network support for Embargo feature
|
||||
jsonfield # Django model field for validated JSON; used in several apps
|
||||
laboratory # Library for testing that code refactors/infrastructure changes produce identical results
|
||||
lxml # XML parser
|
||||
learner-pathway-progress # A plugin for lms to track learners progress in pathays
|
||||
lti-consumer-xblock>=4.1.1
|
||||
mailsnake # Needed for mailchimp (mailing djangoapp)
|
||||
mako # Primary template language used for server-side page rendering
|
||||
|
||||
@@ -55,7 +55,7 @@ attrs==21.4.0
|
||||
# blockstore
|
||||
# edx-ace
|
||||
# openedx-events
|
||||
babel==2.10.1
|
||||
babel==2.10.2
|
||||
# via
|
||||
# -r requirements/edx/base.in
|
||||
# enmerkar
|
||||
@@ -237,6 +237,7 @@ django==3.2.13
|
||||
# event-tracking
|
||||
# help-tokens
|
||||
# jsonfield
|
||||
# learner-pathway-progress
|
||||
# lti-consumer-xblock
|
||||
# openedx-events
|
||||
# openedx-filters
|
||||
@@ -312,6 +313,7 @@ django-model-utils==4.2.0
|
||||
# edx-submissions
|
||||
# edx-when
|
||||
# edxval
|
||||
# learner-pathway-progress
|
||||
# ora2
|
||||
# super-csv
|
||||
django-mptt==0.13.4
|
||||
@@ -350,6 +352,7 @@ django-simple-history==3.0.0
|
||||
# edx-name-affirmation
|
||||
# edx-organizations
|
||||
# edx-proctoring
|
||||
# learner-pathway-progress
|
||||
# ora2
|
||||
django-splash==1.2.1
|
||||
# via -r requirements/edx/base.in
|
||||
@@ -455,6 +458,7 @@ edx-django-utils==5.0.0
|
||||
# edx-toggles
|
||||
# edx-when
|
||||
# event-tracking
|
||||
# learner-pathway-progress
|
||||
# ora2
|
||||
# super-csv
|
||||
edx-drf-extensions==8.0.1
|
||||
@@ -491,6 +495,7 @@ edx-opaque-keys[django]==2.3.0
|
||||
# edx-proctoring
|
||||
# edx-user-state-client
|
||||
# edx-when
|
||||
# learner-pathway-progress
|
||||
# lti-consumer-xblock
|
||||
# openedx-events
|
||||
# ora2
|
||||
@@ -620,6 +625,7 @@ jsonfield==3.1.0
|
||||
# edx-enterprise
|
||||
# edx-proctoring
|
||||
# edx-submissions
|
||||
# learner-pathway-progress
|
||||
# lti-consumer-xblock
|
||||
# ora2
|
||||
jwcrypto==1.3.1
|
||||
@@ -634,6 +640,8 @@ lazy==1.4
|
||||
# acid-xblock
|
||||
# lti-consumer-xblock
|
||||
# ora2
|
||||
learner-pathway-progress==1.2.0
|
||||
# via -r requirements/edx/base.in
|
||||
libsass==0.10.0
|
||||
# via
|
||||
# -r requirements/edx/paver.txt
|
||||
@@ -881,6 +889,7 @@ pytz==2022.1
|
||||
# fs
|
||||
# icalendar
|
||||
# interchange
|
||||
# learner-pathway-progress
|
||||
# olxcleaner
|
||||
# ora2
|
||||
# xblock
|
||||
|
||||
@@ -82,7 +82,7 @@ attrs==21.4.0
|
||||
# jsonschema
|
||||
# openedx-events
|
||||
# pytest
|
||||
babel==2.10.1
|
||||
babel==2.10.2
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
# enmerkar
|
||||
@@ -323,6 +323,7 @@ django==3.2.13
|
||||
# event-tracking
|
||||
# help-tokens
|
||||
# jsonfield
|
||||
# learner-pathway-progress
|
||||
# lti-consumer-xblock
|
||||
# openedx-events
|
||||
# openedx-filters
|
||||
@@ -408,6 +409,7 @@ django-model-utils==4.2.0
|
||||
# edx-submissions
|
||||
# edx-when
|
||||
# edxval
|
||||
# learner-pathway-progress
|
||||
# ora2
|
||||
# super-csv
|
||||
django-mptt==0.13.4
|
||||
@@ -450,6 +452,7 @@ django-simple-history==3.0.0
|
||||
# edx-name-affirmation
|
||||
# edx-organizations
|
||||
# edx-proctoring
|
||||
# learner-pathway-progress
|
||||
# ora2
|
||||
django-splash==1.2.1
|
||||
# via -r requirements/edx/testing.txt
|
||||
@@ -568,6 +571,7 @@ edx-django-utils==5.0.0
|
||||
# edx-toggles
|
||||
# edx-when
|
||||
# event-tracking
|
||||
# learner-pathway-progress
|
||||
# ora2
|
||||
# super-csv
|
||||
edx-drf-extensions==8.0.1
|
||||
@@ -608,6 +612,7 @@ edx-opaque-keys[django]==2.3.0
|
||||
# edx-proctoring
|
||||
# edx-user-state-client
|
||||
# edx-when
|
||||
# learner-pathway-progress
|
||||
# lti-consumer-xblock
|
||||
# openedx-events
|
||||
# ora2
|
||||
@@ -810,6 +815,7 @@ jsonfield==3.1.0
|
||||
# edx-enterprise
|
||||
# edx-proctoring
|
||||
# edx-submissions
|
||||
# learner-pathway-progress
|
||||
# lti-consumer-xblock
|
||||
# ora2
|
||||
jsonschema==4.6.0
|
||||
@@ -835,6 +841,8 @@ lazy-object-proxy==1.7.1
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
# astroid
|
||||
learner-pathway-progress==1.2.0
|
||||
# via -r requirements/edx/testing.txt
|
||||
libsass==0.10.0
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
@@ -1228,6 +1236,7 @@ pytz==2022.1
|
||||
# fs
|
||||
# icalendar
|
||||
# interchange
|
||||
# learner-pathway-progress
|
||||
# olxcleaner
|
||||
# ora2
|
||||
# xblock
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
alabaster==0.7.12
|
||||
# via sphinx
|
||||
babel==2.10.1
|
||||
babel==2.10.2
|
||||
# via sphinx
|
||||
certifi==2022.5.18.1
|
||||
# via requests
|
||||
|
||||
@@ -77,7 +77,7 @@ attrs==21.4.0
|
||||
# openedx-events
|
||||
# outcome
|
||||
# pytest
|
||||
babel==2.10.1
|
||||
babel==2.10.2
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# enmerkar
|
||||
@@ -310,6 +310,7 @@ distlib==0.3.4
|
||||
# event-tracking
|
||||
# help-tokens
|
||||
# jsonfield
|
||||
# learner-pathway-progress
|
||||
# lti-consumer-xblock
|
||||
# openedx-events
|
||||
# openedx-filters
|
||||
@@ -393,6 +394,7 @@ django-model-utils==4.2.0
|
||||
# edx-submissions
|
||||
# edx-when
|
||||
# edxval
|
||||
# learner-pathway-progress
|
||||
# ora2
|
||||
# super-csv
|
||||
django-mptt==0.13.4
|
||||
@@ -435,6 +437,7 @@ django-simple-history==3.0.0
|
||||
# edx-name-affirmation
|
||||
# edx-organizations
|
||||
# edx-proctoring
|
||||
# learner-pathway-progress
|
||||
# ora2
|
||||
django-splash==1.2.1
|
||||
# via -r requirements/edx/base.txt
|
||||
@@ -551,6 +554,7 @@ edx-django-utils==5.0.0
|
||||
# edx-toggles
|
||||
# edx-when
|
||||
# event-tracking
|
||||
# learner-pathway-progress
|
||||
# ora2
|
||||
# super-csv
|
||||
edx-drf-extensions==8.0.1
|
||||
@@ -592,6 +596,7 @@ edx-opaque-keys[django]==2.3.0
|
||||
# edx-proctoring
|
||||
# edx-user-state-client
|
||||
# edx-when
|
||||
# learner-pathway-progress
|
||||
# lti-consumer-xblock
|
||||
# openedx-events
|
||||
# ora2
|
||||
@@ -776,6 +781,7 @@ jsonfield==3.1.0
|
||||
# edx-enterprise
|
||||
# edx-proctoring
|
||||
# edx-submissions
|
||||
# learner-pathway-progress
|
||||
# lti-consumer-xblock
|
||||
# ora2
|
||||
jwcrypto==1.3.1
|
||||
@@ -797,6 +803,8 @@ lazy==1.4
|
||||
# ora2
|
||||
lazy-object-proxy==1.7.1
|
||||
# via astroid
|
||||
learner-pathway-progress==1.2.0
|
||||
# via -r requirements/edx/base.txt
|
||||
libsass==0.10.0
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
@@ -1159,6 +1167,7 @@ pytz==2022.1
|
||||
# fs
|
||||
# icalendar
|
||||
# interchange
|
||||
# learner-pathway-progress
|
||||
# olxcleaner
|
||||
# ora2
|
||||
# xblock
|
||||
|
||||
Reference in New Issue
Block a user