Commit d47043b6 authored by Lincoln Smith's avatar Lincoln Smith
Browse files

Improved handling of weird alignments in build_reqnode_tree().

parent 0a120889
Library for scraping [Program Orders](https://policies.anu.edu.au/ppl/document/ANUP_006803) from the ANU [Programs and Courses](https://programsandcourses.anu.edu.au/) website.
Provides functions for processing P&C html into an intermediate `dr_scarper.ReqNode` representation that attempts to preserve layout information, and a `dr_scraper.DegreeRuleScraper` class that can be used to generate a `dr_scraper.ProgramOrder` tree representaion of a set of requirements that should be amenable to further processing.
Provides functions for processing P&C html into an intermediate `dr_scraper.ReqNode` representation that attempts to preserve layout information, and a `dr_scraper.DegreeRuleScraper` class that can be used to generate a `dr_scraper.ProgramOrder` tree representaion of a set of requirements that should be amenable to further processing.
## Usage
Typical usage is to create a `DegreeRuleScraper` with a url:
```python
from dr_scraper import DegreeRuleScraper
scraper = DegreeRuleScraper('https://programsandcourses.anu.edu.au/program/VCOMP')
orders = scraper.build_program_order_struct()
```
The scraper class will attempt to interpret the path as a file path if processing it as a URL fails. You can force interpretation as a file path by setting `path_is_file`:
```python
scraper = DegreeRuleScraper('/some/file/here.html', path_is_file=True)
```
The other common parameter to change is `header_id`. Degree/program level plans use the default `header_id`, subplans like majors and minors have a different page structure and you will need to set `header_id` as below:
```python
scraper = DegreeRuleScraper('https://programsandcourses.anu.edu.au/specialisation/HCSD-SPEC', header_id='requirements')
```
This diff is collapsed.
<html>
<head>
</head>
<body id="top">
<div class="main">
<div class="main-inner">
<div class="body transition">
<div id="study" class="tab-content" style="display: inline;">
<div class="body__inner w-doublewide copy">
<h2 id="program-requirements">Program Requirements</h2>
<p>Either:</p>
<p style="margin-left: 40px;">30 units from completion of</p>
<p>Or:</p>
<p style="margin-left: 40px;">24 units from completion of</p>
<p style="margin-left: 80px;">Artificial Intelligence&nbsp;<br>Data Science<br>Human Centred Design and Software Development</p>
<p><br>&nbsp;</p>
<p style="margin-left: 40px;">Unless otherwise stated</p>
</div>
</div>
<a class="back-to-top" href="#top">Back to the top</a>
</div>
</div>
</div>
</body>
</html>
<html>
<head>
</head>
<body id="top">
<div class="subplan">
<div class="main">
<div class="main-inner">
<div class="body transition">
<div id="study" class="tab-content" style="display: inline;">
<div class="body__inner w-doublewide copy">
<h2 id="requirements">Requirements</h2>
<p>The specialisation requires completion of 24 units</p>
<p>A minimum of 12 units</p>
<p style="margin-left: 40px;">A maximum of 12 units</p>
<p style="margin-left: 80px;"><a href="/2019/course/COMP6353">COMP6353</a> Systems Engineering for Software Engineers<br><a href="/2019/course/COMP6461">COMP6461</a> Computer Graphics</p>
<p style="margin-left: 40px;">A minimum of 12 units</p>
<p style="margin-left: 80px;"><a href="/2019/course/COMP8173">COMP8173</a> Software Engineering Processes<br><a href="/2019/course/COMP8190">COMP8190</a> Model-Driven Software Development<br>&nbsp;</p>
<a class="back-to-top" href="#top">Back to the top</a>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
......@@ -11,7 +11,6 @@
<p style="margin-left: 40px;">A minimum of 48 units</p>
<p>The 96 units must consist of:</p>
<p style="margin-left: 40px;">36 units from completion of:</p>
<p style="margin-left: -40px;">Negative margin!</p>
<p style="margin-left: 80px;"><a href="/2019/course/COMP6442">COMP6442</a> Software Construction&nbsp;<br><a href="/2019/course/COMP8260">COMP8260</a> Professional Practice 2<br><a href="/2019/course/COMP6445">COMP6445</a> Computing Research Methods<br><a href="/2019/course/COMP6331">COMP6331</a> Computer Networks<br><a href="/2019/course/COMP6420">COMP6420</a> Introduction to Data Management, Analysis and Security<br><a href="/2019/course/COMP6120">COMP6120</a> Software Engineering&nbsp;</p>
<p><br>&nbsp;</p>
<p>Either:</p>
......
<html>
<head>
</head>
<body id="top">
<div class="subplan">
<div class="main">
<div class="main-inner">
<div class="body transition">
<div id="study" class="tab-content" style="display: inline;">
<div class="body__inner w-doublewide copy">
<h2 id="requirements">Requirements</h2>
<p>The specialisation requires completion of 24 units, which must consist of</p>
<p>A minimum of 12 units must come from 8000-level courses</p>
<p style="margin-left: 40px;">A maximum of 12 units from completion of courses from the following list:</p>
<p style="margin-left: 80px;"><a href="/2019/course/COMP6353">COMP6353</a> Systems Engineering for Software Engineers<br><a href="/2019/course/COMP6390">COMP6390</a> HCI and Usability Engineering<br><a href="/2019/course/COMP6461">COMP6461</a> Computer Graphics</p>
<p style="margin-left: 40px;">A minimum of 12 units from completion of courses from the following list:</p>
<p style="margin-left: 80px;"><a href="/2019/course/COMP8100">COMP8100</a> Requirements Elicitation and Analysis Techniques<br><a href="/2019/course/COMP8173">COMP8173</a> Software Engineering Processes<br><a href="/2019/course/COMP8190">COMP8190</a> Model-Driven Software Development<br><a href="/2019/course/COMP8420">COMP8420</a> Bio-Inspired Computing Applications &amp; Interfaces<br><a href="/2019/course/VCPG6001">VCPG6001</a> Unravelling Complexity<br><a href="/2019/course/VCPG6004">VCPG6004</a> Creating Impact<br><a href="/2019/course/VCPG8001">VCPG8001</a> Dealing with Wicked Problems<br>&nbsp;</p>
<a class="back-to-top" href="#top">Back to the top</a>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
<html>
<head></head>
<body id="top">
<div class="main">
<div class="main-inner">
<div class="body transition">
<div id="study" class="tab-content" style="display: inline;">
<div class="body__inner w-doublewide copy">
<h2 id="program-requirements">Program Requirements</h2>
<p style="padding-left: 30px;">24 units from completion of one of the following Engineering minors:</p>
<p style="padding-left: 60px;"><a href="../minor/BMSY-MIN">Biomedical Systems</a></p>
<p style="padding-left: 60px;"><a href="../minor/ELCO-MIN">Electronic and Communication Systems</a></p>
<p style="padding-left: 60px;"><a href="../minor/MMSY-MIN">Mechanical and Material Systems</a></p>
<h2 id="minors">Minors</h2>
<div class="body__inner__columns">
<div class="w-narrow body__inner__column body__inner__column-first">
<ul>
<li>
<a href="/minor/BMSY-MIN">Biomedical Systems</a>
</li>
<li>
<a href="/minor/ELCO-MIN">Electronic and Communication Systems</a>
</li>
<li>
<a href="/minor/MMSY-MIN">Mechanical and Material Systems</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<a class="back-to-top" href="#top">Back to the top</a>
</div>
</div>
</body>
</html>
<html>
<head>
</head>
<body id="top">
<div class="main">
<div class="main-inner">
<div class="body transition">
<div id="study" class="tab-content" style="display: inline;">
<div class="body__inner w-doublewide copy">
<h2 id="program-requirements">Program Requirements</h2>
<p style="margin-left: 40px;">Students who do not achieve a GPA of 6 in the first 48 units of courses attempted will be transferred to the Master of Computing.</p>
</div>
</div>
<a class="back-to-top" href="#top">Back to the top</a>
</div>
</div>
</div>
</body>
</html>
<html><head>
</head>
<body id="top">
<div class="main">
<div class="main-inner">
<div class="body transition">
<div id="study" class="tab-content" style="display: inline;">
<div class="body__inner w-doublewide copy">
<h2 id="program-requirements">Program Requirements</h2>
<p style="margin-left: 40px;">This specialisation must be taken in conjunction with the Cyber Security major</p>
<h2 id="majors">Majors</h2>
<div class="body__inner__columns">
<div class="w-narrow body__inner__column body__inner__column-first">
<ul>
<li>
<a href="/2019/major/CSEC-MAJ">Cyber Security</a>
</li>
</ul>
</div>
<div class="w-narrow body__inner__column">
<ul>
</ul>
</div>
<div class="w-narrow body__inner__column body__inner__column-last">
<ul>
</ul>
</div>
</div>
</div>
</div>
</div>
<a class="back-to-top" href="#top">Back to the top</a>
</div>
</div>
</body>
</html>
<html>
<head></head>
<body id="top">
<div class="main">
<div class="main-inner">
<div class="body transition">
<div id="study" class="tab-content" style="display: inline;">
<div class="body__inner w-doublewide copy">
<h2 id="program-requirements">Program Requirements</h2>
<p style="padding-left: 30px;">This specialisation must be taken in conjunction with a major from the following list</p>
<p style="padding-left: 60px;"><a href="../major/BMSY-MAJ">Biomedical Systems</a></p>
<p style="padding-left: 60px;"><a href="../major/ELCO-MAJ">Electronic and Communication Systems</a></p>
<p style="padding-left: 60px;"><a href="../major/MMSY-MAJ">Mechanical and Material Systems</a></p>
<h2 id="majors">Majors</h2>
<div class="body__inner__columns">
<div class="w-narrow body__inner__column body__inner__column-first">
<ul>
<li>
<a href="/major/BMSY-MAJ">Biomedical Systems</a>
</li>
<li>
<a href="/major/ELCO-MAJ">Electronic and Communication Systems</a>
</li>
<li>
<a href="/major/MMSY-MAJ">Mechanical and Material Systems</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<a class="back-to-top" href="#top">Back to the top</a>
</div>
</div>
</body>
</html>
<html class="js rgba opacity fontface wf-proximanova-n7-active wf-proximanova-i7-active wf-proximanova-n6-active wf-proximanova-i6-active wf-proximanova-n4-active wf-proximanova-i4-active wf-active" style=""><head>
</head>
<body id="top">
<div class="main">
<div class="main-inner">
<div class="body transition">
<div id="study" class="tab-content">
<div class="body__inner w-doublewide copy">
<h2 id="program-requirements">Program Requirements</h2>
<p>The Master of Computing (Advanced) requires the completion of 96 units, of which:</p>
<p style="margin-left: 40px;">A minimum of 48 units must come from completion of 8000-level courses</p>
<p>The 96 units must consist of:</p>
<p style="margin-left: 40px;">36 units from completion of the following compulsory courses:</p>
<p style="margin-left: 80px;"><a href="/2019/course/COMP6442">COMP6442</a> Software Construction&nbsp;<br><a href="/2019/course/COMP8260">COMP8260</a> Professional Practice 2<br><a href="/2019/course/COMP6445">COMP6445</a> Computing Research Methods<br><a href="/2019/course/COMP6331">COMP6331</a> Computer Networks<br><a href="/2019/course/COMP6420">COMP6420</a> Introduction to Data Management, Analysis and Security<br><a href="/2019/course/COMP6120">COMP6120</a> Software Engineering&nbsp;</p>
<p><br>&nbsp;</p>
<p>Either:</p>
<p style="margin-left: 40px;">30 units from completion of further courses from the subject area COMP Computer Science, including non-COMP courses included in specialisation lists.</p>
<p>Or:</p>
<p style="margin-left: 40px;">6 units from completion of further courses from the subject area COMP Computer Science</p>
<h2 id="specialisations">Specialisations</h2>
<div class="body__inner__columns">
<div class="w-narrow body__inner__column body__inner__column-first">
<ul>
<li>
<a href="/2019/specialisation/AINT-SPEC">Artificial Intelligence</a>
</li>
</ul>
</div>
<div class="w-narrow body__inner__column">
<ul>
<li>
<a href="/2019/specialisation/DTSC-SPEC">Data Science</a>
</li>
</ul>
</div>
<div class="w-narrow body__inner__column body__inner__column-last">
<ul>
<li>
<a href="/2019/specialisation/HCSD-SPEC">Human Centred Design and Software Development</a>
</li>
</ul>
</div>
</div>
<h2 id="studyoptions">Study Options</h2>
<div class="table-container">
<table class="table-program">
<tbody>
<tr>
<td class="table-program__year" rowspan="2">
<span class="table-program__year-label">Year 1</span>
<span class="table-program__units-label">48 units</span>
</td>
<td class="table-program__course ">
<span class="table-program__course-type"><a href="/2019/course/COMP6442">COMP6442</a></span>
<span class="table-program__course-level">Software Construction</span>
<span class="table-program__course-units">6 units</span>
</td>
<td class="table-program__course ">
<span class="table-program__course-type"><a href="/2019/course/COMP6445">COMP6445</a></span>
<span class="table-program__course-level">Advanced Computing Research Methods</span>
<span class="table-program__course-units">6 units</span>
</td>
<td class="table-program__course ">
<span class="table-program__course-type"><a href="/2019/course/COMP6420">COMP6420</a></span>
<span class="table-program__course-level">Introduction to Data Management, Analysis and Security</span>
<span class="table-program__course-units">6 units</span>
</td>
<td class="table-program__course ">
<span class="table-program__course-type"><a href="/2019/course/COMP6331">COMP6331</a></span>
<span class="table-program__course-level">Computer Networks</span>
<span class="table-program__course-units">6 units</span>
</td>
</tr>
<tr>
<td class="table-program__course ">
<span class="table-program__course-type"><a href="/2019/course/COMP8260">COMP8260</a></span>
<span class="table-program__course-level">Professional Practice 2</span>
<span class="table-program__course-units">6 units</span>
</td>
<td class="table-program__course ">
<span class="table-program__course-type"><a href="/2019/course/COMP6120">COMP6120</a></span>
<span class="table-program__course-level">Software Engineering</span>
<span class="table-program__course-units">6 units</span>
</td>
<td class="table-program__course ">
<span class="table-program__course-type">Computing Elective</span>
<span class="table-program__course-units">6 units</span>
</td>
<td class="table-program__course ">
<span class="table-program__course-type">Specialisation Course/ Computing Elective</span>
<span class="table-program__course-units">6 units</span>
</td>
</tr>
<tr>
<td class="table-program__year" rowspan="2">
<span class="table-program__year-label">Year 2</span>
</td>
<td class="table-program__course ">
<span class="table-program__course-type"><a href="/2019/course/COMP8800">COMP8800</a></span>
<span class="table-program__course-level">Computing Research Project</span>
<span class="table-program__course-units">12 units</span>
</td>
<td class="table-program__course ">
<span class="table-program__course-type"></span>
</td>
<td class="table-program__course ">
<span class="table-program__course-type">Computing Elective</span>
<span class="table-program__course-units">6 units</span>
</td>
<td class="table-program__course ">
<span class="table-program__course-type">Specialisation Course/ Computing Elective</span>
<span class="table-program__course-units">6 units</span>
</td>
</tr>
<tr>
<td class="table-program__course ">
<span class="table-program__course-type"><a href="/2019/course/COMP8800">COMP8800</a></span>
<span class="table-program__course-level">Computing Research Project</span>
<span class="table-program__course-units">12 units</span>
</td>
<td class="table-program__course ">
<span class="table-program__course-type"></span>
</td>
<td class="table-program__course ">
<span class="table-program__course-type">Specialisation Course/ Computing Elective</span>
<span class="table-program__course-units">6 units</span>
</td>
<td class="table-program__course ">
<span class="table-program__course-type">Specialisation Course/ Computing Elective</span>
<span class="table-program__course-units">6 units</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<a class="back-to-top" href="#top">Back to the top</a>
</div>
</div>
</div>
</body></html>
......@@ -43,20 +43,18 @@ class TestMiscMethods(unittest.TestCase):
self.assertTupleEqual(
req_tree.dump_tree(),
([''], -1, [
(['The Master of Computing:'], 0, [(['A minimum of 48 units'], 40, []), ]),
(['The 96 units must consist of:'], 0, [
(['36 units from completion of:'], 40, [
(['COMP6442', 'Software Construction', 'COMP8260',
'Professional Practice 2', 'COMP6445', 'Computing Research Methods',
'COMP6331', 'Computer Networks', 'COMP6420',
'Introduction to Data Management, Analysis and Security', 'COMP6120',
'Software Engineering'], 80, []),
]),
(['The Master of Computing:'], 0, []),
(['A minimum of 48 units'], 40, []),
(['36 units from completion of:'], 40, [
(['COMP6442', 'Software Construction', 'COMP8260',
'Professional Practice 2', 'COMP6445', 'Computing Research Methods',
'COMP6331', 'Computer Networks', 'COMP6420',
'Introduction to Data Management, Analysis and Security', 'COMP6120',
'Software Engineering'], 80, []),
]),
(['Either:'], 0, [(['30 units from completion of.'], 40, []), ]),
(['Or:'], 0, [(['6 units from completion of'], 40, []), ]),
]
)
])
)
def test_build_reqnode_tree_basic_tree2(self):
......@@ -73,31 +71,26 @@ class TestMiscMethods(unittest.TestCase):
self.assertTupleEqual(
req_tree.dump_tree(),
([''], -1, [
(['The Bachelor of Advanced Computing:'], 0, [
(['A maximum of 60 units'], 40, []),
(['A minimum of 24 units.'], 40, [])
(['The Bachelor of Advanced Computing:'], 0, []),
(['A maximum of 60 units'], 40, []),
(['A minimum of 24 units.'], 40, []),
(['54 units from completion of:'], 40, [
(['COMP1600', 'Foundations of Computing'], 80, []),
(['COMP2100', 'Software Design Methodologies'], 80, [])
]),
(['The 192 units must include:'], 0, [
(['54 units from completion of:'], 40, [
(['COMP1600', 'Foundations of Computing'], 80, []),
(['COMP2100', 'Software Design Methodologies'], 80, [])
]),
(['6 units from completion of:'], 40, [
(['COMP1100', 'Programming as Problem Solving'], 80, []),
(['COMP1130', 'Programming as Problem Solving (Advanced)'], 80, [])
]),
(['Either:'], 40, [
(['12 units from completion of'], 80, [])
]),
(['Or:'], 40, [
(['12 units from completion of:'], 80, [
(['ENGN3230', 'Engineering Innovation'], 120, []),
(['VCUG3001', 'Unravelling Complexity'], 120, []),
(['VCUG3002', 'Mobilising Research'], 120, [])
])
]),
(['48 units from completion of'], 40, [])
])
(['6 units from completion of:'], 40, [
(['COMP1100', 'Programming as Problem Solving'], 80, []),
(['COMP1130', 'Programming as Problem Solving (Advanced)'], 80, [])
]),
(['Either:'], 40, [(['12 units from completion of'], 80, [])]),
(['Or:'], 40, [
(['12 units from completion of:'], 80, [
(['ENGN3230', 'Engineering Innovation'], 120, []),
(['VCUG3001', 'Unravelling Complexity'], 120, []),
(['VCUG3002', 'Mobilising Research'], 120, [])
])
]),
(['48 units from completion of'], 40, [])
])
)
......@@ -133,6 +126,47 @@ class TestMiscMethods(unittest.TestCase):
])
)
def test_build_reqnode_tree_bad_indent(self):
"""
Process a page with mis-aligned requirements
"""
with open('tests/sample_html/bad_indent.html') as sample_file:
soup = BeautifulSoup(sample_file.read(), 'html5lib')
req_tree = build_reqnode_tree(get_requirements_block(soup))
self.assertTupleEqual(
req_tree.dump_tree(),
([''], -1, [
(['Either:'], 0, [(['30 units from completion of'], 40, [])]),
(['Or:'], 0, [
(['24 units from completion of'], 40, [
(['Artificial Intelligence',
'Data Science',
'Human Centred Design and Software Development'], 80, [])
])
]),
(['Unless otherwise stated'], 40, [])
])
)
with open('tests/sample_html/bad_indent2.html') as sample_file:
soup = BeautifulSoup(sample_file.read(), 'html5lib')
req_tree = build_reqnode_tree(get_requirements_block(soup), header_id='requirements')
self.assertTupleEqual(
req_tree.dump_tree(),
([''], -1, [
(['The specialisation requires completion of 24 units'], 0, []),
(['A minimum of 12 units'], 0, []),
(['A maximum of 12 units'], 40, [
(['COMP6353', 'Systems Engineering for Software Engineers', 'COMP6461',
'Computer Graphics'], 80, [])
]),
(['A minimum of 12 units'], 40, [
(['COMP8173', 'Software Engineering Processes', 'COMP8190',
'Model-Driven Software Development'], 80, [])
])
])
)
class TestDegreeRuleScraper(unittest.TestCase):
def test_init(self):
......@@ -143,10 +177,13 @@ class TestDegreeRuleScraper(unittest.TestCase):
self.assertEqual(scraper.year, str(date.today().year))
self.assertEqual(scraper.plan_type, 'program')
self.assertEqual(scraper.plan_code, 'VCOMP')
scraper = DegreeRuleScraper('https://programsandcourses.anu.edu.au/program/VCOMP')
# Parse a more comlicated URL
scraper = DegreeRuleScraper(
'https://programsandcourses.anu.edu.au/program/7706XMCOMP#program-requirements'
)
self.assertEqual(scraper.year, date.today().year)
self.assertEqual(scraper.plan_type, 'program')
self.assertEqual(scraper.plan_code, 'VCOMP')
self.assertEqual(scraper.plan_code, '7706XMCOMP')
def test_process_global_reqs(self):
"""
......@@ -155,8 +192,7 @@ class TestDegreeRuleScraper(unittest.TestCase):
"""
scraper = DegreeRuleScraper('tests/sample_html/global_reqs.html', path_is_file=True)
scraper.plan_code = 'AACOM'
req_tree = build_reqnode_tree(get_requirements_block(scraper.soup))
orders = list(scraper.process_requirement_node(req_tree.children))
orders = scraper.build_program_order_struct().children
self.assertEqual(len(orders), 4)
self.assertEqual(
'AACOM Bachelor of Advanced Computing (Honours): >= 192 units from courses '
......@@ -201,11 +237,11 @@ class TestDegreeRuleScraper(unittest.TestCase):
self.assertEqual(order.operator, dr_scraper.REQUIREMENT_OPERATORS['=='])
order = orders.children[1]
self.assertEqual(order.title, dr_scraper.ORDER_LABEL['PRINCIPAL_SINGLE_SET'])
self.assertEqual(order.unit_value,12 )
self.assertEqual(order.unit_value, 12)
self.assertEqual(order.operator, dr_scraper.REQUIREMENT_OPERATORS['MAX'])
order = orders.children[2]
self.assertEqual(order.title, dr_scraper.ORDER_LABEL['PRINCIPAL_SINGLE_SET'])
self.assertEqual(order.unit_value,12 )
self.assertEqual(order.unit_value, 12)
self.assertEqual(order.operator, dr_scraper.REQUIREMENT_OPERATORS['MIN'])
def test_process_principal_requirement_single_course(self):
......@@ -528,3 +564,103 @@ class TestDegreeRuleScraper(unittest.TestCase):
self.assertEqual(child_order.code, 'ELCO-MAJ')
child_order = orders.children[2]
self.assertEqual(child_order.code, 'MMSY-MAJ')
def test_process_principal_requirement_minor_options(self):
"""
Process minor alternatives subplan requirement.
"""
scraper = DegreeRuleScraper('tests/sample_html/minor_options.html', path_is_file=True)
scraper.plan_code = 'VCOMP'
scraper.path = 'https://programsandcourses.anu.edu.au/'
orders = scraper.build_program_order_struct()
self.assertEqual(len(orders.children), 1)
# Check the root node of the subplan orders
child_order = orders.children[0]
self.assertEqual(child_order.code, 'VCOMP')
self.assertEqual(child_order.title, dr_scraper.ORDER_LABEL['PRINCIPAL_SUBPLAN_CHOICE'])
self.assertEqual(child_order.unit_value, -1)
self.assertEqual(child_order.operator, dr_scraper.REQUIREMENT_OPERATORS['OR'])
self.assertIsNone(child_order.course_filter)
# Check a subplan order tree was added for each subplan option
self.assertEqual(len(child_order.children), 3)
orders = child_order
child_order = orders.children[0]
self.assertEqual(child_order.code, 'BMSY-MIN')
child_order = orders.children[1]
self.assertEqual(child_order.code, 'ELCO-MIN')
child_order = orders.children[2]
self.assertEqual(child_order.code, 'MMSY-MIN')
def test_process_principal_requirement_specialisation_coreq(self):
"""
Process undergrad specialisation that requires a major.
"""