11"""
22This file contains celery tasks for contentstore views
33"""
4-
4+ from pytz import UTC
5+ from datetime import datetime
56import asyncio
67import base64
78import json
99100from .outlines_regenerate import CourseOutlineRegenerate
100101from .toggles import bypass_olx_failure_enabled
101102from .utils import course_import_olx_validation_is_enabled
103+ import logging
102104
103105User = get_user_model ()
104106
@@ -466,6 +468,7 @@ def create_export_tarball(course_block, course_key, context, status=None):
466468 return export_file
467469
468470
471+
469472class CourseImportTask (UserTask ): # pylint: disable=abstract-method
470473 """
471474 Base class for course and library import tasks.
@@ -529,11 +532,271 @@ def sync_discussion_settings(course_key, user):
529532 except Exception as exc : # pylint: disable=broad-except
530533 LOGGER .info (f'Course import { course .id } : DiscussionsConfiguration sync failed: { exc } ' )
531534
535+ # ========== PREREQUISITE IMPORT FUNCTIONS ==========
536+
537+
538+
539+ def _set_prerequisite_fields (sequential , is_prerequisite = False , serves_for_location = None ):
540+ """
541+ Set prerequisite-related fields on a sequential block.
542+ This function tries multiple possible field names.
543+ """
544+ field_set_success = False
545+
546+ # Try different field names for is_prerequisite ## TODO DEBUGGING ONLY I WILL HAVE TO CLEAN IT LATER
547+ possible_prereq_fields = [
548+ 'is_prereq' , 'is_prerequisite' , 'prereq' , 'prerequisite' ,
549+ 'is_gating_prerequisite' , 'gating_prereq'
550+ ]
551+
552+ for field_name in possible_prereq_fields :
553+ if hasattr (sequential , field_name ):
554+ setattr (sequential , field_name , is_prerequisite )
555+ logging .info (f"MOOVE: ✓ Set { field_name } = { is_prerequisite } " )
556+ field_set_success = True
557+ break
558+
559+ # Try different field names for serves_as_prereq_for ## TODO DEBUGGING ONLY I WILL HAVE TO CLEAN IT LATER
560+ if serves_for_location :
561+ possible_serves_fields = [
562+ 'serves_as_prereq_for' , 'prereq_for' , 'gating_prereq_for' ,
563+ 'required_for' , 'milestone_for'
564+ ]
565+
566+ for field_name in possible_serves_fields :
567+ if hasattr (sequential , field_name ):
568+ setattr (sequential , field_name , str (serves_for_location ))
569+ logging .info (f"MOOVE: ✓ Set { field_name } = { serves_for_location } " )
570+ field_set_success = True
571+ break
572+
573+ if not field_set_success :
574+ logging .warning ("MOOVE: ✗ No prerequisite fields found to set" )
575+
576+ return field_set_success
577+
578+ def _find_sequential_by_url_name (store , course_key , url_name ):
579+ """
580+ Find a sequential block by its url_name (block_id).
581+ """
582+ logging .info (f"MOOVE: Looking for sequential with url_name: { url_name } " )
583+
584+ # Method 1: Try qualifier search
585+ try :
586+ sequentials = store .get_items (
587+ course_key ,
588+ qualifiers = {'category' : 'sequential' , 'name' : url_name }
589+ )
590+ if sequentials :
591+ logging .info (f"MOOVE: ✓ Found via qualifier: { sequentials [0 ].location } " )
592+ return sequentials [0 ]
593+ except Exception as e :
594+ logging .info (f"MOOVE: Qualifier search failed: { e } " )
595+
596+ # Method 2: Manual search by block_id
597+ all_sequentials = store .get_items (
598+ course_key ,
599+ qualifiers = {'category' : 'sequential' }
600+ )
601+
602+ for sequential in all_sequentials :
603+ if sequential .location .block_id == url_name :
604+ logging .info (f"MOOVE: ✓ Found via block_id match: { sequential .location } " )
605+ return sequential
606+
607+ # Method 3: Try to construct the usage key directly
608+ try :
609+ sequential_key = BlockUsageLocator (
610+ course_key = course_key ,
611+ block_type = 'sequential' ,
612+ block_id = url_name
613+ )
614+ sequential = store .get_item (sequential_key )
615+ if sequential :
616+ logging .info (f"MOOVE: ✓ Found via direct key: { sequential .location } " )
617+ return sequential
618+ except Exception as e :
619+ logging .info (f"MOOVE: Direct key lookup failed: { e } " )
620+
621+ logging .warning (f"MOOVE: ✗ Sequential not found with url_name: { url_name } after all methods" )
622+
623+ return None
624+
625+ def _store_prerequisite_fallback (store , user_id , gated_sequential , required_sequential , prerequisite_info ):
626+ """
627+ Fallback method to store prerequisite information when GatingService is not available.
628+ """
629+ try :
630+ logging .info ("MOOVE: === FALLBACK PREREQUISITE STORAGE ===" )
631+
632+ # Store the relationship information on the GATED sequential
633+ if hasattr (gated_sequential , 'imported_prerequisite' ):
634+ gated_sequential .imported_prerequisite = {
635+ ** prerequisite_info ,
636+ 'required_sequential' : str (required_sequential .location ),
637+ 'setup_time' : datetime .now (UTC ).isoformat (),
638+ 'using_fallback' : True
639+ }
640+
641+ # Mark the REQUIRED sequential as being a prerequisite - THIS IS CRITICAL
642+ if hasattr (required_sequential , 'is_prerequisite' ):
643+ required_sequential .is_prerequisite = True
644+ logging .info (f"MOOVE: Fallback - Set is_prerequisite = True on { required_sequential .location } " )
645+ else :
646+ logging .warning ("MOOVE: Fallback - Required sequential doesn't have is_prerequisite field" )
647+
648+ # Store which sequential this serves as prerequisite for
649+ if hasattr (required_sequential , 'prerequisite_usage_key' ):
650+ required_sequential .prerequisite_usage_key = str (gated_sequential .location )
651+ logging .info (f"MOOVE: Fallback - Set prerequisite_usage_key = { gated_sequential .location } " )
652+
653+ # Update both blocks in the modulestore
654+ logging .info (f"MOOVE: Updating gated sequential: { gated_sequential .location } " )
655+ store .update_item (gated_sequential , user_id )
656+
657+ logging .info (f"MOOVE: Updating required sequential: { required_sequential .location } " )
658+ store .update_item (required_sequential , user_id )
659+
660+ return True
661+
662+ except Exception as e :
663+ logging .error (f"MOOVE: Fallback storage failed: { e } " )
664+ return False
665+
666+
667+ def _setup_prerequisite_relationship (store , user_id , sequential , prerequisite_info ):
668+ """
669+ Set up prerequisite relationship for a sequential block.
670+ """
671+ try :
672+ required_sequential_url = prerequisite_info .get ('required_sequential_url' )
673+ min_score = prerequisite_info .get ('min_score' )
674+ min_completion = prerequisite_info .get ('min_completion' )
675+
676+ if not required_sequential_url :
677+ return False
678+
679+ # Find the required sequential
680+ required_sequential = _find_sequential_by_url_name (
681+ store , sequential .location .course_key , required_sequential_url
682+ )
683+
684+ if not required_sequential :
685+ return False
686+
687+ # METHOD 1: Set the prerequisite field
688+ if hasattr (required_sequential , 'is_prerequisite' ):
689+ required_sequential .is_prerequisite = True
690+ store .update_item (required_sequential , user_id )
691+
692+ # METHOD 2: Use the gating API to create the actual relationship
693+ try :
694+ success = _create_proper_gating_milestone (
695+ store , user_id ,
696+ sequential , required_sequential ,
697+ min_score ,
698+ min_completion
699+ )
700+
701+ if success :
702+ return True
703+ else :
704+ return _store_prerequisite_fallback (store , user_id , sequential , required_sequential , prerequisite_info )
705+
706+ except Exception :
707+ return _store_prerequisite_fallback (store , user_id , sequential , required_sequential , prerequisite_info )
708+
709+ return True
710+
711+ except Exception :
712+ return False
713+
714+ def _process_course_prerequisites (course_key , user_id ):
715+ """
716+ Process all prerequisite relationships in the course after import.
717+ """
718+
719+ try :
720+ store = modulestore ()
721+
722+ # Get all sequential blocks
723+ sequentials = store .get_items (
724+ course_key ,
725+ qualifiers = {'category' : 'sequential' }
726+ )
727+
728+ logging .info (f"MOOVE: Found { len (sequentials )} sequential blocks to process" )
729+
730+ prerequisite_count = 0
731+ for sequential in sequentials :
732+ if hasattr (sequential , 'prerequisite' ) and sequential .prerequisite :
733+ prerequisite_info = sequential .prerequisite
734+ required_sequential_url = prerequisite_info .get ('required_sequential_url' )
735+
736+ if required_sequential_url :
737+ logging .info (f"MOOVE: ✓ Processing prerequisite for { sequential .location } " )
738+
739+ # Try multiple times with delays (in case of timing issues)
740+ max_retries = 3
741+ for attempt in range (max_retries ):
742+ logging .info (f"MOOVE: Attempt { attempt + 1 } to find required sequential" )
743+ required_sequential = _find_sequential_by_url_name (
744+ store , course_key , required_sequential_url
745+ )
746+
747+ if required_sequential :
748+ logging .info (f"MOOVE: ✓ Found required sequential on attempt { attempt + 1 } " )
749+ success = _setup_prerequisite_relationship (
750+ store , user_id , sequential , prerequisite_info
751+ )
752+ if success :
753+ prerequisite_count += 1
754+ logging .info (f"MOOVE: ✓ Successfully processed prerequisite for { sequential .location } " )
755+ else :
756+ logging .error (f"MOOVE: ✗ Failed to process prerequisite for { sequential .location } " )
757+ break
758+ else :
759+ if attempt < max_retries - 1 :
760+ logging .info (f"MOOVE: Required sequential not found, retrying... (attempt { attempt + 1 } /{ max_retries } )" )
761+ import time
762+ time .sleep (1 ) # Wait 1 second before retry
763+ else :
764+ logging .error (f"MOOVE: ✗ Required sequential not found after { max_retries } attempts: { required_sequential_url } " )
765+
766+ return prerequisite_count
767+
768+ except Exception as e :
769+ logging .error (f"MOOVE: Error processing prerequisites for course { course_key } : { str (e )} " )
770+ return 0
771+
772+
773+ def _create_proper_gating_milestone (store , user_id , gated_sequential , required_sequential , min_score , min_completion ):
774+ """
775+ Create the proper gating milestones that Studio actually checks.
776+ """
777+ try :
778+ from openedx .core .lib .gating import api as gating_api
779+
780+ course_key = gated_sequential .location .course_key
781+
782+ # Create the prerequisite milestone on the required sequential
783+ gating_api .add_prerequisite (course_key , required_sequential .location )
784+
785+ # Create the gating relationship
786+ gating_api .set_required_content (
787+ course_key ,
788+ gated_sequential .location ,
789+ required_sequential .location ,
790+ min_score ,
791+ min_completion
792+ )
793+
794+ return True
795+
796+ except Exception :
797+ return False
532798
533799@shared_task (base = CourseImportTask , bind = True )
534- # Note: The decorator @set_code_owner_attribute cannot be used here because the UserTaskMixin
535- # does stack inspection and can't handle additional decorators.
536- # lint-amnesty, pylint: disable=too-many-statements
537800def import_olx (self , user_id , course_key_string , archive_path , archive_name , language ):
538801 """
539802 Import a course or library from a provided OLX .tar.gz or .zip archive.
@@ -732,7 +995,7 @@ def read_chunk():
732995 LOGGER .info (f'{ log_prefix } : Extracted file verified. Updating course started' )
733996
734997 courselike_items = import_func (
735- modulestore (), user . id ,
998+ modulestore (), user_id ,
736999 settings .GITHUB_REPO_ROOT , [dirpath ],
7371000 load_error_blocks = False ,
7381001 static_content_store = contentstore (),
@@ -743,6 +1006,11 @@ def read_chunk():
7431006 new_location = courselike_items [0 ].location
7441007 LOGGER .debug ('new course at %s' , new_location )
7451008
1009+ # Process prerequisites after successful import
1010+ if is_course :
1011+ prerequisite_count = _process_course_prerequisites (courselike_key , user_id )
1012+ LOGGER .info (f'{ log_prefix } : Import completed with { prerequisite_count } prerequisites processed' )
1013+
7461014 LOGGER .info (f'{ log_prefix } : Course import successful' )
7471015 set_custom_attribute ('course_import_completed' , True )
7481016 except (CourseImportException , InvalidProctoringProvider , DuplicateCourseError ) as known_exe :
@@ -769,10 +1037,10 @@ def read_chunk():
7691037 from .views .entrance_exam import add_entrance_exam_milestone
7701038 add_entrance_exam_milestone (course .id , entrance_exam_chapter )
7711039 LOGGER .info (f'Course import { course .id } : Entrance exam imported' )
1040+
7721041 if is_course :
7731042 sync_discussion_settings (courselike_key , user )
7741043
775-
7761044@shared_task
7771045@set_code_owner_attribute
7781046def update_all_outlines_from_modulestore_task ():
0 commit comments