From d9684fea850d25d66e1c90f8d60e4cdf6fb803f8 Mon Sep 17 00:00:00 2001 From: Marlon Keating Date: Thu, 24 Oct 2024 14:58:50 +0000 Subject: [PATCH] feat: take exec ed course data from course run instead of additional_metadata attempt 4 --- .../apps/api/tests/test_tasks.py | 37 ++++ .../apps/api/v1/export_utils.py | 206 +++++++++--------- .../apps/api/v1/tests/test_export_utils.py | 13 -- .../apps/api/v1/tests/test_views.py | 1 + .../apps/catalog/algolia_utils.py | 40 ++-- .../apps/catalog/serializers.py | 12 +- .../apps/catalog/tests/test_algolia_utils.py | 154 ++++++++----- 7 files changed, 263 insertions(+), 200 deletions(-) diff --git a/enterprise_catalog/apps/api/tests/test_tasks.py b/enterprise_catalog/apps/api/tests/test_tasks.py index 7a7d51372..3a5f7456c 100644 --- a/enterprise_catalog/apps/api/tests/test_tasks.py +++ b/enterprise_catalog/apps/api/tests/test_tasks.py @@ -27,6 +27,7 @@ from enterprise_catalog.apps.catalog.models import CatalogQuery, ContentMetadata from enterprise_catalog.apps.catalog.serializers import ( DEFAULT_NORMALIZED_PRICE, + NormalizedContentMetadataSerializer, _find_best_mode_seat, ) from enterprise_catalog.apps.catalog.tests.factories import ( @@ -57,6 +58,37 @@ def mock_task(self, *args, **kwargs): # pylint: disable=unused-argument mock_task.name = 'mock_task' +def _hydrate_normalized_metadata(metadata_record): + """ + Populate normalized_metadata fields for ContentMetadata + """ + normalized_metadata_input = { + 'course_metadata': metadata_record.json_metadata, + } + metadata_record.json_metadata['normalized_metadata'] =\ + NormalizedContentMetadataSerializer(normalized_metadata_input).data + metadata_record.json_metadata['normalized_metadata_by_run'] = {} + for run in metadata_record.json_metadata.get('course_runs', []): + metadata_record.json_metadata['normalized_metadata_by_run'].update({ + run['key']: NormalizedContentMetadataSerializer({ + 'course_run_metadata': run, + 'course_metadata': metadata_record.json_metadata, + }).data + }) + + +def _hydrate_course_normalized_metadata(): + """ + Populate normalized_metadata fields for all course ContentMetadata + Needed for tests that generate test ContentMetadata, which does not have + normalized_metadata populated by default. + """ + all_course_metadata = ContentMetadata.objects.filter(content_type=COURSE) + for course_metadata in all_course_metadata: + _hydrate_normalized_metadata(course_metadata) + course_metadata.save() + + @ddt.ddt class TestTaskResultFunctions(TestCase): """ @@ -830,6 +862,8 @@ def setUp(self): self.course_run_metadata_unpublished.catalog_queries.set([course_run_catalog_query]) self.course_run_metadata_unpublished.save() + _hydrate_course_normalized_metadata() + def _set_up_factory_data_for_algolia(self): expected_catalog_uuids = sorted([ str(self.enterprise_catalog_courses.uuid), @@ -1030,6 +1064,7 @@ def test_index_algolia_program_common_uuids_only(self, mock_search_client): test_course_1.save() test_course_2.save() test_course_3.save() + _hydrate_course_normalized_metadata() actual_algolia_products_sent = [] @@ -1129,6 +1164,7 @@ def test_index_algolia_program_unindexable_content(self, mock_search_client): test_course_1.save() test_course_2.save() test_course_3.save() + _hydrate_course_normalized_metadata() actual_algolia_products_sent = [] @@ -2328,6 +2364,7 @@ def test_index_algolia_duplicate_content_uuids(self, mock_search_client): ) course_run_for_duplicate = ContentMetadataFactory(content_type=COURSE_RUN, parent_content_key='duplicateX') course_run_for_duplicate.catalog_queries.set([self.enterprise_catalog_course_runs.catalog_query]) + _hydrate_course_normalized_metadata() actual_algolia_products_sent_sequence = [] diff --git a/enterprise_catalog/apps/api/v1/export_utils.py b/enterprise_catalog/apps/api/v1/export_utils.py index ef451b83e..d988c48e0 100644 --- a/enterprise_catalog/apps/api/v1/export_utils.py +++ b/enterprise_catalog/apps/api/v1/export_utils.py @@ -170,69 +170,102 @@ def program_hit_to_row(hit): return csv_row -def course_hit_to_row(hit): - """ - Helper function to construct a CSV row according to a single Algolia result course hit. - """ - csv_row = [] - csv_row.append(hit.get('title')) - - if hit.get('partners'): - csv_row.append(hit['partners'][0]['name']) - else: - csv_row.append(None) +def _base_csv_row_data(hit): + """ Returns the formatted, shared attributes common across all course types. """ + title = hit.get('title') + aggregation_key = hit.get('aggregation_key') + language = hit.get('language') + transcript_languages = ', '.join(hit.get('transcript_languages', [])) + marketing_url = hit.get('marketing_url') + short_description = strip_tags(hit.get('short_description', '')) + subjects = ', '.join(hit.get('subjects', [])) + skills = ', '.join([skill['name'] for skill in hit.get('skills', [])]) + outcome = strip_tags(hit.get('outcome', '')) # What You’ll Learn + + # FIXME: currently ignores partner names when a course has multiple partners + partner_name = hit['partners'][0]['name'] if hit.get('partners') else None empty_advertised_course_run = {} advertised_course_run = hit.get('advertised_course_run', empty_advertised_course_run) + advertised_course_run_key = advertised_course_run.get('key') + min_effort = advertised_course_run.get('min_effort') + max_effort = advertised_course_run.get('max_effort') + weeks_to_complete = advertised_course_run.get('weeks_to_complete') # Length + if start_date := advertised_course_run.get('start'): start_date = parser.parse(start_date).strftime(DATE_FORMAT) - csv_row.append(start_date) if end_date := advertised_course_run.get('end'): end_date = parser.parse(end_date).strftime(DATE_FORMAT) - csv_row.append(end_date) - - # upgrade_deadline deprecated in favor of enroll_by - if upgrade_deadline := advertised_course_run.get('upgrade_deadline'): - upgrade_deadline = datetime.datetime.fromtimestamp(upgrade_deadline).strftime(DATE_FORMAT) - csv_row.append(upgrade_deadline) if enroll_by := advertised_course_run.get('enroll_by'): enroll_by = datetime.datetime.fromtimestamp(enroll_by).strftime(DATE_FORMAT) - csv_row.append(enroll_by) - - pacing_type = advertised_course_run.get('pacing_type') - key = advertised_course_run.get('key') - - csv_row.append(', '.join(hit.get('programs', []))) - csv_row.append(', '.join(hit.get('program_titles', []))) - - csv_row.append(pacing_type) - - csv_row.append(hit.get('level_type')) + content_price = None if content_price := advertised_course_run.get('content_price'): content_price = math.trunc(float(content_price)) - csv_row.append(content_price) + return { + 'title': title, + 'partner_name': partner_name, + 'start_date': start_date, + 'end_date': end_date, + 'enroll_by': enroll_by, + 'aggregation_key': aggregation_key, + 'advertised_course_run_key': advertised_course_run_key, + 'language': language, + 'transcript_languages': transcript_languages, + 'marketing_url': marketing_url, + 'short_description': short_description, + 'subjects': subjects, + 'skills': skills, + 'min_effort': min_effort, + 'max_effort': max_effort, + 'weeks_to_complete': weeks_to_complete, + 'outcome': outcome, + 'advertised_course_run': advertised_course_run, + 'content_price': content_price + } - csv_row.append(hit.get('language')) - csv_row.append(', '.join(hit.get('transcript_languages', []))) - csv_row.append(hit.get('marketing_url')) - csv_row.append(strip_tags(hit.get('short_description', ''))) - csv_row.append(', '.join(hit.get('subjects', []))) - csv_row.append(key) - csv_row.append(hit.get('aggregation_key')) +def course_hit_to_row(hit): + """ + Helper function to construct a CSV row according to a single Algolia result course hit. + """ + row_data = _base_csv_row_data(hit) + csv_row = [] + csv_row.append(row_data.get('title')) + csv_row.append(row_data.get('partner_name')) - skills = [skill['name'] for skill in hit.get('skills', [])] - csv_row.append(', '.join(skills)) + advertised_course_run = row_data.get('advertised_course_run') - advertised_course_run = hit.get('advertised_course_run', {}) - csv_row.append(advertised_course_run.get('min_effort')) - csv_row.append(advertised_course_run.get('max_effort')) - csv_row.append(advertised_course_run.get('weeks_to_complete')) # Length + csv_row.append(row_data.get('start_date')) + csv_row.append(row_data.get('end_date')) - csv_row.append(strip_tags(hit.get('outcome', ''))) # What You’ll Learn + # upgrade_deadline deprecated in favor of enroll_by + if upgrade_deadline := advertised_course_run.get('upgrade_deadline'): + upgrade_deadline = datetime.datetime.fromtimestamp(upgrade_deadline).strftime(DATE_FORMAT) + csv_row.append(upgrade_deadline) + csv_row.append(row_data.get('enroll_by')) + csv_row.append(', '.join(hit.get('programs', []))) + csv_row.append(', '.join(hit.get('program_titles', []))) + + pacing_type = advertised_course_run.get('pacing_type') + csv_row.append(pacing_type) + + csv_row.append(hit.get('level_type')) + csv_row.append(row_data.get('content_price')) + csv_row.append(row_data.get('language')) + csv_row.append(row_data.get('transcript_languages')) + csv_row.append(row_data.get('marketing_url')) + csv_row.append(row_data.get('short_description')) + csv_row.append(row_data.get('subjects')) + csv_row.append(row_data.get('advertised_course_run_key')) + csv_row.append(row_data.get('aggregation_key')) + csv_row.append(row_data.get('skills')) + csv_row.append(row_data.get('min_effort')) + csv_row.append(row_data.get('max_effort')) + csv_row.append(row_data.get('weeks_to_complete')) + csv_row.append(row_data.get('outcome')) csv_row.append(strip_tags(hit.get('prerequisites_raw', ''))) # Pre-requisites @@ -242,75 +275,32 @@ def course_hit_to_row(hit): return csv_row -def fetch_and_format_registration_date(obj): - enroll_by_date = obj.get('registration_deadline') - stripped_enroll_by = enroll_by_date.split("T")[0] - formatted_enroll_by = None - try: - enroll_by_datetime_obj = datetime.datetime.strptime(stripped_enroll_by, '%Y-%m-%d') - formatted_enroll_by = enroll_by_datetime_obj.strftime('%m-%d-%Y') - except ValueError as exc: - logger.info(f"Unable to format registration deadline, failed with error: {exc}") - return formatted_enroll_by - - def exec_ed_course_to_row(hit): """ Helper function to construct a CSV row according to a single executive education course hit. """ + row_data = _base_csv_row_data(hit) csv_row = [] - csv_row.append(hit.get('title')) - - if hit.get('partners'): - 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) - - 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 - formatted_enroll_by = None - - csv_row.append(formatted_enroll_by) - - adv_course_run = hit.get('advertised_course_run', {}) - key = adv_course_run.get('key') - - empty_advertised_course_run = {} - advertised_course_run = hit.get('advertised_course_run', empty_advertised_course_run) - if content_price := advertised_course_run.get('content_price'): - content_price = math.trunc(float(content_price)) - csv_row.append(content_price) - - csv_row.append(hit.get('language')) - csv_row.append(', '.join(hit.get('transcript_languages', []))) - csv_row.append(hit.get('marketing_url')) - csv_row.append(strip_tags(hit.get('short_description', ''))) - - csv_row.append(', '.join(hit.get('subjects', []))) - csv_row.append(key) - csv_row.append(hit.get('aggregation_key')) - - skills = [skill['name'] for skill in hit.get('skills', [])] - csv_row.append(', '.join(skills)) - - csv_row.append(adv_course_run.get('min_effort')) - csv_row.append(adv_course_run.get('max_effort')) - csv_row.append(adv_course_run.get('weeks_to_complete')) # Length - - csv_row.append(strip_tags(hit.get('outcome', ''))) # What You’ll Learn - + csv_row.append(row_data.get('title')) + csv_row.append(row_data.get('partners')) + + csv_row.append(row_data.get('start_date')) + csv_row.append(row_data.get('end_date')) + csv_row.append(row_data.get('enroll_by')) + + csv_row.append(row_data.get('content_price')) + csv_row.append(row_data.get('language')) + csv_row.append(row_data.get('transcript_languages')) + csv_row.append(row_data.get('marketing_url')) + csv_row.append(row_data.get('short_description')) + csv_row.append(row_data.get('subjects')) + csv_row.append(row_data.get('advertised_course_run_key')) + csv_row.append(row_data.get('aggregation_key')) + csv_row.append(row_data.get('skills')) + csv_row.append(row_data.get('min_effort')) + csv_row.append(row_data.get('max_effort')) + csv_row.append(row_data.get('weeks_to_complete')) + csv_row.append(row_data.get('outcome')) csv_row.append(strip_tags(hit.get('full_description', ''))) return csv_row 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 de6d8263f..03f63d992 100644 --- a/enterprise_catalog/apps/api/v1/tests/test_export_utils.py +++ b/enterprise_catalog/apps/api/v1/tests/test_export_utils.py @@ -15,16 +15,3 @@ def test_retrieve_available_fields(self): """ # assert that ALGOLIA_ATTRIBUTES_TO_RETRIEVE is a SUBSET of ALGOLIA_FIELDS assert set(export_utils.ALGOLIA_ATTRIBUTES_TO_RETRIEVE) <= set(algolia_utils.ALGOLIA_FIELDS) - - def test_fetch_and_format_registration_date(self): - """ - Test the export properly fetches executive education registration dates - """ - # expected hit format from algolia, porperly reformatted for csv download - assert export_utils.fetch_and_format_registration_date( - {'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( - {'registration_deadline': '02-15-2015T12:12:200'} - ) is None diff --git a/enterprise_catalog/apps/api/v1/tests/test_views.py b/enterprise_catalog/apps/api/v1/tests/test_views.py index 5df03b251..be9b0f2de 100644 --- a/enterprise_catalog/apps/api/v1/tests/test_views.py +++ b/enterprise_catalog/apps/api/v1/tests/test_views.py @@ -959,6 +959,7 @@ class EnterpriseCatalogWorkbookViewTests(APITestMixin): "weeks_to_complete": 6, "upgrade_deadline": 32503680000.0, "enroll_by": 32503680000.0, + "content_price": 2843.00 }, "course_runs": [ diff --git a/enterprise_catalog/apps/catalog/algolia_utils.py b/enterprise_catalog/apps/catalog/algolia_utils.py index 33b89212d..c926c4652 100644 --- a/enterprise_catalog/apps/catalog/algolia_utils.py +++ b/enterprise_catalog/apps/catalog/algolia_utils.py @@ -229,7 +229,7 @@ def course_run_not_active_checker(): return not is_course_run_active(advertised_course_run) def deadline_passed_checker(): - return _has_enroll_by_deadline_passed(course_json_metadata, advertised_course_run) + return _has_enroll_by_deadline_passed(course_json_metadata) for should_not_index_function, log_message in ( (no_advertised_course_run_checker, 'no advertised course run'), @@ -246,23 +246,18 @@ def deadline_passed_checker(): return True -def _has_enroll_by_deadline_passed(course_json_metadata, advertised_course_run): +def _has_enroll_by_deadline_passed(course_json_metadata): """ 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 registration deadline. - """ - enroll_by_deadline_timestamp = 0 - if course_json_metadata.get('course_type') == EXEC_ED_2U_COURSE_TYPE: - additional_metadata = course_json_metadata.get('additional_metadata') or {} - registration_deadline = additional_metadata.get('registration_deadline') - if registration_deadline: - enroll_by_deadline_timestamp = parse_datetime(registration_deadline).timestamp() + based on normalized_metadata's enroll_by_date + """ + enroll_by_deadline = course_json_metadata.get('normalized_metadata')['enroll_by_date'] + if isinstance(enroll_by_deadline, str): + enroll_by_deadline_timestamp = parse_datetime(enroll_by_deadline).timestamp() + return enroll_by_deadline_timestamp < localized_utcnow().timestamp() else: - enroll_by_deadline_timestamp = _get_verified_upgrade_deadline(advertised_course_run) - - return enroll_by_deadline_timestamp < localized_utcnow().timestamp() + # The check should fail if the enroll by date isn't present or can't be parsed + return True def partition_course_keys_for_indexing(courses_content_metadata): @@ -281,11 +276,16 @@ def partition_course_keys_for_indexing(courses_content_metadata): nonindexable_course_keys = set() for course_metadata in courses_content_metadata: - if _should_index_course(course_metadata): - indexable_course_keys.add(course_metadata.content_key) - else: - nonindexable_course_keys.add(course_metadata.content_key) - + try: + if _should_index_course(course_metadata): + indexable_course_keys.add(course_metadata.content_key) + else: + nonindexable_course_keys.add(course_metadata.content_key) + except Exception as e: # pylint: disable=broad-exception-caught + logger.warning( + f"Failed determining indexable status for course_metadata" + f"'{course_metadata.content_key}' due to: {e}" + ) return list(indexable_course_keys), list(nonindexable_course_keys) diff --git a/enterprise_catalog/apps/catalog/serializers.py b/enterprise_catalog/apps/catalog/serializers.py index 1238f6705..e9d7e8d41 100644 --- a/enterprise_catalog/apps/catalog/serializers.py +++ b/enterprise_catalog/apps/catalog/serializers.py @@ -138,12 +138,18 @@ def get_enroll_start_date(self, obj) -> str: # pylint: disable=unused-argument def get_enroll_by_date(self, obj) -> str: # pylint: disable=unused-argument if not self.course_run_metadata: return None + + if self.is_exec_ed_2u_course: + return self.course_run_metadata.get('enrollment_end') + all_seats = self.course_run_metadata.get('seats', []) - seat = _find_best_mode_seat(all_seats) + upgrade_deadline = None - if seat: + if seat := _find_best_mode_seat(all_seats): upgrade_deadline = seat.get('upgrade_deadline_override') or seat.get('upgrade_deadline') - return upgrade_deadline or self.course_run_metadata.get('enrollment_end') + + enrollment_end = self.course_run_metadata.get('enrollment_end') + return min(filter(None, [upgrade_deadline, enrollment_end]), default=None) @extend_schema_field(serializers.FloatField) def get_content_price(self, obj) -> float: # pylint: disable=unused-argument diff --git a/enterprise_catalog/apps/catalog/tests/test_algolia_utils.py b/enterprise_catalog/apps/catalog/tests/test_algolia_utils.py index ad4c07332..15efc5ab6 100644 --- a/enterprise_catalog/apps/catalog/tests/test_algolia_utils.py +++ b/enterprise_catalog/apps/catalog/tests/test_algolia_utils.py @@ -25,15 +25,28 @@ FUTURE_COURSE_RUN_UUID_1 = uuid4() FUTURE_COURSE_RUN_UUID_2 = uuid4() PAST_COURSE_RUN_UUID_1 = uuid4() +_days_cache = {} -def _days_from_now(days_from_now=0): - deadline = localized_utcnow() + timedelta(days=days_from_now) +def _days_from_now(days=0): + deadline = localized_utcnow() + timedelta(days=days) return deadline.strftime('%Y-%m-%dT%H:%M:%SZ') -def _days_from_now_timestamp(days_from_now): - return to_timestamp(_days_from_now(days_from_now)) +def _get_from_cache(days): + if days not in _days_cache: + deadline = _days_from_now(days) + + _days_cache[days] = {'str': deadline, 'timestamp': to_timestamp(deadline)} + return _days_cache[days] + + +def days_from_now(days): + return _get_from_cache(days)['str'] + + +def days_from_now_timestamp(days): + return _get_from_cache(days)['timestamp'] @ddt.ddt @@ -49,40 +62,56 @@ class AlgoliaUtilsTests(TestCase): {'expected_result': False, 'advertised_course_run_status': 'unpublished'}, {'expected_result': False, 'is_enrollable': False}, {'expected_result': False, 'is_marketable': False}, - {'expected_result': True, }, - {'expected_result': True, 'course_run_availability': None}, + {'expected_result': True, 'enrollment_end': days_from_now(30)}, + {'expected_result': True, 'course_run_availability': None, 'enrollment_end': days_from_now(30)}, { 'expected_result': True, 'seats': [ - {'type': 'verified', 'upgrade_deadline': _days_from_now(100)} + {'type': 'verified', 'upgrade_deadline': days_from_now(100)} ], + 'enrollment_end': days_from_now(100) }, { 'expected_result': True, 'seats': [ - {'type': 'something-else', 'upgrade_deadline': _days_from_now(-100)} + {'type': 'something-else', 'upgrade_deadline': days_from_now(-100)} ], + 'enrollment_end': days_from_now(100) }, { 'expected_result': False, 'seats': [ - {'type': 'verified', 'upgrade_deadline': _days_from_now(-1)} + {'type': 'verified', 'upgrade_deadline': days_from_now(-1)} ], }, { 'course_type': EXEC_ED_2U_COURSE_TYPE, 'expected_result': True, - 'additional_metadata': {'registration_deadline': '2073-03-21T23:59:59Z'}, + 'start': days_from_now(1), + 'end': days_from_now(30), + 'enrollment_end': days_from_now(1) }, { 'course_type': EXEC_ED_2U_COURSE_TYPE, 'expected_result': False, - 'additional_metadata': {'registration_deadline': '2021-03-21T23:59:59Z'}, + 'start': days_from_now(-30), + 'end': days_from_now(-1), + 'enrollment_end': days_from_now(-30) }, + # Error cases for invalid enrollment_end values { 'course_type': EXEC_ED_2U_COURSE_TYPE, 'expected_result': False, - 'additional_metadata': {'rando-key': 'blah'}, + 'start': days_from_now(-30), + 'end': days_from_now(-1), + 'enrollment_end': 12345 + }, + { + 'course_type': EXEC_ED_2U_COURSE_TYPE, + 'expected_result': False, + 'start': days_from_now(-30), + 'end': days_from_now(-1), + 'enrollment_end': None }, ) @ddt.unpack @@ -98,7 +127,9 @@ 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' ): """ Verify that only a course that has a non-hidden advertised course run, at least one owner, and a marketing slug @@ -119,10 +150,15 @@ 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, - 'additional_metadata': additional_metadata or {}, + 'normalized_metadata': { + 'enroll_by_date': enrollment_end + } } course_metadata = ContentMetadataFactory.create( content_type=COURSE, @@ -310,6 +346,7 @@ 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, @@ -331,10 +368,10 @@ def test_get_course_subjects(self, course_metadata, expected_subjects): 'max_effort': 14, 'weeks_to_complete': 13, 'upgrade_deadline': 32503680000.0, - 'enroll_by': None, 'enroll_start': 1380636000, - 'has_enroll_by': False, 'has_enroll_start': True, + 'has_enroll_by': True, + 'enroll_by': 1382018400.0, 'is_active': True, 'is_late_enrollment_eligible': False, 'content_price': 0.0, @@ -357,6 +394,8 @@ 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', + 'enrollment_start_date': '2013-10-01T14:00:00Z', 'availability': 'Current', 'min_effort': 10, 'max_effort': 14, @@ -390,11 +429,11 @@ def test_get_course_subjects(self, course_metadata, expected_subjects): 'max_effort': 14, 'weeks_to_complete': 13, 'upgrade_deadline': 1420386720.0, - 'enroll_by': 1420386720.0, - 'enroll_start': 1380636000, + 'enroll_by': 1382018400.0, + 'enroll_start': 1380636000.0, 'has_enroll_by': True, 'has_enroll_start': True, - 'content_price': 50.0, + 'content_price': 50, 'is_active': True, 'is_late_enrollment_eligible': False, } @@ -407,6 +446,7 @@ 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, @@ -433,9 +473,9 @@ def test_get_course_subjects(self, course_metadata, expected_subjects): 'max_effort': 14, 'weeks_to_complete': 13, 'upgrade_deadline': 32503680000.0, - 'enroll_by': None, + 'enroll_by': 1382018400.0, 'enroll_start': None, - 'has_enroll_by': False, + 'has_enroll_by': True, 'has_enroll_start': False, 'content_price': 50, 'is_active': True, @@ -510,6 +550,7 @@ 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, @@ -523,7 +564,7 @@ def test_get_upcoming_course_runs(self, searchable_course, expected_course_runs) 'is_enrollable': False, 'is_marketable': False, 'availability': 'Current', - 'start': _days_from_now(-20), + 'start': days_from_now(-20), 'end': "3022-12-31T23:59:00Z", 'min_effort': 2, 'max_effort': 6, @@ -533,7 +574,7 @@ def test_get_upcoming_course_runs(self, searchable_course, expected_course_runs) 'upgrade_deadline': None, 'price': "0.00", }], - 'enrollment_end': _days_from_now(-10), # enroll_by is within the late enrollment cutoff + 'enrollment_end': days_from_now(-10), # enroll_by is within the late enrollment cutoff 'first_enrollable_paid_seat_price': None, 'marketing_url': 'https://openedx.org', }, @@ -546,7 +587,7 @@ def test_get_upcoming_course_runs(self, searchable_course, expected_course_runs) 'is_enrollable': False, 'is_marketable': False, 'availability': 'Current', - 'start': _days_from_now(-50), + 'start': days_from_now(-50), 'end': "3022-12-31T23:59:00Z", 'min_effort': 2, 'max_effort': 6, @@ -557,7 +598,7 @@ def test_get_upcoming_course_runs(self, searchable_course, expected_course_runs) 'price': '0.00', }], 'first_enrollable_paid_seat_price': None, - 'enrollment_end': _days_from_now(-40), # enroll_by is beyond the late enrollment cutoff + 'enrollment_end': days_from_now(-40), # enroll_by is beyond the late enrollment cutoff 'marketing_url': 'https://openedx.org', }, # Late enrollment case (NOT eligible due to Archived; otherwise within the late enrollment cutoff) @@ -569,7 +610,7 @@ def test_get_upcoming_course_runs(self, searchable_course, expected_course_runs) 'is_enrollable': False, 'is_marketable': False, 'availability': 'Archived', - 'start': _days_from_now(-20), + 'start': days_from_now(-20), 'end': "3022-12-31T23:59:00Z", 'min_effort': 2, 'max_effort': 6, @@ -579,7 +620,7 @@ def test_get_upcoming_course_runs(self, searchable_course, expected_course_runs) 'upgrade_deadline': None, 'price': '0.00', }], - 'enrollment_end': _days_from_now(-10), # enroll_by is within the late enrollment cutoff + 'enrollment_end': days_from_now(-10), # enroll_by is within the late enrollment cutoff 'first_enrollable_paid_seat_price': None, 'marketing_url': 'https://openedx.org', }, @@ -593,7 +634,7 @@ def test_get_upcoming_course_runs(self, searchable_course, expected_course_runs) 'is_enrollable': False, 'is_marketable': False, 'availability': 'Current', - 'start': _days_from_now(-20), + 'start': days_from_now(-20), 'end': "3022-12-31T23:59:00Z", 'min_effort': 2, 'max_effort': 6, @@ -604,7 +645,7 @@ def test_get_upcoming_course_runs(self, searchable_course, expected_course_runs) 'price': '0.00', }], 'first_enrollable_paid_seat_price': None, - 'enrollment_end': _days_from_now(-10), # enroll_by is within the late enrollment cutoff + 'enrollment_end': days_from_now(-10), # enroll_by is within the late enrollment cutoff 'marketing_url': None, }, { @@ -615,18 +656,18 @@ def test_get_upcoming_course_runs(self, searchable_course, expected_course_runs) 'is_enrollable': True, 'is_marketable': True, 'availability': 'Current', - 'start': _days_from_now(-20), - 'end': _days_from_now(20), + 'start': days_from_now(-20), + 'end': days_from_now(20), 'min_effort': 2, 'max_effort': 6, 'weeks_to_complete': 6, 'seats': [{ 'type': 'verified', - 'upgrade_deadline': _days_from_now(10), + 'upgrade_deadline': days_from_now(10), 'price': '50.00', }], 'first_enrollable_paid_seat_price': 50, - 'enrollment_start': _days_from_now(-25), + 'enrollment_start': days_from_now(-25), }, { 'key': 'course-v1:org+course+1T3000', @@ -636,12 +677,12 @@ def test_get_upcoming_course_runs(self, searchable_course, expected_course_runs) 'is_enrollable': True, 'is_marketable': True, 'availability': 'Upcoming', - 'start': _days_from_now(10), - 'end': _days_from_now(20), + 'start': days_from_now(10), + 'end': days_from_now(20), 'min_effort': 2, 'max_effort': 6, 'weeks_to_complete': 6, - 'enrollment_start': _days_from_now(5), + 'enrollment_start': days_from_now(5), }, { 'key': 'course-v1:org+course+1T3022', @@ -651,8 +692,9 @@ def test_get_upcoming_course_runs(self, searchable_course, expected_course_runs) 'is_enrollable': True, 'is_marketable': True, 'availability': 'Starting Soon', - 'start': "3000-01-04T00:00:00Z", - 'end': "3022-12-31T23:59:00Z", + 'start': days_from_now(1000), + 'end': days_from_now(1100), + 'enrollment_end': days_from_now(1000), 'min_effort': 2, 'max_effort': 6, 'weeks_to_complete': 6, @@ -665,13 +707,13 @@ def test_get_upcoming_course_runs(self, searchable_course, expected_course_runs) 'key': 'course-v1:org+course+1T2025', 'pacing_type': 'instructor_paced', 'availability': 'Current', - 'start': _days_from_now(-20), + 'start': days_from_now(-20), 'end': "3022-12-31T23:59:00Z", 'min_effort': 2, 'max_effort': 6, 'weeks_to_complete': 6, 'upgrade_deadline': ALGOLIA_DEFAULT_TIMESTAMP, - 'enroll_by': _days_from_now_timestamp(-10), + 'enroll_by': days_from_now_timestamp(-10), 'enroll_start': None, 'has_enroll_by': True, 'has_enroll_start': False, @@ -683,13 +725,13 @@ def test_get_upcoming_course_runs(self, searchable_course, expected_course_runs) 'key': 'course-v1:org+course+1T2027', 'pacing_type': 'instructor_paced', 'availability': 'Archived', - 'start': _days_from_now(-20), + 'start': days_from_now(-20), 'end': "3022-12-31T23:59:00Z", 'min_effort': 2, 'max_effort': 6, 'weeks_to_complete': 6, 'upgrade_deadline': ALGOLIA_DEFAULT_TIMESTAMP, - 'enroll_by': _days_from_now_timestamp(-10), + 'enroll_by': days_from_now_timestamp(-10), 'enroll_start': None, 'has_enroll_by': True, 'has_enroll_start': False, @@ -701,13 +743,13 @@ def test_get_upcoming_course_runs(self, searchable_course, expected_course_runs) 'key': 'course-v1:org+course+1T2028', 'pacing_type': 'instructor_paced', 'availability': 'Current', - 'start': _days_from_now(-20), + 'start': days_from_now(-20), 'end': "3022-12-31T23:59:00Z", 'min_effort': 2, 'max_effort': 6, 'weeks_to_complete': 6, 'upgrade_deadline': ALGOLIA_DEFAULT_TIMESTAMP, - 'enroll_by': _days_from_now_timestamp(-10), + 'enroll_by': days_from_now_timestamp(-10), 'enroll_start': None, 'has_enroll_by': True, 'has_enroll_start': False, @@ -719,14 +761,14 @@ def test_get_upcoming_course_runs(self, searchable_course, expected_course_runs) 'key': 'course-v1:org+course+1T2021', 'pacing_type': 'instructor_paced', 'availability': 'Current', - 'start': _days_from_now(-20), - 'end': _days_from_now(20), + 'start': days_from_now(-20), + 'end': days_from_now(20), 'min_effort': 2, 'max_effort': 6, 'weeks_to_complete': 6, - 'upgrade_deadline': _days_from_now_timestamp(10), - 'enroll_by': _days_from_now_timestamp(10), - 'enroll_start': _days_from_now_timestamp(-25), + 'upgrade_deadline': days_from_now_timestamp(10), + 'enroll_by': days_from_now_timestamp(10), + 'enroll_start': days_from_now_timestamp(-25), 'has_enroll_by': True, 'has_enroll_start': True, 'content_price': 50, @@ -737,14 +779,14 @@ def test_get_upcoming_course_runs(self, searchable_course, expected_course_runs) 'key': 'course-v1:org+course+1T3000', 'pacing_type': 'instructor_paced', 'availability': 'Upcoming', - 'start': _days_from_now(10), - 'end': _days_from_now(20), + 'start': days_from_now(10), + 'end': days_from_now(20), 'min_effort': 2, 'max_effort': 6, 'weeks_to_complete': 6, 'upgrade_deadline': 32503680000.0, 'enroll_by': None, - 'enroll_start': _days_from_now_timestamp(5), + 'enroll_start': days_from_now_timestamp(5), 'has_enroll_by': False, 'has_enroll_start': True, 'content_price': 0.0, @@ -755,16 +797,16 @@ def test_get_upcoming_course_runs(self, searchable_course, expected_course_runs) 'key': 'course-v1:org+course+1T3022', 'pacing_type': 'instructor_paced', 'availability': 'Starting Soon', - 'start': "3000-01-04T00:00:00Z", - 'end': "3022-12-31T23:59:00Z", + 'start': days_from_now(1000), + 'end': days_from_now(1100), 'min_effort': 2, 'max_effort': 6, 'weeks_to_complete': 6, 'upgrade_deadline': 32503680000.0, - 'enroll_by': None, 'enroll_start': None, - 'has_enroll_by': False, 'has_enroll_start': False, + 'enroll_by': days_from_now_timestamp(1000), + 'has_enroll_by': True, 'content_price': 0.0, 'is_active': False, 'is_late_enrollment_eligible': False,