diff --git a/lms/djangoapps/certificates/tests/test_signals.py b/lms/djangoapps/certificates/tests/test_signals.py index abc10411ec..4a85f895d8 100644 --- a/lms/djangoapps/certificates/tests/test_signals.py +++ b/lms/djangoapps/certificates/tests/test_signals.py @@ -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, diff --git a/lms/djangoapps/commerce/management/commands/tests/test_create_orders_for_old_enterprise_course_enrollmnet.py b/lms/djangoapps/commerce/management/commands/tests/test_create_orders_for_old_enterprise_course_enrollmnet.py index 8b2472f214..7af3cdcf69 100644 --- a/lms/djangoapps/commerce/management/commands/tests/test_create_orders_for_old_enterprise_course_enrollmnet.py +++ b/lms/djangoapps/commerce/management/commands/tests/test_create_orders_for_old_enterprise_course_enrollmnet.py @@ -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') diff --git a/lms/djangoapps/courseware/tests/test_submitting_problems.py b/lms/djangoapps/courseware/tests/test_submitting_problems.py index fa9f381160..3ab728bb8e 100644 --- a/lms/djangoapps/courseware/tests/test_submitting_problems.py +++ b/lms/djangoapps/courseware/tests/test_submitting_problems.py @@ -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) diff --git a/lms/djangoapps/grades/course_grade_factory.py b/lms/djangoapps/grades/course_grade_factory.py index 65f3a9c4cc..a68331860b 100644 --- a/lms/djangoapps/grades/course_grade_factory.py +++ b/lms/djangoapps/grades/course_grade_factory.py @@ -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, diff --git a/lms/djangoapps/grades/models.py b/lms/djangoapps/grades/models.py index 0d87873f20..76b2e37cb0 100644 --- a/lms/djangoapps/grades/models.py +++ b/lms/djangoapps/grades/models.py @@ -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() diff --git a/lms/djangoapps/grades/signals/signals.py b/lms/djangoapps/grades/signals/signals.py index 0edd48b4d5..73a31f937c 100644 --- a/lms/djangoapps/grades/signals/signals.py +++ b/lms/djangoapps/grades/signals/signals.py @@ -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() diff --git a/lms/djangoapps/grades/tests/test_course_grade_factory.py b/lms/djangoapps/grades/tests/test_course_grade_factory.py index 39b2cfb60c..080caf3ff8 100644 --- a/lms/djangoapps/grades/tests/test_course_grade_factory.py +++ b/lms/djangoapps/grades/tests/test_course_grade_factory.py @@ -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): diff --git a/lms/djangoapps/grades/tests/test_models.py b/lms/djangoapps/grades/tests/test_models.py index 5405b03e94..3ea25b178f 100644 --- a/lms/djangoapps/grades/tests/test_models.py +++ b/lms/djangoapps/grades/tests/test_models.py @@ -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) diff --git a/lms/djangoapps/grades/tests/test_signals.py b/lms/djangoapps/grades/tests/test_signals.py index 8c161d86ac..4e16c0bdbf 100644 --- a/lms/djangoapps/grades/tests/test_signals.py +++ b/lms/djangoapps/grades/tests/test_signals.py @@ -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( diff --git a/lms/djangoapps/instructor/tests/test_spoc_gradebook.py b/lms/djangoapps/instructor/tests/test_spoc_gradebook.py index 1f2485f1b3..d063c056e2 100644 --- a/lms/djangoapps/instructor/tests/test_spoc_gradebook.py +++ b/lms/djangoapps/instructor/tests/test_spoc_gradebook.py @@ -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) diff --git a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py index 1807a8af96..14d49fb24f 100644 --- a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py +++ b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py @@ -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), diff --git a/lms/djangoapps/learner_dashboard/api/v0/tests/test_views.py b/lms/djangoapps/learner_dashboard/api/v0/tests/test_views.py index 7072235ef1..eb985f058e 100644 --- a/lms/djangoapps/learner_dashboard/api/v0/tests/test_views.py +++ b/lms/djangoapps/learner_dashboard/api/v0/tests/test_views.py @@ -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, diff --git a/lms/djangoapps/support/tests/test_views.py b/lms/djangoapps/support/tests/test_views.py index 3aa56a0a76..f6b4206aa6 100644 --- a/lms/djangoapps/support/tests/test_views.py +++ b/lms/djangoapps/support/tests/test_views.py @@ -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, diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py b/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py index c1ee73a970..c416b7a4c2 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py @@ -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. diff --git a/openedx/features/enterprise_support/tests/test_api.py b/openedx/features/enterprise_support/tests/test_api.py index 3731e81a1b..cd1958353e 100644 --- a/openedx/features/enterprise_support/tests/test_api.py +++ b/openedx/features/enterprise_support/tests/test_api.py @@ -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') diff --git a/openedx/features/enterprise_support/tests/test_context.py b/openedx/features/enterprise_support/tests/test_context.py index d775859b1b..6a671bd9a2 100644 --- a/openedx/features/enterprise_support/tests/test_context.py +++ b/openedx/features/enterprise_support/tests/test_context.py @@ -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)} diff --git a/openedx/features/enterprise_support/tests/test_serializers.py b/openedx/features/enterprise_support/tests/test_serializers.py index aa82b8dbb0..876bf3e348 100644 --- a/openedx/features/enterprise_support/tests/test_serializers.py +++ b/openedx/features/enterprise_support/tests/test_serializers.py @@ -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. """ diff --git a/openedx/features/enterprise_support/tests/test_signals.py b/openedx/features/enterprise_support/tests/test_signals.py index 5ebb1123e1..a4545dff70 100644 --- a/openedx/features/enterprise_support/tests/test_signals.py +++ b/openedx/features/enterprise_support/tests/test_signals.py @@ -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 diff --git a/requirements/common_constraints.txt b/requirements/common_constraints.txt index 93ecbec605..30ba0afaed 100644 --- a/requirements/common_constraints.txt +++ b/requirements/common_constraints.txt @@ -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. # diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 78b1bb4d3f..c4259ab7e8 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -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 + diff --git a/requirements/edx-sandbox/py38.txt b/requirements/edx-sandbox/py38.txt index 7364982ff3..808482e7a3 100644 --- a/requirements/edx-sandbox/py38.txt +++ b/requirements/edx-sandbox/py38.txt @@ -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 diff --git a/requirements/edx/base.in b/requirements/edx/base.in index 4f834e95d4..5acb033d18 100644 --- a/requirements/edx/base.in +++ b/requirements/edx/base.in @@ -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 diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 5c3e18121d..ec4b5b7231 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -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 diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 76c9b3ea8c..4ce53ec003 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -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 diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index efd98c1678..e2a875b30b 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -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 diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 3831c1ad72..c6f5e50fc7 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -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