3636 (ETLSource .oll .name , PlatformType .edx .name ),
3737 ],
3838)
39- @pytest .mark .parametrize ("published" , [True , False ])
4039def test_sync_edx_course_files (
4140 mock_course_archive_bucket ,
4241 mocker ,
4342 source ,
4443 platform ,
45- published ,
4644): # pylint: disable=too-many-arguments,too-many-locals
4745 """Sync edx courses from a tarball stored in S3"""
4846 mock_load_content_files = mocker .patch (
@@ -61,47 +59,69 @@ def test_sync_edx_course_files(
6159 "learning_resources.etl.edx_shared.get_bucket_by_name" ,
6260 return_value = mock_course_archive_bucket .bucket ,
6361 )
62+ platform_obj = LearningResourcePlatformFactory .create (code = platform )
6463 courses = LearningResourceFactory .create_batch (
6564 2 ,
66- platform = LearningResourcePlatformFactory . create ( code = platform ) ,
65+ platform = platform_obj ,
6766 etl_source = source ,
6867 is_course = True ,
6968 published = True ,
7069 create_runs = False ,
7170 )
71+ # A fully-retired (unpublished, non-test_mode) course must be excluded
72+ retired_course = LearningResourceFactory .create (
73+ platform = platform_obj ,
74+ etl_source = source ,
75+ is_course = True ,
76+ published = False ,
77+ test_mode = False ,
78+ create_runs = False ,
79+ )
7280 keys = []
7381 s3_prefix = get_s3_prefix_for_source (source )
74- for course in courses :
75- runs = LearningResourceRunFactory .create_batch (
76- 2 ,
77- learning_resource = course ,
78- published = published ,
79- )
80- course .refresh_from_db ()
81- if published :
82- assert course .best_run in runs
83- keys .extend (
82+
83+ def add_keys_for_runs (runs ):
84+ new_keys = (
8485 [f"{ s3_prefix } /{ run .run_id } /foo.tar.gz" for run in runs ]
8586 if source != ETLSource .oll .name
8687 else [f"{ s3_prefix } /{ run .run_id } _OLL.tar.gz" for run in runs ]
8788 )
88- for key in keys :
89+ keys .extend (new_keys )
90+ for key in new_keys :
8991 with Path .open (
9092 Path ("test_json/course-v1:MITxT+8.01.3x+3T2022.tar.gz" ), "rb"
9193 ) as infile :
92- bucket .put_object (
93- Key = key ,
94- Body = infile .read (),
95- ACL = "public-read" ,
96- )
97- sync_edx_course_files (source , [course .id for course in courses ], keys )
98- # Only best runs for published courses are processed, so 2 runs (one per course) not 4
99- expected_calls = 2 if published else 0
100- assert mock_transform .call_count == expected_calls
101- assert mock_load_content_files .call_count == expected_calls
102- if published :
103- for course in courses :
104- mock_load_content_files .assert_any_call (course .best_run , fake_data )
94+ bucket .put_object (Key = key , Body = infile .read (), ACL = "public-read" )
95+
96+ for course in courses :
97+ runs = LearningResourceRunFactory .create_batch (
98+ 2 ,
99+ learning_resource = course ,
100+ published = True ,
101+ )
102+ course .refresh_from_db ()
103+ assert course .best_run in runs
104+ add_keys_for_runs (runs )
105+
106+ retired_runs = LearningResourceRunFactory .create_batch (
107+ 2 ,
108+ learning_resource = retired_course ,
109+ published = True ,
110+ )
111+ add_keys_for_runs (retired_runs )
112+
113+ sync_edx_course_files (
114+ source , [course .id for course in [* courses , retired_course ]], keys
115+ )
116+ # All runs of published courses are processed; retired-course runs are excluded
117+ assert mock_transform .call_count == 4
118+ assert mock_load_content_files .call_count == 4
119+ loaded_runs = {call .args [0 ] for call in mock_load_content_files .call_args_list }
120+ for course in courses :
121+ for run in course .runs .all ():
122+ assert run in loaded_runs
123+ for run in retired_runs :
124+ assert run not in loaded_runs
105125 mock_log .assert_not_called ()
106126
107127
@@ -692,24 +712,23 @@ def test_sync_edx_archive_no_run_found(mocker, mock_course_archive_bucket, etl_s
692712
693713
694714@pytest .mark .parametrize ("etl_source" , [ETLSource .mitxonline .name , ETLSource .xpro .name ])
695- def test_sync_edx_archive_not_best_run (mocker , mock_course_archive_bucket , etl_source ):
696- """Test sync_edx_archive skips processing when run is not the best run"""
715+ def test_sync_edx_archive_non_best_run_processed (
716+ mocker , mock_course_archive_bucket , etl_source
717+ ):
718+ """sync_edx_archive now processes a non-best run of a published course."""
697719 from learning_resources .etl .edx_shared import sync_edx_archive
698720
699721 platform = (
700722 PlatformType .mitxonline .name
701723 if etl_source == ETLSource .mitxonline .name
702724 else PlatformType .xpro .name
703725 )
704-
705- # Create a course with multiple runs
706726 course = LearningResourceFactory .create (
707727 platform = LearningResourcePlatformFactory .create (code = platform ),
708728 etl_source = etl_source ,
709729 published = True ,
710730 create_runs = False ,
711731 )
712- # Create older run (not best) with earlier start date
713732 from datetime import UTC , datetime
714733
715734 old_run = LearningResourceRunFactory .create (
@@ -718,43 +737,79 @@ def test_sync_edx_archive_not_best_run(mocker, mock_course_archive_bucket, etl_s
718737 run_id = "course-v1:Test+Course+R1" ,
719738 start_date = datetime (2022 , 1 , 1 , tzinfo = UTC ),
720739 )
721- # Create newer run (will be best) with later start date
722740 LearningResourceRunFactory .create (
723741 learning_resource = course ,
724742 published = True ,
725743 run_id = "course-v1:Test+Course+R2" ,
726744 start_date = datetime (2023 , 1 , 1 , tzinfo = UTC ),
727745 )
728746 course .refresh_from_db ()
729-
730- # Verify the newer run is the best run
731747 assert course .best_run .run_id == "course-v1:Test+Course+R2"
732748
733- # Archive is for the old run, not the best run
734749 bucket = mock_course_archive_bucket .bucket
735750 s3_key = "20220101/courses/course-v1:Test+Course+R1/abcdefghijklmnop.tar.gz"
736-
737751 with Path .open (
738752 Path ("test_json/course-v1:MITxT+8.01.3x+3T2022.tar.gz" ), "rb"
739753 ) as infile :
740754 bucket .put_object (Key = s3_key , Body = infile .read (), ACL = "public-read" )
755+ mocker .patch (
756+ "learning_resources.etl.edx_shared.get_bucket_by_name" , return_value = bucket
757+ )
758+ mock_process = mocker .patch (
759+ "learning_resources.etl.edx_shared.process_course_archive"
760+ )
741761
762+ sync_edx_archive (etl_source , s3_key , overwrite = False )
763+
764+ mock_process .assert_called_once ()
765+ assert mock_process .call_args [0 ][2 ] == old_run
766+
767+
768+ @pytest .mark .parametrize ("etl_source" , [ETLSource .mitxonline .name , ETLSource .xpro .name ])
769+ def test_sync_edx_archive_retired_course_skipped (
770+ mocker , mock_course_archive_bucket , etl_source
771+ ):
772+ """sync_edx_archive still skips runs of a fully-retired (unpublished, non-test_mode) course."""
773+ from learning_resources .etl .edx_shared import sync_edx_archive
774+
775+ platform = (
776+ PlatformType .mitxonline .name
777+ if etl_source == ETLSource .mitxonline .name
778+ else PlatformType .xpro .name
779+ )
780+ course = LearningResourceFactory .create (
781+ platform = LearningResourcePlatformFactory .create (code = platform ),
782+ etl_source = etl_source ,
783+ published = False ,
784+ test_mode = False ,
785+ create_runs = False ,
786+ )
787+ LearningResourceRunFactory .create (
788+ learning_resource = course ,
789+ published = False ,
790+ run_id = "course-v1:Test+Course+R1" ,
791+ )
792+ bucket = mock_course_archive_bucket .bucket
793+ s3_key = "20220101/courses/course-v1:Test+Course+R1/abcdefghijklmnop.tar.gz"
794+ with Path .open (
795+ Path ("test_json/course-v1:MITxT+8.01.3x+3T2022.tar.gz" ), "rb"
796+ ) as infile :
797+ bucket .put_object (Key = s3_key , Body = infile .read (), ACL = "public-read" )
742798 mocker .patch (
743- "learning_resources.etl.edx_shared.get_bucket_by_name" ,
744- return_value = bucket ,
799+ "learning_resources.etl.edx_shared.get_bucket_by_name" , return_value = bucket
800+ )
801+ # No matching run is returned for a retired course, so ETL is triggered instead.
802+ mock_trigger = mocker .patch (
803+ "learning_resources.etl.edx_shared.trigger_resource_etl"
745804 )
746805 mock_process = mocker .patch (
747806 "learning_resources.etl.edx_shared.process_course_archive"
748807 )
749- mock_log = mocker .patch ("learning_resources.etl.edx_shared.log.warning" )
750808
751809 sync_edx_archive (etl_source , s3_key , overwrite = False )
752810
753- # Should log warning and not process
754- assert mock_log .called
755- assert "not the best run" in mock_log .call_args [0 ][0 ]
756- assert old_run .run_id in mock_log .call_args [0 ][1 ]
757811 mock_process .assert_not_called ()
812+ mock_trigger .assert_called_once_with (etl_source )
758813
759814
760815@pytest .mark .parametrize ("etl_source" , [ETLSource .mitxonline .name , ETLSource .xpro .name ])
@@ -1063,6 +1118,22 @@ def test_build_run_lookup(source, platform):
10631118 assert lookup [normalized ][0 ].id == run .id
10641119
10651120
1121+ def test_build_run_lookup_oll_strips_mitx_prefix ():
1122+ """OLL runs are also indexed without the MITx prefix the archives omit"""
1123+ source = ETLSource .oll .name
1124+ course = LearningResourceFactory .create (
1125+ etl_source = source , published = True , create_runs = False
1126+ )
1127+ run = LearningResourceRunFactory .create (
1128+ learning_resource = course , run_id = "MITx+0.501x+2T2019" , published = True
1129+ )
1130+ lookup = build_run_lookup (source , [course .id ])
1131+ # archive filenames like 0_501x_2T2019_OLL.tar.gz normalize without the prefix
1132+ assert "0.501x.2t2019" in lookup
1133+ assert lookup ["0.501x.2t2019" ][0 ].id == run .id
1134+ assert "mitx.0.501x.2t2019" in lookup
1135+
1136+
10661137def test_build_run_lookup_filters_by_ids ():
10671138 """build_run_lookup only includes runs for the specified course ids"""
10681139 source = ETLSource .mitxonline .name
0 commit comments