From 4f7fa489cddaa2dc726febf68090026165f49227 Mon Sep 17 00:00:00 2001 From: = <=> Date: Sat, 23 Sep 2023 01:22:55 -0500 Subject: [PATCH 01/24] diff script --- .github/workflows/versioning.yml | 27 ++++++++ validator/requirements.txt | 4 ++ validator/scripts/diff.py | 104 +++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 .github/workflows/versioning.yml create mode 100644 validator/scripts/diff.py diff --git a/.github/workflows/versioning.yml b/.github/workflows/versioning.yml new file mode 100644 index 000000000..b1eb7199d --- /dev/null +++ b/.github/workflows/versioning.yml @@ -0,0 +1,27 @@ +name: Scheduled Versioning + +on: + workflow_dispatch: + schedule: + - cron: '0 0 15 8 *' + +jobs: + versioning: + name: Versioning System + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + pip3 install -r requirements.txt + + - name: Execute versioning script + working-directory: ./validator/scripts + run: python diff.py diff --git a/validator/requirements.txt b/validator/requirements.txt index ab53634e9..d42c95fa5 100644 --- a/validator/requirements.txt +++ b/validator/requirements.txt @@ -1,6 +1,7 @@ absl-py==1.2.0 async-timeout==4.0.2 attrs==22.2.0 +beautifulsoup4==4.12.0 black==23.1.0 certifi==2023.7.22 charset-normalizer==2.1.1 @@ -17,6 +18,7 @@ idna==3.4 iniconfig==2.0.0 itsdangerous==2.1.2 Jinja2==3.1.2 +jira==3.5.2 jsonschema==4.17.3 limits==2.8.0 MarkupSafe==2.1.1 @@ -43,8 +45,10 @@ ruamel.yaml==0.17.21 ruamel.yaml.clib==0.2.7 six==1.16.0 tomli==2.0.1 +types-beautifulsoup4==4.12.0.6 types-Flask-Cors==3.0.10.2 types-jsonschema==4.17.0.6 +types-requests==2.31.0.2 typing_extensions==4.4.0 urllib3==1.26.13 Werkzeug==2.2.3 diff --git a/validator/scripts/diff.py b/validator/scripts/diff.py new file mode 100644 index 000000000..ce46bafc5 --- /dev/null +++ b/validator/scripts/diff.py @@ -0,0 +1,104 @@ +import requests +import json +import re +import os +import difflib +from bs4 import BeautifulSoup +from dotenv import load_dotenv +from jira import JIRA + +"""This script looks through all major/concentration +json files to find if any requirements have changed +over the year. If so, it raises a JIRA ticket with +requirement change information +""" + +load_dotenv() +jira_api_key = os.getenv('JIRA_API_KEY') +major_json_path = "/home/runner/work/planner/planner/validator/degree_data" + +#Extracts html from url and sends it to course extractor +def get_req_content(url: str) -> str: + response = requests.get(url) + if(response.status_code == 200): + return response.text + else: + return "Webpage not found" + +#Extracts the courses from each major and sends them to a set +def extract_courses(webData: str) -> set[str]: + bs = BeautifulSoup(webData, features="html.parser") + courses = set() + course_elements = bs.find_all('a', href=True) + + for course_element in course_elements: + course_name = course_element.text.strip() + pattern = r'\b[A-Z]{2,4} \d{4}\b' + + if re.search(pattern, course_name): + courses.add(course_name) + return courses + +#Diffs between webpages and works with the course diff sets +def htmldiff(previousYearURL: str, currentYearURL: str, oldCourses: set[str], newCourses: set[str]) -> str: + oldContent = get_req_content(previousYearURL) + newContent = get_req_content(currentYearURL) + + oldCourses.update(extract_courses(oldContent)) + newCourses.update(extract_courses(newContent)) + + bsOld = BeautifulSoup(oldContent, features="lxml").find('div', attrs = {'id':'bukku-page'}).get_text().split('\n') + bsNew = BeautifulSoup(newContent, features="lxml").find('div', attrs = {'id':'bukku-page'}) + + if bsNew is None: + return "" + bsNew = bsNew.get_text().split('\n') + + diff = difflib.ndiff(bsOld, bsNew) + diffString = "```" + for line in diff: + diffString+=line+'\n' + + return diffString + "```" + +#Creates a ticket based on issue type, including URI and impacted courses in ticket +#C issue type = Course renamed/added/removed +#R issue type = Major/concentration removed +def createTicket(issueType: str, jira_connection: JIRA, URI: str, coursesImpacted: set[str], diffCodeBlock: str) -> None: + description = "This is an automated diff script used to detect discrepancies between major requirements\nURI: " + URI + "\n" + description += "Major: " + URI.split("/")[-1] + "\n" + if issueType == 'R': + description += "This major/concentration has been renamed or removed\n\n" + elif issueType == 'C': + description += "The following course(s) have been renamed/added/removed:\n" + str(coursesImpacted) + "\n\n" + description+="Below is a preview of the diff:\n" + diffCodeBlock + jira_connection.create_issue( + project='NP', + summary='Course requirement version changes', + description=description, + issuetype={'name': 'Task'} + ) + +#Establishes JIRA connection and ierates through each major for versioning issues +if __name__ == "__main__": + jira_connection = JIRA( + basic_auth=('planner@utdnebula.com', jira_api_key), + server="https://nebula-labs.atlassian.net" + ) + for majorReqJson in os.scandir(major_json_path): + data = json.loads(open(f"/home/runner/work/planner/planner/validator/degree_data/" + majorReqJson.name, "r").read()) + catalog_uri=data["catalog_uri"] + yearRegex = r'/(\d{4})/' + result = re.search(yearRegex, catalog_uri) + if result: + match = str(int(result.group(1))+1) + previousYearURL = data["catalog_uri"] + currentYearURL = re.sub(yearRegex, f'/{ str(match) }/', data["catalog_uri"]) + oldCourses = set() + newCourses = set() + pageDiff = htmldiff(previousYearURL, currentYearURL, oldCourses, newCourses) + if len(newCourses) == 0: + createTicket('R', jira_connection, re.sub(yearRegex, f'/{ match }/', data["catalog_uri"]), set(), pageDiff) + else: + createTicket('C', jira_connection, re.sub(yearRegex, f'/{ match }/', data["catalog_uri"]), (newCourses-oldCourses).union(oldCourses-newCourses), pageDiff) + \ No newline at end of file From 84fb0314a0e70769876baf9af74e330043f87863 Mon Sep 17 00:00:00 2001 From: = <=> Date: Sat, 23 Sep 2023 01:53:47 -0500 Subject: [PATCH 02/24] Fixed typing issue --- validator/scripts/diff.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/validator/scripts/diff.py b/validator/scripts/diff.py index ce46bafc5..6e2fa07d6 100644 --- a/validator/scripts/diff.py +++ b/validator/scripts/diff.py @@ -47,14 +47,16 @@ def htmldiff(previousYearURL: str, currentYearURL: str, oldCourses: set[str], ne oldCourses.update(extract_courses(oldContent)) newCourses.update(extract_courses(newContent)) - bsOld = BeautifulSoup(oldContent, features="lxml").find('div', attrs = {'id':'bukku-page'}).get_text().split('\n') + bsOld = BeautifulSoup(oldContent, features="lxml").find('div', attrs = {'id':'bukku-page'}) bsNew = BeautifulSoup(newContent, features="lxml").find('div', attrs = {'id':'bukku-page'}) if bsNew is None: return "" - bsNew = bsNew.get_text().split('\n') - diff = difflib.ndiff(bsOld, bsNew) + bsOldLines = bsOld.get_text().split('\n') + bsNewLines = bsNew.get_text().split('\n') + + diff = difflib.ndiff(bsOldLines, bsNewLines) diffString = "```" for line in diff: diffString+=line+'\n' @@ -94,8 +96,8 @@ def createTicket(issueType: str, jira_connection: JIRA, URI: str, coursesImpacte match = str(int(result.group(1))+1) previousYearURL = data["catalog_uri"] currentYearURL = re.sub(yearRegex, f'/{ str(match) }/', data["catalog_uri"]) - oldCourses = set() - newCourses = set() + oldCourses: set[str] = set() + newCourses: set[str] = set() pageDiff = htmldiff(previousYearURL, currentYearURL, oldCourses, newCourses) if len(newCourses) == 0: createTicket('R', jira_connection, re.sub(yearRegex, f'/{ match }/', data["catalog_uri"]), set(), pageDiff) From de8b0f27d23cbfbcd461c85a1eee40a948749315 Mon Sep 17 00:00:00 2001 From: = <=> Date: Sat, 23 Sep 2023 01:55:50 -0500 Subject: [PATCH 03/24] Fixed typing issues --- validator/scripts/diff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validator/scripts/diff.py b/validator/scripts/diff.py index 6e2fa07d6..6817919da 100644 --- a/validator/scripts/diff.py +++ b/validator/scripts/diff.py @@ -50,7 +50,7 @@ def htmldiff(previousYearURL: str, currentYearURL: str, oldCourses: set[str], ne bsOld = BeautifulSoup(oldContent, features="lxml").find('div', attrs = {'id':'bukku-page'}) bsNew = BeautifulSoup(newContent, features="lxml").find('div', attrs = {'id':'bukku-page'}) - if bsNew is None: + if bsNew is None or bsOld is None: return "" bsOldLines = bsOld.get_text().split('\n') From 0f96dc0baf81c54d546cb18fcfa0cdf01091c1ae Mon Sep 17 00:00:00 2001 From: Kevin Ge Date: Mon, 25 Sep 2023 13:54:52 -0500 Subject: [PATCH 04/24] refactor: move requirement map to Loader class to remove cyclic dependencies --- validator/major/requirements/__init__.py | 2 +- validator/major/requirements/base.py | 1 - .../arts_technology_emerging_communication.py | 9 +-- .../edge_cases/business_administration.py | 10 ++- .../edge_cases/computer_science.py | 8 +-- .../requirements/edge_cases/psychology.py | 1 - validator/major/requirements/loader.py | 67 +++++++++++++++++++ validator/major/requirements/map.py | 30 --------- validator/major/requirements/shared.py | 37 +++------- 9 files changed, 87 insertions(+), 78 deletions(-) create mode 100644 validator/major/requirements/loader.py delete mode 100644 validator/major/requirements/map.py diff --git a/validator/major/requirements/__init__.py b/validator/major/requirements/__init__.py index 04db8b58c..cdf2148c2 100644 --- a/validator/major/requirements/__init__.py +++ b/validator/major/requirements/__init__.py @@ -1,3 +1,3 @@ from .base import AbstractRequirement from .shared import * -from .map import * +from .loader import * diff --git a/validator/major/requirements/base.py b/validator/major/requirements/base.py index b3df34097..90c8a6f02 100644 --- a/validator/major/requirements/base.py +++ b/validator/major/requirements/base.py @@ -3,7 +3,6 @@ from __future__ import annotations from abc import abstractmethod, ABC from dataclasses import dataclass - from typing import Any from pydantic import Json diff --git a/validator/major/requirements/edge_cases/arts_technology_emerging_communication.py b/validator/major/requirements/edge_cases/arts_technology_emerging_communication.py index ee6172bef..dbe76c15d 100644 --- a/validator/major/requirements/edge_cases/arts_technology_emerging_communication.py +++ b/validator/major/requirements/edge_cases/arts_technology_emerging_communication.py @@ -1,7 +1,8 @@ import json -from typing import Any, TypedDict +from typing import Any from pydantic import Json +from major.requirements import loader from major.requirements.base import AbstractRequirement from major.requirements.shared import MultiGroupElectiveRequirement import utils @@ -45,13 +46,9 @@ def __init__( @classmethod def from_json(cls, json: JSON) -> MultiGroupElectiveRequirement: # type: ignore[override] - from ..map import REQUIREMENTS_MAP - requirements: list[AbstractRequirement] = [] for requirement_data in json["requirements"]: - requirement = REQUIREMENTS_MAP[requirement_data["matcher"]].from_json( - requirement_data - ) + requirement = loader.Loader().requirement_from_json(requirement_data) requirements.append(requirement) return cls( diff --git a/validator/major/requirements/edge_cases/business_administration.py b/validator/major/requirements/edge_cases/business_administration.py index 08e605816..06a92c2b5 100644 --- a/validator/major/requirements/edge_cases/business_administration.py +++ b/validator/major/requirements/edge_cases/business_administration.py @@ -2,13 +2,13 @@ import json from pydantic import Json -from major.requirements import AbstractRequirement, map +from major.requirements import AbstractRequirement -from functools import reduce from typing import Any, TypedDict from major.requirements.shared import OrRequirement import utils +from major.requirements import loader """ Note: assuming BA 4V90 & BA 4090 cover one of the groups @@ -146,9 +146,7 @@ def from_json(cls, json: JSON) -> BusinessAdministrationElectiveRequirement: requirements: list[AbstractRequirement] = [] for requirement_data in json["prefix_groups"]: - requirement = map.REQUIREMENTS_MAP[requirement_data["matcher"]].from_json( - requirement_data - ) + requirement = loader.Loader().requirement_from_json(requirement_data) requirements.append(requirement) return cls( @@ -182,4 +180,4 @@ def __str__(self) -> str: _______________ Required fulfilled: {self.is_fulfilled()} """ - return s \ No newline at end of file + return s diff --git a/validator/major/requirements/edge_cases/computer_science.py b/validator/major/requirements/edge_cases/computer_science.py index 6bbad5f39..6ac6d4fe2 100644 --- a/validator/major/requirements/edge_cases/computer_science.py +++ b/validator/major/requirements/edge_cases/computer_science.py @@ -2,12 +2,13 @@ import json from pydantic import Json -from major.requirements import AbstractRequirement, map +from major.requirements import AbstractRequirement from functools import reduce from typing import Any, TypedDict import utils +from major.requirements import loader class MajorGuidedElectiveRequirement(AbstractRequirement): @@ -57,7 +58,6 @@ def attempt_fulfill(self, course: str) -> bool: for requirement in self.also_fulfills: if requirement.attempt_fulfill(course): return True - self.valid_courses.append(course) return False @@ -112,9 +112,7 @@ def from_json(cls, json: JSON) -> MajorGuidedElectiveRequirement: also_fulfills: list[AbstractRequirement] = [] for requirement in json["also_fulfills"]: - also_fulfills.append( - map.REQUIREMENTS_MAP[requirement["matcher"]].from_json(requirement) - ) + also_fulfills.append(loader.Loader().requirement_from_json(requirement)) return cls( json["required_count"], json["starts_with"], also_fulfills, json["metadata"] diff --git a/validator/major/requirements/edge_cases/psychology.py b/validator/major/requirements/edge_cases/psychology.py index 85de6f8bd..ebce7382b 100644 --- a/validator/major/requirements/edge_cases/psychology.py +++ b/validator/major/requirements/edge_cases/psychology.py @@ -1,7 +1,6 @@ from pydantic import Json from typing import Any, TypedDict from major.requirements import base -from major.requirements.base import AbstractRequirement import utils import json diff --git a/validator/major/requirements/loader.py b/validator/major/requirements/loader.py new file mode 100644 index 000000000..0fdc9ee26 --- /dev/null +++ b/validator/major/requirements/loader.py @@ -0,0 +1,67 @@ +from typing import Type, Literal, get_args, Mapping, TypeGuard + +RequirementNameT = Literal[ + "CourseRequirement", + "AndRequirement", + "OrRequirement", + "FreeElectiveRequirement", + "SelectRequirement", + "HoursRequirement", + "PrefixBucketRequirement", + "OtherRequirement", + "CSGuidedElectiveRequirement", + "BAGuidedElectiveRequirement", + "SomeRequirement", + "MultiGroupElectiveRequirement", + "ATECPrescribedElectiveRequirement", + "PsychologyPrefixesOrCourses", +] +RequirementNames: list[RequirementNameT] = list(get_args(RequirementNameT)) + + +class Loader: + from major.requirements.base import AbstractRequirement + + def __init__(self) -> None: + from major.requirements import shared + from .edge_cases import ( + business_administration, + computer_science, + arts_technology_emerging_communication, + psychology, + ) + + self.REQUIREMENTS_MAP: dict[ + RequirementNameT, + Type[Loader.AbstractRequirement], + ] = { + # Shared requirements + "CourseRequirement": shared.CourseRequirement, + "AndRequirement": shared.AndRequirement, + "OrRequirement": shared.OrRequirement, + "FreeElectiveRequirement": shared.FreeElectiveRequirement, + "SelectRequirement": shared.SelectRequirement, + "HoursRequirement": shared.HoursRequirement, + "PrefixBucketRequirement": shared.PrefixBucketRequirement, + "OtherRequirement": shared.OtherRequirement, + # Computer Science Edge Cases + "CSGuidedElectiveRequirement": computer_science.MajorGuidedElectiveRequirement, + # Business Administration Edge Cases + "BAGuidedElectiveRequirement": business_administration.BusinessAdministrationElectiveRequirement, + "SomeRequirement": business_administration.SomeRequirement, + "MultiGroupElectiveRequirement": shared.MultiGroupElectiveRequirement, + "ATECPrescribedElectiveRequirement": arts_technology_emerging_communication.ATECPrescribedElectiveRequirement, + # Psychology + "PsychologyPrefixesOrCourses": psychology.PsychologyPrefixesOrCourses, + } + + def requirement_from_json(self, json: Mapping) -> AbstractRequirement: + if not "matcher" in json: + raise ValueError(f"Invalid requirement: {json}, missing 'matcher' key.") + if not self._is_valid_requirement(json["matcher"]): + raise ValueError(f"Invalid requirement: {json}") + + return self.REQUIREMENTS_MAP[json["matcher"]].from_json(json) + + def _is_valid_requirement(self, requirement: str) -> TypeGuard[RequirementNameT]: + return requirement in self.REQUIREMENTS_MAP diff --git a/validator/major/requirements/map.py b/validator/major/requirements/map.py deleted file mode 100644 index ee30eabb4..000000000 --- a/validator/major/requirements/map.py +++ /dev/null @@ -1,30 +0,0 @@ -from .base import AbstractRequirement -from .shared import * -from .edge_cases import ( - business_administration, - computer_science, - arts_technology_emerging_communication, - psychology, -) -from typing import Type - -REQUIREMENTS_MAP: dict[str, Type[AbstractRequirement]] = { - # Shared requirements - "CourseRequirement": CourseRequirement, - "AndRequirement": AndRequirement, - "OrRequirement": OrRequirement, - "FreeElectiveRequirement": FreeElectiveRequirement, - "SelectRequirement": SelectRequirement, - "HoursRequirement": HoursRequirement, - "PrefixBucketRequirement": PrefixBucketRequirement, - "OtherRequirement": OtherRequirement, - # Computer Science Edge Cases - "CSGuidedElectiveRequirement": computer_science.MajorGuidedElectiveRequirement, - # Business Administration Edge Cases - "BAGuidedElectiveRequirement": business_administration.BusinessAdministrationElectiveRequirement, - "SomeRequirement": business_administration.SomeRequirement, - "MultiGroupElectiveRequirement": MultiGroupElectiveRequirement, - "ATECPrescribedElectiveRequirement": arts_technology_emerging_communication.ATECPrescribedElectiveRequirement, - # Psychology - "PsychologyPrefixesOrCourses": psychology.PsychologyPrefixesOrCourses, -} diff --git a/validator/major/requirements/shared.py b/validator/major/requirements/shared.py index 2980770f6..e1e5079b9 100644 --- a/validator/major/requirements/shared.py +++ b/validator/major/requirements/shared.py @@ -9,6 +9,7 @@ from typing import Any, TypedDict from major.requirements import AbstractRequirement +from major.requirements import loader from functools import reduce @@ -115,14 +116,10 @@ class JSON(TypedDict): @classmethod def from_json(cls, json: JSON) -> AndRequirement: - from .map import REQUIREMENTS_MAP - # Get all requirements that are inside AndRequirement requirements: list[AbstractRequirement] = [] for requirement_data in json["requirements"]: - requirement = REQUIREMENTS_MAP[requirement_data["matcher"]].from_json( - requirement_data - ) + requirement = loader.Loader().requirement_from_json(requirement_data) requirements.append(requirement) return cls(requirements, json["metadata"]) @@ -190,13 +187,9 @@ class JSON(TypedDict): @classmethod def from_json(cls, json: JSON) -> OrRequirement: - from .map import REQUIREMENTS_MAP - requirements: list[AbstractRequirement] = [] for requirement_data in json["requirements"]: - requirement = REQUIREMENTS_MAP[requirement_data["matcher"]].from_json( - requirement_data - ) + requirement = loader.Loader().requirement_from_json(requirement_data) requirements.append(requirement) metadata = {} @@ -281,13 +274,11 @@ class JSON(TypedDict): @classmethod def from_json(cls, json: JSON) -> SelectRequirement: - from .map import REQUIREMENTS_MAP + from .loader import Loader requirements: list[AbstractRequirement] = [] for requirement_data in json["requirements"]: - requirement = REQUIREMENTS_MAP[requirement_data["matcher"]].from_json( - requirement_data - ) + requirement = loader.Loader().requirement_from_json(requirement_data) requirements.append(requirement) # Check if there's any metadata @@ -396,13 +387,9 @@ def from_json(cls, json: JSON) -> HoursRequirement: ] """ - from .map import REQUIREMENTS_MAP - requirements: list[AbstractRequirement] = [] for requirement_data in json["requirements"]: - requirement = REQUIREMENTS_MAP[requirement_data["matcher"]].from_json( - requirement_data - ) + requirement = loader.Loader().requirement_from_json(requirement_data) requirements.append(requirement) # Check if there's any metadata @@ -747,12 +734,8 @@ def from_json(cls, json: JSON) -> MajorGuidedElectiveRequirement: """ also_fulfills: list[AbstractRequirement] = [] - from .map import REQUIREMENTS_MAP - for requirement in json["also_fulfills"]: - also_fulfills.append( - REQUIREMENTS_MAP[requirement["matcher"]].from_json(requirement) - ) + also_fulfills.append(loader.Loader().requirement_from_json(requirement)) return cls( json["required_count"], json["starts_with"], also_fulfills, json["metadata"] @@ -822,13 +805,11 @@ def __init__( @classmethod def from_json(cls, json: JSON) -> MultiGroupElectiveRequirement: - from .map import REQUIREMENTS_MAP + from .loader import Loader requirements: list[AbstractRequirement] = [] for requirement_data in json["requirements"]: - requirement = REQUIREMENTS_MAP[requirement_data["matcher"]].from_json( - requirement_data - ) + requirement = Loader().requirement_from_json(requirement_data) requirements.append(requirement) return cls( From 176f0deb32e9c7178702d5e34a3655f070fc66f1 Mon Sep 17 00:00:00 2001 From: Kevin Ge Date: Mon, 25 Sep 2023 13:55:40 -0500 Subject: [PATCH 05/24] fix: exclude free elective requirement from used core course check --- validator/degree_solver.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/validator/degree_solver.py b/validator/degree_solver.py index 8d35dba03..0cef530eb 100644 --- a/validator/degree_solver.py +++ b/validator/degree_solver.py @@ -10,7 +10,7 @@ from major.requirements import AbstractRequirement from dataclasses import dataclass -from major.requirements.map import REQUIREMENTS_MAP +from major.requirements import loader import json from major.requirements.shared import ( @@ -188,7 +188,7 @@ def load_requirements( # Add requirements for req_data in requirements_data: major_req.requirements.append( - REQUIREMENTS_MAP[req_data["matcher"]].from_json(req_data) + loader.Loader().requirement_from_json(req_data) ) degree_requirements.append(major_req) # We don't need to check the other JSON files @@ -214,7 +214,11 @@ def solve(self) -> DegreeRequirementsSolver: for degree_req in self.degree_requirements: for course in self.courses: # Make sure it's not a core course - if course in used_core_courses: + if ( + course in used_core_courses + and not type(degree_req.requirements) + == loader.Loader().REQUIREMENTS_MAP["FreeElectiveRequirement"] + ): continue for requirement in degree_req.requirements: if requirement.attempt_fulfill(course): From 733d9a6b7aabf088c86a1fe7cbb61899312efbcf Mon Sep 17 00:00:00 2001 From: Kevin Ge Date: Mon, 25 Sep 2023 16:30:50 -0500 Subject: [PATCH 06/24] fix: allow major to double dip but not core --- validator/degree_solver.py | 34 ++++++++++--------- validator/major/requirements/base.py | 1 + .../arts_technology_emerging_communication.py | 2 +- .../edge_cases/business_administration.py | 4 +-- .../edge_cases/computer_science.py | 2 +- .../requirements/edge_cases/psychology.py | 2 +- validator/major/requirements/shared.py | 27 +++++++-------- 7 files changed, 37 insertions(+), 35 deletions(-) diff --git a/validator/degree_solver.py b/validator/degree_solver.py index 0cef530eb..b2b8c8b53 100644 --- a/validator/degree_solver.py +++ b/validator/degree_solver.py @@ -1,13 +1,14 @@ from __future__ import annotations from enum import Enum from glob import glob +from collections import Counter from pydantic import Json from typing import Any import core -from major.requirements import AbstractRequirement +from major.requirements import AbstractRequirement, FreeElectiveRequirement from dataclasses import dataclass from major.requirements import loader @@ -144,7 +145,7 @@ def __init__( requirements: DegreeRequirementsInput, bypasses: BypassInput, ) -> None: - self.courses = set(courses) + self.courses = set([Course.from_name(course) for course in courses]) self.degree_requirements = self.load_requirements(requirements) self.solved_core: core.store.AssignmentStore | None = None self.bypasses = bypasses @@ -201,27 +202,28 @@ def load_requirements( def solve(self) -> DegreeRequirementsSolver: # Run for core core_solver = self.load_core() - self.solved_core = core_solver.solve( - [Course.from_name(course) for course in self.courses], [] - ) - # Set of the core courses that are fulfilled, so they won't be considered as free electives - used_core_courses = set() + self.solved_core = core_solver.solve(list(self.courses), []) + + # Counter of the core courses and their used hours, so they won't be considered as free electives. + used_core_courses: Counter[Course] = Counter() if self.solved_core is not None: for req_fill in self.solved_core.reqs_to_courses.values(): - used_core_courses.update([course.name for course in req_fill.keys()]) + used_core_courses.update(req_fill) # Run for major for degree_req in self.degree_requirements: for course in self.courses: - # Make sure it's not a core course - if ( - course in used_core_courses - and not type(degree_req.requirements) - == loader.Loader().REQUIREMENTS_MAP["FreeElectiveRequirement"] - ): - continue for requirement in degree_req.requirements: - if requirement.attempt_fulfill(course): + # Free elective requirements are special, since they can take left over hours from core courses. + if type(requirement) == FreeElectiveRequirement: + if requirement.attempt_fulfill( + course.name, + available_hours=( + int(course.hours) - used_core_courses[course] + ), + ): + break + elif requirement.attempt_fulfill(course.name): break # Handle requirements bypasses for major diff --git a/validator/major/requirements/base.py b/validator/major/requirements/base.py index 90c8a6f02..1b2141f4b 100644 --- a/validator/major/requirements/base.py +++ b/validator/major/requirements/base.py @@ -18,6 +18,7 @@ class AbstractRequirement(ABC): def attempt_fulfill( self, course: str, + available_hours: int = 0, ) -> bool: pass diff --git a/validator/major/requirements/edge_cases/arts_technology_emerging_communication.py b/validator/major/requirements/edge_cases/arts_technology_emerging_communication.py index dbe76c15d..049fe2258 100644 --- a/validator/major/requirements/edge_cases/arts_technology_emerging_communication.py +++ b/validator/major/requirements/edge_cases/arts_technology_emerging_communication.py @@ -84,7 +84,7 @@ def to_json(self) -> Json[Any]: } ) - def attempt_fulfill(self, course: str) -> bool: + def attempt_fulfill(self, course: str, _: int = 0) -> bool: fulfilled = super().attempt_fulfill(course) if fulfilled: if utils.get_level_from_course(course) == 4: diff --git a/validator/major/requirements/edge_cases/business_administration.py b/validator/major/requirements/edge_cases/business_administration.py index 06a92c2b5..5d3ce93aa 100644 --- a/validator/major/requirements/edge_cases/business_administration.py +++ b/validator/major/requirements/edge_cases/business_administration.py @@ -21,7 +21,7 @@ class SomeRequirement(OrRequirement): NOTE: Allows attempt_filled to work even if is_fulfilled() is true """ - def attempt_fulfill(self, course: str) -> bool: + def attempt_fulfill(self, course: str, _: int = 0) -> bool: for requirement in self.requirements: if requirement.attempt_fulfill(course): return True @@ -66,7 +66,7 @@ def __init__( self.metadata = metadata self.override_filled = False - def attempt_fulfill(self, course: str) -> bool: + def attempt_fulfill(self, course: str, _: int = 0) -> bool: if self.is_fulfilled(): return False diff --git a/validator/major/requirements/edge_cases/computer_science.py b/validator/major/requirements/edge_cases/computer_science.py index 6ac6d4fe2..ebcc0aaac 100644 --- a/validator/major/requirements/edge_cases/computer_science.py +++ b/validator/major/requirements/edge_cases/computer_science.py @@ -46,7 +46,7 @@ def __init__( self.metadata = metadata self.override_filled = False - def attempt_fulfill(self, course: str) -> bool: + def attempt_fulfill(self, course: str, _: int = 0) -> bool: if self.is_fulfilled(): return False diff --git a/validator/major/requirements/edge_cases/psychology.py b/validator/major/requirements/edge_cases/psychology.py index ebce7382b..ca048c2ff 100644 --- a/validator/major/requirements/edge_cases/psychology.py +++ b/validator/major/requirements/edge_cases/psychology.py @@ -24,7 +24,7 @@ def __init__( self.accepted_courses = accepted_courses self.metadata = metadata - def attempt_fulfill(self, course: str) -> bool: + def attempt_fulfill(self, course: str, _: int = 0) -> bool: if self.is_fulfilled(): return False diff --git a/validator/major/requirements/shared.py b/validator/major/requirements/shared.py index e1e5079b9..f9e945af6 100644 --- a/validator/major/requirements/shared.py +++ b/validator/major/requirements/shared.py @@ -25,7 +25,7 @@ def __init__( self.metadata = metadata self.filled = filled - def attempt_fulfill(self, course: str) -> bool: + def attempt_fulfill(self, course: str, _: int = 0) -> bool: # fail duplicate attempt to fulfill if self.is_fulfilled(): return False @@ -80,7 +80,7 @@ def __init__( self.metadata = metadata self.override_filled = False - def attempt_fulfill(self, course: str) -> bool: + def attempt_fulfill(self, course: str, _: int = 0) -> bool: if self.is_fulfilled(): return False @@ -154,7 +154,7 @@ def __init__( self.metadata = metadata self.override_filled = False # use this as override fill - def attempt_fulfill(self, course: str) -> bool: + def attempt_fulfill(self, course: str, _: int = 0) -> bool: if self.is_fulfilled(): return False @@ -236,7 +236,7 @@ def __init__( self.metadata = metadata self.override_filled = False - def attempt_fulfill(self, course: str) -> bool: + def attempt_fulfill(self, course: str, _: int = 0) -> bool: if self.is_fulfilled(): return False @@ -336,7 +336,7 @@ def __init__( self.metadata: dict[str, Any] = metadata self.override_filled = False - def attempt_fulfill(self, course: str) -> bool: + def attempt_fulfill(self, course: str, _: int = 0) -> bool: if self.is_fulfilled(): return False @@ -443,7 +443,7 @@ def __init__( def is_fulfilled(self) -> bool: return self.override_filled - def attempt_fulfill(self, course: str) -> bool: + def attempt_fulfill(self, course: str, _: int = 0) -> bool: return False def override_fill(self, index: str) -> bool: @@ -509,14 +509,13 @@ def __init__( self.metadata = metadata self.override_filled = False - def attempt_fulfill(self, course: str) -> bool: + def attempt_fulfill(self, course: str, available_hours: int = 0) -> bool: if self.is_fulfilled(): return False - if not course in self.excluded_courses: - course_hrs = utils.get_hours_from_course(course) - self.fulfilled_hours += course_hrs - self.valid_courses[course] = course_hrs + if not course in self.excluded_courses and available_hours > 0: + self.fulfilled_hours += available_hours + self.valid_courses[course] = available_hours return True return False @@ -594,7 +593,7 @@ def __init__(self, prefix: str) -> None: # NOTE: This allows courses to satisfy the requirement even after it's filled, # as it doesn't check for whether or not the requirement has been filled - def attempt_fulfill(self, course: str) -> bool: + def attempt_fulfill(self, course: str, _: int = 0) -> bool: if course.startswith(self.prefix): self.filled = True self.valid_courses[course] = utils.get_hours_from_course(course) @@ -667,7 +666,7 @@ def __init__( self.metadata = metadata self.override_filled = False - def attempt_fulfill(self, course: str) -> bool: + def attempt_fulfill(self, course: str, _: int = 0) -> bool: if self.is_fulfilled(): return False @@ -845,7 +844,7 @@ def to_json(self) -> Json[Any]: } ) - def attempt_fulfill(self, course: str) -> bool: + def attempt_fulfill(self, course: str, _: int = 0) -> bool: if self.is_fulfilled(): return False From 8c7ee771ce7a34995ddd664be86df34064587044 Mon Sep 17 00:00:00 2001 From: Kevin Ge Date: Mon, 25 Sep 2023 16:38:47 -0500 Subject: [PATCH 07/24] fix: type errors --- validator/degree_solver.py | 5 ++--- validator/gen_schema.py | 7 ++++--- validator/major/requirements/__init__.py | 2 +- validator/major/requirements/loader.py | 4 ++-- validator/major/tests/test_degrees.py | 11 +++++++---- validator/major/tests/test_solver.py | 17 ++++++++++++----- 6 files changed, 28 insertions(+), 18 deletions(-) diff --git a/validator/degree_solver.py b/validator/degree_solver.py index b2b8c8b53..b2b5ae06f 100644 --- a/validator/degree_solver.py +++ b/validator/degree_solver.py @@ -20,6 +20,7 @@ ) from course import Course +LOADER = loader.Loader() # Read all degree plan JSON files and store their contents in a hashmap # This is so that we can avoid reading all the files each time we want to get the data for a certain course @@ -188,9 +189,7 @@ def load_requirements( # Add requirements for req_data in requirements_data: - major_req.requirements.append( - loader.Loader().requirement_from_json(req_data) - ) + major_req.requirements.append(LOADER.requirement_from_json(req_data)) degree_requirements.append(major_req) # We don't need to check the other JSON files break diff --git a/validator/gen_schema.py b/validator/gen_schema.py index d9ceabeb7..4cc3b6752 100644 --- a/validator/gen_schema.py +++ b/validator/gen_schema.py @@ -6,7 +6,7 @@ from types import GenericAlias from typing import Any, ForwardRef from jsonschema import Draft7Validator -from major.requirements import REQUIREMENTS_MAP +from major.requirements import loader schema: dict[str, Any] = { "$schema": Draft7Validator.META_SCHEMA["$id"], @@ -89,8 +89,9 @@ def forward_ref_to_schema(ref: ForwardRef) -> dict[str, Any]: raise Exception("Expected type, got", type(ref_type), ref_type) -for req_name in REQUIREMENTS_MAP: - req = REQUIREMENTS_MAP[req_name] +req_loader = loader.Loader() +for req_name in req_loader.REQUIREMENTS_MAP: + req = req_loader.REQUIREMENTS_MAP[req_name] requirement_schema_props: dict[str, Any] = {"matcher": {"const": req_name}} for prop_name, prop_type in req.JSON.__annotations__.items(): diff --git a/validator/major/requirements/__init__.py b/validator/major/requirements/__init__.py index cdf2148c2..cc8cf0300 100644 --- a/validator/major/requirements/__init__.py +++ b/validator/major/requirements/__init__.py @@ -1,3 +1,3 @@ from .base import AbstractRequirement from .shared import * -from .loader import * +from . import loader diff --git a/validator/major/requirements/loader.py b/validator/major/requirements/loader.py index 0fdc9ee26..c24602393 100644 --- a/validator/major/requirements/loader.py +++ b/validator/major/requirements/loader.py @@ -1,4 +1,4 @@ -from typing import Type, Literal, get_args, Mapping, TypeGuard +from typing import Type, Literal, get_args, Mapping, TypeGuard, Any RequirementNameT = Literal[ "CourseRequirement", @@ -55,7 +55,7 @@ def __init__(self) -> None: "PsychologyPrefixesOrCourses": psychology.PsychologyPrefixesOrCourses, } - def requirement_from_json(self, json: Mapping) -> AbstractRequirement: + def requirement_from_json(self, json: Mapping[str, Any]) -> AbstractRequirement: if not "matcher" in json: raise ValueError(f"Invalid requirement: {json}, missing 'matcher' key.") if not self._is_valid_requirement(json["matcher"]): diff --git a/validator/major/tests/test_degrees.py b/validator/major/tests/test_degrees.py index 7f27e86a2..f578dd790 100644 --- a/validator/major/tests/test_degrees.py +++ b/validator/major/tests/test_degrees.py @@ -1,7 +1,7 @@ from typing import Any from jsonschema import Draft7Validator, validate -from major.requirements import REQUIREMENTS_MAP +from major.requirements import loader import json from os import DirEntry, scandir import pytest @@ -18,15 +18,18 @@ def test_degrees(file: DirEntry[str]) -> None: data = json.loads(open(file, "r").read()) requirements = data["requirements"]["major"] + req_loader = loader.Loader() for requirement in requirements: if not "matcher" in requirement: pytest.fail(f"'matcher' not in {requirement}") - if not requirement["matcher"] in REQUIREMENTS_MAP: - pytest.fail(f"{requirement['matcher']} not in {REQUIREMENTS_MAP}") + if not requirement["matcher"] in req_loader.REQUIREMENTS_MAP: + pytest.fail( + f"{requirement['matcher']} not in {req_loader.REQUIREMENTS_MAP}" + ) - REQUIREMENTS_MAP[requirement["matcher"]].from_json(requirement) + req_loader.REQUIREMENTS_MAP[requirement["matcher"]].from_json(requirement) @pytest.mark.parametrize( diff --git a/validator/major/tests/test_solver.py b/validator/major/tests/test_solver.py index fea2ab3f8..8e0f05b37 100644 --- a/validator/major/tests/test_solver.py +++ b/validator/major/tests/test_solver.py @@ -1,8 +1,10 @@ from major.solver import MajorRequirementsSolver -from major.requirements import AbstractRequirement, REQUIREMENTS_MAP +from major.requirements import AbstractRequirement, loader import json import copy +LOADER = loader.Loader() + def test_computer_science_solver() -> None: MISSING_FREE_ELECTIVES = [ @@ -89,9 +91,10 @@ def test_computer_science_solver() -> None: requirements_data = data["requirements"]["major"] requirements: list[AbstractRequirement] = [] - for req_data in requirements_data: - requirements.append(REQUIREMENTS_MAP[req_data["matcher"]].from_json(req_data)) + requirements.append( + LOADER.REQUIREMENTS_MAP[req_data["matcher"]].from_json(req_data) + ) solver = MajorRequirementsSolver( MISSING_FREE_ELECTIVES, copy.deepcopy(requirements) @@ -183,7 +186,9 @@ def test_accounting_solver() -> None: requirements: list[AbstractRequirement] = [] for req_data in requirements_data: - requirements.append(REQUIREMENTS_MAP[req_data["matcher"]].from_json(req_data)) + requirements.append( + LOADER.REQUIREMENTS_MAP[req_data["matcher"]].from_json(req_data) + ) solver = MajorRequirementsSolver( MISSING_FREE_ELECTIVES, copy.deepcopy(requirements) @@ -290,7 +295,9 @@ def test_software_engineering_solver() -> None: requirements: list[AbstractRequirement] = [] for req_data in requirements_data: - requirements.append(REQUIREMENTS_MAP[req_data["matcher"]].from_json(req_data)) + requirements.append( + LOADER.REQUIREMENTS_MAP[req_data["matcher"]].from_json(req_data) + ) solver = MajorRequirementsSolver( MISSING_GUIDED_ELECTIVE, copy.deepcopy(requirements) From 2ecdaf06d35f0e32ad8818c47042967ff919957f Mon Sep 17 00:00:00 2001 From: Kevin Ge Date: Mon, 25 Sep 2023 16:56:32 -0500 Subject: [PATCH 08/24] fix: sync major.solver with degree_solver so tests are happy --- validator/major/solver.py | 25 ++- .../major/tests/test_shared_requirements.py | 156 ++++++++---------- validator/major/tests/test_solver.py | 22 ++- 3 files changed, 106 insertions(+), 97 deletions(-) diff --git a/validator/major/solver.py b/validator/major/solver.py index 5f338d6e3..d16c77fc4 100644 --- a/validator/major/solver.py +++ b/validator/major/solver.py @@ -1,19 +1,34 @@ from __future__ import annotations -from major.requirements import AbstractRequirement + +from collections import Counter + +from course import Course +from major.requirements import AbstractRequirement, FreeElectiveRequirement class MajorRequirementsSolver: def __init__( - self, courses: list[str], requirements: list[AbstractRequirement] + self, + courses: list[str], + requirements: list[AbstractRequirement], + used_core_courses: Counter[Course], ) -> None: - self.courses = set(courses) + self.courses = set([Course.from_name(course) for course in courses]) self.requirements = requirements + self.used_core_courses = used_core_courses def solve(self) -> MajorRequirementsSolver: for course in self.courses: for requirement in self.requirements: - fulfilled = requirement.attempt_fulfill(course) - if fulfilled: + if type(requirement) == FreeElectiveRequirement: + if requirement.attempt_fulfill( + course.name, + available_hours=( + int(course.hours) - self.used_core_courses[course] + ), + ): + break + elif requirement.attempt_fulfill(course.name): break return self diff --git a/validator/major/tests/test_shared_requirements.py b/validator/major/tests/test_shared_requirements.py index 4ba8f301e..54eceb10d 100644 --- a/validator/major/tests/test_shared_requirements.py +++ b/validator/major/tests/test_shared_requirements.py @@ -184,16 +184,18 @@ def test_hours_requirement() -> None: def test_free_elective_requirement() -> None: free_elective_req = FreeElectiveRequirement(10, ["HIST 1301", "HIST 1302"]) - free_elective_req.attempt_fulfill("HIST 1301") - free_elective_req.attempt_fulfill("HIST 1302") + free_elective_req.attempt_fulfill("HIST 1301", 3) + free_elective_req.attempt_fulfill("HIST 1302", 3) assert free_elective_req.fulfilled_hours == 0 - assert free_elective_req.attempt_fulfill("CS 9999") + assert free_elective_req.attempt_fulfill("CS 9999", 9) assert free_elective_req.fulfilled_hours == 9 - assert free_elective_req.attempt_fulfill("CS 9199") - assert free_elective_req.attempt_fulfill("CS 9199") == False # already at 10 hours + assert free_elective_req.attempt_fulfill("CS 9199", 1) + assert ( + free_elective_req.attempt_fulfill("CS 9199", 1) == False + ) # already at 10 hours assert free_elective_req.is_fulfilled() data = json.loads( @@ -334,7 +336,6 @@ def test_multi_group_elective_requirement() -> None: # Shouldn't be fulfilled on instantiation assert valid_req_with_no_hrs_requirement.is_fulfilled() == False - for course in [ "ATCM 2330", "ATCM 2303", @@ -357,7 +358,7 @@ def test_multi_group_elective_requirement() -> None: ), ], 2, - 6 + 6, ) for course in [ @@ -367,7 +368,7 @@ def test_multi_group_elective_requirement() -> None: assert req_with_invalid_hrs_requirement.attempt_fulfill(course) assert req_with_invalid_hrs_requirement.is_fulfilled() == False - + req_with_valid_hrs_requirement = MultiGroupElectiveRequirement( [ CourseRequirement( @@ -381,7 +382,7 @@ def test_multi_group_elective_requirement() -> None: ), ], 2, - 3 + 3, ) for course in [ @@ -392,91 +393,80 @@ def test_multi_group_elective_requirement() -> None: assert req_with_valid_hrs_requirement.is_fulfilled() == True - dat = { - "matcher": "MultiGroupElectiveRequirement", - "requirement_count": 2, - "requirements": [ - { + "matcher": "MultiGroupElectiveRequirement", + "requirement_count": 2, + "requirements": [ + { "matcher": "SomeRequirement", "requirements": [ - { - "matcher": "CourseRequirement", - "course": "ATCM 3305", - "metadata": { "id": "006c6d6c-14e3-4fe6-bb62-719c77907f82" } - }, - { - "matcher": "CourseRequirement", - "course": "ATCM 3306", - "metadata": { "id": "6c34c3cc-e826-4e2e-a3e4-e409025a9811" } - }, + { + "matcher": "CourseRequirement", + "course": "ATCM 3305", + "metadata": {"id": "006c6d6c-14e3-4fe6-bb62-719c77907f82"}, + }, + { + "matcher": "CourseRequirement", + "course": "ATCM 3306", + "metadata": {"id": "6c34c3cc-e826-4e2e-a3e4-e409025a9811"}, + }, ], - "metadata": { - "id": "eb2f3604-fca7-4981-abe1-5bb9fadfc6f6" - } - }, - { + "metadata": {"id": "eb2f3604-fca7-4981-abe1-5bb9fadfc6f6"}, + }, + { "matcher": "SomeRequirement", "requirements": [ - { - "matcher": "CourseRequirement", - "course": "ATCM 3315", - "metadata": { "id": "6af07aa1-deb4-45c3-abf4-95e068eb3647" } - }, - { - "matcher": "CourseRequirement", - "course": "ATCM 3320", - "metadata": { "id": "9bacebdb-4400-4e14-8b26-a9ad121399ca" } - }, + { + "matcher": "CourseRequirement", + "course": "ATCM 3315", + "metadata": {"id": "6af07aa1-deb4-45c3-abf4-95e068eb3647"}, + }, + { + "matcher": "CourseRequirement", + "course": "ATCM 3320", + "metadata": {"id": "9bacebdb-4400-4e14-8b26-a9ad121399ca"}, + }, ], - "metadata": { - "id": "7ad01eac-fe39-4112-917d-cd224ec64894" - } - }, - { + "metadata": {"id": "7ad01eac-fe39-4112-917d-cd224ec64894"}, + }, + { "matcher": "SomeRequirement", "requirements": [ - { - "matcher": "CourseRequirement", - "course": "ATCM 3336", - "metadata": { "id": "6bb2b9e7-c062-4ac6-ba71-51ccc46eb04a" } - }, - { - "matcher": "CourseRequirement", - "course": "ATCM 3337", - "metadata": { "id": "b4f1e2d5-1686-4ecc-855c-8f3d929c41bd" } - }, + { + "matcher": "CourseRequirement", + "course": "ATCM 3336", + "metadata": {"id": "6bb2b9e7-c062-4ac6-ba71-51ccc46eb04a"}, + }, + { + "matcher": "CourseRequirement", + "course": "ATCM 3337", + "metadata": {"id": "b4f1e2d5-1686-4ecc-855c-8f3d929c41bd"}, + }, ], - "metadata": { - "id": "4597d751-626f-43cf-b168-a035af70651f" - } - }, - { + "metadata": {"id": "4597d751-626f-43cf-b168-a035af70651f"}, + }, + { "matcher": "SomeRequirement", "requirements": [ - { - "matcher": "CourseRequirement", - "course": "ATCM 3346", - "metadata": { "id": "9cff514f-a531-4ce1-aed7-6a8356dcc092" } - }, - { - "matcher": "CourseRequirement", - "course": "ATCM 3350", - "metadata": { "id": "c21336f9-2b76-43ab-8812-2f1e5d0923ff" } - }, + { + "matcher": "CourseRequirement", + "course": "ATCM 3346", + "metadata": {"id": "9cff514f-a531-4ce1-aed7-6a8356dcc092"}, + }, + { + "matcher": "CourseRequirement", + "course": "ATCM 3350", + "metadata": {"id": "c21336f9-2b76-43ab-8812-2f1e5d0923ff"}, + }, ], - "metadata": { - "id": "37dca0a9-f954-4c04-8bec-fc93fa54539c" - } - } - ], - "minimum_hours_in_area": 6, - "metadata": { - "id": "4d066b30-77e1-4a17-9cea-7a3fb4246fe0" - } - } - - valid_atec_req = MultiGroupElectiveRequirement.from_json(dat) # type: ignore + "metadata": {"id": "37dca0a9-f954-4c04-8bec-fc93fa54539c"}, + }, + ], + "minimum_hours_in_area": 6, + "metadata": {"id": "4d066b30-77e1-4a17-9cea-7a3fb4246fe0"}, + } + + valid_atec_req = MultiGroupElectiveRequirement.from_json(dat) # type: ignore assert valid_atec_req.is_fulfilled() == False @@ -485,10 +475,8 @@ def test_multi_group_elective_requirement() -> None: assert valid_atec_req.is_fulfilled() - unfillable_atec_req = MultiGroupElectiveRequirement.from_json(dat) # type: ignore + unfillable_atec_req = MultiGroupElectiveRequirement.from_json(dat) # type: ignore assert unfillable_atec_req.is_fulfilled() == False for course in ["ATCM 3315", "ATCM 3337"]: assert unfillable_atec_req.attempt_fulfill(course) assert unfillable_atec_req.is_fulfilled() == False - - diff --git a/validator/major/tests/test_solver.py b/validator/major/tests/test_solver.py index 8e0f05b37..7d51979b1 100644 --- a/validator/major/tests/test_solver.py +++ b/validator/major/tests/test_solver.py @@ -1,8 +1,10 @@ -from major.solver import MajorRequirementsSolver -from major.requirements import AbstractRequirement, loader +from collections import Counter import json import copy +from major.solver import MajorRequirementsSolver +from major.requirements import AbstractRequirement, loader + LOADER = loader.Loader() @@ -83,6 +85,10 @@ def test_computer_science_solver() -> None: "CS 4334", # Free Electives "ABC 9999", + "ABd 9999", + "ABt 9999", + "ABy 9999", + "AB1 9999", "DEF 9199", ] @@ -97,14 +103,14 @@ def test_computer_science_solver() -> None: ) solver = MajorRequirementsSolver( - MISSING_FREE_ELECTIVES, copy.deepcopy(requirements) + MISSING_FREE_ELECTIVES, copy.deepcopy(requirements), Counter() ).solve() print(str(solver)) assert solver.can_graduate() == False solver = MajorRequirementsSolver( - GRADUATEABLE_COURSES, copy.deepcopy(requirements) + GRADUATEABLE_COURSES, copy.deepcopy(requirements), Counter() ).solve() print(str(solver)) @@ -191,14 +197,14 @@ def test_accounting_solver() -> None: ) solver = MajorRequirementsSolver( - MISSING_FREE_ELECTIVES, copy.deepcopy(requirements) + MISSING_FREE_ELECTIVES, copy.deepcopy(requirements), Counter() ).solve() print(str(solver)) assert solver.can_graduate() == False solver = MajorRequirementsSolver( - GRADUATEABLE_COURSES, copy.deepcopy(requirements) + GRADUATEABLE_COURSES, copy.deepcopy(requirements), Counter() ).solve() print(str(solver)) @@ -300,13 +306,13 @@ def test_software_engineering_solver() -> None: ) solver = MajorRequirementsSolver( - MISSING_GUIDED_ELECTIVE, copy.deepcopy(requirements) + MISSING_GUIDED_ELECTIVE, copy.deepcopy(requirements), Counter() ).solve() assert solver.can_graduate() == False solver = MajorRequirementsSolver( - GRADUATEABLE_COURSES, copy.deepcopy(requirements) + GRADUATEABLE_COURSES, copy.deepcopy(requirements), Counter() ).solve() assert solver.can_graduate() From 737d2688b54e751b1a8a94e98ce6602d74e5b760 Mon Sep 17 00:00:00 2001 From: peytonbarre Date: Mon, 25 Sep 2023 19:07:13 -0500 Subject: [PATCH 09/24] Added working directory to workflow --- .github/workflows/versioning.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/versioning.yml b/.github/workflows/versioning.yml index b1eb7199d..f9feb58a0 100644 --- a/.github/workflows/versioning.yml +++ b/.github/workflows/versioning.yml @@ -19,6 +19,7 @@ jobs: python-version: '3.10' - name: Install dependencies + working-directory: validator run: | pip3 install -r requirements.txt From e40a1b3bd102b764512043760c88fd92d606da87 Mon Sep 17 00:00:00 2001 From: enkyuan Date: Tue, 26 Sep 2023 12:29:17 -0500 Subject: [PATCH 10/24] fixed keyword highlighting in course description --- src/components/planner/CourseInfoHoverCard.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/planner/CourseInfoHoverCard.tsx b/src/components/planner/CourseInfoHoverCard.tsx index 3881ffc24..0244abfc9 100644 --- a/src/components/planner/CourseInfoHoverCard.tsx +++ b/src/components/planner/CourseInfoHoverCard.tsx @@ -60,10 +60,12 @@ export default CourseInfoHoverCard; */ const CourseDescription = ({ description }: { description: string }) => { const [showMore, setShowMore] = useState(false); + const boldDescription = description.replaceAll(/(\b[A-Z]{2,4} \d{4}\b)/ig, '$1'); return (

- {!showMore ? `${description.substring(0, 200)}... ` : `${description} `} + + {" "}