diff --git a/enterprise_catalog/apps/api/tasks.py b/enterprise_catalog/apps/api/tasks.py index 51921aa2b..cfdf33336 100644 --- a/enterprise_catalog/apps/api/tasks.py +++ b/enterprise_catalog/apps/api/tasks.py @@ -288,6 +288,17 @@ def _update_full_content_metadata_course(content_keys, dry_run=False): # Merge the full metadata from discovery's /api/v1/courses into the local metadata object. metadata_record.json_metadata.update(course_metadata_dict) + # Exec ed provides the start/end dates in additional_metadata, so we should copy those over to the keys that + # we use (inside the advertised course run). + if metadata_record.is_exec_ed_2u_course: + json_meta = metadata_record.json_metadata + start_date = json_meta.get('additional_metadata', {}).get('start_date') + end_date = json_meta.get('additional_metadata', {}).get('end_date') + course_run_uuid = json_meta.get('advertised_course_run_uuid') + for run in json_meta.get('course_runs', []): + if run.get('uuid') == course_run_uuid: + run.update({'start': start_date, 'end': end_date}) + # Perform more steps to normalize and move keys around # for more consistency across content types. metadata_record.json_metadata['normalized_metadata'] =\ diff --git a/enterprise_catalog/apps/api/tests/test_tasks.py b/enterprise_catalog/apps/api/tests/test_tasks.py index f8754e176..cc6333f53 100644 --- a/enterprise_catalog/apps/api/tests/test_tasks.py +++ b/enterprise_catalog/apps/api/tests/test_tasks.py @@ -450,23 +450,6 @@ def test_update_full_metadata(self, mock_oauth_client, mock_partition_course_key ] }], 'advertised_course_run_uuid': course_run_3_uuid, - 'advertised_course_run': { - 'key': f'course-v1:{course_key_3}+1', - 'uuid': course_run_3_uuid, - 'start': '2023-03-01T00:00:00Z', - 'end': '2023-03-01T00:00:00Z', - 'first_enrollable_paid_seat_price': 90, - 'seats': [ - { - 'type': CourseMode.VERIFIED, - 'upgrade_deadline': '2023-02-01T00:00:00Z', - }, - { - "type": str(CourseMode.PROFESSIONAL), - "upgrade_deadline": '2022-02-01T00:00:00Z', - }, - ] - }, } course_key_4 = 'edX+superDuperFakeX' course_data_4 = {'key': course_key_4, 'full_course_only_field': 'test_4', 'programs': []} @@ -657,10 +640,8 @@ def test_update_full_metadata_exec_ed(self, mock_oauth_client, mock_partition_co 'key': course_run_key, 'uuid': course_run_uuid, # Use dummy 2022 dates that we will assert are overwritten. - 'start': '2023-03-01T00:00:00Z', - 'end': '2023-04-09T23:59:59Z', - 'enrollment_end': '2023-02-01T00:00:00Z', - "first_enrollable_paid_seat_price": 2900, + 'start': '2022-03-01T00:00:00Z', + 'end': '2022-03-01T00:00:00Z', }], 'programs': [], 'additional_metadata': { diff --git a/enterprise_catalog/apps/api/v1/export_utils.py b/enterprise_catalog/apps/api/v1/export_utils.py index 46d599ee9..33c03dce3 100644 --- a/enterprise_catalog/apps/api/v1/export_utils.py +++ b/enterprise_catalog/apps/api/v1/export_utils.py @@ -242,13 +242,7 @@ def course_hit_to_row(hit): def fetch_and_format_registration_date(obj): - """ - Args: - obj: CourseRun object - Returns: - string: date to register by - """ - enroll_by_date = obj.get('end') + enroll_by_date = obj.get('registration_deadline') stripped_enroll_by = enroll_by_date.split("T")[0] formatted_enroll_by = None try: @@ -270,12 +264,18 @@ def exec_ed_course_to_row(hit): csv_row.append(hit['partners'][0]['name']) else: csv_row.append(None) + if hit.get('additional_metadata'): + start_date = None + additional_md = hit['additional_metadata'] + if additional_md.get('start_date'): + start_date = parser.parse(additional_md['start_date']).strftime(DATE_FORMAT) + csv_row.append(start_date) - adv_course_run = hit.get('advertised_course_run', {}) - if (start_date := adv_course_run.get('start'), end_date := adv_course_run.get('end')) and start_date and end_date: - csv_row.append(start_date and parser.parse(start_date).strftime(DATE_FORMAT)) - csv_row.append(end_date and parser.parse(end_date).strftime(DATE_FORMAT)) - formatted_enroll_by = fetch_and_format_registration_date(adv_course_run) + end_date = None + if additional_md.get('end_date'): + end_date = parser.parse(additional_md['end_date']).strftime(DATE_FORMAT) + csv_row.append(end_date) + formatted_enroll_by = fetch_and_format_registration_date(additional_md) else: csv_row.append(None) # no start date csv_row.append(None) # no end date @@ -283,6 +283,7 @@ def exec_ed_course_to_row(hit): csv_row.append(formatted_enroll_by) + adv_course_run = hit.get('advertised_course_run', {}) key = adv_course_run.get('key') price = float(hit['entitlements'][0]['price']) diff --git a/enterprise_catalog/apps/api/v1/tests/test_export_utils.py b/enterprise_catalog/apps/api/v1/tests/test_export_utils.py index d7603b65c..de6d8263f 100644 --- a/enterprise_catalog/apps/api/v1/tests/test_export_utils.py +++ b/enterprise_catalog/apps/api/v1/tests/test_export_utils.py @@ -22,9 +22,9 @@ def test_fetch_and_format_registration_date(self): """ # expected hit format from algolia, porperly reformatted for csv download assert export_utils.fetch_and_format_registration_date( - {'end': '2002-02-15T12:12:200'} + {'registration_deadline': '2002-02-15T12:12:200'} ) == '02-15-2002' # some other format from algolia, should return None assert export_utils.fetch_and_format_registration_date( - {'end': '02-15-2015T12:12:200'} + {'registration_deadline': '02-15-2015T12:12:200'} ) is None diff --git a/enterprise_catalog/apps/catalog/algolia_utils.py b/enterprise_catalog/apps/catalog/algolia_utils.py index 988703129..d09d34e41 100644 --- a/enterprise_catalog/apps/catalog/algolia_utils.py +++ b/enterprise_catalog/apps/catalog/algolia_utils.py @@ -236,18 +236,20 @@ def _has_enroll_by_deadline_passed(course_json_metadata, advertised_course_run): Helper to determine if the enrollment deadline has passed for the given course and advertised course run. For course metadata records with a `course_type` of "course" (e.g. OCM courses), this is based on the verified upgrade deadline. - For 2u exec ed courses, this is based on the enrollment_end deadline. + For 2u exec ed courses, this is based on the registration deadline. """ enroll_by_deadline_timestamp = 0 if course_json_metadata.get('course_type') == EXEC_ED_2U_COURSE_TYPE: - enrollment_end = advertised_course_run.get('end') or {} - if enrollment_end: + additional_metadata = course_json_metadata.get('additional_metadata') or {} + registration_deadline = additional_metadata.get('registration_deadline') + if registration_deadline: enroll_by_deadline_timestamp = datetime.datetime.strptime( - enrollment_end, + registration_deadline, '%Y-%m-%dT%H:%M:%S%z', ).timestamp() else: enroll_by_deadline_timestamp = _get_verified_upgrade_deadline(advertised_course_run) + return enroll_by_deadline_timestamp < localized_utcnow().timestamp() @@ -1094,7 +1096,6 @@ def _get_course_run(full_course_run): 'availability': full_course_run.get('availability'), 'start': full_course_run.get('start'), 'end': full_course_run.get('end'), - 'enrollment_end': full_course_run.get('enrollment_end'), 'min_effort': full_course_run.get('min_effort'), 'max_effort': full_course_run.get('max_effort'), 'weeks_to_complete': full_course_run.get('weeks_to_complete'), diff --git a/enterprise_catalog/apps/catalog/serializers.py b/enterprise_catalog/apps/catalog/serializers.py index 903179653..0927692d4 100644 --- a/enterprise_catalog/apps/catalog/serializers.py +++ b/enterprise_catalog/apps/catalog/serializers.py @@ -95,8 +95,15 @@ def advertised_course_run(self): advertised_course_run_uuid = self.instance.json_metadata.get('advertised_course_run_uuid') return _get_course_run_by_uuid(self.instance.json_metadata, advertised_course_run_uuid) + @cached_property + def additional_metadata(self): + return self.instance.json_metadata.get('additional_metadata', {}) + @extend_schema_field(serializers.DateTimeField) - def get_start_date(self, obj) -> str: # pylint: disable=unused-argument + def get_start_date(self, obj) -> str: + if obj.is_exec_ed_2u_course: + return self.additional_metadata.get('start_date') + if not self.advertised_course_run: return None @@ -106,7 +113,10 @@ def get_start_date(self, obj) -> str: # pylint: disable=unused-argument return None @extend_schema_field(serializers.DateTimeField) - def get_end_date(self, obj) -> str: # pylint: disable=unused-argument + def get_end_date(self, obj) -> str: + if obj.is_exec_ed_2u_course: + return self.additional_metadata.get('end_date') + if not self.advertised_course_run: return None @@ -116,7 +126,10 @@ def get_end_date(self, obj) -> str: # pylint: disable=unused-argument return None @extend_schema_field(serializers.DateTimeField) - def get_enroll_by_date(self, obj) -> str: # pylint: disable=unused-argument + def get_enroll_by_date(self, obj) -> str: + if obj.is_exec_ed_2u_course: + return self.additional_metadata.get('registration_deadline') + if not self.advertised_course_run: return None @@ -124,8 +137,6 @@ def get_enroll_by_date(self, obj) -> str: # pylint: disable=unused-argument seat = _find_best_mode_seat(all_seats) if seat: return seat.get('upgrade_deadline') - if enrollment_end := self.advertised_course_run.get('enrollment_end'): - return enrollment_end else: logger.info( f"No Seat Found for course run '{self.advertised_course_run.get('key')}'. " @@ -134,7 +145,12 @@ def get_enroll_by_date(self, obj) -> str: # pylint: disable=unused-argument return None @extend_schema_field(serializers.FloatField) - def get_content_price(self, obj) -> float: # pylint: disable=unused-argument + def get_content_price(self, obj) -> float: + if obj.is_exec_ed_2u_course: + for entitlement in obj.json_metadata.get('entitlements', []): + if entitlement.get('mode') == CourseMode.PAID_EXECUTIVE_EDUCATION: + return entitlement.get('price') or DEFAULT_NORMALIZED_PRICE + if not self.advertised_course_run: return None diff --git a/enterprise_catalog/apps/catalog/tests/test_algolia_utils.py b/enterprise_catalog/apps/catalog/tests/test_algolia_utils.py index f9f21818a..40d127a8f 100644 --- a/enterprise_catalog/apps/catalog/tests/test_algolia_utils.py +++ b/enterprise_catalog/apps/catalog/tests/test_algolia_utils.py @@ -66,16 +66,12 @@ class AlgoliaUtilsTests(TestCase): { 'course_type': EXEC_ED_2U_COURSE_TYPE, 'expected_result': True, - 'start': _fake_upgrade_deadline(1), - 'end': _fake_upgrade_deadline(30), - 'enrollment_end': _fake_upgrade_deadline(1) + 'additional_metadata': {'registration_deadline': '2073-03-21T23:59:59Z'}, }, { 'course_type': EXEC_ED_2U_COURSE_TYPE, 'expected_result': False, - 'start': _fake_upgrade_deadline(-30), - 'end': _fake_upgrade_deadline(-1), - 'enrollment_end': _fake_upgrade_deadline(-30) + 'additional_metadata': {'registration_deadline': '2021-03-21T23:59:59Z'}, }, { 'course_type': EXEC_ED_2U_COURSE_TYPE, @@ -96,10 +92,7 @@ def test_should_index_course( course_run_availability='current', seats=None, course_type=COURSE, - additional_metadata=None, - start='2023-01-29T23:59:59Z', - end='2023-02-28T23:59:59Z', - enrollment_end='2023-01-29T23:59:59Z' + additional_metadata=None ): """ Verify that only a course that has a non-hidden advertised course run, at least one owner, and a marketing slug @@ -120,9 +113,6 @@ def test_should_index_course( 'is_marketable': is_marketable, 'availability': course_run_availability, 'seats': seats or [], - 'start': start, - 'end': end, - 'enrollment_end': enrollment_end }, ], 'owners': owners, @@ -314,7 +304,6 @@ def test_get_course_subjects(self, course_metadata, expected_subjects): 'pacing_type': 'instructor_paced', 'start': '2013-10-16T14:00:00Z', 'end': '2014-10-16T14:00:00Z', - 'enrollment_end': '2013-10-17T14:00:00Z', 'availability': 'Current', 'min_effort': 10, 'max_effort': 14, @@ -327,7 +316,6 @@ def test_get_course_subjects(self, course_metadata, expected_subjects): 'pacing_type': 'instructor_paced', 'start': '2013-10-16T14:00:00Z', 'end': '2014-10-16T14:00:00Z', - 'enrollment_end': '2013-10-17T14:00:00Z', 'availability': 'Current', 'min_effort': 10, 'max_effort': 14, @@ -352,7 +340,6 @@ def test_get_course_subjects(self, course_metadata, expected_subjects): 'pacing_type': 'instructor_paced', 'start': '2013-10-16T14:00:00Z', 'end': '2014-10-16T14:00:00Z', - 'enrollment_end': '2013-10-17T14:00:00Z', 'availability': 'Current', 'min_effort': 10, 'max_effort': 14, @@ -372,7 +359,6 @@ def test_get_course_subjects(self, course_metadata, expected_subjects): 'pacing_type': 'instructor_paced', 'start': '2013-10-16T14:00:00Z', 'end': '2014-10-16T14:00:00Z', - 'enrollment_end': '2013-10-17T14:00:00Z', 'availability': 'Current', 'min_effort': 10, 'max_effort': 14, @@ -388,7 +374,6 @@ def test_get_course_subjects(self, course_metadata, expected_subjects): 'pacing_type': 'instructor_paced', 'start': '2013-10-16T14:00:00Z', 'end': '2014-10-16T14:00:00Z', - 'enrollment_end': '2013-10-17T14:00:00Z', 'availability': 'Current', 'min_effort': 10, 'max_effort': 14, @@ -405,7 +390,6 @@ def test_get_course_subjects(self, course_metadata, expected_subjects): 'pacing_type': 'instructor_paced', 'start': '2013-10-16T14:00:00Z', 'end': '2014-10-16T14:00:00Z', - 'enrollment_end': '2013-10-17T14:00:00Z', 'availability': 'Current', 'min_effort': 10, 'max_effort': 14, @@ -481,7 +465,6 @@ def test_get_upcoming_course_runs(self, searchable_course, expected_course_runs) 'availability': 'Archived', 'start': "2000-01-04T00:00:00Z", 'end': "2001-12-31T23:59:00Z", - 'enrollment_end': '2000-01-04T00:00:00Z', 'min_effort': 2, 'max_effort': 6, 'weeks_to_complete': 6, @@ -496,7 +479,6 @@ def test_get_upcoming_course_runs(self, searchable_course, expected_course_runs) 'availability': 'Current', 'start': "2018-01-04T00:00:00Z", 'end': "3022-12-31T23:59:00Z", - 'enrollment_end': '2018-01-04T00:00:00Z', 'min_effort': 2, 'max_effort': 6, 'weeks_to_complete': 6, @@ -511,7 +493,6 @@ def test_get_upcoming_course_runs(self, searchable_course, expected_course_runs) 'availability': 'Upcoming', 'start': "3000-01-04T00:00:00Z", 'end': "3022-12-31T23:59:00Z", - 'enrollment_end': '3000-01-04T00:00:00Z', 'min_effort': 2, 'max_effort': 6, 'weeks_to_complete': 6, @@ -526,7 +507,6 @@ def test_get_upcoming_course_runs(self, searchable_course, expected_course_runs) 'availability': 'Starting Soon', 'start': "3000-01-04T00:00:00Z", 'end': "3022-12-31T23:59:00Z", - 'enrollment_end': '3000-01-04T00:00:00Z', 'min_effort': 2, 'max_effort': 6, 'weeks_to_complete': 6, @@ -541,7 +521,6 @@ def test_get_upcoming_course_runs(self, searchable_course, expected_course_runs) 'availability': 'Current', 'start': "2018-01-04T00:00:00Z", 'end': "3022-12-31T23:59:00Z", - 'enrollment_end': '2018-01-04T00:00:00Z', 'min_effort': 2, 'max_effort': 6, 'weeks_to_complete': 6, @@ -553,7 +532,6 @@ def test_get_upcoming_course_runs(self, searchable_course, expected_course_runs) 'availability': 'Upcoming', 'start': "3000-01-04T00:00:00Z", 'end': "3022-12-31T23:59:00Z", - 'enrollment_end': '3000-01-04T00:00:00Z', 'min_effort': 2, 'max_effort': 6, 'weeks_to_complete': 6, @@ -565,7 +543,6 @@ def test_get_upcoming_course_runs(self, searchable_course, expected_course_runs) 'availability': 'Starting Soon', 'start': "3000-01-04T00:00:00Z", 'end': "3022-12-31T23:59:00Z", - 'enrollment_end': '3000-01-04T00:00:00Z', 'min_effort': 2, 'max_effort': 6, 'weeks_to_complete': 6,