@@ -319,6 +319,34 @@ def add_elective(self, course):
319319
320320 return new_req # noqa: RET504
321321
322+ def add_program_requirement (self , required_program ):
323+ """Makes the specified program a required program"""
324+ self .get_requirements_root ().get_children ().filter (
325+ required_program = required_program
326+ ).delete ()
327+
328+ new_req = self ._add_course_node (ProgramRequirement .Operator .ALL_OF ).add_child (
329+ required_program = required_program ,
330+ node_type = ProgramRequirementNodeType .PROGRAM ,
331+ )
332+
333+ return new_req # noqa: RET504
334+
335+ def add_program_elective (self , required_program ):
336+ """Makes the specified program an elective program"""
337+ self .get_requirements_root ().get_children ().filter (
338+ required_program = required_program
339+ ).delete ()
340+
341+ new_req = self ._add_course_node (
342+ ProgramRequirement .Operator .MIN_NUMBER_OF
343+ ).add_child (
344+ required_program = required_program ,
345+ node_type = ProgramRequirementNodeType .PROGRAM ,
346+ )
347+
348+ return new_req # noqa: RET504
349+
322350 def save (self , * args , ** kwargs ):
323351 super ().save (* args , ** kwargs )
324352
@@ -413,6 +441,46 @@ def elective_courses(self) -> list:
413441 """
414442 return [course for (course , type ) in self .courses if type == "Elective Courses" ] # noqa: A001
415443
444+ @property
445+ def required_programs (self ):
446+ """
447+ Returns the programs that are required by this program.
448+
449+ Returns:
450+ - list of Program: programs that are requirements
451+ """
452+ return [
453+ req .required_program
454+ for req in ProgramRequirement .objects .filter (
455+ program = self ,
456+ node_type = ProgramRequirementNodeType .PROGRAM ,
457+ required_program__isnull = False ,
458+ )
459+ .select_related ("required_program" )
460+ .all ()
461+ if not req .get_parent ().elective_flag
462+ ]
463+
464+ @property
465+ def elective_programs (self ):
466+ """
467+ Returns the programs that are electives for this program.
468+
469+ Returns:
470+ - list of Program: programs that are electives
471+ """
472+ return [
473+ req .required_program
474+ for req in ProgramRequirement .objects .filter (
475+ program = self ,
476+ node_type = ProgramRequirementNodeType .PROGRAM ,
477+ required_program__isnull = False ,
478+ )
479+ .select_related ("required_program" )
480+ .all ()
481+ if req .get_parent ().elective_flag
482+ ]
483+
416484 def __str__ (self ):
417485 title = f"{ self .readable_id } | { self .title } "
418486 return title if len (title ) <= 100 else title [:97 ] + "..." # noqa: PLR2004
@@ -1479,6 +1547,7 @@ class ProgramRequirementNodeType(models.TextChoices):
14791547 PROGRAM_ROOT = "program_root" , "Program Root"
14801548 OPERATOR = "operator" , "Operator"
14811549 COURSE = "course" , "Course"
1550+ PROGRAM = "program" , "Program"
14821551
14831552
14841553class ProgramRequirement (MP_Node ):
@@ -1548,6 +1617,14 @@ class Operator(models.TextChoices):
15481617 blank = True ,
15491618 related_name = "in_programs" ,
15501619 )
1620+ required_program = models .ForeignKey (
1621+ "courses.Program" ,
1622+ on_delete = models .CASCADE ,
1623+ null = True ,
1624+ blank = True ,
1625+ related_name = "required_by" ,
1626+ help_text = "Program that is required to be completed" ,
1627+ )
15511628
15521629 title = models .TextField (null = True , blank = True , default = "" ) # noqa: DJ001
15531630 elective_flag = models .BooleanField (null = True , blank = True , default = False )
@@ -1572,6 +1649,11 @@ def is_course(self):
15721649 """True if the node references a course"""
15731650 return self .node_type == ProgramRequirementNodeType .COURSE
15741651
1652+ @property
1653+ def is_program (self ):
1654+ """True if the node references a program"""
1655+ return self .node_type == ProgramRequirementNodeType .PROGRAM
1656+
15751657 @property
15761658 def is_root (self ):
15771659 """True if the node is the root"""
@@ -1594,6 +1676,8 @@ def __str__(self):
15941676 attrs ["operator_value" ] = self .operator_value
15951677 elif self .is_course :
15961678 attrs ["course" ] = self .course
1679+ elif self .is_program :
1680+ attrs ["required_program" ] = self .required_program
15971681
15981682 return " " .join (f"{ key } ={ value } " for key , value in attrs .items ())
15991683
@@ -1609,13 +1693,15 @@ class Meta:
16091693 operator__isnull = True ,
16101694 operator_value__isnull = True ,
16111695 course__isnull = True ,
1696+ required_program__isnull = True ,
16121697 depth = 1 ,
16131698 )
16141699 # operator nodes
16151700 | Q (
16161701 node_type = ProgramRequirementNodeType .OPERATOR .value ,
16171702 operator__isnull = False ,
16181703 course__isnull = True ,
1704+ required_program__isnull = True ,
16191705 depth__gt = 1 ,
16201706 )
16211707 # course nodes
@@ -1624,6 +1710,16 @@ class Meta:
16241710 operator__isnull = True ,
16251711 operator_value__isnull = True ,
16261712 course__isnull = False ,
1713+ required_program__isnull = True ,
1714+ depth__gt = 1 ,
1715+ )
1716+ # program nodes
1717+ | Q (
1718+ node_type = ProgramRequirementNodeType .PROGRAM .value ,
1719+ operator__isnull = True ,
1720+ operator_value__isnull = True ,
1721+ course__isnull = True ,
1722+ required_program__isnull = False ,
16271723 depth__gt = 1 ,
16281724 )
16291725 ),
@@ -1638,6 +1734,8 @@ class Meta:
16381734 indexes = [
16391735 models .Index (fields = ("program" , "course" )),
16401736 models .Index (fields = ("course" , "program" )),
1737+ models .Index (fields = ("program" , "required_program" )),
1738+ models .Index (fields = ("required_program" , "program" )),
16411739 ]
16421740
16431741
0 commit comments