From 760555b41cf9f02d1c06c7dd700e9425314ec86a Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Mon, 11 Mar 2024 18:04:45 -0600 Subject: [PATCH 01/30] Error on apps with duplicate YTS + PDC Resolves #51 --- config_sample.json | 5 +++-- ps_core.py | 31 ++++++++++++++++++++++++++++++- ps_format.py | 2 ++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/config_sample.json b/config_sample.json index 25d6397..4790227 100644 --- a/config_sample.json +++ b/config_sample.json @@ -99,8 +99,8 @@ }, "teams": { "enabled": false, - "webHookURL" : "[YOUR TEAMS WEBHOOK GOES HERE]", - "title" : "PowerSlate [Production] Alert" + "webHookURL": "[YOUR TEAMS WEBHOOK GOES HERE]", + "title": "PowerSlate [Production] Alert" }, "scheduled_actions": { "enabled": false, @@ -138,6 +138,7 @@ "error_academic_row_not_found": "Record not found by Year/Term/Session/Program/Degree/Curriculum.", "error_invalid_college_attend": "College Attend is set to {} in PowerCampus, which is not valid for applicants.", "error_api_missing_database": "The PowerCampus Web API is not functioning properly. You may need to remove and reinstall the application.", + "error_duplicate_apps": "Person has multiple applications with the same YTS + PCD.", "sync_done": "Sync completed with no errors.", "sync_done_not_found": "Sync completed, but one or more applications had integration errors." }, diff --git a/ps_core.py b/ps_core.py index 26fbae6..f81af23 100644 --- a/ps_core.py +++ b/ps_core.py @@ -319,6 +319,25 @@ def main_sync(pid=None): CURRENT_RECORD = k apps[k] = format_app_generic(v, CONFIG["slate_upload_active"]) + # Set error flag if one pid has multiple applications with the same YTS + PCD + duplicates = [] + for k, v in apps.items(): + duplicates.extend( + [ + kk + for kk, vv in apps.items() + if vv["pid"] == v["pid"] + and vv["Program"] == v["Program"] + and vv["Degree"] == v["Degree"] + and vv["YearTerm"] == v["YearTerm"] + and k != kk + ] + ) + + for k in duplicates: + apps[k]["error_flag"] = True + apps[k]["error_message"] = MSG_STRINGS["error_duplicate_apps"] + if SETTINGS.powercampus.autoconfigure_mappings.enabled: verbose_print("Auto-configure ProgramOfStudy and recruiterMapping.xml") CURRENT_RECORD = None @@ -337,6 +356,8 @@ def main_sync(pid=None): verbose_print("Check each app's status flags/PCID in PowerCampus") for k, v in apps.items(): + if v["error_flag"] == True: + continue CURRENT_RECORD = k status_ra, status_app, status_calc, pcid = ps_powercampus.scan_status(v) apps[k].update( @@ -350,6 +371,8 @@ def main_sync(pid=None): verbose_print("Post new or repost unprocessed applications to PowerCampus API") for k, v in apps.items(): + if v["error_flag"] == True: + continue CURRENT_RECORD = k if (v["status_ra"] == None) or ( v["status_ra"] in (1, 2) and v["status_app"] is None @@ -377,7 +400,11 @@ def main_sync(pid=None): CURRENT_RECORD = None # Send list of app GUID's to Slate; get back checklist items actions_list = slate_get_actions( - [k for (k, v) in apps.items() if v["status_calc"] == "Active"] + [ + k + for (k, v) in apps.items() + if v["status_calc"] == "Active" and v["error_flag"] == False + ] ) if CONFIG["scheduled_actions"]["autolearn_action_codes"] == True: @@ -386,6 +413,8 @@ def main_sync(pid=None): verbose_print("Update existing applications in PowerCampus and extract information") edu_sync_results = [] for k, v in apps.items(): + if v["error_flag"] == False: + continue CURRENT_RECORD = k if v["status_calc"] == "Active": # Transform to PowerCampus format diff --git a/ps_format.py b/ps_format.py index 463822f..017e9d9 100644 --- a/ps_format.py +++ b/ps_format.py @@ -149,6 +149,8 @@ def format_app_generic(app, cfg_fields): """Supply missing fields and correct datatypes. Returns a flat dict.""" mapped = format_blank_to_null(app) + mapped["error_flag"] = False + mapped["error_message"] = None fields_null = [k for (k, v) in ps_models.fields.items() if v["supply_null"] == True] fields_bool = [k for (k, v) in ps_models.fields.items() if v["type"] == bool] From 63580ee2736883ef51e557916cf13b515e2736bf Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Thu, 21 Mar 2024 19:30:22 -0600 Subject: [PATCH 02/30] 9.2.3 API initial compatibility Very lightly tested. Swagger definition includes 3-valued Programs but does not actually work. Use the old 2-valued style for now. New features include: -Token-based auth -Auto-configure Program of Study Data Filters in Application Form -More use of settings class -Changes to Veteran fields -Modified error handling; API now returns 202 instead of 200 --- SQL/[custom].[PS_updProgramOfStudy].sql | 40 +++++++++++++--- config_sample.json | 6 ++- ps_core.py | 42 ++++++++++------- ps_format.py | 34 +++++++------ ps_models.py | 2 +- ps_powercampus.py | 63 +++++++++++++++---------- 6 files changed, 121 insertions(+), 66 deletions(-) diff --git a/SQL/[custom].[PS_updProgramOfStudy].sql b/SQL/[custom].[PS_updProgramOfStudy].sql index 93e99f1..272de7a 100644 --- a/SQL/[custom].[PS_updProgramOfStudy].sql +++ b/SQL/[custom].[PS_updProgramOfStudy].sql @@ -11,13 +11,16 @@ GO -- ============================================= -- Author: Wyatt Best -- Create date: 2021-05-17 --- Description: Inserts a PDC combination into ProgramOfStudy if it doesn't already exist. +-- Description: Inserts a PDC combination into ProgramOfStudy and ApplicationProgramSetting if it doesn't already exist. -- If @DegReqMinYear is not null, PDC combination will be valided against DEGREQ. +-- +-- 2024-03-21 Wyatt Best: Also insert into ApplicationProgramSetting. -- ============================================= CREATE PROCEDURE [custom].[PS_updProgramOfStudy] @Program NVARCHAR(6) ,@Degree NVARCHAR(6) ,@Curriculum NVARCHAR(6) ,@DegReqMinYear NVARCHAR(4) = NULL + ,@AppFormSettingId INT AS BEGIN SET NOCOUNT ON; @@ -36,7 +39,7 @@ BEGIN SELECT CurriculumId FROM CODE_CURRICULUM WHERE CODE_VALUE_KEY = @Curriculum - ) + ); --Error checks IF @ProgramId IS NULL @@ -92,14 +95,17 @@ BEGIN RETURN END - --Check for existing ProgramOfStudy row - IF NOT EXISTS ( - SELECT * + --Get existing ProgramOfStudyId + DECLARE @ProgramOfStudyId INT = ( + SELECT ProgramOfStudyId FROM ProgramOfStudy WHERE Program = @ProgramId AND Degree = @DegreeId AND Curriculum = @CurriculumId - ) + ); + + --If ProgramOfStudyId is null, insert it + IF @ProgramOfStudyId IS NULL BEGIN --Optionally check against DEGREQ IF @DegReqMinYear IS NOT NULL @@ -135,6 +141,28 @@ BEGIN @ProgramId ,@DegreeId ,@CurriculumId + ); + + --Get the newly-inserted ProgramOfStudyId + SET @ProgramOfStudyId = SCOPE_IDENTITY(); + END + + --Check for existing ApplicationProgramSetting row + IF NOT EXISTS ( + SELECT * + FROM ApplicationProgramSetting + WHERE ApplicationFormSettingId = @AppFormSettingId + AND ProgramOfStudyId = @ProgramOfStudyId ) + BEGIN + --Insert new ProgramOfStudyId + INSERT INTO ApplicationProgramSetting ( + ApplicationFormSettingId + ,ProgramOfStudyId + ) + VALUES ( + @AppFormSettingId + ,@ProgramOfStudyId + ); END END diff --git a/config_sample.json b/config_sample.json index 4790227..676d0cd 100644 --- a/config_sample.json +++ b/config_sample.json @@ -2,8 +2,11 @@ "powercampus": { "api": { "url": "https://webapi.school.edu/", + "auth_method": "[token|basic]", "username": "username", - "password": "astrongpassword" + "password": "astrongpassword", + "token": "Bearer [YOUR TOKEN]", + "app_form_setting_id": 3 }, "autoconfigure_mappings": { "enabled": false, @@ -32,7 +35,6 @@ "READ", "blank" ], - "app_form_setting_id": 3, "campus_emailtype": "CAMPUS", "database_string": "Driver={ODBC Driver 17 for SQL Server};Server=servername;Database=campus6;Trusted_Connection=yes;ServerSPN=MSSQLSvc/servername.local.domain.edu;", "mapping_file_location": "\\\\servername\\PowerCampus Mapper\\recruiterMapping.xml", diff --git a/ps_core.py b/ps_core.py index f81af23..52a8e3c 100644 --- a/ps_core.py +++ b/ps_core.py @@ -20,6 +20,7 @@ def __init__(self, config): self.powercampus = self.PowerCampus(config["powercampus"]) self.console_verbose = config["console_verbose"] self.msg_strings = self.FlatDict(config["msg_strings"]) + self.defaults = self.FlatDict(config["defaults"]) class PowerCampus: def __init__(self, config): @@ -327,10 +328,12 @@ def main_sync(pid=None): kk for kk, vv in apps.items() if vv["pid"] == v["pid"] - and vv["Program"] == v["Program"] - and vv["Degree"] == v["Degree"] + and k != kk # Not self + and vv["program"] == v["program"] + and vv["degree"] == v["degree"] + # Needed if and when switching from two to three fields for 9.2.3 + and vv["curriculum"] == v["curriculum"] and vv["YearTerm"] == v["YearTerm"] - and k != kk ] ) @@ -344,14 +347,22 @@ def main_sync(pid=None): mfl = SETTINGS.powercampus.mapping_file_location vd = SETTINGS.powercampus.autoconfigure_mappings.validate_degreq mdy = SETTINGS.powercampus.autoconfigure_mappings.minimum_degreq_year - dp_list = [ - (apps[app]["Program"], apps[app]["Degree"]) - for app in apps - if "Degree" in apps[app] - ] + # Accept either two or three fields for 9.2.3 + if ({k for k in apps if 'degree' in apps[k]}): + program_list = [ + (apps[app]["program"], apps[app]["degree"], apps[app]["curriculum"]) + for app in apps + if "curriculum" in apps[app] + ] + else: + program_list = [ + (apps[app]["Program"], apps[app]["Degree"]) + for app in apps + if "Degree" in apps[app] + ] yt_list = [apps[app]["YearTerm"] for app in apps if "YearTerm" in apps[app]] - if ps_powercampus.autoconfigure_mappings(dp_list, yt_list, vd, mdy, mfl): + if ps_powercampus.autoconfigure_mappings(program_list, yt_list, vd, mdy, mfl): RM_MAPPING = ps_powercampus.get_recruiter_mapping(mfl) verbose_print("Check each app's status flags/PCID in PowerCampus") @@ -374,14 +385,13 @@ def main_sync(pid=None): if v["error_flag"] == True: continue CURRENT_RECORD = k - if (v["status_ra"] == None) or ( - v["status_ra"] in (1, 2) and v["status_app"] is None + if ( + (v["status_ra"] == None) + or (v["status_ra"] in (1, 2) and v["status_app"] is None) + or (v["status_ra"] == 0 and v["status_app"] == None) # 9.2.3 new bad status ): - pcid = ps_powercampus.post_api( - format_app_api(v, CONFIG["defaults"]), - MSG_STRINGS, - SETTINGS.powercampus.app_form_setting_id, - ) + app = format_app_api(v, SETTINGS.defaults) + pcid = ps_powercampus.post_api(app, SETTINGS.powercampus.api, MSG_STRINGS) apps[k]["PEOPLE_CODE_ID"] = pcid # Rescan status diff --git a/ps_format.py b/ps_format.py index 017e9d9..0ecb86c 100644 --- a/ps_format.py +++ b/ps_format.py @@ -235,7 +235,7 @@ def format_app_api(app, cfg_defaults): if "PostalCode" not in k: k["PostalCode"] = None if "County" not in k: - k["County"] = cfg_defaults["address_country"] + k["County"] = cfg_defaults.address_country if len([k for k in app if k[:5] == "Phone"]) > 0: has_phones = True @@ -263,30 +263,39 @@ def format_app_api(app, cfg_defaults): item["Number"] = format_phone_number(item["Number"]) if "Type" not in item: - item["Type"] = cfg_defaults["phone_type"] + item["Type"] = cfg_defaults.phone_type else: item["Type"] = int(item["Type"]) if "Country" not in item: - item["Country"] = cfg_defaults["phone_country"] + item["Country"] = cfg_defaults.phone_country else: # PowerCampus WebAPI requires Type -1 instead of a blank or null when not submitting any phones. mapped["PhoneNumbers"] = [{"Type": -1, "Country": None, "Number": None}] - # Veteran has funny logic, and API 8.8.3 is broken (passing in 1 will write 2 into [Application].[VeteranStatus]). - # Impact is low because custom SQL routines will fix Veteran field once person has passed Handle Applications. + # Veteran logic was updated in API 9.2.x if app["Veteran"] is None: - mapped["Veteran"] = 0 + mapped["Veteran"] = None mapped["VeteranStatus"] = False else: - mapped["Veteran"] = int(app["Veteran"]) + mapped["Veteran"] = app["Veteran"] mapped["VeteranStatus"] = True # Academic program - mapped["Programs"] = [ - {"Program": app["Program"], "Degree": app["Degree"], "Curriculum": None} - ] + # API 9.2.3 seems to accept either three or two fields + if "curriculum" in app: + mapped["Programs"] = [ + { + "program": app["program"], + "degree": app["degree"], + "curriculum": app["curriculum"], + } + ] + else: + mapped["Programs"] = [ + {"Program": app["Program"], "Degree": app["Degree"], "Curriculum": None} + ] # GUID's mapped["ApplicationNumber"] = app["aid"] @@ -358,11 +367,6 @@ def format_app_sql(app, mapping, config): else: mapped["VISA"] = None - if app["Veteran"] is not None: - mapped["VETERAN"] = mapping["Veteran"][str(app["Veteran"])] - else: - mapped["VETERAN"] = None - if app["SecondaryCitizenship"] is not None: mapped["SECONDARYCITIZENSHIP"] = mapping["CitizenshipStatus"][ app["SecondaryCitizenship"] diff --git a/ps_models.py b/ps_models.py index 33c848a..c859a45 100644 --- a/ps_models.py +++ b/ps_models.py @@ -301,7 +301,7 @@ }, "Veteran": { "api_verbatim": False, - "sql_verbatim": False, + "sql_verbatim": True, "supply_null": True, "type": str, }, diff --git a/ps_powercampus.py b/ps_powercampus.py index 0817e31..4120ba6 100644 --- a/ps_powercampus.py +++ b/ps_powercampus.py @@ -6,8 +6,6 @@ def init(config, verbose, msg_strings): - global PC_API_URL - global PC_API_CRED global CNXN global CURSOR global CONFIG @@ -18,23 +16,19 @@ def init(config, verbose, msg_strings): VERBOSE = verbose MSG_STRINGS = msg_strings - # PowerCampus Web API connection - PC_API_URL = config.api.url - PC_API_CRED = (config.api.username, config.api.password) - # Microsoft SQL Server connection. CNXN = pyodbc.connect(config.database_string) CURSOR = CNXN.cursor() # Print a test of connections - r = requests.get(PC_API_URL + "api/version", auth=PC_API_CRED) + r = requests.get(config.api.url + "api/version") verbose_print("PowerCampus API Status: " + str(r.status_code)) verbose_print(r.text) r.raise_for_status() verbose_print("Database:" + CNXN.getinfo(pyodbc.SQL_DATABASE_NAME)) # Enable ApplicationFormSetting's ProcessAutomatically in case program exited abnormally last time with setting toggled off. - update_app_form_autoprocess(config.app_form_setting_id, True) + update_app_form_autoprocess(config.api.app_form_setting_id, True) def de_init(): @@ -56,7 +50,7 @@ def verbose_print(x): def autoconfigure_mappings( - dp_list, yt_list, validate_degreq, minimum_degreq_year, mapping_file_location + program_list, yt_list, validate_degreq, minimum_degreq_year, mapping_file_location ): """ Automatically insert new Program/Degree/Curriculum combinations into ProgramOfStudy and recruiterMapping.xml @@ -65,24 +59,29 @@ def autoconfigure_mappings( and that YearTerm values are like YEAR/TERM/SESSION. Keyword aguments: - dp_list -- a list of tuples like [('PROGRAM','DEGREE/CURRICULUM'), (...)] + program_list -- list of tuples like [('PROGRAM','DEGREE/CURRICULUM'), (...)] or [('PROGRAM','DEGREE','CURRICULUM'), (...)] + yt_list -- list of strings like ['YEAR/TERM/SESSION', ...] validate_degreq -- bool. If True, check against DEGREQ for sanity using minimum_degreq_year. minimum_degreq_year -- str + mapping_file_location -- str. Path to recruiterMapping.xml Returns True if XML mapping changed. """ - dp_set = set(dp_list) + program_set = set(program_list) yt_set = set(yt_list) if validate_degreq == False: minimum_degreq_year = None # Create set of tuples like {('PROGRAM','DEGREE', 'CURRICULUM'), (...)} pdc_set = set() - for dp in dp_set: - pdc = [dp[0]] - for dc in dp[1].split("/"): - pdc.append(dc) - pdc_set.add(tuple(pdc)) + if len(program_list[0]) == 2: + for dp in program_set: + pdc = [dp[0]] + for dc in dp[1].split("/"): + pdc.append(dc) + pdc_set.add(tuple(pdc)) + elif len(program_list[0]) == 3: + pdc_set = program_set # Create a set like {'PROGRAM', 'PROGRAM'} p_set = set() @@ -255,7 +254,7 @@ def get_recruiter_mapping(mapping_file_location): return rm_mapping -def post_api(x, cfg_strings, app_form_setting_id): +def post_api(app, config, msg_strings): """Post an application to PowerCampus. Return PEOPLE_CODE_ID if application was automatically accepted or None for all other conditions. @@ -263,17 +262,26 @@ def post_api(x, cfg_strings, app_form_setting_id): x -- an application dict """ + creds = None + headers = None + if config.auth_method == "basic": + creds = (config.username, config.password) + elif config.auth_method == "token": + headers = {"Authorization": config.token} + # Check for duplicate person. If found, temporarily toggle auto-process off. dup_found = False - CURSOR.execute("EXEC [custom].[PS_selPersonDuplicate] ?", x["GovernmentId"]) + CURSOR.execute("EXEC [custom].[PS_selPersonDuplicate] ?", app["GovernmentId"]) row = CURSOR.fetchone() dup_found = row.DuplicateFound if dup_found: - update_app_form_autoprocess(app_form_setting_id, False) + update_app_form_autoprocess(config.app_form_setting_id, False) # Expose error text response from API, replace useless error message(s). try: - r = requests.post(PC_API_URL + "api/applications", json=x, auth=PC_API_CRED) + r = requests.post( + config.url + "api/applications", json=app, auth=creds, headers=headers + ) r.raise_for_status() # The API returns 202 for mapping errors. Technically 202 is appropriate, but it should bubble up to the user. if r.status_code == 202: @@ -283,26 +291,29 @@ def post_api(x, cfg_strings, app_form_setting_id): rtext = r.text.replace("\r\n", "\n") if dup_found: - update_app_form_autoprocess(app_form_setting_id, True) + update_app_form_autoprocess(config.app_form_setting_id, True) if ( "BadRequest Object reference not set to an instance of an object." in rtext and "ApplicationsController.cs:line 183" in rtext ): - raise ValueError(cfg_strings["error_no_phones"], rtext, e) + raise ValueError(msg_strings["error_no_phones"], rtext, e) elif ( "BadRequest Activation error occured while trying to get instance of type Database, key" in rtext and "ServiceLocatorImplBase.cs:line 53" in rtext ): - raise ValueError(cfg_strings["error_api_missing_database"], rtext, e) - elif r.status_code == 202 or r.status_code == 400: + raise ValueError(msg_strings["error_api_missing_database"], rtext, e) + elif ( + r.status_code == 202 + and "was created successfully in PowerCampus" not in r.text + ) or r.status_code == 400: raise ValueError(rtext) - else: + elif "was created successfully in PowerCampus" not in r.text: raise requests.HTTPError(rtext) if dup_found: - update_app_form_autoprocess(app_form_setting_id, True) + update_app_form_autoprocess(config.app_form_setting_id, True) if r.text[-25:-12] == "New People Id": try: From 85f5c4e139ba9c37faaa090be7ab5c8ab743f136 Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Fri, 22 Mar 2024 10:07:04 -0600 Subject: [PATCH 03/30] Handle missing YearTerm error Breaks msg_strings into its own config file. Also fix missing parameter for PS_updProgramOfStudy Program, Degree, Curriculum should always be capitalized --- config_messages.json | 15 ++++++ config_sample.json | 10 ---- ps_core.py | 118 +++++++++++++++++++++++++------------------ ps_format.py | 19 ++++--- ps_powercampus.py | 26 ++++++---- 5 files changed, 112 insertions(+), 76 deletions(-) create mode 100644 config_messages.json diff --git a/config_messages.json b/config_messages.json new file mode 100644 index 0000000..e7b1d00 --- /dev/null +++ b/config_messages.json @@ -0,0 +1,15 @@ +{ + "error": { + "no_apps": "

No applications found. Perhaps the application(s) are not submitted or are missing required fields?

", + "no_phones": "Application must have at least one phone number.", + "academic_row_not_found": "Record not found by Year/Term/Session/Program/Degree/Curriculum.", + "invalid_college_attend": "College Attend is set to {} in PowerCampus, which is not valid for applicants.", + "api_missing_database": "The PowerCampus Web API is not functioning properly. You may need to remove and reinstall the application.", + "duplicate_apps": "Person has multiple applications with the same YTS + PCD.", + "missing_yt": "Year/Term/Session is missing from the application." + }, + "success": { + "done": "Sync completed with no errors.", + "done_not_found": "Sync completed, but one or more applications had integration errors." + } +} diff --git a/config_sample.json b/config_sample.json index 676d0cd..0a84833 100644 --- a/config_sample.json +++ b/config_sample.json @@ -134,16 +134,6 @@ "phone_country": "US", "phone_type": 1 }, - "msg_strings": { - "error_no_apps": "

No applications found. Perhaps the application(s) are not submitted or are missing required fields?

", - "error_no_phones": "Application must have at least one phone number.", - "error_academic_row_not_found": "Record not found by Year/Term/Session/Program/Degree/Curriculum.", - "error_invalid_college_attend": "College Attend is set to {} in PowerCampus, which is not valid for applicants.", - "error_api_missing_database": "The PowerCampus Web API is not functioning properly. You may need to remove and reinstall the application.", - "error_duplicate_apps": "Person has multiple applications with the same YTS + PCD.", - "sync_done": "Sync completed with no errors.", - "sync_done_not_found": "Sync completed, but one or more applications had integration errors." - }, "http_port": null, "http_ip": null } diff --git a/ps_core.py b/ps_core.py index 52a8e3c..f06cfd5 100644 --- a/ps_core.py +++ b/ps_core.py @@ -15,12 +15,17 @@ # The Settings class should replace the CONFIG global in all new code. class Settings: + class FlatDict: + def __init__(self, contents): + for field in contents: + setattr(self, field, contents[field]) + def __init__(self, config): self.fa_awards = self.FlatDict(config["fa_awards"]) - self.powercampus = self.PowerCampus(config["powercampus"]) self.console_verbose = config["console_verbose"] - self.msg_strings = self.FlatDict(config["msg_strings"]) self.defaults = self.FlatDict(config["defaults"]) + self.PowerCampus = self.PowerCampus(config["powercampus"]) + self.Messages = self.Messages() class PowerCampus: def __init__(self, config): @@ -32,35 +37,34 @@ def __init__(self, config): for d in dicts: setattr(self, d, Settings.FlatDict(config[d])) - class FlatDict: - def __init__(self, contents): - for field in contents: - setattr(self, field, contents[field]) + class Messages: + def __init__(self): + with open("config_messages.json") as file: + messages = json.loads(file.read()) + self.error = Settings.FlatDict(messages["error"]) + self.success = Settings.FlatDict(messages["success"]) def init(config_path): """Reads config file to global CONFIG dict. Many frequently-used variables are copied to their own globals for convenince.""" global CONFIG global CONFIG_PATH - global FIELDS global RM_MAPPING global MSG_STRINGS - global SETTINGS # New global for Settings class + global Settings # New global for Settings class CONFIG_PATH = config_path with open(CONFIG_PATH) as file: CONFIG = json.loads(file.read()) - SETTINGS = Settings(CONFIG) + Settings = Settings(CONFIG) RM_MAPPING = ps_powercampus.get_recruiter_mapping( - SETTINGS.powercampus.mapping_file_location + Settings.PowerCampus.mapping_file_location ) MSG_STRINGS = CONFIG["msg_strings"] # Init PowerCampus API and SQL connections - ps_powercampus.init( - SETTINGS.powercampus, SETTINGS.console_verbose, SETTINGS.msg_strings - ) + ps_powercampus.init(Settings.PowerCampus, Settings.console_verbose) return CONFIG @@ -310,7 +314,7 @@ def main_sync(pid=None): apps = {k["aid"]: k for k in apps} if len(apps) == 0 and pid is not None: # Assuming we're running in interactive (HTTP) mode if pid param exists - raise EOFError(MSG_STRINGS["error_no_apps"]) + raise EOFError(Settings.Messages.error.no_apps) elif len(apps) == 0: # Don't raise an error for scheduled mode return None @@ -318,7 +322,9 @@ def main_sync(pid=None): verbose_print("Clean up app data from Slate (datatypes, supply nulls, etc.)") for k, v in apps.items(): CURRENT_RECORD = k - apps[k] = format_app_generic(v, CONFIG["slate_upload_active"]) + apps[k] = format_app_generic( + v, CONFIG["slate_upload_active"], Settings.Messages + ) # Set error flag if one pid has multiple applications with the same YTS + PCD duplicates = [] @@ -329,30 +335,31 @@ def main_sync(pid=None): for kk, vv in apps.items() if vv["pid"] == v["pid"] and k != kk # Not self - and vv["program"] == v["program"] - and vv["degree"] == v["degree"] + and vv["Program"] == v["Program"] + and vv["Degree"] == v["Degree"] # Needed if and when switching from two to three fields for 9.2.3 - and vv["curriculum"] == v["curriculum"] + and vv["Curriculum"] == v["Curriculum"] and vv["YearTerm"] == v["YearTerm"] ] ) for k in duplicates: apps[k]["error_flag"] = True - apps[k]["error_message"] = MSG_STRINGS["error_duplicate_apps"] + apps[k]["error_message"] = Settings.Messages.error.duplicate_apps - if SETTINGS.powercampus.autoconfigure_mappings.enabled: + if Settings.PowerCampus.autoconfigure_mappings.enabled: verbose_print("Auto-configure ProgramOfStudy and recruiterMapping.xml") CURRENT_RECORD = None - mfl = SETTINGS.powercampus.mapping_file_location - vd = SETTINGS.powercampus.autoconfigure_mappings.validate_degreq - mdy = SETTINGS.powercampus.autoconfigure_mappings.minimum_degreq_year + mfl = Settings.PowerCampus.mapping_file_location + vd = Settings.PowerCampus.autoconfigure_mappings.validate_degreq + mdy = Settings.PowerCampus.autoconfigure_mappings.minimum_degreq_year + afsi = Settings.PowerCampus.api.app_form_setting_id # Accept either two or three fields for 9.2.3 - if ({k for k in apps if 'degree' in apps[k]}): + if {k for k in apps if "Degree" in apps[k]}: program_list = [ - (apps[app]["program"], apps[app]["degree"], apps[app]["curriculum"]) + (apps[app]["Program"], apps[app]["Degree"], apps[app]["Curriculum"]) for app in apps - if "curriculum" in apps[app] + if "Curriculum" in apps[app] ] else: program_list = [ @@ -362,7 +369,14 @@ def main_sync(pid=None): ] yt_list = [apps[app]["YearTerm"] for app in apps if "YearTerm" in apps[app]] - if ps_powercampus.autoconfigure_mappings(program_list, yt_list, vd, mdy, mfl): + if ps_powercampus.autoconfigure_mappings( + program_list, + yt_list, + vd, + mdy, + mfl, + afsi, + ): RM_MAPPING = ps_powercampus.get_recruiter_mapping(mfl) verbose_print("Check each app's status flags/PCID in PowerCampus") @@ -390,20 +404,28 @@ def main_sync(pid=None): or (v["status_ra"] in (1, 2) and v["status_app"] is None) or (v["status_ra"] == 0 and v["status_app"] == None) # 9.2.3 new bad status ): - app = format_app_api(v, SETTINGS.defaults) - pcid = ps_powercampus.post_api(app, SETTINGS.powercampus.api, MSG_STRINGS) - apps[k]["PEOPLE_CODE_ID"] = pcid - - # Rescan status - status_ra, status_app, status_calc, pcid = ps_powercampus.scan_status(v) - apps[k].update( - { - "status_ra": status_ra, - "status_app": status_app, - "status_calc": status_calc, - } + app, error_flag, error_message = format_app_api( + v, Settings.defaults, Settings.Messages ) - apps[k]["PEOPLE_CODE_ID"] = pcid + if error_flag: + apps[k]["error_flag"] = error_flag + apps[k]["error_message"] = error_message + else: + pcid = ps_powercampus.post_api( + app, Settings.PowerCampus.api, Settings.Messages + ) + apps[k]["PEOPLE_CODE_ID"] = pcid + + # Rescan status + status_ra, status_app, status_calc, pcid = ps_powercampus.scan_status(v) + apps[k].update( + { + "status_ra": status_ra, + "status_app": status_app, + "status_calc": status_calc, + } + ) + apps[k]["PEOPLE_CODE_ID"] = pcid verbose_print("Get scheduled actions from Slate") if CONFIG["scheduled_actions"]["enabled"] == True: @@ -428,14 +450,14 @@ def main_sync(pid=None): CURRENT_RECORD = k if v["status_calc"] == "Active": # Transform to PowerCampus format - app_pc = format_app_sql(v, RM_MAPPING, SETTINGS.powercampus) + app_pc = format_app_sql(v, RM_MAPPING, Settings.PowerCampus) pcid = app_pc["PEOPLE_CODE_ID"] academic_year = app_pc["ACADEMIC_YEAR"] academic_term = app_pc["ACADEMIC_TERM"] academic_session = app_pc["ACADEMIC_SESSION"] # Single-row updates - if SETTINGS.powercampus.update_academic_key: + if Settings.PowerCampus.update_academic_key: ps_powercampus.update_academic_key(app_pc) ps_powercampus.update_demographics(app_pc) ps_powercampus.update_academic(app_pc) @@ -476,7 +498,7 @@ def main_sync(pid=None): ps_powercampus.update_test_scores(pcid, test) # Update any PowerCampus Notes defined in config - for note in SETTINGS.powercampus.notes: + for note in Settings.PowerCampus.notes: if ( note["slate_field"] in app_pc and len(app_pc[note["slate_field"]]) > 0 @@ -486,7 +508,7 @@ def main_sync(pid=None): ) # Update any PowerCampus User Defined fields defined in config - for udf in SETTINGS.powercampus.user_defined_fields: + for udf in Settings.PowerCampus.user_defined_fields: if udf["slate_field"] in app_pc and len(app_pc[udf["slate_field"]]) > 0: ps_powercampus.update_udf( app_pc, udf["slate_field"], udf["pc_field"] @@ -505,7 +527,7 @@ def main_sync(pid=None): ps_powercampus.update_scholarship( pcid, scholarship, - SETTINGS.powercampus.validate_scholarship_levels, + Settings.PowerCampus.validate_scholarship_levels, ) # Update PowerCampus Associations @@ -532,7 +554,7 @@ def main_sync(pid=None): custom_4, custom_5, ) = ps_powercampus.get_profile( - app_pc, SETTINGS.powercampus.campus_emailtype + app_pc, Settings.PowerCampus.campus_emailtype, Settings.Messages ) apps[k].update( { @@ -557,7 +579,7 @@ def main_sync(pid=None): sync_errors == True # Get PowerFAIDS awards and tracking status - if SETTINGS.fa_awards.enabled: + if Settings.fa_awards.enabled: fa_awards, fa_status = ps_powercampus.pf_get_awards( pcid, v["GovernmentId"], @@ -591,7 +613,7 @@ def main_sync(pid=None): CURRENT_RECORD = k if v["status_calc"] == "Active": # Transform to PowerCampus format - app_pc = format_app_sql(v, RM_MAPPING, SETTINGS.powercampus) + app_pc = format_app_sql(v, RM_MAPPING, Settings.PowerCampus) fa_checklists = ps_powercampus.pf_get_fachecklist( app_pc["PEOPLE_CODE_ID"], diff --git a/ps_format.py b/ps_format.py index 0ecb86c..f4c351f 100644 --- a/ps_format.py +++ b/ps_format.py @@ -145,7 +145,7 @@ def format_str_digits(s): return s.translate(non_digits) -def format_app_generic(app, cfg_fields): +def format_app_generic(app, cfg_fields, Messages): """Supply missing fields and correct datatypes. Returns a flat dict.""" mapped = format_blank_to_null(app) @@ -183,7 +183,7 @@ def format_app_generic(app, cfg_fields): return mapped -def format_app_api(app, cfg_defaults): +def format_app_api(app, cfg_defaults, Messages): """Remap application to Recruiter/Web API format. Keyword arguments: @@ -192,6 +192,11 @@ def format_app_api(app, cfg_defaults): mapped = {} + # Error checks + if "YearTerm" not in app: + error_message = Messages.error.missing_yt + error_flag = True + # Pass through fields fields_verbatim = [ k for (k, v) in ps_models.fields.items() if v["api_verbatim"] == True @@ -284,12 +289,12 @@ def format_app_api(app, cfg_defaults): # Academic program # API 9.2.3 seems to accept either three or two fields - if "curriculum" in app: + if "Curriculum" in app: mapped["Programs"] = [ { - "program": app["program"], - "degree": app["degree"], - "curriculum": app["curriculum"], + "Program": app["Program"], + "Degree": app["Degree"], + "Curriculum": app["Curriculum"], } ] else: @@ -301,7 +306,7 @@ def format_app_api(app, cfg_defaults): mapped["ApplicationNumber"] = app["aid"] mapped["ProspectId"] = app["pid"] - return mapped + return mapped, error_flag, error_message def format_app_sql(app, mapping, config): diff --git a/ps_powercampus.py b/ps_powercampus.py index 4120ba6..de4a612 100644 --- a/ps_powercampus.py +++ b/ps_powercampus.py @@ -5,16 +5,14 @@ import ps_models -def init(config, verbose, msg_strings): +def init(config, verbose): global CNXN global CURSOR global CONFIG global VERBOSE - global MSG_STRINGS CONFIG = config VERBOSE = verbose - MSG_STRINGS = msg_strings # Microsoft SQL Server connection. CNXN = pyodbc.connect(config.database_string) @@ -50,7 +48,12 @@ def verbose_print(x): def autoconfigure_mappings( - program_list, yt_list, validate_degreq, minimum_degreq_year, mapping_file_location + program_list, + yt_list, + validate_degreq, + minimum_degreq_year, + mapping_file_location, + app_form_setting_id, ): """ Automatically insert new Program/Degree/Curriculum combinations into ProgramOfStudy and recruiterMapping.xml @@ -110,11 +113,12 @@ def autoconfigure_mappings( # Update ProgramOfStudy table; optionally validate against DEGREQ table for pdc in pdc_set: CURSOR.execute( - "execute [custom].[PS_updProgramOfStudy] ?, ?, ?, ?", + "execute [custom].[PS_updProgramOfStudy] ?, ?, ?, ?, ?", pdc[0], pdc[1], pdc[2], minimum_degreq_year, + app_form_setting_id, ) CNXN.commit() @@ -254,7 +258,7 @@ def get_recruiter_mapping(mapping_file_location): return rm_mapping -def post_api(app, config, msg_strings): +def post_api(app, config, Messages): """Post an application to PowerCampus. Return PEOPLE_CODE_ID if application was automatically accepted or None for all other conditions. @@ -297,13 +301,13 @@ def post_api(app, config, msg_strings): "BadRequest Object reference not set to an instance of an object." in rtext and "ApplicationsController.cs:line 183" in rtext ): - raise ValueError(msg_strings["error_no_phones"], rtext, e) + raise ValueError(Messages.error.no_phones, rtext, e) elif ( "BadRequest Activation error occured while trying to get instance of type Database, key" in rtext and "ServiceLocatorImplBase.cs:line 53" in rtext ): - raise ValueError(msg_strings["error_api_missing_database"], rtext, e) + raise ValueError(Messages.error.api_missing_database, rtext, e) elif ( r.status_code == 202 and "was created successfully in PowerCampus" not in r.text @@ -396,7 +400,7 @@ def scan_status(x): return ra_status, apl_status, computed_status, pcid -def get_profile(app, campus_email_type): +def get_profile(app, campus_email_type, Messages): """Fetch ACADEMIC row data and email address from PowerCampus. Returns: @@ -410,7 +414,7 @@ def get_profile(app, campus_email_type): """ error_flag = True - error_message = MSG_STRINGS.error_academic_row_not_found + error_message = Messages.error.academic_row_not_found registered = False reg_date = None readmit = False @@ -464,7 +468,7 @@ def get_profile(app, campus_email_type): if college_attend not in CONFIG.valid_college_attend: error_flag = True - error_message = MSG_STRINGS.error_invalid_college_attend.format( + error_message = Settings.Messages.error.invalid_college_attend.format( college_attend ) From 34c7adb14c4d64931f6b314a71432889c0c3c477 Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Fri, 22 Mar 2024 10:15:50 -0600 Subject: [PATCH 04/30] Improve final success message --- config_messages.json | 2 +- ps_core.py | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/config_messages.json b/config_messages.json index e7b1d00..318af80 100644 --- a/config_messages.json +++ b/config_messages.json @@ -10,6 +10,6 @@ }, "success": { "done": "Sync completed with no errors.", - "done_not_found": "Sync completed, but one or more applications had integration errors." + "done_with_errors": "Sync completed, but one or more applications had integration errors." } } diff --git a/ps_core.py b/ps_core.py index f06cfd5..65dbe62 100644 --- a/ps_core.py +++ b/ps_core.py @@ -50,7 +50,6 @@ def init(config_path): global CONFIG global CONFIG_PATH global RM_MAPPING - global MSG_STRINGS global Settings # New global for Settings class CONFIG_PATH = config_path @@ -61,7 +60,6 @@ def init(config_path): RM_MAPPING = ps_powercampus.get_recruiter_mapping( Settings.PowerCampus.mapping_file_location ) - MSG_STRINGS = CONFIG["msg_strings"] # Init PowerCampus API and SQL connections ps_powercampus.init(Settings.PowerCampus, Settings.console_verbose) @@ -629,10 +627,10 @@ def main_sync(pid=None): slate_post_fa_checklist(slate_upload_list) # Warn if any apps returned an error flag from ps_powercampus.get_profile() - if sync_errors == True: - output_msg = MSG_STRINGS["sync_done_not_found"] + if sync_errors == True or [k for k in apps if apps[k]["error_flag"] == True]: + output_msg = Settings.Messages.success.done_with_errors else: - output_msg = MSG_STRINGS["sync_done"] + output_msg = Settings.Messages.success.done verbose_print(output_msg) return output_msg From 6b9de21f32ea4d2b4fef99dc2bdee7ba7ffd7e8b Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Fri, 22 Mar 2024 10:34:23 -0600 Subject: [PATCH 05/30] API compatibility tweaks -Transform PDC to PD for API -Fix uninitialized variables -Fix incorrect handling of good 202 status --- ps_format.py | 8 +++++--- ps_powercampus.py | 8 +++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ps_format.py b/ps_format.py index f4c351f..15a95ea 100644 --- a/ps_format.py +++ b/ps_format.py @@ -191,6 +191,8 @@ def format_app_api(app, cfg_defaults, Messages): """ mapped = {} + error_flag = False + error_message = None # Error checks if "YearTerm" not in app: @@ -288,13 +290,13 @@ def format_app_api(app, cfg_defaults, Messages): mapped["VeteranStatus"] = True # Academic program - # API 9.2.3 seems to accept either three or two fields + # API 9.2.3 still requires two fields for Program and Degree, even though the Swagger schema contains three fields. if "Curriculum" in app: mapped["Programs"] = [ { "Program": app["Program"], - "Degree": app["Degree"], - "Curriculum": app["Curriculum"], + "Degree": app["Degree"] + "/" + app["Curriculum"], + "Curriculum": None, } ] else: diff --git a/ps_powercampus.py b/ps_powercampus.py index de4a612..993c704 100644 --- a/ps_powercampus.py +++ b/ps_powercampus.py @@ -310,10 +310,10 @@ def post_api(app, config, Messages): raise ValueError(Messages.error.api_missing_database, rtext, e) elif ( r.status_code == 202 - and "was created successfully in PowerCampus" not in r.text + and "was created successfully in PowerCampus" in r.text == False ) or r.status_code == 400: raise ValueError(rtext) - elif "was created successfully in PowerCampus" not in r.text: + elif "was created successfully in PowerCampus" not in r.text == False: raise requests.HTTPError(rtext) if dup_found: @@ -468,9 +468,7 @@ def get_profile(app, campus_email_type, Messages): if college_attend not in CONFIG.valid_college_attend: error_flag = True - error_message = Settings.Messages.error.invalid_college_attend.format( - college_attend - ) + error_message = Messages.error.invalid_college_attend.format(college_attend) if row.Withdrawn == "Y": withdrawn = True From 3b6a34fbfdf2e22aa88a7a427d4c485330f6600c Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Fri, 22 Mar 2024 10:39:38 -0600 Subject: [PATCH 06/30] Remove SQL logging feature Slow and rarely used. API 9.2.3 doesn't write most (any?) errors to RecruiterApplication table anymore, so this would need rewritten. --- .../Create Table PowerSlate_AppStatus_Log.sql | 33 ------------------- Tools/GRANT EXEC.sql | 3 -- Tools/Prune log table.sql | 20 ----------- Tools/Select Most Recent Status.sql | 28 ---------------- config_sample.json | 4 --- ps_powercampus.py | 25 -------------- 6 files changed, 113 deletions(-) delete mode 100644 Tools/Create Table PowerSlate_AppStatus_Log.sql delete mode 100644 Tools/Prune log table.sql delete mode 100644 Tools/Select Most Recent Status.sql diff --git a/Tools/Create Table PowerSlate_AppStatus_Log.sql b/Tools/Create Table PowerSlate_AppStatus_Log.sql deleted file mode 100644 index 10c3aec..0000000 --- a/Tools/Create Table PowerSlate_AppStatus_Log.sql +++ /dev/null @@ -1,33 +0,0 @@ -USE [PowerCampusMapper] -GO - -/****** Object: Table [dbo].[PowerSlate_AppStatus_Log_test] Script Date: 2/18/2021 3:50:29 PM ******/ -SET ANSI_NULLS ON -GO - -SET QUOTED_IDENTIFIER ON -GO - -CREATE TABLE [dbo].[PowerSlate_AppStatus_Log]( - [ApplicationNumber] [uniqueidentifier] NULL, - [ProspectId] [uniqueidentifier] NULL, - [FirstName] [nvarchar](50) NULL, - [LastName] [nvarchar](50) NULL, - [ComputedStatus] [nvarchar](50) NULL, - [Notes] [nvarchar](max) NULL, - [RecruiterApplicationStatus] [int] NULL, - [ApplicationStatus] [int] NULL, - [PEOPLE_CODE_ID] [nvarchar](10) NULL, - [UpdateTime] [datetime2](7) NULL, - [ID] [int] IDENTITY(1,1) NOT NULL, - [Ref] [nvarchar](16) NULL, -PRIMARY KEY CLUSTERED -( - [ID] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] -GO - -ALTER TABLE [dbo].[PowerSlate_AppStatus_Log] ADD CONSTRAINT [PowerSlate_AppStatus_Log_UpdateTime] DEFAULT (getdate()) FOR [UpdateTime] -GO - diff --git a/Tools/GRANT EXEC.sql b/Tools/GRANT EXEC.sql index 54472ef..4903c0e 100644 --- a/Tools/GRANT EXEC.sql +++ b/Tools/GRANT EXEC.sql @@ -27,6 +27,3 @@ GRANT EXEC ON [custom].[PS_selPFAwardsXML] to [$(service_user)] GRANT EXEC ON [custom].[PS_selAcademicCalendar] to [$(service_user)] GRANT EXEC ON [custom].[PS_updScholarships] to [$(service_user)] GRANT EXEC ON [custom].[PS_updAssociation] to [$(service_user)] - -USE [PowerCampusMapper] -GRANT INSERT ON PowerSlate_AppStatus_Log TO [$(service_user)] diff --git a/Tools/Prune log table.sql b/Tools/Prune log table.sql deleted file mode 100644 index 49147f6..0000000 --- a/Tools/Prune log table.sql +++ /dev/null @@ -1,20 +0,0 @@ -USE powercampusmapper; - -WITH RankedIDs_CTE -AS ( - SELECT ID - ,RANK() OVER ( - PARTITION BY ApplicationNumber ORDER BY ID DESC - ) [RankDesc] - ,NULL AS [RankAsc] - ,ApplicationNumber - FROM [PowerSlate_AppStatus_Log_test] - ) -DELETE TOP (9000) -FROM RankedIDs_CTE -WHERE [RankDesc] >= 6 - AND ID <> ( - SELECT MIN(ID) - FROM [PowerSlate_AppStatus_Log_test] L2 - WHERE L2.ApplicationNumber = RankedIDs_CTE.ApplicationNumber - ); diff --git a/Tools/Select Most Recent Status.sql b/Tools/Select Most Recent Status.sql deleted file mode 100644 index 3f1765a..0000000 --- a/Tools/Select Most Recent Status.sql +++ /dev/null @@ -1,28 +0,0 @@ -USE [your_supplementary_database] - -SELECT Ref - ,ApplicationNumber - ,ProspectId - ,FirstName - ,LastName - ,ComputedStatus - ,Notes - ,RecruiterApplicationStatus - ,ApplicationStatus - ,PEOPLE_CODE_ID - ,UpdateTime -FROM - (SELECT Ref - ,ApplicationNumber - ,ProspectId - ,FirstName - ,LastName - ,ComputedStatus - ,Notes - ,RecruiterApplicationStatus - ,ApplicationStatus - ,PEOPLE_CODE_ID - ,UpdateTime - ,RANK() OVER (PARTITION BY ApplicationNumber ORDER BY ID DESC) N - FROM SlaPowInt_AppStatus_Log) E -WHERE N = 1 diff --git a/config_sample.json b/config_sample.json index 0a84833..24b5331 100644 --- a/config_sample.json +++ b/config_sample.json @@ -20,10 +20,6 @@ "note_type": "GENRL" } ], - "logging": { - "enabled": true, - "log_table": "[SomeDatabase].[dbo].[PowerSlate_AppStatus_Log]" - }, "user_defined_fields": [ { "slate_field": "FirstGeneration", diff --git a/ps_powercampus.py b/ps_powercampus.py index 993c704..ec03732 100644 --- a/ps_powercampus.py +++ b/ps_powercampus.py @@ -372,31 +372,6 @@ def scan_status(x): else: computed_status = "Unrecognized Status: " + str(row.ra_status) - if CONFIG.logging.enabled: - # Write errors to external database for end-user presentation via SSRS. - CURSOR.execute( - "INSERT INTO" - + CONFIG.logging.log_table - + """ - ([Ref],[ApplicationNumber],[ProspectId],[FirstName],[LastName], - [ComputedStatus],[Notes],[RecruiterApplicationStatus],[ApplicationStatus],[PEOPLE_CODE_ID]) - VALUES - (?,?,?,?,?,?,?,?,?,?)""", - [ - x["Ref"], - x["aid"], - x["pid"], - x["FirstName"], - x["LastName"], - computed_status, - row.ra_errormessage, - row.ra_status, - row.apl_status, - pcid, - ], - ) - CNXN.commit() - return ra_status, apl_status, computed_status, pcid From 4d3757c68a36cf30534c70796e2f5890daf04dc3 Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Fri, 29 Mar 2024 12:08:57 -0600 Subject: [PATCH 07/30] Utilize new ACADEMIC Guid First draft; may contain bugs! --- SQL/[custom].[PS_selProfile].sql | 236 +++++++++++++++++---------- SQL/[custom].[PS_updAcademicKey].sql | 126 +++++--------- config_sample.json | 3 +- ps_core.py | 7 +- ps_models.py | 6 + ps_powercampus.py | 82 ++++++---- 6 files changed, 250 insertions(+), 210 deletions(-) diff --git a/SQL/[custom].[PS_selProfile].sql b/SQL/[custom].[PS_selProfile].sql index 3d3d190..7497323 100644 --- a/SQL/[custom].[PS_selProfile].sql +++ b/SQL/[custom].[PS_selProfile].sql @@ -26,6 +26,7 @@ GO -- 2021-12-01 Wyatt Best: Renamed MoodleOrientationComplete to custom_1 and added 4 more custom fields. -- 2022-02-16 Wyatt Best: Added @EmailType parameter. -- 2022-09-27 Wyatt Best: Return AD username (SSO ID) to Slate from PersonUser. +-- 2024-03-29 Wyatt Best: Return Guid column (new in 9.2.3) or error message about changed YTSPDC. -- ============================================= CREATE PROCEDURE [custom].[PS_selProfile] @PCID NVARCHAR(10) ,@Year NVARCHAR(4) @@ -35,10 +36,16 @@ CREATE PROCEDURE [custom].[PS_selProfile] @PCID NVARCHAR(10) ,@Degree NVARCHAR(6) ,@Curriculum NVARCHAR(6) ,@EmailType NVARCHAR(10) + ,@AcademicGuid UNIQUEIDENTIFIER AS BEGIN SET NOCOUNT ON; + --Writing a simple method for handling non-fatal errors because differentiating real + --errors in Python is hard and varies based on the ODBC driver used. + DECLARE @ErrorFlag BIT = 0 + ,@ErrorMessage NVARCHAR(max) + --Error check IF ( @EmailType IS NOT NULL @@ -59,10 +66,10 @@ BEGIN RETURN END - --Select credits from rollup to avoid duplicate hits to table + --Select credits from rollup DECLARE @Credits NUMERIC(6, 2) = ( SELECT CREDITS - FROM ACADEMIC A + FROM ACADEMIC WHERE PEOPLE_CODE_ID = @PCID AND ACADEMIC_YEAR = @Year AND ACADEMIC_TERM = @Term @@ -72,87 +79,146 @@ BEGIN AND CURRICULUM = @Curriculum ) - --If someone has multiple apps for one YTS with different PDC's but the same transcript sequence, you will not be able to - --separate the credits because TRANSCRIPTDETAIL doesn't have PDC. Custom logic is required to sort out things like-zero credit certificate - --dual enrollment with a for-credit program. - SELECT CASE - WHEN @Credits > 0 - THEN 'Y' - WHEN PROGRAM = 'CERT' - AND EXISTS ( - SELECT TD.PEOPLE_ID - FROM TRANSCRIPTDETAIL TD - INNER JOIN ACADEMIC A - ON A.PEOPLE_CODE_ID = TD.PEOPLE_CODE_ID - AND A.ACADEMIC_YEAR = TD.ACADEMIC_YEAR - AND A.ACADEMIC_TERM = TD.ACADEMIC_TERM - AND A.ACADEMIC_SESSION = TD.ACADEMIC_SESSION - AND A.PROGRAM = @Program - AND A.DEGREE = @Degree - AND A.CURRICULUM = @Curriculum - AND A.TRANSCRIPT_SEQ = TD.TRANSCRIPT_SEQ - --AND A.ACADEMIC_FLAG = 'Y' --Can mask some issues of registrations w/out acceptance, but needed for someone who applies for CERT and UNDER and only registers for undergrad. - AND A.APPLICATION_FLAG = 'Y' - WHERE TD.PEOPLE_CODE_ID = @PCID - AND TD.ACADEMIC_YEAR = @Year - AND TD.ACADEMIC_TERM = @Term - AND TD.ACADEMIC_SESSION = @Session - AND TD.ADD_DROP_WAIT = 'A' - ) - THEN 'Y' - ELSE 'N' - END AS 'Registered' - ,CAST(COALESCE(PREREG_VAL_DATE, REG_VAL_DATE) AS DATE) [REG_VAL_DATE] - ,cast(@Credits AS VARCHAR(6)) AS CREDITS - ,A.COLLEGE_ATTEND - ,( - SELECT REQUIRE_SEPDATE - FROM CODE_ENROLLMENT - WHERE CODE_VALUE_KEY = A.ENROLL_SEPARATION - ) AS Withdrawn - ,oE.Email AS CampusEmail - ,( - SELECT NonQualifiedUserName - FROM PersonUser - WHERE PersonId = dbo.fnGetPersonId(A.ADVISOR) - ) [AdvisorUsername] - ,( - SELECT NonQualifiedUserName - FROM PersonUser - WHERE PersonId = dbo.fnGetPersonId(A.PEOPLE_CODE_ID) - ) [Username] - ,CASE - WHEN EXISTS ( - SELECT * - FROM TESTSCORES T - WHERE TEST_ID = 'MOOD' - AND TEST_TYPE = 'STU' - AND ALPHA_SCORE_1 = 'P' - AND T.PEOPLE_CODE_ID = A.PEOPLE_CODE_ID - ) - THEN 'Y' - ELSE 'N' - END [custom_1] - ,NULL [custom_2] - ,NULL [custom_3] - ,NULL [custom_4] - ,NULL [custom_5] - FROM ACADEMIC A - OUTER APPLY ( - SELECT TOP 1 Email - FROM EmailAddress E - WHERE E.PeopleOrgCodeId = A.PEOPLE_CODE_ID - AND E.EmailType = @EmailType - AND E.IsActive = 1 - ORDER BY E.REVISION_DATE DESC - ,REVISION_TIME DESC - ) oE - WHERE PEOPLE_CODE_ID = @PCID - AND ACADEMIC_YEAR = @Year - AND ACADEMIC_TERM = @Term - AND ACADEMIC_SESSION = @Session - AND PROGRAM = @Program - AND DEGREE = @Degree - AND CURRICULUM = @Curriculum - AND APPLICATION_FLAG = 'Y' --Ought to be an application, or there's a problem somewhere. + --Search for @AcademicGuid if NULL + IF @AcademicGuid IS NULL + SELECT @AcademicGuid = [Guid] + FROM ACADEMIC + WHERE PEOPLE_CODE_ID = @PCID + AND ACADEMIC_YEAR = @Year + AND ACADEMIC_TERM = @Term + AND ACADEMIC_SESSION = @Session + AND PROGRAM = @Program + AND DEGREE = @Degree + AND CURRICULUM = @Curriculum + ELSE + BEGIN + --Verify that YTSPDC match existing @AcademicGuid + DECLARE @TempYear NVARCHAR(4) + ,@TempTerm NVARCHAR(10) + ,@TempSession NVARCHAR(10) + ,@TempProgram NVARCHAR(6) + ,@TempDegree NVARCHAR(6) + ,@TempCurriculum NVARCHAR(6) + + SELECT @TempYear = ACADEMIC_YEAR + ,@TempTerm = ACADEMIC_TERM + ,@TempSession = ACADEMIC_SESSION + ,@TempProgram = PROGRAM + ,@TempDegree = DEGREE + ,@TempCurriculum = CURRICULUM + FROM [ACADEMIC] + WHERE [Guid] = @AcademicGuid + + IF @TempYear <> @Year + OR @TempTerm <> @Term + OR @TempSession <> @Session + OR @TempProgram <> @Program + OR @TempDegree <> @Degree + OR @TempCurriculum <> @Curriculum + SELECT @ErrorFlag = 1 + ,@ErrorMessage = 'The Application in PowerCampus has a different YTS + PDC than the Slate application.
+ Expected: ' + @Year + '/' + @Term + '/' + @Session + '/' + @Program + '/' + @Degree + '/' + @Curriculum + '
+ Found: ' + @TempYear + '/' + @TempTerm + '/' + @TempSession + '/' + @TempProgram + '/' + @TempDegree + '/' + @TempCurriculum + END + + --Return error if @ErrorFlag = 1 + IF @ErrorFlag = 1 + SELECT NULL [Registered] + ,NULL [REG_VAL_DATE] + ,NULL [CREDITS] + ,NULL [COLLEGE_ATTEND] + ,NULL [Withdrawn] + ,NULL [CampusEmail] + ,NULL [AdvisorUsername] + ,NULL [Username] + ,NULL [custom_1] + ,NULL [custom_2] + ,NULL [custom_3] + ,NULL [custom_4] + ,NULL [custom_5] + ,@ErrorFlag [ErrorFlag] + ,@ErrorMessage [ErrorMessage] + ELSE + BEGIN + --Else return ACADEMIC columns + -- + --If someone has multiple apps for one YTS with different PDC's but the same transcript sequence, you will not be able to + --separate the credits because TRANSCRIPTDETAIL doesn't have PDC. Custom logic is required to sort out things like-zero credit certificate + --dual enrollment with a for-credit program. + SELECT CASE + WHEN @Credits > 0 + THEN 'Y' + WHEN PROGRAM = 'CERT' + AND EXISTS ( + SELECT TD.PEOPLE_ID + FROM TRANSCRIPTDETAIL TD + INNER JOIN ACADEMIC A + ON A.[Guid] = @AcademicGuid + --A.PEOPLE_CODE_ID = TD.PEOPLE_CODE_ID + --AND A.ACADEMIC_YEAR = TD.ACADEMIC_YEAR + --AND A.ACADEMIC_TERM = TD.ACADEMIC_TERM + --AND A.ACADEMIC_SESSION = TD.ACADEMIC_SESSION + --AND A.PROGRAM = @Program + --AND A.DEGREE = @Degree + --AND A.CURRICULUM = @Curriculum + --AND A.TRANSCRIPT_SEQ = TD.TRANSCRIPT_SEQ + --AND A.ACADEMIC_FLAG = 'Y' --Can mask some issues of registrations w/out acceptance, but needed for someone who applies for CERT and UNDER and only registers for undergrad. + AND A.APPLICATION_FLAG = 'Y' + WHERE TD.PEOPLE_CODE_ID = @PCID + AND TD.ACADEMIC_YEAR = @Year + AND TD.ACADEMIC_TERM = @Term + AND TD.ACADEMIC_SESSION = @Session + AND TD.ADD_DROP_WAIT = 'A' + ) + THEN 'Y' + ELSE 'N' + END AS [Registered] + ,CAST(COALESCE(PREREG_VAL_DATE, REG_VAL_DATE) AS DATE) AS [REG_VAL_DATE] + ,cast(@Credits AS VARCHAR(6)) AS [CREDITS] + ,A.[COLLEGE_ATTEND] + ,( + SELECT REQUIRE_SEPDATE + FROM CODE_ENROLLMENT + WHERE CODE_VALUE_KEY = A.ENROLL_SEPARATION + ) AS [Withdrawn] + ,oE.Email AS [CampusEmail] + ,( + SELECT NonQualifiedUserName + FROM PersonUser + WHERE PersonId = dbo.fnGetPersonId(A.ADVISOR) + ) AS [AdvisorUsername] + ,( + SELECT NonQualifiedUserName + FROM PersonUser + WHERE PersonId = dbo.fnGetPersonId(A.PEOPLE_CODE_ID) + ) AS [Username] + ,[Guid] + ,NULL [custom_1] + ,NULL [custom_2] + ,NULL [custom_3] + ,NULL [custom_4] + ,NULL [custom_5] + ,@ErrorFlag [ErrorFlag] + ,@ErrorMessage [ErrorMessage] + FROM ACADEMIC A + OUTER APPLY ( + SELECT TOP 1 Email + FROM EmailAddress E + WHERE E.PeopleOrgCodeId = A.PEOPLE_CODE_ID + AND E.EmailType = @EmailType + AND E.IsActive = 1 + ORDER BY E.REVISION_DATE DESC + ,REVISION_TIME DESC + ) oE + WHERE 1 = 1 + AND [Guid] = @AcademicGuid + --AND PEOPLE_CODE_ID = @PCID + --AND ACADEMIC_YEAR = @Year + --AND ACADEMIC_TERM = @Term + --AND ACADEMIC_SESSION = @Session + --AND PROGRAM = @Program + --AND DEGREE = @Degree + --AND CURRICULUM = @Curriculum + AND APPLICATION_FLAG = 'Y' --Ought to be an application, or there's a problem somewhere. + END END diff --git a/SQL/[custom].[PS_updAcademicKey].sql b/SQL/[custom].[PS_updAcademicKey].sql index aa00a70..4dad871 100644 --- a/SQL/[custom].[PS_updAcademicKey].sql +++ b/SQL/[custom].[PS_updAcademicKey].sql @@ -11,11 +11,13 @@ GO -- ============================================= -- Author: Wyatt Best -- Create date: 2021-03-17 --- Description: Updates [custom].AcademicKey with RecruiterApplicationId. See https://github.com/WyattBest/PowerCampus-AcademicKey --- Updates PROGRAM, DEGREE, and CURRICULUM rows in ACADEMIC if program change happens in Slate before registration or academic plan assignment. +-- Description: Updates PROGRAM, DEGREE, and CURRICULUM rows in ACADEMIC if program change happens in Slate before registration or academic plan assignment. +-- Set APPLICATION_FLAG to 'Y' if necessary (fix rows manually entered in Academic Records before application was inserted). -- -- 2021-09-01 Wyatt Best: Clear bad RecruiterApplicationId entries. -- 2022-04-04 Wyatt Best: Add ability to update PROGRAM/DEGREE/CURRICULUM in ACADEMIC. Stop clearing bad RecruiterApplicationId entries. +-- 2024-03-29 Wyatt Best: Rewritten to use 9.2.3's built-in Academic.Guid instead of a custom key. Formerly used https://github.com/WyattBest/PowerCampus-AcademicKey. +-- Set APPLICATION_FLAG to 'Y' if necessary. -- ============================================= CREATE PROCEDURE [custom].[PS_updAcademicKey] @PCID NVARCHAR(10) ,@Year NVARCHAR(4) @@ -24,94 +26,44 @@ CREATE PROCEDURE [custom].[PS_updAcademicKey] @PCID NVARCHAR(10) ,@Program NVARCHAR(6) ,@Degree NVARCHAR(6) ,@Curriculum NVARCHAR(6) - ,@SlateAppGuid UNIQUEIDENTIFIER NULL + ,@AcademicGuid UNIQUEIDENTIFIER NULL AS BEGIN SET NOCOUNT ON; - DECLARE @RecruiterApplicationId INT = ( - SELECT RecruiterApplicationId - FROM RecruiterApplication - WHERE ApplicationNumber = @SlateAppGuid - AND ApplicationId IS NOT NULL - ) - - IF @RecruiterApplicationId IS NOT NULL - BEGIN - -- Find ACADEMIC row ID - DECLARE @AcademicGuid UNIQUEIDENTIFIER = ( - SELECT [id] - FROM [custom].AcademicKey - WHERE RecruiterApplicationId = @RecruiterApplicationId - ) - - IF @AcademicGuid IS NOT NULL - BEGIN - -- Potentially update ACADEMIC row PDC - UPDATE A - SET PROGRAM = @Program - ,DEGREE = @Degree - ,CURRICULUM = @Curriculum - FROM ACADEMIC A - INNER JOIN [custom].academickey AK - ON AK.PEOPLE_CODE_ID = A.PEOPLE_CODE_ID - AND AK.ACADEMIC_YEAR = A.ACADEMIC_YEAR - AND AK.ACADEMIC_SESSION = A.ACADEMIC_SESSION - AND AK.ACADEMIC_TERM = A.ACADEMIC_TERM - AND AK.PROGRAM = A.PROGRAM - AND AK.DEGREE = A.DEGREE - AND AK.CURRICULUM = A.CURRICULUM - WHERE 1 = 1 - AND AK.ID = @AcademicGuid - AND A.PEOPLE_CODE_ID = @PCID - AND A.ACADEMIC_YEAR = @Year - AND A.ACADEMIC_SESSION = @Session - AND A.ACADEMIC_TERM = @Term - AND ( - A.PROGRAM <> @Program - OR A.DEGREE <> @Degree - OR A.CURRICULUM <> @Curriculum - ) - AND [STATUS] <> 'N' - AND APPLICATION_FLAG = 'Y' - AND CREDITS = 0 - AND REG_VALIDATE = 'N' - AND PREREG_VALIDATE = 'N' - AND ACA_PLAN_SETUP = 'N' - END - - ---- Find and clear RecruiterApplicationId from [custom].AcademicKey if not matched - --UPDATE [custom].AcademicKey - --SET RecruiterApplicationId = NULL - --WHERE RecruiterApplicationId = @RecruiterApplicationId - -- AND ( - -- PEOPLE_CODE_ID <> @PCID - -- OR ACADEMIC_YEAR <> @Year - -- OR ACADEMIC_TERM <> @Term - -- OR ACADEMIC_SESSION <> @Session - -- OR PROGRAM <> @Program - -- OR DEGREE <> @Degree - -- OR CURRICULUM <> @Curriculum - -- ) - -- Update AcademicKey if needed - IF NOT EXISTS ( - SELECT * - FROM RecruiterApplication RA - INNER JOIN [custom].AcademicKey AK - ON AK.RecruiterApplicationId = RA.RecruiterApplicationId - WHERE RA.ApplicationNumber = @SlateAppGuid + IF @AcademicGuid IS NOT NULL + --If the GUID is provided, potentially update PDC and APPLICATION_FLAG + UPDATE A + SET PROGRAM = @Program + ,DEGREE = @Degree + ,CURRICULUM = @Curriculum + ,APPLICATION_FLAG = 'Y' + FROM ACADEMIC A + WHERE 1 = 1 + AND A.[Guid] = @AcademicGuid + AND ( + A.PROGRAM <> @Program + OR A.DEGREE <> @Degree + OR A.CURRICULUM <> @Curriculum + OR A.APPLICATION_FLAG <> 'Y' ) - UPDATE AK - SET AK.RecruiterApplicationId = RA.RecruiterApplicationId - FROM [custom].AcademicKey AK - INNER JOIN RecruiterApplication RA - ON RA.ApplicationNumber = @SlateAppGuid - WHERE PEOPLE_CODE_ID = @PCID - AND ACADEMIC_YEAR = @Year - AND ACADEMIC_TERM = @Term - AND ACADEMIC_SESSION = @Session - AND PROGRAM = @Program - AND DEGREE = @Degree - AND CURRICULUM = @Curriculum - END + AND [STATUS] <> 'N' + AND CREDITS = 0 + AND REG_VALIDATE = 'N' + AND PREREG_VALIDATE = 'N' + AND ACA_PLAN_SETUP = 'N' + ELSE + --If the GUID is not provided, update APPLICATION_FLAG based on the provided YTSPDC + UPDATE ACADEMIC + SET APPLICATION_FLAG = 'Y' + WHERE 1 = 1 + AND PEOPLE_CODE_ID = @PCID + AND ACADEMIC_YEAR = @Year + AND ACADEMIC_SESSION = @Session + AND ACADEMIC_TERM = @Term + AND PROGRAM = @Program + AND DEGREE = @Degree + AND CURRICULUM = @Curriculum + AND [STATUS] <> 'N' + AND APPLICATION_FLAG <> 'Y' END diff --git a/config_sample.json b/config_sample.json index 24b5331..69e4120 100644 --- a/config_sample.json +++ b/config_sample.json @@ -68,7 +68,8 @@ "advisor", "fa_awards", "fa_status", - "sso_id" + "sso_id", + "academic_guid" ], "url": "https://apply.school.edu/manage/service/import?cmd=load&format=xxxx", "username": "service_user", diff --git a/ps_core.py b/ps_core.py index 65dbe62..a16f779 100644 --- a/ps_core.py +++ b/ps_core.py @@ -455,7 +455,10 @@ def main_sync(pid=None): academic_session = app_pc["ACADEMIC_SESSION"] # Single-row updates - if Settings.PowerCampus.update_academic_key: + if ( + Settings.PowerCampus.update_academic_key + and app_pc["AcademicGUID"] is not None + ): ps_powercampus.update_academic_key(app_pc) ps_powercampus.update_demographics(app_pc) ps_powercampus.update_academic(app_pc) @@ -546,6 +549,7 @@ def main_sync(pid=None): campus_email, advisor, sso_id, + academic_guid, custom_1, custom_2, custom_3, @@ -566,6 +570,7 @@ def main_sync(pid=None): "campus_email": campus_email, "advisor": advisor, "sso_id": sso_id, + "academic_guid": academic_guid, "custom_1": custom_1, "custom_2": custom_2, "custom_3": custom_3, diff --git a/ps_models.py b/ps_models.py index c859a45..cd815ac 100644 --- a/ps_models.py +++ b/ps_models.py @@ -329,6 +329,12 @@ "supply_null": False, "type": str, }, + "AcademicGUID": { + "api_verbatim": False, + "sql_verbatim": True, + "supply_null": True, + "type": str, + }, } arrays = { diff --git a/ps_powercampus.py b/ps_powercampus.py index ec03732..648b7b2 100644 --- a/ps_powercampus.py +++ b/ps_powercampus.py @@ -389,7 +389,7 @@ def get_profile(app, campus_email_type, Messages): """ error_flag = True - error_message = Messages.error.academic_row_not_found + error_message = None registered = False reg_date = None readmit = False @@ -398,6 +398,7 @@ def get_profile(app, campus_email_type, Messages): campus_email = None advisor = None sso_id = None + academic_guid = None custom_1 = None custom_2 = None custom_3 = None @@ -405,7 +406,7 @@ def get_profile(app, campus_email_type, Messages): custom_5 = None CURSOR.execute( - "EXEC [custom].[PS_selProfile] ?,?,?,?,?,?,?,?", + "EXEC [custom].[PS_selProfile] ?, ?, ?, ?, ?, ?, ?, ?, ?", app["PEOPLE_CODE_ID"], app["ACADEMIC_YEAR"], app["ACADEMIC_TERM"], @@ -414,42 +415,49 @@ def get_profile(app, campus_email_type, Messages): app["DEGREE"], app["CURRICULUM"], campus_email_type, + app["AcademicGUID"], ) row = CURSOR.fetchone() - if row is not None: - error_flag = False - - if row.Registered == "Y": - registered = True - reg_date = str(row.REG_VAL_DATE) - credits = str(row.CREDITS) - - campus_email = row.CampusEmail - advisor = row.AdvisorUsername - sso_id = row.Username - custom_1 = row.custom_1 - custom_2 = row.custom_2 - custom_3 = row.custom_3 - custom_4 = row.custom_4 - custom_5 = row.custom_5 - - # College Attend and Readmits - college_attend = row.COLLEGE_ATTEND - if college_attend == CONFIG.readmit_code: - readmit = True - elif college_attend == "" or college_attend is None: - college_attend = "blank" - - if college_attend not in CONFIG.valid_college_attend: - error_flag = True - error_message = Messages.error.invalid_college_attend.format(college_attend) - - if row.Withdrawn == "Y": - withdrawn = True - - if not error_flag: - error_message = None + if row is None: + # ACADEMIC row not found by YTSPDC or GUID. + error_message = Messages.error.academic_row_not_found + else: + if row.ErrorFlag == 1: + # ACADEMIC row found by GUID but YTSPDC does not match. + error_message = row.ErrorMessage + else: + error_flag = False + if row.Registered == "Y": + registered = True + reg_date = str(row.REG_VAL_DATE) + credits = str(row.CREDITS) + + campus_email = row.CampusEmail + advisor = row.AdvisorUsername + sso_id = row.Username + academic_guid = row.Guid + custom_1 = row.custom_1 + custom_2 = row.custom_2 + custom_3 = row.custom_3 + custom_4 = row.custom_4 + custom_5 = row.custom_5 + + # College Attend and Readmits + college_attend = row.COLLEGE_ATTEND + if college_attend == CONFIG.readmit_code: + readmit = True + elif college_attend == "" or college_attend is None: + college_attend = "blank" + + if college_attend not in CONFIG.valid_college_attend: + error_flag = True + error_message = Messages.error.invalid_college_attend.format( + college_attend + ) + + if row.Withdrawn == "Y": + withdrawn = True return ( error_flag, @@ -462,6 +470,7 @@ def get_profile(app, campus_email_type, Messages): campus_email, advisor, sso_id, + academic_guid, custom_1, custom_2, custom_3, @@ -534,7 +543,7 @@ def update_academic_key(app): P/C/D will only be updated if application is not registered and does not have an academic plan assigned. """ CURSOR.execute( - "exec [custom].[PS_updAcademicKey] ?, ?, ?, ?, ?, ?, ?, ?", + "exec [custom].[PS_updAcademicKey] ?, ?, ?, ?, ?, ?, ?, ?, ?", app["PEOPLE_CODE_ID"], app["ACADEMIC_YEAR"], app["ACADEMIC_TERM"], @@ -543,6 +552,7 @@ def update_academic_key(app): app["DEGREE"], app["CURRICULUM"], app["aid"], + app["AcademicGUID"], ) CNXN.commit() From de1e232224e919454ad9b52b5aeb086bf10ddf34 Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Thu, 4 Apr 2024 11:28:40 -0600 Subject: [PATCH 08/30] Bugfix rollup --- ps_core.py | 22 +++++---------- ps_format.py | 44 ++++++++++++++++++----------- ps_powercampus.py | 15 ++++------ sync_ondemand.py | 4 +-- ~$werSlate Integration Fields.docx | Bin 0 -> 162 bytes 5 files changed, 43 insertions(+), 42 deletions(-) create mode 100644 ~$werSlate Integration Fields.docx diff --git a/ps_core.py b/ps_core.py index a16f779..50d7c86 100644 --- a/ps_core.py +++ b/ps_core.py @@ -352,19 +352,11 @@ def main_sync(pid=None): vd = Settings.PowerCampus.autoconfigure_mappings.validate_degreq mdy = Settings.PowerCampus.autoconfigure_mappings.minimum_degreq_year afsi = Settings.PowerCampus.api.app_form_setting_id - # Accept either two or three fields for 9.2.3 - if {k for k in apps if "Degree" in apps[k]}: - program_list = [ - (apps[app]["Program"], apps[app]["Degree"], apps[app]["Curriculum"]) - for app in apps - if "Curriculum" in apps[app] - ] - else: - program_list = [ - (apps[app]["Program"], apps[app]["Degree"]) - for app in apps - if "Degree" in apps[app] - ] + program_list = [ + (apps[app]["Program"], apps[app]["Degree"]) + for app in apps + if "Degree" in apps[app] + ] yt_list = [apps[app]["YearTerm"] for app in apps if "YearTerm" in apps[app]] if ps_powercampus.autoconfigure_mappings( @@ -425,8 +417,8 @@ def main_sync(pid=None): ) apps[k]["PEOPLE_CODE_ID"] = pcid - verbose_print("Get scheduled actions from Slate") if CONFIG["scheduled_actions"]["enabled"] == True: + verbose_print("Get scheduled actions from Slate") CURRENT_RECORD = None # Send list of app GUID's to Slate; get back checklist items actions_list = slate_get_actions( @@ -443,7 +435,7 @@ def main_sync(pid=None): verbose_print("Update existing applications in PowerCampus and extract information") edu_sync_results = [] for k, v in apps.items(): - if v["error_flag"] == False: + if v["error_flag"] == True: continue CURRENT_RECORD = k if v["status_calc"] == "Active": diff --git a/ps_format.py b/ps_format.py index 15a95ea..4fdf85b 100644 --- a/ps_format.py +++ b/ps_format.py @@ -162,7 +162,9 @@ def format_app_generic(app, cfg_fields, Messages): fields_int.extend(["compare_" + field for field in cfg_fields["fields_int"]]) # Copy nullable strings from input to output, then fill in nulls - mapped.update({k: v for (k, v) in app.items() if k in fields_null}) + mapped.update( + {k: v for (k, v) in app.items() if k in fields_null and k not in mapped} + ) mapped.update({k: None for k in fields_null if k not in app}) # Convert integers and booleans @@ -177,6 +179,18 @@ def format_app_generic(app, cfg_fields, Messages): else: mapped["GovernmentDateOfEntry"] = app["GovernmentDateOfEntry"] + # Academic program + # API 9.2.3 still requires two fields for Program and Degree, even though the Swagger schema contains three fields. + # Done here instead of in format_app_api() because format_app_sql() also needs these fields standardized. + if "Curriculum" in app: + mapped["Program"] = app["Program"] + mapped["Degree"] = app["Degree"] + "/" + app["Curriculum"] + mapped["Curriculum"] = None + else: + mapped["Program"] = app["Program"] + mapped["Degree"] = app["Degree"] + mapped["Curriculum"] = None + # Pass through all other fields mapped.update({k: v for (k, v) in app.items() if k not in mapped}) @@ -289,20 +303,13 @@ def format_app_api(app, cfg_defaults, Messages): mapped["Veteran"] = app["Veteran"] mapped["VeteranStatus"] = True - # Academic program - # API 9.2.3 still requires two fields for Program and Degree, even though the Swagger schema contains three fields. - if "Curriculum" in app: - mapped["Programs"] = [ - { - "Program": app["Program"], - "Degree": app["Degree"] + "/" + app["Curriculum"], - "Curriculum": None, - } - ] - else: - mapped["Programs"] = [ - {"Program": app["Program"], "Degree": app["Degree"], "Curriculum": None} - ] + mapped["Programs"] = [ + { + "Program": app["Program"], + "Degree": app["Degree"], + "Curriculum": None, + } + ] # GUID's mapped["ApplicationNumber"] = app["aid"] @@ -350,7 +357,7 @@ def format_app_sql(app, mapping, config): mapped["ACADEMIC_SESSION"] = mapping["AcademicTerm"]["PCSessionCodeValue"][ app["YearTerm"] ] - # Todo: Fix inconsistency of 1-field vs 2-field mappings + mapped["PROGRAM"] = mapping["AcademicLevel"][app["Program"]] mapped["DEGREE"] = mapping["AcademicProgram"]["PCDegreeCodeValue"][app["Degree"]] mapped["CURRICULUM"] = mapping["AcademicProgram"]["PCCurriculumCodeValue"][ @@ -406,6 +413,11 @@ def format_app_sql(app, mapping, config): else: mapped["OrganizationId"] = None + if app["Veteran"] is not None: + mapped["VETERAN"] = mapping["Veteran"][app["Veteran"]] + else: + mapped["VETERAN"] = None + # Format Education and TestScoresNumeric if present. Newer arrays are implemented as classes. # Currently only supplies nulls; no datatype manipulations performed. array_models = ps_models.get_arrays() diff --git a/ps_powercampus.py b/ps_powercampus.py index 648b7b2..3f7ddc5 100644 --- a/ps_powercampus.py +++ b/ps_powercampus.py @@ -62,7 +62,7 @@ def autoconfigure_mappings( and that YearTerm values are like YEAR/TERM/SESSION. Keyword aguments: - program_list -- list of tuples like [('PROGRAM','DEGREE/CURRICULUM'), (...)] or [('PROGRAM','DEGREE','CURRICULUM'), (...)] + program_list -- list of tuples like [('PROGRAM','DEGREE/CURRICULUM'), (...)] yt_list -- list of strings like ['YEAR/TERM/SESSION', ...] validate_degreq -- bool. If True, check against DEGREQ for sanity using minimum_degreq_year. minimum_degreq_year -- str @@ -77,14 +77,11 @@ def autoconfigure_mappings( # Create set of tuples like {('PROGRAM','DEGREE', 'CURRICULUM'), (...)} pdc_set = set() - if len(program_list[0]) == 2: - for dp in program_set: - pdc = [dp[0]] - for dc in dp[1].split("/"): - pdc.append(dc) - pdc_set.add(tuple(pdc)) - elif len(program_list[0]) == 3: - pdc_set = program_set + for dp in program_set: + pdc = [dp[0]] + for dc in dp[1].split("/"): + pdc.append(dc) + pdc_set.add(tuple(pdc)) # Create a set like {'PROGRAM', 'PROGRAM'} p_set = set() diff --git a/sync_ondemand.py b/sync_ondemand.py index 91b279d..ce1ced8 100644 --- a/sync_ondemand.py +++ b/sync_ondemand.py @@ -85,10 +85,10 @@ msg = MIMEText(body) msg["To"] = email_config["to"] - msg["From"] = email_config["from"] + msg["From"] = email_config["smtp"]["from"] msg["Subject"] = email_config["subject"] - with smtplib.SMTP(email_config["server"]) as smtp: + with smtplib.SMTP(email_config["smtp"]["server"]) as smtp: smtp.starttls() smtp.login( email_config["smtp"]["username"], email_config["smtp"]["password"] diff --git a/~$werSlate Integration Fields.docx b/~$werSlate Integration Fields.docx new file mode 100644 index 0000000000000000000000000000000000000000..abc521e7cf04341d932bae95fae7a548caec3d20 GIT binary patch literal 162 zcmd-IuS_f{@ySn4%wZrNa5024R5BzolmMX*Lq0{7ngIZFKp9s6 literal 0 HcmV?d00001 From 1cd2ce5214b22e00553bfcfd3eb2127047e0efb6 Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Tue, 9 Apr 2024 15:50:15 -0600 Subject: [PATCH 09/30] Bugfixes, documentation, new tool -Fix Comments and County bugs -Update fields doc -New tool to select Religion mapping XML --- PowerSlate Integration Fields.docx | Bin 35865 -> 36264 bytes Tools/Mapper Generate Religion XML.sql | 9 +++++++++ ps_format.py | 8 +++++--- ~$werSlate Integration Fields.docx | Bin 162 -> 0 bytes 4 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 Tools/Mapper Generate Religion XML.sql delete mode 100644 ~$werSlate Integration Fields.docx diff --git a/PowerSlate Integration Fields.docx b/PowerSlate Integration Fields.docx index 97c5dcc3a0569f85301bc1ac150a1bdd3d76a7a9..07c6f4216513a03dffed204995b45526e3ee560a 100644 GIT binary patch delta 29067 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 delta 28727 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 diff --git a/Tools/Mapper Generate Religion XML.sql b/Tools/Mapper Generate Religion XML.sql new file mode 100644 index 0000000..60bf940 --- /dev/null +++ b/Tools/Mapper Generate Religion XML.sql @@ -0,0 +1,9 @@ +USE Campus6 + +SELECT LONG_DESC + ,CODE_VALUE_KEY + --,ISO3166_CODE + ,'' [xml] +FROM CODE_RELIGION +WHERE 1 = 1 + AND [STATUS] = 'A' diff --git a/ps_format.py b/ps_format.py index 4fdf85b..1f5ee30 100644 --- a/ps_format.py +++ b/ps_format.py @@ -31,7 +31,9 @@ def __init__(self, row): self.cleared_date = row["ClearedDate"] else: self.cleared_date = None - if "comments" in row: + if "Comments" in row: + self.comments = format_blank_to_null(row["Comments"]) + elif "comments" in row: self.comments = format_blank_to_null(row["comments"]) else: self.comments = None @@ -255,8 +257,8 @@ def format_app_api(app, cfg_defaults, Messages): k["StateProvince"] = None if "PostalCode" not in k: k["PostalCode"] = None - if "County" not in k: - k["County"] = cfg_defaults.address_country + if "Country" not in k: + k["Country"] = cfg_defaults.address_country if len([k for k in app if k[:5] == "Phone"]) > 0: has_phones = True diff --git a/~$werSlate Integration Fields.docx b/~$werSlate Integration Fields.docx deleted file mode 100644 index abc521e7cf04341d932bae95fae7a548caec3d20..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 162 zcmd-IuS_f{@ySn4%wZrNa5024R5BzolmMX*Lq0{7ngIZFKp9s6 From dbb723669ff20b81124760a622b4df3e6510eaf8 Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Wed, 10 Apr 2024 12:33:08 -0600 Subject: [PATCH 10/30] 9.2.3 Demographics updates Ellucian stopped raising errors in spSetDemographics and just returns an int. Also changed @Gender parameter type. --- SQL/[custom].[PS_updDemographics].sql | 59 +++++++++++++++++++++++---- ps_format.py | 10 ++--- ps_powercampus.py | 2 +- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/SQL/[custom].[PS_updDemographics].sql b/SQL/[custom].[PS_updDemographics].sql index 8470cee..67f3195 100644 --- a/SQL/[custom].[PS_updDemographics].sql +++ b/SQL/[custom].[PS_updDemographics].sql @@ -23,10 +23,11 @@ GO -- 2021-09-01 Wyatt Best: Named transaction. -- 2023-10-05 Rafael Gomez: Added @Religion. -- 2023-11-09 Wyatt Best: Updated error message when GOVERNMENT_ID already assigned to other record. +-- 2024-04-10 Wyatt Best: Updated for 9.2.3 changes to [WebServices].[spSetDemographics]'s @Gender parameter. -- ============================================= CREATE PROCEDURE [custom].[PS_updDemographics] @PCID NVARCHAR(10) ,@Opid NVARCHAR(8) - ,@Gender TINYINT + ,@GenderId TINYINT ,@Ethnicity TINYINT --0 = None, 1 = Hispanic, 2 = NonHispanic. Ellucian's API was supposed to record nothing for ethnicity for 0. I don't think it supports multi-value, but this sproc does. ,@DemographicsEthnicity NVARCHAR(6) ,@MaritalStatus NVARCHAR(4) NULL @@ -56,6 +57,25 @@ BEGIN ,@Now DATETIME = dbo.fnMakeTime(@getdate) --Error check + IF ( + @GenderId IS NOT NULL + AND NOT EXISTS ( + SELECT * + FROM CODE_GENDER + WHERE GenderId = @GenderId + ) + ) + BEGIN + RAISERROR ( + '@GenderId %d not found in CODE_ETHNICITY.' + ,11 + ,1 + ,@GenderId + ) + + RETURN + END + IF ( @DemographicsEthnicity IS NOT NULL AND NOT EXISTS ( @@ -112,7 +132,7 @@ BEGIN RETURN END - + DECLARE @DupPCID NVARCHAR(10) = ( SELECT TOP 1 PEOPLE_CODE_ID FROM PEOPLE @@ -124,6 +144,14 @@ BEGIN FROM PEOPLE WHERE PEOPLE_CODE_ID = @PCID ) + DECLARE @GenderCode NVARCHAR(1) + ,@GenderMed NVARCHAR(20) + + SELECT @GenderCode = CODE_VALUE_KEY + ,@GenderMed = MEDIUM_DESC + FROM CODE_GENDER + WHERE GenderId = @GenderId + --Treat blanks as NULL SET @ExistingGovId = NULLIF(@ExistingGovId, '') @@ -193,7 +221,7 @@ BEGIN AND NOT EXISTS (SELECT PersonId, IpedsFederalCategoryId FROM PersonEthnicity WHERE PersonId = @PersonId and IpedsFederalCategoryId = 6)) EXEC [custom].[PS_insPersonEthnicity] @PersonId, @Opid, @Today, @Now, 6; - + --Update DEMOGRAPHICS rollup if needed IF NOT EXISTS ( SELECT * @@ -202,20 +230,23 @@ BEGIN AND ACADEMIC_YEAR = '' AND ACADEMIC_TERM = '' AND ACADEMIC_SESSION = '' - AND GENDER = @Gender + AND GENDER = @GenderCode AND ETHNICITY = @DemographicsEthnicity AND MARITAL_STATUS = @MaritalStatus AND VETERAN = @Veteran AND CITIZENSHIP = @PrimaryCitizenship AND DUAL_CITIZENSHIP = @SecondaryCitizenship AND PRIMARY_LANGUAGE = @PrimaryLanguage - and HOME_LANGUAGE = @HomeLanguage + AND HOME_LANGUAGE = @HomeLanguage AND RELIGION = @Religion ) - EXECUTE [WebServices].[spSetDemographics] @PersonId + BEGIN + DECLARE @return_value INT + + EXECUTE @return_value = [WebServices].[spSetDemographics] @PersonId ,@Opid ,'001' - ,@Gender + ,@GenderMed ,@DemographicsEthnicity ,@MaritalStatus ,@Religion @@ -229,6 +260,20 @@ BEGIN ,@HomeLanguage ,NULL + --Ellucian stopped raising errors in spSetDemographics and just returns an int?? + --Probably some modern trend to make it harder for end users to see meaningful error messages. + IF @return_value <> 0 + BEGIN + RAISERROR ( + 'WebServices.spSetDemographics returned error code %d.' + ,11 + ,1 + ,@return_value + ) + + RETURN + END + END --Update GOVERNMENT_ID if needed. IF @GovernmentId IS NOT NULL diff --git a/ps_format.py b/ps_format.py index 1f5ee30..7fb0b3e 100644 --- a/ps_format.py +++ b/ps_format.py @@ -297,7 +297,8 @@ def format_app_api(app, cfg_defaults, Messages): # PowerCampus WebAPI requires Type -1 instead of a blank or null when not submitting any phones. mapped["PhoneNumbers"] = [{"Type": -1, "Country": None, "Number": None}] - # Veteran logic was updated in API 9.2.x + # Suspect Veteran logic was updated in API 9.2.x + # Changed how this is handled to be less confusing. if app["Veteran"] is None: mapped["Veteran"] = None mapped["VeteranStatus"] = False @@ -325,6 +326,8 @@ def format_app_sql(app, mapping, config): Keyword arguments: app -- an application dict + mapping -- a mapping dict derived from recruiterMapping.xml + config -- Settings class object """ mapped = {} @@ -415,11 +418,6 @@ def format_app_sql(app, mapping, config): else: mapped["OrganizationId"] = None - if app["Veteran"] is not None: - mapped["VETERAN"] = mapping["Veteran"][app["Veteran"]] - else: - mapped["VETERAN"] = None - # Format Education and TestScoresNumeric if present. Newer arrays are implemented as classes. # Currently only supplies nulls; no datatype manipulations performed. array_models = ps_models.get_arrays() diff --git a/ps_powercampus.py b/ps_powercampus.py index 3f7ddc5..15489c2 100644 --- a/ps_powercampus.py +++ b/ps_powercampus.py @@ -486,7 +486,7 @@ def update_demographics(app): app["DemographicsEthnicity"], app["MARITALSTATUS"], app["Religion"], - app["VETERAN"], + app["Veteran"], app["PRIMARYCITIZENSHIP"], app["SECONDARYCITIZENSHIP"], app["VISA"], From 747fefa02cfc867372dc052c2d2af599ed6267e3 Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Wed, 10 Apr 2024 13:25:47 -0600 Subject: [PATCH 11/30] Fix Settings class name collision The re-init process in HTTP server exposed the issue. Also added linebreaks to HTML stacktrace. --- ps_core.py | 48 ++++++++++++++++++++++++------------------------ sync_http.py | 7 ++++--- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/ps_core.py b/ps_core.py index 50d7c86..b27b645 100644 --- a/ps_core.py +++ b/ps_core.py @@ -50,19 +50,19 @@ def init(config_path): global CONFIG global CONFIG_PATH global RM_MAPPING - global Settings # New global for Settings class + global SETTINGS # New global for Settings class CONFIG_PATH = config_path with open(CONFIG_PATH) as file: CONFIG = json.loads(file.read()) - Settings = Settings(CONFIG) + SETTINGS = Settings(CONFIG) RM_MAPPING = ps_powercampus.get_recruiter_mapping( - Settings.PowerCampus.mapping_file_location + SETTINGS.PowerCampus.mapping_file_location ) # Init PowerCampus API and SQL connections - ps_powercampus.init(Settings.PowerCampus, Settings.console_verbose) + ps_powercampus.init(SETTINGS.PowerCampus, SETTINGS.console_verbose) return CONFIG @@ -312,7 +312,7 @@ def main_sync(pid=None): apps = {k["aid"]: k for k in apps} if len(apps) == 0 and pid is not None: # Assuming we're running in interactive (HTTP) mode if pid param exists - raise EOFError(Settings.Messages.error.no_apps) + raise EOFError(SETTINGS.Messages.error.no_apps) elif len(apps) == 0: # Don't raise an error for scheduled mode return None @@ -321,7 +321,7 @@ def main_sync(pid=None): for k, v in apps.items(): CURRENT_RECORD = k apps[k] = format_app_generic( - v, CONFIG["slate_upload_active"], Settings.Messages + v, CONFIG["slate_upload_active"], SETTINGS.Messages ) # Set error flag if one pid has multiple applications with the same YTS + PCD @@ -343,15 +343,15 @@ def main_sync(pid=None): for k in duplicates: apps[k]["error_flag"] = True - apps[k]["error_message"] = Settings.Messages.error.duplicate_apps + apps[k]["error_message"] = SETTINGS.Messages.error.duplicate_apps - if Settings.PowerCampus.autoconfigure_mappings.enabled: + if SETTINGS.PowerCampus.autoconfigure_mappings.enabled: verbose_print("Auto-configure ProgramOfStudy and recruiterMapping.xml") CURRENT_RECORD = None - mfl = Settings.PowerCampus.mapping_file_location - vd = Settings.PowerCampus.autoconfigure_mappings.validate_degreq - mdy = Settings.PowerCampus.autoconfigure_mappings.minimum_degreq_year - afsi = Settings.PowerCampus.api.app_form_setting_id + mfl = SETTINGS.PowerCampus.mapping_file_location + vd = SETTINGS.PowerCampus.autoconfigure_mappings.validate_degreq + mdy = SETTINGS.PowerCampus.autoconfigure_mappings.minimum_degreq_year + afsi = SETTINGS.PowerCampus.api.app_form_setting_id program_list = [ (apps[app]["Program"], apps[app]["Degree"]) for app in apps @@ -395,14 +395,14 @@ def main_sync(pid=None): or (v["status_ra"] == 0 and v["status_app"] == None) # 9.2.3 new bad status ): app, error_flag, error_message = format_app_api( - v, Settings.defaults, Settings.Messages + v, SETTINGS.defaults, SETTINGS.Messages ) if error_flag: apps[k]["error_flag"] = error_flag apps[k]["error_message"] = error_message else: pcid = ps_powercampus.post_api( - app, Settings.PowerCampus.api, Settings.Messages + app, SETTINGS.PowerCampus.api, SETTINGS.Messages ) apps[k]["PEOPLE_CODE_ID"] = pcid @@ -440,7 +440,7 @@ def main_sync(pid=None): CURRENT_RECORD = k if v["status_calc"] == "Active": # Transform to PowerCampus format - app_pc = format_app_sql(v, RM_MAPPING, Settings.PowerCampus) + app_pc = format_app_sql(v, RM_MAPPING, SETTINGS.PowerCampus) pcid = app_pc["PEOPLE_CODE_ID"] academic_year = app_pc["ACADEMIC_YEAR"] academic_term = app_pc["ACADEMIC_TERM"] @@ -448,7 +448,7 @@ def main_sync(pid=None): # Single-row updates if ( - Settings.PowerCampus.update_academic_key + SETTINGS.PowerCampus.update_academic_key and app_pc["AcademicGUID"] is not None ): ps_powercampus.update_academic_key(app_pc) @@ -491,7 +491,7 @@ def main_sync(pid=None): ps_powercampus.update_test_scores(pcid, test) # Update any PowerCampus Notes defined in config - for note in Settings.PowerCampus.notes: + for note in SETTINGS.PowerCampus.notes: if ( note["slate_field"] in app_pc and len(app_pc[note["slate_field"]]) > 0 @@ -501,7 +501,7 @@ def main_sync(pid=None): ) # Update any PowerCampus User Defined fields defined in config - for udf in Settings.PowerCampus.user_defined_fields: + for udf in SETTINGS.PowerCampus.user_defined_fields: if udf["slate_field"] in app_pc and len(app_pc[udf["slate_field"]]) > 0: ps_powercampus.update_udf( app_pc, udf["slate_field"], udf["pc_field"] @@ -520,7 +520,7 @@ def main_sync(pid=None): ps_powercampus.update_scholarship( pcid, scholarship, - Settings.PowerCampus.validate_scholarship_levels, + SETTINGS.PowerCampus.validate_scholarship_levels, ) # Update PowerCampus Associations @@ -548,7 +548,7 @@ def main_sync(pid=None): custom_4, custom_5, ) = ps_powercampus.get_profile( - app_pc, Settings.PowerCampus.campus_emailtype, Settings.Messages + app_pc, SETTINGS.PowerCampus.campus_emailtype, SETTINGS.Messages ) apps[k].update( { @@ -574,7 +574,7 @@ def main_sync(pid=None): sync_errors == True # Get PowerFAIDS awards and tracking status - if Settings.fa_awards.enabled: + if SETTINGS.fa_awards.enabled: fa_awards, fa_status = ps_powercampus.pf_get_awards( pcid, v["GovernmentId"], @@ -608,7 +608,7 @@ def main_sync(pid=None): CURRENT_RECORD = k if v["status_calc"] == "Active": # Transform to PowerCampus format - app_pc = format_app_sql(v, RM_MAPPING, Settings.PowerCampus) + app_pc = format_app_sql(v, RM_MAPPING, SETTINGS.PowerCampus) fa_checklists = ps_powercampus.pf_get_fachecklist( app_pc["PEOPLE_CODE_ID"], @@ -625,9 +625,9 @@ def main_sync(pid=None): # Warn if any apps returned an error flag from ps_powercampus.get_profile() if sync_errors == True or [k for k in apps if apps[k]["error_flag"] == True]: - output_msg = Settings.Messages.success.done_with_errors + output_msg = SETTINGS.Messages.success.done_with_errors else: - output_msg = Settings.Messages.success.done + output_msg = SETTINGS.Messages.success.done verbose_print(output_msg) return output_msg diff --git a/sync_http.py b/sync_http.py index ff472df..6c106f4 100644 --- a/sync_http.py +++ b/sync_http.py @@ -17,7 +17,7 @@ def emit_traceback(): return message -class testHTTPServer_RequestHandler(BaseHTTPRequestHandler): +class HTTPRequestHandler(BaseHTTPRequestHandler): def do_GET(self): # Send response status code self.send_response(200) @@ -48,7 +48,8 @@ def do_GET(self): ps_core.de_init() CONFIG = ps_core.init(sys.argv[1]) - # Write content as utf-8 data + # Sent message back to client after replacing newlines with HTML line breaks + message = message.replace("\n", "
") self.wfile.write(message.encode("utf8")) return @@ -65,7 +66,7 @@ def run_server(): else: local_ip = socket.gethostbyname(socket.gethostname()) server_address = (local_ip, CONFIG["http_port"]) - httpd = HTTPServer(server_address, testHTTPServer_RequestHandler) + httpd = HTTPServer(server_address, HTTPRequestHandler) print("running server...") httpd.serve_forever() From 2fcbdf193ce809275a48f605384e7a1c6e0b4657 Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:00:25 -0600 Subject: [PATCH 12/30] Default Legal Name if blank --- SQL/[custom].[PS_updDemographics].sql | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/SQL/[custom].[PS_updDemographics].sql b/SQL/[custom].[PS_updDemographics].sql index 67f3195..cb02274 100644 --- a/SQL/[custom].[PS_updDemographics].sql +++ b/SQL/[custom].[PS_updDemographics].sql @@ -24,6 +24,7 @@ GO -- 2023-10-05 Rafael Gomez: Added @Religion. -- 2023-11-09 Wyatt Best: Updated error message when GOVERNMENT_ID already assigned to other record. -- 2024-04-10 Wyatt Best: Updated for 9.2.3 changes to [WebServices].[spSetDemographics]'s @Gender parameter. +-- Default Legal Name if blank. -- ============================================= CREATE PROCEDURE [custom].[PS_updDemographics] @PCID NVARCHAR(10) ,@Opid NVARCHAR(8) @@ -287,6 +288,15 @@ BEGIN SET GOVERNMENT_ID = @GovernmentId WHERE PEOPLE_CODE_ID = @PCID + --Update Legal Name if blank + UPDATE PEOPLE + SET LegalName = dbo.fnPeopleOrgName(@PCID, 'LN, |FN |MN, |SX') + WHERE PEOPLE_CODE_ID = @PCID + AND ( + LegalName = '' + OR LegalName IS NULL + ) + COMMIT TRANSACTION PS_updDemographics END GO From f5a4bab07906437a0c8d181cac562d2b1ce89757 Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Thu, 11 Apr 2024 17:25:56 -0600 Subject: [PATCH 13/30] New error message highlighting case sensitivity --- config_messages.json | 3 ++- ps_powercampus.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/config_messages.json b/config_messages.json index 318af80..727ff69 100644 --- a/config_messages.json +++ b/config_messages.json @@ -6,7 +6,8 @@ "invalid_college_attend": "College Attend is set to {} in PowerCampus, which is not valid for applicants.", "api_missing_database": "The PowerCampus Web API is not functioning properly. You may need to remove and reinstall the application.", "duplicate_apps": "Person has multiple applications with the same YTS + PCD.", - "missing_yt": "Year/Term/Session is missing from the application." + "missing_yt": "Year/Term/Session is missing from the application.", + "pdc_mapping": "Check Application Form Data Filters, Program of Study, and recruiterMapping.xml if auto-mapping is not enabled. Code values are case-sensitive." }, "success": { "done": "Sync completed with no errors.", diff --git a/ps_powercampus.py b/ps_powercampus.py index 15489c2..b3c96ea 100644 --- a/ps_powercampus.py +++ b/ps_powercampus.py @@ -305,12 +305,14 @@ def post_api(app, config, Messages): and "ServiceLocatorImplBase.cs:line 53" in rtext ): raise ValueError(Messages.error.api_missing_database, rtext, e) + elif "/ was not found in Mapping file." in rtext: + raise ValueError(rtext, Messages.error.pdc_mapping) elif ( r.status_code == 202 - and "was created successfully in PowerCampus" in r.text == False + and "was created successfully in PowerCampus" in rtext == False ) or r.status_code == 400: raise ValueError(rtext) - elif "was created successfully in PowerCampus" not in r.text == False: + elif "was created successfully in PowerCampus" not in rtext == False: raise requests.HTTPError(rtext) if dup_found: From 3e39dcfc39d45520377223795e3c4df19143672b Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Wed, 17 Apr 2024 15:32:12 -0600 Subject: [PATCH 14/30] Update Mapper tooling scripts --- Tools/Mapper Generate Language XML.sql | 8 ++++++++ Tools/Mapper Generate Religion XML.sql | 1 - 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 Tools/Mapper Generate Language XML.sql diff --git a/Tools/Mapper Generate Language XML.sql b/Tools/Mapper Generate Language XML.sql new file mode 100644 index 0000000..e5af470 --- /dev/null +++ b/Tools/Mapper Generate Language XML.sql @@ -0,0 +1,8 @@ +USE Campus6 + +SELECT LONG_DESC + ,CODE_VALUE_KEY + ,'' [xml] +FROM CODE_LANGUAGE +WHERE 1 = 1 + AND [STATUS] = 'A' diff --git a/Tools/Mapper Generate Religion XML.sql b/Tools/Mapper Generate Religion XML.sql index 60bf940..bce849f 100644 --- a/Tools/Mapper Generate Religion XML.sql +++ b/Tools/Mapper Generate Religion XML.sql @@ -2,7 +2,6 @@ USE Campus6 SELECT LONG_DESC ,CODE_VALUE_KEY - --,ISO3166_CODE ,'' [xml] FROM CODE_RELIGION WHERE 1 = 1 From fc3048002b39b07a02a10769e9a4fc3c9c8d99f9 Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Tue, 23 Apr 2024 15:49:42 -0600 Subject: [PATCH 15/30] Error handling fixes -API 401 error was not being caught -Ignore blank Address fields --- ps_format.py | 2 +- ps_powercampus.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ps_format.py b/ps_format.py index 7fb0b3e..07d017c 100644 --- a/ps_format.py +++ b/ps_format.py @@ -231,7 +231,7 @@ def format_app_api(app, cfg_defaults, Messages): { k[8:]: v for (k, v) in app.items() - if k[0:7] == "Address" and int(k[7:8]) - 1 == i + if k[0:7] == "Address" and int(k[7:8]) - 1 == i and v is not None } for i in range(10) ] diff --git a/ps_powercampus.py b/ps_powercampus.py index b3c96ea..f4947a6 100644 --- a/ps_powercampus.py +++ b/ps_powercampus.py @@ -309,10 +309,10 @@ def post_api(app, config, Messages): raise ValueError(rtext, Messages.error.pdc_mapping) elif ( r.status_code == 202 - and "was created successfully in PowerCampus" in rtext == False - ) or r.status_code == 400: + and "was created successfully in PowerCampus" not in rtext + ): raise ValueError(rtext) - elif "was created successfully in PowerCampus" not in rtext == False: + elif "was created successfully in PowerCampus" not in rtext: raise requests.HTTPError(rtext) if dup_found: From e5293d4b4dc2dd186134bf29ffa28ba1c2d012f3 Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Wed, 24 Apr 2024 09:10:04 -0600 Subject: [PATCH 16/30] API token format check --- config_messages.json | 1 + ps_core.py | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/config_messages.json b/config_messages.json index 727ff69..fb57f94 100644 --- a/config_messages.json +++ b/config_messages.json @@ -5,6 +5,7 @@ "academic_row_not_found": "Record not found by Year/Term/Session/Program/Degree/Curriculum.", "invalid_college_attend": "College Attend is set to {} in PowerCampus, which is not valid for applicants.", "api_missing_database": "The PowerCampus Web API is not functioning properly. You may need to remove and reinstall the application.", + "api_token_format": "PowerCampus Web API token should start with 'Bearer '.", "duplicate_apps": "Person has multiple applications with the same YTS + PCD.", "missing_yt": "Year/Term/Session is missing from the application.", "pdc_mapping": "Check Application Form Data Filters, Program of Study, and recruiterMapping.xml if auto-mapping is not enabled. Code values are case-sensitive." diff --git a/ps_core.py b/ps_core.py index b27b645..5acd91e 100644 --- a/ps_core.py +++ b/ps_core.py @@ -26,6 +26,7 @@ def __init__(self, config): self.defaults = self.FlatDict(config["defaults"]) self.PowerCampus = self.PowerCampus(config["powercampus"]) self.Messages = self.Messages() + self.check_api_token() class PowerCampus: def __init__(self, config): @@ -44,6 +45,13 @@ def __init__(self): self.error = Settings.FlatDict(messages["error"]) self.success = Settings.FlatDict(messages["success"]) + def check_api_token(self): + if ( + self.PowerCampus.api.auth_method == "token" + and self.PowerCampus.api.token[:7] != "Bearer " + ): + raise ValueError(self.Messages.error.api_token_format) + def init(config_path): """Reads config file to global CONFIG dict. Many frequently-used variables are copied to their own globals for convenince.""" From 6ce7be05c428edaf3644a7882f302535c7d32ac4 Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Thu, 25 Apr 2024 14:35:39 -0600 Subject: [PATCH 17/30] API error handling fix Account for two different spellings in return message Better parsing of PEOPLE_ID from return message --- ps_powercampus.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/ps_powercampus.py b/ps_powercampus.py index f4947a6..84f4769 100644 --- a/ps_powercampus.py +++ b/ps_powercampus.py @@ -308,25 +308,26 @@ def post_api(app, config, Messages): elif "/ was not found in Mapping file." in rtext: raise ValueError(rtext, Messages.error.pdc_mapping) elif ( - r.status_code == 202 + "was created succesfully in PowerCampus" not in rtext and "was created successfully in PowerCampus" not in rtext ): - raise ValueError(rtext) - elif "was created successfully in PowerCampus" not in rtext: raise requests.HTTPError(rtext) if dup_found: update_app_form_autoprocess(config.app_form_setting_id, True) - if r.text[-25:-12] == "New People Id": + if "New People Id" in r.text: + # Example 9.2.3 response: "The application 13 was created successfully in PowerCampus. New People Id 000123456." try: - people_code = r.text[-11:-2] - # Error check. After slice because leading zeros need preserved. + people_code = r.text.split("New People Id ")[1].split(".")[0] int(people_code) PEOPLE_CODE_ID = "P" + people_code return PEOPLE_CODE_ID except: - return None + raise ValueError( + "Unable to parse PEOPLE_ID from API response.", + r.text, + ) else: return None From 2b73efbca822f9881b61e88a6764d0f605246aa2 Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Mon, 29 Apr 2024 18:02:42 -0600 Subject: [PATCH 18/30] Update Populate RecruiterApplication and Application for old apps.sql --- ...plication and Application for old apps.sql | 81 ++++++++++++++----- 1 file changed, 59 insertions(+), 22 deletions(-) diff --git a/Tools/Populate RecruiterApplication and Application for old apps.sql b/Tools/Populate RecruiterApplication and Application for old apps.sql index 02d6dc6..b733e73 100644 --- a/Tools/Populate RecruiterApplication and Application for old apps.sql +++ b/Tools/Populate RecruiterApplication and Application for old apps.sql @@ -1,10 +1,15 @@ USE PowerCampusMapper +DECLARE @ApplicationFormSettingId INT = 1 + -- -- Tool for populating RecruiterApplication and Application with just enough data to allow -- syncing via PowerSlate as if the applications had been inserted organically via the API. --- I.e. sync your old apps that were manually typed into PowerCampus before you implemented PowerSlate. +-- I.e. sync your old apps that were already in PowerCampus before you implemented PowerSlate. +-- +-- Recommend using string_split()'s enable_ordinal parameter when available (SQL Server 2022+) -- +-- Exclude apps without APPLICATION_FLAG = Y SELECT DISTINCT aid INTO #Exclusions FROM PowerCampusMapper.dbo.Slate_Apps @@ -18,6 +23,7 @@ WHERE PEOPLE_CODE_ID IN ( WHERE APPLICATION_FLAG = 'Y' ) +--Exclude apps with invalid PCID's INSERT INTO #Exclusions SELECT DISTINCT aid FROM PowerCampusMapper.dbo.Slate_Apps @@ -31,8 +37,35 @@ SELECT DISTINCT aid FROM PowerCampusMapper.dbo.Slate_Apps WHERE PEOPLE_CODE_ID IS NULL +--Exclude apps within invalid SessionPeriodId +INSERT INTO #Exclusions +SELECT DISTINCT aid +FROM PowerCampusMapper.dbo.Slate_Apps +WHERE NOT EXISTS ( + SELECT SessionPeriodId + FROM Campus6.dbo.ACADEMICCALENDAR + WHERE ACADEMIC_YEAR = ( + SELECT value + FROM string_split(yearterm, '/') + ORDER BY @@rowcount offset 0 rows FETCH NEXT 1 rows ONLY + ) + AND ACADEMIC_TERM = ( + SELECT value + FROM string_split(yearterm, '/') + ORDER BY @@rowcount offset 1 rows FETCH NEXT 1 rows ONLY + ) + AND ACADEMIC_SESSION = ( + SELECT value + FROM string_split(yearterm, '/') + ORDER BY @@rowcount offset 2 rows FETCH NEXT 1 rows ONLY + ) + ) + PRINT '#Exclusions table built.' +SELECT * +FROM #Exclusions + BEGIN TRAN INSERT INTO [Campus6].[dbo].[Application] ( @@ -49,23 +82,23 @@ INSERT INTO [Campus6].[dbo].[Application] ( ) SELECT getdate() [CreateDatetime] ,2 [Status] - ,[Campus6].[dbo].fngetpersonid(PEOPLE_CODE_ID) [PersonId] + ,P.PersonId [PersonId] ,FirstName ,LastName ,( - SELECT sessionperiodid + SELECT SessionPeriodId FROM Campus6.dbo.ACADEMICCALENDAR - WHERE academic_year = ( + WHERE ACADEMIC_YEAR = ( SELECT value FROM string_split(yearterm, '/') ORDER BY @@rowcount offset 0 rows FETCH NEXT 1 rows ONLY ) - AND academic_term = ( + AND ACADEMIC_TERM = ( SELECT value FROM string_split(yearterm, '/') ORDER BY @@rowcount offset 1 rows FETCH NEXT 1 rows ONLY ) - AND academic_session = ( + AND ACADEMIC_SESSION = ( SELECT value FROM string_split(yearterm, '/') ORDER BY @@rowcount offset 2 rows FETCH NEXT 1 rows ONLY @@ -73,21 +106,27 @@ SELECT getdate() [CreateDatetime] ) [SessionPeriodId] ,0 [FoodPlanInterest] ,0 [DormPlanInterest] - ,1 [ApplicationFormSettingId] + ,@ApplicationFormSettingId [ApplicationFormSettingId] ,aid [OtherSource] -FROM PowerCampusMapper.dbo.Slate_Apps -WHERE aid NOT IN ( - SELECT aid - FROM #Exclusions +FROM PowerCampusMapper.dbo.Slate_Apps SA +LEFT JOIN Campus6.dbo.PEOPLE P + ON P.PEOPLE_CODE_ID = SA.PEOPLE_CODE_ID +WHERE 1 = 1 + AND NOT EXISTS ( + SELECT E.aid + FROM #Exclusions E + WHERE E.aid = SA.aid ) - AND aid NOT IN ( - SELECT applicationnumber + AND NOT EXISTS ( + SELECT ApplicationNumber FROM [Campus6].[dbo].[RecruiterApplication] WHERE ApplicationId IS NOT NULL + AND ApplicationNumber = SA.aid ) - AND aid NOT IN ( - SELECT othersource + AND NOT EXISTS ( + SELECT * FROM [Campus6].[dbo].[Application] + WHERE OtherSource = SA.aid ) PRINT 'Insert into [Application] done.' @@ -105,22 +144,20 @@ INSERT INTO [Campus6].[dbo].[RecruiterApplication] ( SELECT aid ,'{}' ,'' - ,( - SELECT applicationid - FROM [Campus6].[dbo].[Application] - WHERE othersource = aid - ) + ,app.applicationid ,getdate() ,getdate() ,0 ,pid -FROM PowerCampusMapper.dbo.Slate_Apps +FROM PowerCampusMapper.dbo.Slate_Apps SA +INNER JOIN [Campus6].[dbo].[Application] APP + ON app.othersource = sa.aid WHERE aid NOT IN ( SELECT aid FROM #exclusions ) AND aid NOT IN ( - SELECT applicationnumber + SELECT ApplicationNumber FROM [Campus6].[dbo].[RecruiterApplication] ) From 79c6b80887050223fb6c41f778090237729de14a Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Fri, 3 May 2024 15:11:07 -0600 Subject: [PATCH 19/30] Fix API Citizenship fields --- ps_format.py | 18 +++++++++--------- ps_models.py | 18 ++++++------------ 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/ps_format.py b/ps_format.py index 07d017c..90723f3 100644 --- a/ps_format.py +++ b/ps_format.py @@ -369,13 +369,20 @@ def format_app_sql(app, mapping, config): app["Degree"] ] - if app["CitizenshipStatus"] is not None: + if app["PrimaryCitizenship"] is not None: mapped["PRIMARYCITIZENSHIP"] = mapping["CitizenshipStatus"][ - app["CitizenshipStatus"] + app["PrimaryCitizenship"] ] else: mapped["PRIMARYCITIZENSHIP"] = None + if app["SecondaryCitizenship"] is not None: + mapped["SECONDARYCITIZENSHIP"] = mapping["CitizenshipStatus"][ + app["SecondaryCitizenship"] + ] + else: + mapped["SECONDARYCITIZENSHIP"] = None + if app["CollegeAttendStatus"] is not None: mapped["COLLEGE_ATTEND"] = mapping["CollegeAttend"][app["CollegeAttendStatus"]] else: @@ -386,13 +393,6 @@ def format_app_sql(app, mapping, config): else: mapped["VISA"] = None - if app["SecondaryCitizenship"] is not None: - mapped["SECONDARYCITIZENSHIP"] = mapping["CitizenshipStatus"][ - app["SecondaryCitizenship"] - ] - else: - mapped["SECONDARYCITIZENSHIP"] = None - if app["MaritalStatus"] is not None: mapped["MARITALSTATUS"] = mapping["MaritalStatus"][app["MaritalStatus"]] else: diff --git a/ps_models.py b/ps_models.py index cd815ac..d65d65c 100644 --- a/ps_models.py +++ b/ps_models.py @@ -41,12 +41,6 @@ "supply_null": False, "type": str, }, - "CitizenshipStatus": { - "api_verbatim": True, - "sql_verbatim": False, - "supply_null": True, - "type": str, - }, "CollegeAttendStatus": { "api_verbatim": True, "sql_verbatim": False, @@ -227,6 +221,12 @@ "supply_null": True, "type": str, }, + "SecondaryCitizenship": { + "api_verbatim": True, + "sql_verbatim": False, + "supply_null": True, + "type": str, + }, "PrimaryLanguage": { "api_verbatim": True, "sql_verbatim": False, @@ -281,12 +281,6 @@ "supply_null": False, "type": int, }, - "SecondaryCitizenship": { - "api_verbatim": True, - "sql_verbatim": False, - "supply_null": True, - "type": str, - }, "Status": { "api_verbatim": True, "sql_verbatim": False, From e5c3a595af98337b1facad533febc6c95f7eb7dd Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Fri, 3 May 2024 15:18:13 -0600 Subject: [PATCH 20/30] Update documentation --- PowerSlate Integration Fields.docx | Bin 36264 -> 36590 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/PowerSlate Integration Fields.docx b/PowerSlate Integration Fields.docx index 07c6f4216513a03dffed204995b45526e3ee560a..7b9e273e05494b91bae40b517ecc1ca3a1198beb 100644 GIT binary patch delta 10871 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 SHi;wyuf}7*K}P+@1^*9=MM$Rr From 070637035cda8908e6db73f9483287730345bb62 Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Fri, 3 May 2024 15:25:28 -0600 Subject: [PATCH 21/30] Implement COLLEGE field Resolve #65 --- SQL/[custom].[PS_updAcademicAppInfo].sql | 41 ++++++++++++++++++++++-- ps_models.py | 6 ++++ ps_powercampus.py | 3 +- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/SQL/[custom].[PS_updAcademicAppInfo].sql b/SQL/[custom].[PS_updAcademicAppInfo].sql index 1c9d748..df9f087 100644 --- a/SQL/[custom].[PS_updAcademicAppInfo].sql +++ b/SQL/[custom].[PS_updAcademicAppInfo].sql @@ -12,7 +12,7 @@ GO -- Author: Wyatt Best -- Create date: 2016-11-17 -- Description: Updates Status and Decision code for application from Slate. --- Sets ACADEMIC_FLAG, PRIMARY_FLAG, ENROLL_SEPARATION, DEPARTMENT, POPULATION, COUNSELOR, EXTRA_CURRICULAR, COLLEGE_ATTEND, APPLICATION_DATE. +-- Sets ACADEMIC_FLAG, PRIMARY_FLAG, ENROLL_SEPARATION, COLLEGE, DEPARTMENT, POPULATION, COUNSELOR, EXTRA_CURRICULAR, COLLEGE_ATTEND, APPLICATION_DATE. -- Sets ADMIT and MATRIC field groups. -- -- 2016-12-15 Wyatt Best: Added 'Defer' ProposedDecision type. @@ -40,6 +40,7 @@ GO -- If ACADEMIC_FLAG isn't yet set to Y, update ACADEMIC.ORG_CODE_ID based on the passed OrganizationId. -- 2021-12-13 Wyatt Best: Ability to set NONTRAD_PROGRAM back to blank (NULL isn't allowed). Formerly, a bad @Nontraditional value later set to NULL in Slate would remain in PowerCampus. -- 2023-03-02 Wyatt Best: Use ADM_APPLICANT_DEFAULT setting instead of STUDENT_CODING_ENROLLED setting for ENROLL_SEPARATION when converting to student. +-- 2024-05-03 Wyatt Best: Added @College. -- ============================================= CREATE PROCEDURE [custom].[PS_updAcademicAppInfo] @PCID NVARCHAR(10) ,@Year NVARCHAR(4) @@ -48,6 +49,7 @@ CREATE PROCEDURE [custom].[PS_updAcademicAppInfo] @PCID NVARCHAR(10) ,@Program NVARCHAR(6) ,@Degree NVARCHAR(6) ,@Curriculum NVARCHAR(6) + ,@College NVARCHAR(6) NULL ,@Department NVARCHAR(10) NULL ,@Nontraditional NVARCHAR(6) NULL ,@Population NVARCHAR(12) NULL @@ -95,6 +97,25 @@ BEGIN END --Error checks + IF ( + @College IS NOT NULL + AND NOT EXISTS ( + SELECT * + FROM CODE_COLLEGE + WHERE CODE_VALUE_KEY = @College + ) + ) + BEGIN + RAISERROR ( + '@College ''%s'' not found in CODE_COLLEGE.' + ,11 + ,1 + ,@College + ) + + RETURN + END + IF ( @Department IS NOT NULL AND NOT EXISTS ( @@ -294,6 +315,22 @@ BEGIN AND @AppStatus IS NOT NULL AND @AppDecision IS NOT NULL + --Update COLLEGE if needed + UPDATE ACADEMIC + SET COLLEGE = @College + WHERE PEOPLE_CODE_ID = @PCID + AND ACADEMIC_YEAR = @Year + AND ACADEMIC_TERM = @Term + AND ACADEMIC_SESSION = @Session + AND PROGRAM = @Program + AND DEGREE = @Degree + AND CURRICULUM = @Curriculum + AND APPLICATION_FLAG = 'Y' + AND ( + COLLEGE <> @College + OR COLLEGE IS NULL + ) + --Update DEPARTMENT if needed UPDATE ACADEMIC SET DEPARTMENT = @Department @@ -339,7 +376,7 @@ BEGIN ) BEGIN --ENROLL_SEPARATION is only updated if the ACADEMIC_FLAG is toggled, otherwise it's left alone. - DECLARE @ConvertedStudentCode NVARCHAR(8) = dbo.fnGetAbtSetting('ADM_APPLICANT_DEFAULT','APPLICANT_SETUP_DEFAULT','CONVERTED_STUDENT_ENROLLSEP') + DECLARE @ConvertedStudentCode NVARCHAR(8) = dbo.fnGetAbtSetting('ADM_APPLICANT_DEFAULT', 'APPLICANT_SETUP_DEFAULT', 'CONVERTED_STUDENT_ENROLLSEP') ,@NewAcademicFlag NVARCHAR(1) = ( SELECT CASE WHEN EXISTS ( diff --git a/ps_models.py b/ps_models.py index d65d65c..95e8bde 100644 --- a/ps_models.py +++ b/ps_models.py @@ -77,6 +77,12 @@ "supply_null": True, "type": str, }, + "College": { + "api_verbatim": False, + "sql_verbatim": True, + "supply_null": True, + "type": str, + }, "Department": { "api_verbatim": False, "sql_verbatim": True, diff --git a/ps_powercampus.py b/ps_powercampus.py index 84f4769..5250967 100644 --- a/ps_powercampus.py +++ b/ps_powercampus.py @@ -512,7 +512,7 @@ def update_academic(app): If ACADEMIC_FLAG isn't yet set to Y, update ACADEMIC.ORG_CODE_ID based on the passed OrganizationId. """ CURSOR.execute( - "exec [custom].[PS_updAcademicAppInfo] ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", + "exec [custom].[PS_updAcademicAppInfo] ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", app["PEOPLE_CODE_ID"], app["ACADEMIC_YEAR"], app["ACADEMIC_TERM"], @@ -520,6 +520,7 @@ def update_academic(app): app["PROGRAM"], app["DEGREE"], app["CURRICULUM"], + app["College"], app["Department"], app["Nontraditional"], app["Population"], From 271d9b31266a57ba7eb82f99c3a345b3bf0239ea Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Tue, 7 May 2024 16:16:05 -0600 Subject: [PATCH 22/30] Multiple POE support Option to use FINAIDMAPPING instead of ACADEMICCALENDAR for POE mappings. --- SQL/[custom].[PS_selPFAwardsXML].sql | 249 +++++++++++++++++++-------- SQL/[custom].[PS_selPFChecklist].sql | 86 +++++++-- 2 files changed, 245 insertions(+), 90 deletions(-) diff --git a/SQL/[custom].[PS_selPFAwardsXML].sql b/SQL/[custom].[PS_selPFAwardsXML].sql index 905d96b..160ad3a 100644 --- a/SQL/[custom].[PS_selPFAwardsXML].sql +++ b/SQL/[custom].[PS_selPFAwardsXML].sql @@ -14,13 +14,15 @@ GO -- Description: Return XML of award data from PowerFAIDS for the award year associated with a YTS and the tracking status. -- Award data is aggregated and intended for display on Slate dashboards. -- The XML structure mimics Slate's own Dictionary subquery export types for compatibility with Liquid looping. +-- PowerFAIDS server/db names may need edited during deployment. @UseFINAIDMAPPING may need toggled. -- --- 2022-04-19 Wyatt Best: Removed gross amounts and added total line. +-- 2022-04-19 Wyatt Best: Removed gross amounts and added total line. +-- 2024-05-07 Wyatt Best: Option to use FINAIDMAPPING instead of ACADEMICCALENDAR for POE mappings. +-- Fixed @GovID datatype. +-- Restructured for efficiency. -- ============================================= -CREATE PROCEDURE [custom].[PS_selPFAwardsXML] - -- Add the parameters for the stored procedure here - @PCID NVARCHAR(10) - ,@GovID INT +CREATE PROCEDURE [custom].[PS_selPFAwardsXML] @PCID NVARCHAR(10) + ,@GovID VARCHAR(9) ,@AcademicYear NVARCHAR(4) ,@AcademicTerm NVARCHAR(10) ,@AcademicSession NVARCHAR(10) @@ -28,14 +30,170 @@ AS BEGIN SET NOCOUNT ON; - DECLARE @FinAidYear NVARCHAR(4) = ( - SELECT FIN_AID_YEAR - FROM ACADEMICCALENDAR - WHERE ACADEMIC_YEAR = @AcademicYear - AND ACADEMIC_TERM = @AcademicTerm - AND ACADEMIC_SESSION = @AcademicSession - ) + --Switch for whether to use ACADEMICCALENDAR (default) or FINAIDMAPPING as the POE source + DECLARE @UseFINAIDMAPPING BIT = 1 + ,@student_token INT + ,@FinAidYear INT + ,@TrackStat VARCHAR(2) + DECLARE @POEs TABLE ( + POE INT + ,ACADEMIC_SESSION NVARCHAR(10) + ,award_year INT + ) + DECLARE @AwardsRaw TABLE ( + [fund_long_name] VARCHAR(40) + ,[amount] NUMERIC(8, 2) + ,[attend_desc] VARCHAR(30) + ) + --Find student + SELECT @student_token = student_token + FROM [POWERFAIDS].[PFaids].[dbo].[student] s + WHERE s.alternate_id = @PCID + OR s.student_ssn = @GovID + + --Using OR in join criteria for [stu_award_year] caused inefficiency, so queries are repeated + IF @UseFINAIDMAPPING = 1 + BEGIN + --Get POEs from FINAIDMAPPING + INSERT INTO @POEs + SELECT POE + ,ACADEMIC_SESSION + ,NULL + FROM FINAIDMAPPING + WHERE 1 = 1 + AND ACADEMIC_YEAR = @AcademicYear + AND ACADEMIC_TERM = @AcademicTerm + AND ( + ACADEMIC_SESSION = @AcademicSession + OR ACADEMIC_SESSION = '' + ) + AND [STATUS] = 'A' + + --If POE's are mapped by Session, delete POE's with blank session + DELETE + FROM @POEs + WHERE ACADEMIC_SESSION = '' + AND EXISTS ( + SELECT * + FROM @POEs + WHERE ACADEMIC_SESSION > '' + ) + + --Get Aid Year by POE from PowerFAIDS + UPDATE p_local + SET award_year = p_remote.award_year_token + FROM @POEs p_local + INNER JOIN [POWERFAIDS].[PFaids].[dbo].[poe] p_remote + ON p_local.POE = p_remote.poe_token + + --Get tracking status + SELECT @TrackStat = tracking_status + FROM [POWERFAIDS].[PFaids].[dbo].[student] s + INNER JOIN [POWERFAIDS].[PFaids].[dbo].[stu_award_year] say + ON say.award_year_token IN ( + SELECT award_year + FROM @POEs + ) + AND s.student_token = say.student_token + WHERE s.student_token = @student_token + + --Get raw award data (multiple POE's method) + INSERT INTO @AwardsRaw + SELECT CASE + WHEN net_disbursement_amount > 0 + AND net_disbursement_amount <> scheduled_amount + THEN fund_long_name + ' (Net)' + ELSE fund_long_name + END [fund_long_name] + ,CASE + WHEN net_disbursement_amount > 0 + AND net_disbursement_amount <> scheduled_amount + THEN net_disbursement_amount + ELSE scheduled_amount + END [amount] + ,CASE + WHEN attend_desc LIKE '%sp%' + THEN 'Spring' + WHEN attend_desc LIKE '%fa%' + THEN 'Fall' + WHEN attend_desc LIKE '%su%' + THEN 'Summer' + END AS [attend_desc] + FROM [POWERFAIDS].[PFaids].[dbo].[student] s + INNER JOIN [POWERFAIDS].[PFaids].[dbo].[stu_award_year] say + ON say.award_year_token IN ( + SELECT award_year + FROM @POEs + ) + AND s.student_token = say.student_token + INNER JOIN [POWERFAIDS].[PFaids].[dbo].[stu_award] sa + ON sa.stu_award_year_token = say.stu_award_year_token + INNER JOIN [POWERFAIDS].[PFaids].[dbo].[stu_award_transactions] sat + ON sat.stu_award_token = sa.stu_award_token + INNER JOIN [POWERFAIDS].[PFaids].[dbo].[funds] f + ON f.fund_token = sa.fund_ay_token + INNER JOIN [POWERFAIDS].[PFaids].[dbo].[poe] + ON poe.poe_token = sat.poe_token + WHERE s.student_token = @student_token + END + ELSE + BEGIN + --Get single POE from ACADEMICCALENDAR + SET @FinAidYear = ( + SELECT FIN_AID_YEAR + FROM ACADEMICCALENDAR + WHERE ACADEMIC_YEAR = @AcademicYear + AND ACADEMIC_TERM = @AcademicTerm + AND ACADEMIC_SESSION = @AcademicSession + ) + + --Get tracking status + SELECT @TrackStat = tracking_status + FROM [POWERFAIDS].[PFaids].[dbo].[student] s + INNER JOIN [POWERFAIDS].[PFaids].[dbo].[stu_award_year] say + ON say.award_year_token = @FinAidYear + AND s.student_token = say.student_token + WHERE s.student_token = @student_token + + --Get raw award data (single POE method) + INSERT INTO @AwardsRaw + SELECT CASE + WHEN net_disbursement_amount > 0 + AND net_disbursement_amount <> scheduled_amount + THEN fund_long_name + ' (Net)' + ELSE fund_long_name + END [fund_long_name] + ,CASE + WHEN net_disbursement_amount > 0 + AND net_disbursement_amount <> scheduled_amount + THEN net_disbursement_amount + ELSE scheduled_amount + END [amount] + ,CASE + WHEN attend_desc LIKE '%sp%' + THEN 'Spring' + WHEN attend_desc LIKE '%fa%' + THEN 'Fall' + WHEN attend_desc LIKE '%su%' + THEN 'Summer' + END AS [attend_desc] + FROM [POWERFAIDS].[PFaids].[dbo].[student] s + INNER JOIN [POWERFAIDS].[PFaids].[dbo].[stu_award_year] say + ON say.award_year_token = @FinAidYear + AND s.student_token = say.student_token + INNER JOIN [POWERFAIDS].[PFaids].[dbo].[stu_award] sa + ON sa.stu_award_year_token = say.stu_award_year_token + INNER JOIN [POWERFAIDS].[PFaids].[dbo].[stu_award_transactions] sat + ON sat.stu_award_token = sa.stu_award_token + INNER JOIN [POWERFAIDS].[PFaids].[dbo].[funds] f + ON f.fund_token = sa.fund_ay_token + INNER JOIN [POWERFAIDS].[PFaids].[dbo].[poe] + ON poe.poe_token = sat.poe_token + WHERE s.student_token = @student_token + END + + --Format awards as XML SELECT ( SELECT ( SELECT 'fund_long_name' AS [k] @@ -71,35 +229,7 @@ BEGIN --Individual awards SELECT * ,COALESCE([Summer], 0) + COALESCE([Fall], 0) + COALESCE([Spring], 0) AS Total - FROM ( - SELECT CASE - WHEN net_disbursement_amount > 0 - AND net_disbursement_amount <> scheduled_amount - THEN fund_long_name + ' (Net)' - ELSE fund_long_name - END [fund_long_name] - ,CASE - WHEN net_disbursement_amount > 0 - AND net_disbursement_amount <> scheduled_amount - THEN net_disbursement_amount - ELSE scheduled_amount - END [amount] - ,IIF(attend_desc = 'T-Summer', 'Summer', attend_desc) [attend_desc] - FROM [VMCNYPF01].[PFaids].[dbo].[student] s - INNER JOIN [VMCNYPF01].[PFaids].[dbo].[stu_award_year] say - ON say.award_year_token = @FinAidYear - AND s.student_token = say.student_token - INNER JOIN [VMCNYPF01].[PFaids].[dbo].[stu_award] sa - ON sa.stu_award_year_token = say.stu_award_year_token - INNER JOIN [VMCNYPF01].[PFaids].[dbo].[stu_award_transactions] sat - ON sat.stu_award_token = sa.stu_award_token - INNER JOIN [VMCNYPF01].[PFaids].[dbo].[funds] f - ON f.fund_token = sa.fund_ay_token - INNER JOIN [VMCNYPF01].[PFaids].[dbo].[poe] - ON poe.poe_token = sat.poe_token - WHERE s.alternate_id = @PCID - OR s.student_ssn = @GovID - ) a_raw + FROM @AwardsRaw PIVOT(SUM([amount]) FOR attend_desc IN ( [Summer] ,[Fall] @@ -111,34 +241,7 @@ BEGIN --Grand total SELECT * ,COALESCE([Summer], 0) + COALESCE([Fall], 0) + COALESCE([Spring], 0) AS Total - FROM ( - SELECT 'Totals' [fund_long_name] - ,CASE - WHEN net_disbursement_amount > 0 - AND net_disbursement_amount <> scheduled_amount - THEN net_disbursement_amount - ELSE scheduled_amount - END [amount] - ,IIF(attend_desc = 'T-Summer', 'Summer', attend_desc) [attend_desc] - FROM [VMCNYPF01].[PFaids].[dbo].[student] s - INNER JOIN [VMCNYPF01].[PFaids].[dbo].[stu_award_year] say - ON say.award_year_token = @FinAidYear - AND s.student_token = say.student_token - INNER JOIN [VMCNYPF01].[PFaids].[dbo].[stu_award] sa - ON sa.stu_award_year_token = say.stu_award_year_token - INNER JOIN [VMCNYPF01].[PFaids].[dbo].[stu_award_transactions] sat - ON sat.stu_award_token = sa.stu_award_token - INNER JOIN [VMCNYPF01].[PFaids].[dbo].[funds] f - ON f.fund_token = sa.fund_ay_token - INNER JOIN [VMCNYPF01].[PFaids].[dbo].[poe] - ON poe.poe_token = sat.poe_token - WHERE ( - s.alternate_id = @PCID - OR s.student_ssn = @GovID - ) - --AND net_disbursement_amount > 0 - --AND net_disbursement_amount <> scheduled_amount - ) a_raw + FROM @AwardsRaw PIVOT(SUM([amount]) FOR attend_desc IN ( [Summer] ,[Fall] @@ -155,11 +258,5 @@ BEGIN FOR XML path('row') ,type ) AS [XML] - ,tracking_status - FROM [VMCNYPF01].[PFaids].[dbo].[student] s - INNER JOIN [VMCNYPF01].[PFaids].[dbo].[stu_award_year] say - ON say.award_year_token = @FinAidYear - AND s.student_token = say.student_token - WHERE s.alternate_id = @PCID - OR s.student_ssn = @GovID + ,@TrackStat AS [tracking_status] END diff --git a/SQL/[custom].[PS_selPFChecklist].sql b/SQL/[custom].[PS_selPFChecklist].sql index bf2497b..78af7f9 100644 --- a/SQL/[custom].[PS_selPFChecklist].sql +++ b/SQL/[custom].[PS_selPFChecklist].sql @@ -11,10 +11,11 @@ GO -- Author: Wyatt Best -- Create date: 2020-09-30 -- Description: Selects a list of missing documents from PowerFAIDS. --- award_year_token in PowerFAIDS is pulled from FIN_AID_YEAR in ACADEMICCALENDAR. +-- PowerFAIDS server/db names may need edited during deployment. @UseFINAIDMAPPING may need toggled. -- -- 2020-11-12 Wyatt Best: Added search by TIN/SSN (@GovID) instead of just PEOPLE_CODE_ID (@PCID). -- 2021-04-02 Wyatt Best: Changed @GovID datatype from INT to match PFaids column. +-- 2024-05-07 Wyatt Best: Option to use FINAIDMAPPING instead of ACADEMICCALENDAR for POE mappings. -- ============================================= CREATE PROCEDURE [custom].[PS_selPFChecklist] @PCID NVARCHAR(10) @@ -26,27 +27,84 @@ AS BEGIN SET NOCOUNT ON; - DECLARE @FinAidYear NVARCHAR(4) = ( - SELECT FIN_AID_YEAR - FROM ACADEMICCALENDAR - WHERE ACADEMIC_YEAR = @AcademicYear - AND ACADEMIC_TERM = @AcademicTerm - AND ACADEMIC_SESSION = @AcademicSession - ) + --Switch for whether to use ACADEMICCALENDAR (default) or FINAIDMAPPING as the POE source + DECLARE @UseFINAIDMAPPING BIT = 0 + ,@FinAidYear INT + DECLARE @POEs TABLE ( + POE INT + ,ACADEMIC_SESSION NVARCHAR(10) + ,award_year INT + ) + + IF @UseFINAIDMAPPING = 1 + BEGIN + --Get POEs from FINAIDMAPPING + INSERT INTO @POEs + SELECT POE + ,ACADEMIC_SESSION + ,NULL + FROM FINAIDMAPPING + WHERE 1 = 1 + AND ACADEMIC_YEAR = @AcademicYear + AND ACADEMIC_TERM = @AcademicTerm + AND ( + ACADEMIC_SESSION = @AcademicSession + OR ACADEMIC_SESSION = '' + ) + AND [STATUS] = 'A' + + --If POE's are mapped by Session, delete POE's with blank session + DELETE + FROM @POEs + WHERE ACADEMIC_SESSION = '' + AND EXISTS ( + SELECT * + FROM @POEs + WHERE ACADEMIC_SESSION > '' + ) + + --Get Aid Year by POE from PowerFAIDS + UPDATE p_local + SET award_year = p_remote.award_year_token + FROM @POEs p_local + INNER JOIN [POWERFAIDS].[PFaids].[dbo].[poe] p_remote + ON p_local.POE = p_remote.poe_token + END + ELSE + BEGIN + --Get single POE from ACADEMICCALENDAR + SET @FinAidYear = ( + SELECT FIN_AID_YEAR + FROM ACADEMICCALENDAR + WHERE ACADEMIC_YEAR = @AcademicYear + AND ACADEMIC_TERM = @AcademicTerm + AND ACADEMIC_SESSION = @AcademicSession + ) + END SELECT srd.doc_token [Code] --,d.doc_name ,doc_status_desc [Status] ,FORMAT(status_effective_dt, 'yyyy-MM-dd') [Date] - FROM [PFaids].[dbo].[student] s - INNER JOIN [PFaids].[dbo].[stu_award_year] say - ON say.award_year_token = @FinAidYear + FROM [POWERFAIDS].[PFaids].[dbo].[student] s + INNER JOIN [POWERFAIDS].[PFaids].[dbo].[stu_award_year] say + ON ( + @UseFINAIDMAPPING = 0 + AND say.award_year_token = @FinAidYear + ) + OR ( + @UseFINAIDMAPPING = 1 + AND say.award_year_token IN ( + SELECT award_year + FROM @POEs + ) + ) AND s.student_token = say.student_token - INNER JOIN [PFaids].[dbo].[student_required_documents] srd + INNER JOIN [POWERFAIDS].[PFaids].[dbo].[student_required_documents] srd ON say.stu_award_year_token = srd.stu_award_year_token - INNER JOIN [PFaids].[dbo].[docs] d + INNER JOIN [POWERFAIDS].[PFaids].[dbo].[docs] d ON d.doc_token = srd.doc_token - INNER JOIN [PFaids].[dbo].[doc_status_code] dsc + INNER JOIN [POWERFAIDS].[PFaids].[dbo].[doc_status_code] dsc ON dsc.doc_required_status_code = srd.doc_status WHERE s.alternate_id = @PCID OR s.student_ssn = @GovID From 35785636fe40a8b6455f2480003857521cca98b2 Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Wed, 8 May 2024 09:09:04 -0600 Subject: [PATCH 23/30] Fix grand total line in FA Awards XML --- SQL/[custom].[PS_selPFAwardsXML].sql | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/SQL/[custom].[PS_selPFAwardsXML].sql b/SQL/[custom].[PS_selPFAwardsXML].sql index 160ad3a..b9819c6 100644 --- a/SQL/[custom].[PS_selPFAwardsXML].sql +++ b/SQL/[custom].[PS_selPFAwardsXML].sql @@ -227,7 +227,10 @@ BEGIN ) FROM ( --Individual awards - SELECT * + SELECT [fund_long_name] + ,[Summer] + ,[Fall] + ,[Spring] ,COALESCE([Summer], 0) + COALESCE([Fall], 0) + COALESCE([Spring], 0) AS Total FROM @AwardsRaw PIVOT(SUM([amount]) FOR attend_desc IN ( @@ -239,7 +242,10 @@ BEGIN UNION ALL --Grand total - SELECT * + SELECT 'Totals' AS [fund_long_name] + ,[Summer] + ,[Fall] + ,[Spring] ,COALESCE([Summer], 0) + COALESCE([Fall], 0) + COALESCE([Spring], 0) AS Total FROM @AwardsRaw PIVOT(SUM([amount]) FOR attend_desc IN ( From 9a4414a0deb4ecc2f4d30d278a42509fa0b13fd4 Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Wed, 8 May 2024 09:25:45 -0600 Subject: [PATCH 24/30] Update fa_status prompts Moved code value to Export Value instead of Index to support auto-mapping in Slate. --- Sample FA Status Prompts.xlsx | Bin 10400 -> 10496 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Sample FA Status Prompts.xlsx b/Sample FA Status Prompts.xlsx index 0641a6f218e307f41d1606ca8d33fefb273131ef..7a826a28a8ec1698c794ec9f09a78e76e4ca1562 100644 GIT binary patch delta 5367 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 q7^UYx;Ik9b{QDBoK%o7Q?7TjfV4{kvdALpK1 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 Date: Wed, 8 May 2024 11:29:53 -0600 Subject: [PATCH 25/30] Config toggle for UseFINAIDMAPPING Pass bit flag for whether to use ACADEMICCALENDAR or FINAIDMAPPING as the POE source instead of hardcoding in SQL. --- SQL/[custom].[PS_selPFAwardsXML].sql | 8 ++++---- SQL/[custom].[PS_selPFChecklist].sql | 7 ++++--- config_sample.json | 6 ++++-- ps_core.py | 5 ++++- ps_powercampus.py | 10 ++++++---- 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/SQL/[custom].[PS_selPFAwardsXML].sql b/SQL/[custom].[PS_selPFAwardsXML].sql index b9819c6..6071a65 100644 --- a/SQL/[custom].[PS_selPFAwardsXML].sql +++ b/SQL/[custom].[PS_selPFAwardsXML].sql @@ -14,7 +14,8 @@ GO -- Description: Return XML of award data from PowerFAIDS for the award year associated with a YTS and the tracking status. -- Award data is aggregated and intended for display on Slate dashboards. -- The XML structure mimics Slate's own Dictionary subquery export types for compatibility with Liquid looping. --- PowerFAIDS server/db names may need edited during deployment. @UseFINAIDMAPPING may need toggled. +-- @UseFINAIDMAPPING toggles between selecting a single POE from ACADEMICCALENDAR or selecting multiple POE's from FINAIDMAPPING. +-- PowerFAIDS server/db names may need edited during deployment. -- -- 2022-04-19 Wyatt Best: Removed gross amounts and added total line. -- 2024-05-07 Wyatt Best: Option to use FINAIDMAPPING instead of ACADEMICCALENDAR for POE mappings. @@ -26,13 +27,12 @@ CREATE PROCEDURE [custom].[PS_selPFAwardsXML] @PCID NVARCHAR(10) ,@AcademicYear NVARCHAR(4) ,@AcademicTerm NVARCHAR(10) ,@AcademicSession NVARCHAR(10) + ,@UseFINAIDMAPPING BIT = 0 AS BEGIN SET NOCOUNT ON; - --Switch for whether to use ACADEMICCALENDAR (default) or FINAIDMAPPING as the POE source - DECLARE @UseFINAIDMAPPING BIT = 1 - ,@student_token INT + DECLARE @student_token INT ,@FinAidYear INT ,@TrackStat VARCHAR(2) DECLARE @POEs TABLE ( diff --git a/SQL/[custom].[PS_selPFChecklist].sql b/SQL/[custom].[PS_selPFChecklist].sql index 78af7f9..a0b349e 100644 --- a/SQL/[custom].[PS_selPFChecklist].sql +++ b/SQL/[custom].[PS_selPFChecklist].sql @@ -11,7 +11,8 @@ GO -- Author: Wyatt Best -- Create date: 2020-09-30 -- Description: Selects a list of missing documents from PowerFAIDS. --- PowerFAIDS server/db names may need edited during deployment. @UseFINAIDMAPPING may need toggled. +-- @UseFINAIDMAPPING toggles between selecting a single POE from ACADEMICCALENDAR or selecting multiple POE's from FINAIDMAPPING. +-- PowerFAIDS server/db names may need edited during deployment. -- -- 2020-11-12 Wyatt Best: Added search by TIN/SSN (@GovID) instead of just PEOPLE_CODE_ID (@PCID). -- 2021-04-02 Wyatt Best: Changed @GovID datatype from INT to match PFaids column. @@ -23,13 +24,13 @@ CREATE PROCEDURE [custom].[PS_selPFChecklist] ,@AcademicYear NVARCHAR(4) ,@AcademicTerm NVARCHAR(10) ,@AcademicSession NVARCHAR(10) + ,@UseFINAIDMAPPING BIT = 0 AS BEGIN SET NOCOUNT ON; --Switch for whether to use ACADEMICCALENDAR (default) or FINAIDMAPPING as the POE source - DECLARE @UseFINAIDMAPPING BIT = 0 - ,@FinAidYear INT + DECLARE @FinAidYear INT DECLARE @POEs TABLE ( POE INT ,ACADEMIC_SESSION NVARCHAR(10) diff --git a/config_sample.json b/config_sample.json index 69e4120..ee57fa5 100644 --- a/config_sample.json +++ b/config_sample.json @@ -116,7 +116,8 @@ ] }, "fa_checklist": { - "enabled": true, + "enabled": false, + "use_finaidmapping": false, "slate_post": { "url": "https://apply.school.edu/manage/query/run?id=xxxx&h=xxxx&cmd=service&output=json", "username": "username", @@ -124,7 +125,8 @@ } }, "fa_awards": { - "enabled": true + "enabled": false, + "use_finaidmapping": false }, "defaults": { "address_country": null, diff --git a/ps_core.py b/ps_core.py index 5acd91e..550deec 100644 --- a/ps_core.py +++ b/ps_core.py @@ -22,6 +22,7 @@ def __init__(self, contents): def __init__(self, config): self.fa_awards = self.FlatDict(config["fa_awards"]) + self.fa_checklist = self.FlatDict(config["fa_checklist"]) self.console_verbose = config["console_verbose"] self.defaults = self.FlatDict(config["defaults"]) self.PowerCampus = self.PowerCampus(config["powercampus"]) @@ -589,6 +590,7 @@ def main_sync(pid=None): academic_year, academic_term, academic_session, + SETTINGS.fa_awards.use_finaidmapping, ) apps[k].update({"fa_awards": fa_awards, "fa_status": fa_status}) @@ -607,7 +609,7 @@ def main_sync(pid=None): ) # Collect Financial Aid checklist and upload to Slate - if CONFIG["fa_checklist"]["enabled"] == True: + if SETTINGS.fa_checklist.enabled == True: verbose_print("Collect Financial Aid checklist and upload to Slate") slate_upload_list = [] # slate_upload_fields = {'AppID', 'Code', 'Status', 'Date'} @@ -625,6 +627,7 @@ def main_sync(pid=None): app_pc["ACADEMIC_YEAR"], app_pc["ACADEMIC_TERM"], app_pc["ACADEMIC_SESSION"], + SETTINGS.fa_checklist.use_finaidmapping, ) slate_upload_list = slate_upload_list + fa_checklists diff --git a/ps_powercampus.py b/ps_powercampus.py index 84f4769..f55a264 100644 --- a/ps_powercampus.py +++ b/ps_powercampus.py @@ -854,16 +854,17 @@ def update_association(pcid, association): CNXN.commit() -def pf_get_fachecklist(pcid, govid, appid, year, term, session): +def pf_get_fachecklist(pcid, govid, appid, year, term, session, use_finaidmapping): """Return the PowerFAIDS missing docs list for uploading to Financial Aid Checklist.""" checklist = [] CURSOR.execute( - "exec [custom].[PS_selPFChecklist] ?, ?, ?, ?, ?", + "exec [custom].[PS_selPFChecklist] ?, ?, ?, ?, ?, ?", pcid, govid, year, term, session, + use_finaidmapping, ) columns = [column[0] for column in CURSOR.description] @@ -877,18 +878,19 @@ def pf_get_fachecklist(pcid, govid, appid, year, term, session): return checklist -def pf_get_awards(pcid, govid, year, term, session): +def pf_get_awards(pcid, govid, year, term, session, use_finaidmapping): """Return the PowerFAIDS awards list as XML and the Tracking Status.""" awards = None tracking_status = None CURSOR.execute( - "exec [custom].[PS_selPFAwardsXML] ?, ?, ?, ?, ?", + "exec [custom].[PS_selPFAwardsXML] ?, ?, ?, ?, ?, ?", pcid, govid, year, term, session, + use_finaidmapping, ) row = CURSOR.fetchone() From 6af5b8d9ff447d0d0d2facea5247e4b20d75fa00 Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Wed, 8 May 2024 13:38:21 -0600 Subject: [PATCH 26/30] Tools for FA Docs/Checklist --- Tools/FA Docs - Extract List.sql | 17 ++++++++ Tools/FA Docs - Insert Checklists.js | 60 ++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 Tools/FA Docs - Extract List.sql create mode 100644 Tools/FA Docs - Insert Checklists.js diff --git a/Tools/FA Docs - Extract List.sql b/Tools/FA Docs - Extract List.sql new file mode 100644 index 0000000..63ea72b --- /dev/null +++ b/Tools/FA Docs - Extract List.sql @@ -0,0 +1,17 @@ +USE Pfaids + +SELECT '[''' + cast(doc_token AS VARCHAR(50)) + ''',''' + replace(doc_name, '''', '\''') + ''',''' + cast(award_year_token AS NVARCHAR(4)) + ''',''' + iif(use_hyperlink = 1, hyperlink_url, '') + '''],' AS [js_array] +FROM docs +WHERE 1 = 1 + AND award_year_token IN ( + 2023 + ,2024 + ) +--AND doc_token IN ( +-- SELECT doc_token +-- FROM [student_required_documents] srd +-- INNER JOIN stu_award_year sar +-- ON sar.stu_award_year_token = srd.stu_award_year_token +-- WHERE award_year_token = 2023 +-- ) +ORDER BY doc_token diff --git a/Tools/FA Docs - Insert Checklists.js b/Tools/FA Docs - Insert Checklists.js new file mode 100644 index 0000000..d9e6f04 --- /dev/null +++ b/Tools/FA Docs - Insert Checklists.js @@ -0,0 +1,60 @@ +function insertChecklist(docs, status_xml) { + docs.forEach(function (doc) { + values = { + "active": "1", + "folder_type": "lookup.checklist", + "folder_level": "0", + "folder_0": "", + "scope": "", + "group": doc[2], + "section": "Financial Aid", + "subject": doc[1], + "href": doc[3], + "key": doc[0], + "order": "", + "material": "", + "material2": "", + "material3": "", + "material4": "", + "material5": "", + "rank": "", + "test": "", + "test2": "", + "test3": "", + "form_fulfillment": "", + "sql": "", + "xml": status_xml, + "internal": "0", + "optional": "0", + "optional_internal": "0", + "right": "", + "right_update": "", + "export": "", + "cmd": "update" + + } + $.ajax({ + url: "/manage/database/admin?cmd=edit&id=lookup.checklist", + type: "POST", + data: values + }).done(function () { + console.log('Successfully inserted ' + doc[0]); + }) + .fail(function () { + console.log('Failed to insert ' + doc[0]); + return; + }); + + }); +} + +// Standard PF doc statuses and suggested mappings +status_xml = '

statusReceivedWaivedWaivedNot ReviewedApprovedIncompleteNot ReceivedNot Signed

' + +// Array of docs: key, subject, group, url +// Replace with doc list from Tools\Get FA Docs List.sql +docs = [ + ['1234', '2021 Student Signed Tax Return', '2023', 'https://www.irs.gov/individuals/get-transcript'], +] + +// insertChecklist(docs, status_xml); From 61b2fa362d6d5251db7e6b1ca92a48cdd57da084 Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Fri, 10 May 2024 15:14:22 -0600 Subject: [PATCH 27/30] Flag to set ACADEMIC.PROGRAM_START_DATE --- SQL/[custom].[PS_updAcademicAppInfo].sql | 18 +++++++++++++++++- ps_models.py | 6 ++++++ ps_powercampus.py | 3 ++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/SQL/[custom].[PS_updAcademicAppInfo].sql b/SQL/[custom].[PS_updAcademicAppInfo].sql index df9f087..cdc8a2c 100644 --- a/SQL/[custom].[PS_updAcademicAppInfo].sql +++ b/SQL/[custom].[PS_updAcademicAppInfo].sql @@ -13,7 +13,7 @@ GO -- Create date: 2016-11-17 -- Description: Updates Status and Decision code for application from Slate. -- Sets ACADEMIC_FLAG, PRIMARY_FLAG, ENROLL_SEPARATION, COLLEGE, DEPARTMENT, POPULATION, COUNSELOR, EXTRA_CURRICULAR, COLLEGE_ATTEND, APPLICATION_DATE. --- Sets ADMIT and MATRIC field groups. +-- Sets ADMIT and MATRIC field groups. Sets PROGRAM_START_DATE. -- -- 2016-12-15 Wyatt Best: Added 'Defer' ProposedDecision type. -- 2016-12-28 Wyatt Best: Changed translation CODE_APPDECISION for Waiver from 'ACCP' to 'WAIV' @@ -41,6 +41,7 @@ GO -- 2021-12-13 Wyatt Best: Ability to set NONTRAD_PROGRAM back to blank (NULL isn't allowed). Formerly, a bad @Nontraditional value later set to NULL in Slate would remain in PowerCampus. -- 2023-03-02 Wyatt Best: Use ADM_APPLICANT_DEFAULT setting instead of STUDENT_CODING_ENROLLED setting for ENROLL_SEPARATION when converting to student. -- 2024-05-03 Wyatt Best: Added @College. +-- 2024-05-10 Wyatt Best: Added flag @SetProgramStartDate to default program start date from academic calendar. -- ============================================= CREATE PROCEDURE [custom].[PS_updAcademicAppInfo] @PCID NVARCHAR(10) ,@Year NVARCHAR(4) @@ -64,6 +65,7 @@ CREATE PROCEDURE [custom].[PS_updAcademicAppInfo] @PCID NVARCHAR(10) ,@CollegeAttend NVARCHAR(4) NULL ,@Extracurricular BIT NULL ,@CreateDateTime DATETIME --Application creation date + ,@SetProgramStartDate BIT NULL AS BEGIN SET NOCOUNT ON; @@ -654,5 +656,19 @@ BEGIN AND APPLICATION_FLAG = 'Y' AND COALESCE(APPLICATION_DATE, '') <> dbo.fnMakeDate(@CreateDateTime); + --Update PROGRAM_START_DATE if needed + UPDATE ACADEMIC + SET PROGRAM_START_DATE = @MatricDate + WHERE PEOPLE_CODE_ID = @PCID + AND ACADEMIC_YEAR = @Year + AND ACADEMIC_TERM = @Term + AND ACADEMIC_SESSION = @Session + AND PROGRAM = @Program + AND DEGREE = @Degree + AND CURRICULUM = @Curriculum + AND APPLICATION_FLAG = 'Y' + AND PROGRAM_START_DATE IS NULL + AND @SetProgramStartDate = 1 + COMMIT END diff --git a/ps_models.py b/ps_models.py index 95e8bde..edbc779 100644 --- a/ps_models.py +++ b/ps_models.py @@ -227,6 +227,12 @@ "supply_null": True, "type": str, }, + "SetProgramStartDate": { + "api_verbatim": False, + "sql_verbatim": True, + "supply_null": True, + "type": bool, + }, "SecondaryCitizenship": { "api_verbatim": True, "sql_verbatim": False, diff --git a/ps_powercampus.py b/ps_powercampus.py index 11c7dc0..3aa3be3 100644 --- a/ps_powercampus.py +++ b/ps_powercampus.py @@ -512,7 +512,7 @@ def update_academic(app): If ACADEMIC_FLAG isn't yet set to Y, update ACADEMIC.ORG_CODE_ID based on the passed OrganizationId. """ CURSOR.execute( - "exec [custom].[PS_updAcademicAppInfo] ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", + "exec [custom].[PS_updAcademicAppInfo] ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", app["PEOPLE_CODE_ID"], app["ACADEMIC_YEAR"], app["ACADEMIC_TERM"], @@ -535,6 +535,7 @@ def update_academic(app): app["COLLEGE_ATTEND"], app["Extracurricular"], app["CreateDateTime"], + app["SetProgramStartDate"], ) CNXN.commit() From f9a34d5be6f0450c41d3af56496d5f55fc5fea40 Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Tue, 21 May 2024 10:56:32 -0600 Subject: [PATCH 28/30] Bugfixes -Remove extra SQL parameter. -Don't attempt to sync aid for apps with error flag true. --- ps_core.py | 2 ++ ps_powercampus.py | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ps_core.py b/ps_core.py index 550deec..26b3172 100644 --- a/ps_core.py +++ b/ps_core.py @@ -616,6 +616,8 @@ def main_sync(pid=None): for k, v in apps.items(): CURRENT_RECORD = k + if v["error_flag"] == True: + continue if v["status_calc"] == "Active": # Transform to PowerCampus format app_pc = format_app_sql(v, RM_MAPPING, SETTINGS.PowerCampus) diff --git a/ps_powercampus.py b/ps_powercampus.py index 3aa3be3..e628e35 100644 --- a/ps_powercampus.py +++ b/ps_powercampus.py @@ -545,7 +545,7 @@ def update_academic_key(app): P/C/D will only be updated if application is not registered and does not have an academic plan assigned. """ CURSOR.execute( - "exec [custom].[PS_updAcademicKey] ?, ?, ?, ?, ?, ?, ?, ?, ?", + "exec [custom].[PS_updAcademicKey] ?, ?, ?, ?, ?, ?, ?, ?", app["PEOPLE_CODE_ID"], app["ACADEMIC_YEAR"], app["ACADEMIC_TERM"], @@ -553,7 +553,6 @@ def update_academic_key(app): app["PROGRAM"], app["DEGREE"], app["CURRICULUM"], - app["aid"], app["AcademicGUID"], ) CNXN.commit() From 79a3a5c4d6a441e8e6e783065eb1372079714bda Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Tue, 21 May 2024 13:47:45 -0600 Subject: [PATCH 29/30] PowerFAIDS performance improvements Search for student token before doing anything else. Improves performance for linked server queries. --- SQL/[custom].[PS_selPFAwardsXML].sql | 8 ++++++++ SQL/[custom].[PS_selPFChecklist].sql | 23 ++++++++++++++++++----- ps_powercampus.py | 13 +++++++------ 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/SQL/[custom].[PS_selPFAwardsXML].sql b/SQL/[custom].[PS_selPFAwardsXML].sql index 6071a65..37573ad 100644 --- a/SQL/[custom].[PS_selPFAwardsXML].sql +++ b/SQL/[custom].[PS_selPFAwardsXML].sql @@ -52,6 +52,14 @@ BEGIN WHERE s.alternate_id = @PCID OR s.student_ssn = @GovID + --If student not found, quit immediately + IF @student_token IS NULL + BEGIN + SELECT NULL + + RETURN + END + --Using OR in join criteria for [stu_award_year] caused inefficiency, so queries are repeated IF @UseFINAIDMAPPING = 1 BEGIN diff --git a/SQL/[custom].[PS_selPFChecklist].sql b/SQL/[custom].[PS_selPFChecklist].sql index a0b349e..36a4df6 100644 --- a/SQL/[custom].[PS_selPFChecklist].sql +++ b/SQL/[custom].[PS_selPFChecklist].sql @@ -11,7 +11,7 @@ GO -- Author: Wyatt Best -- Create date: 2020-09-30 -- Description: Selects a list of missing documents from PowerFAIDS. --- @UseFINAIDMAPPING toggles between selecting a single POE from ACADEMICCALENDAR or selecting multiple POE's from FINAIDMAPPING. +-- @UseFINAIDMAPPING toggles between selecting a single POE from ACADEMICCALENDAR (0) or selecting multiple POE's (1) from FINAIDMAPPING. -- PowerFAIDS server/db names may need edited during deployment. -- -- 2020-11-12 Wyatt Best: Added search by TIN/SSN (@GovID) instead of just PEOPLE_CODE_ID (@PCID). @@ -29,14 +29,28 @@ AS BEGIN SET NOCOUNT ON; - --Switch for whether to use ACADEMICCALENDAR (default) or FINAIDMAPPING as the POE source - DECLARE @FinAidYear INT + DECLARE @student_token INT + ,@FinAidYear INT DECLARE @POEs TABLE ( POE INT ,ACADEMIC_SESSION NVARCHAR(10) ,award_year INT ) + --Find student + SELECT @student_token = student_token + FROM [POWERFAIDS].[PFaids].[dbo].[student] s + WHERE s.alternate_id = @PCID + OR s.student_ssn = @GovID + + --If student not found, quit immediately + IF @student_token IS NULL + BEGIN + SELECT NULL + + RETURN + END + IF @UseFINAIDMAPPING = 1 BEGIN --Get POEs from FINAIDMAPPING @@ -107,6 +121,5 @@ BEGIN ON d.doc_token = srd.doc_token INNER JOIN [POWERFAIDS].[PFaids].[dbo].[doc_status_code] dsc ON dsc.doc_required_status_code = srd.doc_status - WHERE s.alternate_id = @PCID - OR s.student_ssn = @GovID + WHERE s.student_token = @student_token END diff --git a/ps_powercampus.py b/ps_powercampus.py index e628e35..4e4bf26 100644 --- a/ps_powercampus.py +++ b/ps_powercampus.py @@ -869,12 +869,13 @@ def pf_get_fachecklist(pcid, govid, appid, year, term, session, use_finaidmappin ) columns = [column[0] for column in CURSOR.description] - for row in CURSOR.fetchall(): - checklist.append(dict(zip(columns, row))) + if 'Code' in columns: + for row in CURSOR.fetchall(): + checklist.append(dict(zip(columns, row))) - # Pass through the Slate Application ID - for doc in checklist: - doc["AppID"] = appid + # Pass through the Slate Application ID + for doc in checklist: + doc["AppID"] = appid return checklist @@ -895,7 +896,7 @@ def pf_get_awards(pcid, govid, year, term, session, use_finaidmapping): ) row = CURSOR.fetchone() - if row is not None: + if row[0] is not None: awards = row.XML tracking_status = row.tracking_status From c66c482f41c7335c778b16ae1469e88bd91a6370 Mon Sep 17 00:00:00 2001 From: WyattBest <32881391+WyattBest@users.noreply.github.com> Date: Thu, 30 May 2024 10:19:36 -0600 Subject: [PATCH 30/30] Update documentation --- PowerSlate Integration Fields.docx | Bin 36590 -> 36394 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/PowerSlate Integration Fields.docx b/PowerSlate Integration Fields.docx index 7b9e273e05494b91bae40b517ecc1ca3a1198beb..52fd48fb1b9bcbba8c409a2637b940820da00453 100644 GIT binary patch delta 23704 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 RFiOaL^lQ|h7il~I@IPgF_P_uD delta 23967 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
-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