From e575687a270d250b2609668c4403127194611b9f Mon Sep 17 00:00:00 2001 From: meows4h <124093142+meows4h@users.noreply.github.com> Date: Tue, 7 Jul 2026 18:06:12 +0000 Subject: [PATCH 01/17] style: automated lint and format fixes --- helpers/analytics.py | 21 +++++++++++---------- helpers/bookstore.py | 14 +++++++------- helpers/grabber.py | 2 +- helpers/gui.py | 1 + helpers/modes.py | 17 +++++++++++------ main.py | 2 +- 6 files changed, 32 insertions(+), 25 deletions(-) diff --git a/helpers/analytics.py b/helpers/analytics.py index e33652d..b68303c 100644 --- a/helpers/analytics.py +++ b/helpers/analytics.py @@ -88,6 +88,7 @@ # this should correct any problems and now it should be feasible to take out the representation # access right sql queries, which will make it easier to access the correct information + def get_columns(key="ebook"): """Gets the columns being used to construct the SQL query.""" sql_columns = [] @@ -121,7 +122,7 @@ def get_columns(key="ebook"): { "Key": "Representation Access Rights", "Cols": ["Access Right Name", "Access Right Desc"], - } + }, ] overall_section = "Digital Inventory" @@ -158,8 +159,7 @@ def get_columns(key="ebook"): elif key == "e-inventory": sql_columns = [ - {"Key": "Bibliographic Details", - "Cols": ["MMS Id", "Title"]}, + {"Key": "Bibliographic Details", "Cols": ["MMS Id", "Title"]}, { "Key": "Electronic Collection", "Cols": ["Electronic Collection Public Name"], @@ -170,14 +170,13 @@ def get_columns(key="ebook"): elif key == "public": sql_columns = [ - {"Key": "Bibliographic Details", - "Cols": ["MMS Id", "Title"]}, + {"Key": "Bibliographic Details", "Cols": ["MMS Id", "Title"]}, { "Key": "Portfolio", "Cols": ["Portfolio Public Note"], }, ] - overall_section = "E-Inventory" + overall_section = "E-Inventory" return overall_section, sql_columns @@ -422,7 +421,7 @@ def process_new_isbn(driver, title, state): if check_list[0] is False: print(f"{title} not found on OneSearch.") return None - + sql_section, sql_columns = get_columns(state) sql_statement = setup_sql(sql_section, sql_columns) @@ -561,7 +560,7 @@ def process_analytics(analytics_driver, isbn): if location != "" and data[mms_id]["Location"] == "": data[mms_id]["Location"] = location - + except Exception as err: print("Print book processing error.") print(err) @@ -589,7 +588,9 @@ def process_analytics(analytics_driver, isbn): # TODO # double check this implementation access_platform = "" - e_collection_list = pull_data(analytics_driver, "MMS Id", mms_id, "e-inventory") + e_collection_list = pull_data( + analytics_driver, "MMS Id", mms_id, "e-inventory" + ) for collection in e_collection_list: if collection["Electronic Collection Public Name"]: access_platform = collection["Electronic Collection Public Name"] @@ -602,7 +603,7 @@ def process_analytics(analytics_driver, isbn): access_name = access["Access Right Name"] if access["Access Right Desc"]: access_desc = access["Access Right Desc"] - + cdl = False if access_name: if "CDL" in access_name: diff --git a/helpers/bookstore.py b/helpers/bookstore.py index 2222054..c1279b9 100644 --- a/helpers/bookstore.py +++ b/helpers/bookstore.py @@ -42,15 +42,15 @@ def get_link(link, term, subject, code, section): # probably leave this alone for now then...? def get_prices(driver, table): """This would in theory grab the lowest price of a given row of bookstore information. - For now, this will go unused as the direct access to the price comparison is - rate limited, meaning it would not be worth doing due to time.""" + For now, this will go unused as the direct access to the price comparison is + rate limited, meaning it would not be worth doing due to time.""" seperator = "%2C" id_template = "TERM__SUBJECT__CODE__SECTION" store_link = "https://osubeaverstore.verbacompare.com/comparison?id=" link_list = [] - + master_link = f"{store_link}" table_size = len(table) course_letter = "A" @@ -71,7 +71,7 @@ def get_prices(driver, table): link_list.append(master_link[:-3]) master_link = f"{store_link}" course_letter = letter - + master_link += get_link(id_template, term, subj_code, subj_num, section) if idx + 1 < table_size: master_link += seperator @@ -120,14 +120,14 @@ def get_prices(driver, table): # https://osubeaverstore.verbacompare.com/comparison?id=2026-Summer__AEC__411__400 html = main_div.get_attribute("innerHTML") lxml = bs4.BeautifulSoup(html, "lxml") - price_list = lxml.find_all(class_='price') - minimum = float('inf') + price_list = lxml.find_all(class_="price") + minimum = float("inf") for idx, price in enumerate(price_list): price_list[idx] = price.strip().replace("$", "") price_list[idx] = round(float(price_list[idx]), 2) if price_list[idx] < minimum: minimum = price_list[idx] - + price_table[row[8]] = f"${minimum}" except Exception as err: diff --git a/helpers/grabber.py b/helpers/grabber.py index 20ae17d..9a60006 100644 --- a/helpers/grabber.py +++ b/helpers/grabber.py @@ -182,7 +182,7 @@ def setup_grabber(): def grabber_gui(textbook_table, email_dict): """This functions similar to the CLI component of the Outlook Email Grabber, - but is more self contained to keep it all in one spot with threads.""" + but is more self contained to keep it all in one spot with threads.""" check_state = False email_store = "" driver = webdriver.Chrome() diff --git a/helpers/gui.py b/helpers/gui.py index 4dc5289..1563231 100644 --- a/helpers/gui.py +++ b/helpers/gui.py @@ -14,6 +14,7 @@ # reset screen in each case, store variables on the class scale class GUI: """This is the primary class in which the main GUI is ran through.""" + def __init__(self, root): self.cfg_path = "config.ini" self.cfg = configparser.ConfigParser() diff --git a/helpers/modes.py b/helpers/modes.py index 43a4be3..be9bb20 100644 --- a/helpers/modes.py +++ b/helpers/modes.py @@ -19,6 +19,7 @@ # refactor this file (or the other parts of the project) to optimize # project size & functionality + def start_mode(flag): """Takes an integer input to start an alternative script method.""" if flag == 2: @@ -208,9 +209,9 @@ def enrollment_update(sheet_name, file_name=""): section_num = 1 skip_rule = 0 - # temp storage per cell sets + # temp storage per cell sets course_code = "" - + # row storage campus_list = [] row_enrollment = 0 @@ -220,7 +221,11 @@ def enrollment_update(sheet_name, file_name=""): continue col = cell.column_letter col_header = worksheet[f"{col}1"].value - section_header = header_format[1].replace("$", f"{course_num}").replace("&", f"{section_num}") + section_header = ( + header_format[1] + .replace("$", f"{course_num}") + .replace("&", f"{section_num}") + ) course_header = header_format[0].replace("$", f"{course_num}") next_header = header_format[0].replace("$", f"{course_num+1}") @@ -244,13 +249,13 @@ def enrollment_update(sheet_name, file_name=""): skip_rule = 3 - if row[idx+3].value != enroll_num: + if row[idx + 3].value != enroll_num: total_changed += 1 - row[idx+3].value = enroll_num + row[idx + 3].value = enroll_num row_enrollment += enroll_num campus = get_campus(campus_letter) if campus not in campus_list: - campus_list.append(campus) + campus_list.append(campus) elif col_header == next_header: course_num += 1 diff --git a/main.py b/main.py index 58cea0d..b95093c 100644 --- a/main.py +++ b/main.py @@ -28,7 +28,7 @@ # to be recorded, but lots of the options are kinda left as is # - resolving leftover TODO statements in the various scripts # not all of these are serious needs, requirements, or fixes -# as much as they are probably spots i have left notes of +# as much as they are probably spots i have left notes of # what COULD be done or what hasn't been fully tested config = configparser.ConfigParser() From 1558aed5388d1d9935b89ad8f2e423d6c5513186 Mon Sep 17 00:00:00 2001 From: meows4h Date: Tue, 7 Jul 2026 11:55:15 -0700 Subject: [PATCH 02/17] starting on clean-up and documentation wrap-up --- documentation.md | 16 ++++++ helpers/analytics.py | 113 ++++++------------------------------------- 2 files changed, 31 insertions(+), 98 deletions(-) diff --git a/documentation.md b/documentation.md index 5da6412..4d0d72b 100644 --- a/documentation.md +++ b/documentation.md @@ -6,6 +6,22 @@ There are two primary methods that this script runs through: The CLI version and This file serves as the starting point for the whole script. If the GUI mode is activated, it will start the GUI from `gui.py`, otherwise, it will take an input from the user and pass it over to `sheetmaker.py` if you're making a new sheet or to `modes.py` if you're using the other features. ## analytics.py Anything related to handling data from Alma gets handled through this script. Between setting up the browser, parsing the HTML, and handling inputs for things such as the SQL. +### get_columns +### get_col_len +### get_table +### pull_one_search +### pull_data +### pull_analytics +### input_sql +### check_element +### click_element +### process_new_isbn +### process_analytics +### setup_analytics +### setup_sql +### export_analytics +### import_analytics + ## bookstore.py Most of this code is pulled from the original bookstore data puller that was made to automate getting bookstore data. It will have the user pass a CAPTCHA and then have them select data to pull from to get the specific term of textbook data they are interested in. ## classes.py diff --git a/helpers/analytics.py b/helpers/analytics.py index b68303c..34f9b92 100644 --- a/helpers/analytics.py +++ b/helpers/analytics.py @@ -12,82 +12,6 @@ import ast import threading -""" -SELECT "Bibliographic Details"."Author" saw_0, -"Bibliographic Details"."Earliest Possible Publication Year" saw_1, -"Bibliographic Details"."Title" saw_2, -"Bibliographic Details"."Publisher" saw_3, -"Bibliographic Details"."MMS Id" saw_4, -"Bibliographic Details"."ISBN" saw_5, -"Bibliographic Details"."Edition" saw_6, -"Bibliographic Details"."Material Type" saw_7, -"Bibliographic Details"."Resource Type" saw_8, -"Edition Simplified"."Edition Simplified (Num)" saw_9, -FROM "Digital Inventory" -WHERE -"Bibliographic Details"."ISBN" LIKE '%9781478651123%' -""" - -""" -SELECT "Bibliographic Details"."Author" saw_0, -"Bibliographic Details"."Earliest Possible Publication Year" saw_1, -"Bibliographic Details"."Title" saw_2, -"Bibliographic Details"."Publisher" saw_3, -"Bibliographic Details"."MMS Id" saw_4, -"Bibliographic Details"."ISBN" saw_5, -"Bibliographic Details"."Edition" saw_6, -"Bibliographic Details"."Material Type" saw_7, -"Bibliographic Details"."Resource Type" saw_8, -"Representation Access Rights"."Access Right Name" saw_9, -"Representation Access Rights"."Access Right Desc" saw_10, -"Edition Simplified"."Edition Simplified (Num)" saw_11, -FROM "Digital Inventory" WHERE "Bibliographic Details"."ISBN" LIKE '%' -""" - -""" -SELECT "Bibliographic Details"."Author" saw_0, -"Bibliographic Details"."Earliest Possible Publication Year" saw_1, -"Bibliographic Details"."Title" saw_2, -"Bibliographic Details"."Publisher" saw_3, -"Bibliographic Details"."MMS Id" saw_4, -"Bibliographic Details"."ISBN" saw_5, -"Bibliographic Details"."Edition" saw_6, -"Bibliographic Details"."Material Type" saw_7, -"Bibliographic Details"."Resource Type" saw_8, -"Representation Access Rights"."Access Right Name" saw_9, -"Representation Access Rights"."Access Right Desc" saw_10, -"Edition Simplified"."Edition Simplified (Num)" saw_11, -FROM "Digital Inventory" WHERE UPPER("Bibliographic Details"."TITLE") LIKE UPPER('%') -""" - -""" -SELECT - "Bibliographic Details"."Title" saw_0, - "Vendor Interface"."Vendor Name" saw_1, - "Vendor Interface"."Interface Name" saw_2, - "-- Bibliographic Details"."MMS Id" saw_3, - "Vendor Interface"."Available" saw_4 - FROM "E-Inventory" - WHERE -UPPER("Bibliographic Details"."TITLE") LIKE UPPER('%CLIMATE CASINO%') -""" - -# TODO -# fixing missing isbn or mismatched values rom the bookstore -# if nothing returns, looking up by the book title instead, comparing -# author, publisher and edition number -# use new isbn and information to backpush updating the book information - -# can search by isbn but needs to be without restrictive filters -# then needs to add on the license filters, log where necessary -# if nothing is found at ISBN, then needs to pivot to searching for book -# still store data at old ISBN when exporting to CSV, but inlude alternative -# ISBN information..? and then reinclude additional ISBN if not in original sheet? - -# new solution : E-Inventory ? -# this should correct any problems and now it should be feasible to take out the representation -# access right sql queries, which will make it easier to access the correct information - def get_columns(key="ebook"): """Gets the columns being used to construct the SQL query.""" @@ -113,7 +37,7 @@ def get_columns(key="ebook"): ] overall_section = "Digital Inventory" - if key == "access": + elif key == "access": sql_columns = [ { "Key": "Bibliographic Details", @@ -298,13 +222,6 @@ def pull_data(driver, bib_section, bib_value, sql_key): return return_list -def pull_ebook_access(driver, mms_id): - """""" - section, sql_cols = get_columns(key="access") - sql = setup_sql(section, sql_cols, "MMS Id") - sql = sql.replace("%", f"%{mms_id}%") - - def pull_analytics(driver, isbn_list, state): """Pulls the analytics information for the given ISBN list from the table.""" return_list = [] @@ -401,20 +318,6 @@ def click_element(driver, tag, selector, detail): print(err) -def setup_sql(sql_section, sql_list, bib_section="ISBN"): - """Helper function to turn the SQL categories and lists into a full single query.""" - sql = "SELECT" - idx = 0 - for dict in sql_list: - section = dict["Key"] - for col in dict["Cols"]: - sql += f' "{section}"."{col}" saw_{idx},' - idx += 1 - sql += f' FROM "{sql_section}" WHERE "Bibliographic Details"."{bib_section}"' - sql += """ LIKE '%' """ - return sql - - def process_new_isbn(driver, title, state): """Helper function to find what ISBN we do own for a particular book.""" check_list = pull_one_search(driver, [title]) @@ -732,6 +635,20 @@ def context_window(text_list): return driver +def setup_sql(sql_section, sql_list, bib_section="ISBN"): + """Helper function to turn the SQL categories and lists into a full single query.""" + sql = "SELECT" + idx = 0 + for dict in sql_list: + section = dict["Key"] + for col in dict["Cols"]: + sql += f' "{section}"."{col}" saw_{idx},' + idx += 1 + sql += f' FROM "{sql_section}" WHERE "Bibliographic Details"."{bib_section}"' + sql += """ LIKE '%' """ + return sql + + def export_analytics(path, info): """Exports the analytics data into a CSV.""" with open(path, "w", newline="") as csvfile: From a1e62562ff12d2cbe9bf648f169408345afc445b Mon Sep 17 00:00:00 2001 From: meows4h Date: Tue, 7 Jul 2026 14:56:59 -0700 Subject: [PATCH 03/17] further documentation --- documentation.md | 118 ++++++++++++++++++++++++++++++++++++++++++- helpers/analytics.py | 3 +- 2 files changed, 119 insertions(+), 2 deletions(-) diff --git a/documentation.md b/documentation.md index 4d0d72b..cf1a881 100644 --- a/documentation.md +++ b/documentation.md @@ -7,10 +7,15 @@ This file serves as the starting point for the whole script. If the GUI mode is ## analytics.py Anything related to handling data from Alma gets handled through this script. Between setting up the browser, parsing the HTML, and handling inputs for things such as the SQL. ### get_columns +This function is where all the information related to generating SQL templates is stored. I found it much easier to track things in lists of dictionaries, with each dictionary containing a "Key" value for the section of the database it is searching, as well as "Cols" or columns which stores a list of the values that are listed under the associated "Key" value. Giving this function different values while calling returns different templates for different purposes. Due to how some listings do not appear while some columns are present, some of these are broken up into really small pieces. ### get_col_len +Returns the total number of columns within the SQL template. This helps with tracking how many columns need to be searched across while reading the tables within Alma analytics. ### get_table +Handles reading and parsing out the table information from the Alma screen. ### pull_one_search +Takes a list of MMS IDs and returns a list of the same size of `True` or `False`. False indicates that NO OneSearch listings appeared under the MMS ID search, while True indicates that something did return. This aids in finding out which MMS IDs and listings are still in circulation. ### pull_data +Given some data, this function automates creating a SQL statement and pulling data from the database. ### pull_analytics ### input_sql ### check_element @@ -24,26 +29,136 @@ Anything related to handling data from Alma gets handled through this script. Be ## bookstore.py Most of this code is pulled from the original bookstore data puller that was made to automate getting bookstore data. It will have the user pass a CAPTCHA and then have them select data to pull from to get the specific term of textbook data they are interested in. +### str_clean +### get_page_soup +### get_link +### get_prices +### pull_textbook_data +### pull_info + ## classes.py Stores the frameworks and methods for interacting with storing and processing book related data. +### Book (Class) +- Contstructor +- add_course +- add_section +- add_isbn +- add_enroll +- add_required +### get_max_index +### get_max_courses +### get_max_sections_list +### process_book +### process_courses +### process_sections +### process_isbns +### import_data + ## emails.py Handles compiling and creating Excel sheets for PowerAutomate emails. +### Book (Class) +- Constructor +### Instructor (Class) +- Constructor +- add_book +### update_excel +### write_to_excel +### create_email_excel + ## enrollment.py Handles pulling the relevant data from the enrollment csv file in order to get the maximum enrollment values. +### get_enrollment_data + ## grabber.py Deals with setting up and handling pulling data from the Outlook browser to get emails for individual professors. +### process_name +### process_suggestion +### get_email +### setup_grabber +### grabber_gui +- set_email_store +- run_process_suggestion +- run_check_ui +- run_check_web +- run_suggestion_ui +- run_get_email +### email_importer +### email_exporter + ## gui.py This hosts all of the primary interactive GUI for the user. +### GUI (Class) +- Constructor +- reset_main +- print_main +- build_main +- build_emails +- build_headers +- build_advanced +- build_sheet_outlook +- build_sheet_alma +- build_sheet_final +- build_import_csv +- start_analytics_csv +- start_bookstore_csv +- start_grabber_csv +- start_mode +- write_cfg +- write_headers +- write_emails +### start_app + ## helpergui.py This is a much slimmer and simpler version of the main GUI class, to be something much more modular and additive. +### AddedGUI (Class) +- Constructor +- reset +- add_label +- add_button +### make_window + ## modes.py Aids in handling the various functions of the script, modularizing individual pieces into useful functions. +### start_mode +### csv_mode +### email_mode +### update_mode +### emails_csv +### analytics_csv +### enrollment_update +### analytics_update +### emails_update +### get_import + ## output.py -This script has a single function: outputting an Excel sheet! +This script has a single function: outputting an Excel sheet! This is where formatting and such gets handled (i.e. color, column sizing, etc.) +### write_to_sheet + ## sheetmaker.py Performs all of the functions related to creating new sheets from scratch. +### make_excel_sheet + ## utilties.py This script hosts helpful functions that might be purposeful in multiple places around the various helper and main scripts. On top of this, it also helps to host hard coded data that is not necessary to keep in a configuration file (such as header values!). The organization is to help cut down on lines of code in other places, as well as keep information consistency so updating one variable does update it in all relevant places when needed. +### get_int +### get_clean +### get_state +### get_directory +### get_filepath +### get_letter +### get_edition_string +### get_format_headers +### get_replace_header +### get_split_course +### get_input +### get_enabled +### get_sheet_headers +### get_config_headers +### get_string_cleaners +### get_row_info +### get_campus +### set_col_format + ## CSV Storage In order to store all the data in a way that is accessible, quick, and aids in subsequent run times, all pulled data is compiled into `.csv` files, each with their own format. This makes it so we don't have to re-run the bookstore scraper, email grabber, or analytics scraper again every single time we wish to do something. ### analytics.csv @@ -63,6 +178,7 @@ Data is formatted as following: Instructor Name, Email ### enrollment.csv This is just a CORE report exported as a csv for all courses in the desired term. This must be done outside of the script itself. + ## Config Files ### config.ini Primary settings for the script, though some of these are redundant / only used by one half of the script. diff --git a/helpers/analytics.py b/helpers/analytics.py index 34f9b92..b0eefa4 100644 --- a/helpers/analytics.py +++ b/helpers/analytics.py @@ -179,7 +179,8 @@ def pull_one_search(driver, mms_list): def pull_data(driver, bib_section, bib_value, sql_key): - """""" + """Given a section, value, and specific SQL template, grabs the + relevant data that is desired.""" section, sql_cols = get_columns(key=f"{sql_key}") sql = setup_sql(section, sql_cols, f"{bib_section}") sql = sql.replace("%", f"%{bib_value}%") From 792a4f225797a2eb75945c4fbc50ab0ee199cf4d Mon Sep 17 00:00:00 2001 From: meows4h Date: Wed, 8 Jul 2026 11:01:04 -0700 Subject: [PATCH 04/17] documentation and clean up --- documentation.md | 47 ++++++++++++++++++++++++++++++++--- helpers/analytics.py | 58 -------------------------------------------- helpers/bookstore.py | 2 +- helpers/classes.py | 4 +-- 4 files changed, 47 insertions(+), 64 deletions(-) diff --git a/documentation.md b/documentation.md index cf1a881..12985b7 100644 --- a/documentation.md +++ b/documentation.md @@ -2,6 +2,7 @@ The goal of this file is to provide a higher level overview of how this script works to help identify issues, improvements, as well as future maintainability. ## At a Glance There are two primary methods that this script runs through: The CLI version and the GUI version. The CLI version is more or less function complete in how I intend for it to function and operate, though it still has some rough edges, especially in a user interaction factor. The GUI version is what will continue to receive updates and more improvements over time. These two versions can be toggled between in the `config.ini` file. +--- ## main.py This file serves as the starting point for the whole script. If the GUI mode is activated, it will start the GUI from `gui.py`, otherwise, it will take an input from the user and pass it over to `sheetmaker.py` if you're making a new sheet or to `modes.py` if you're using the other features. ## analytics.py @@ -16,58 +17,94 @@ Handles reading and parsing out the table information from the Alma screen. Takes a list of MMS IDs and returns a list of the same size of `True` or `False`. False indicates that NO OneSearch listings appeared under the MMS ID search, while True indicates that something did return. This aids in finding out which MMS IDs and listings are still in circulation. ### pull_data Given some data, this function automates creating a SQL statement and pulling data from the database. -### pull_analytics ### input_sql +Automates opening the SQL input window in Alma and inputting the given text. ### check_element +Aids in automating checking whether a given HTML element is present on the page, to know when it is okay to move to the next step. ### click_element +A template for telling the web driver what to click given some details about the specific HTML element. ### process_new_isbn +UNFINISHED and UNUSED; This function would ideally be used to find what ISBNs we do own for particular books from the bookstore, where a different ISBN is provided. ### process_analytics +Function that interacts with the other scripts to aid in pulling the analytics data given the web driver. Contains all the logic for how to process each set of information. ### setup_analytics +Creates and sets up the web browser to be prepared for automation tasks. ### setup_sql +Given a set of keys and columns to read, creates a complete SQL statement to use. ### export_analytics +Exports the data to a given csv file path. ### import_analytics +Imports the data from the given csv file path. ## bookstore.py Most of this code is pulled from the original bookstore data puller that was made to automate getting bookstore data. It will have the user pass a CAPTCHA and then have them select data to pull from to get the specific term of textbook data they are interested in. ### str_clean +Removes certain special characters and whitespace from strings, aids in standardizing book titles. ### get_page_soup +Uses the BS4 library to grab a given URL, take the HTML contents and turn it into a workable form of data. ### get_link +Helper function to reduce number of replace statements later, simply helps create a link to use for price comparisons. ### get_prices +UNFINISHED and UNUSED; If there were to be a cost analysis function, this is what it would be, but the volume of books makes it a little too difficult to query this webserver as much as we would like to so it remains commented out. ### pull_textbook_data +The master information pulling function for the bookstore data. ### pull_info +Imports data from the bookstore csv file, mainly used by other parts of the program. ## classes.py Stores the frameworks and methods for interacting with storing and processing book related data. ### Book (Class) - Contstructor +Creating an object requires a dictionary of a couple different values: Related course, section, instructor name and email, enrollment information, the ISBN, book title, author, edition, publisher, requirements state, requisition date, the bookstore comment, and the Alma Analytics data for the given book. The constructor will parse out the information to make it more easily accessible for processing later. - add_course +When another entry for a book is found that already exists as an object, the new course information gets added via this function. - add_section +Similar to the course entry, but instead adding a section to an existing course within an existing book. - add_isbn +Adding ISBN values for different variants of the same book. - add_enroll +Putting in additional enrollment information into the book to track all the campuses and possible enrollment values. - add_required +If at any point a bookstore listing has the book as "Required", it sets the book to that status using this function. ### get_max_index +Finds the index of the course with the most sections within it in order to aid in reducing column count, as well as ensuring the largest courses are always first (leftmost). ### get_max_courses +Gets the total number of course sections to have (the number of courses the book with the most courses has). ### get_max_sections_list +Finds the number of sections needed for each course section. As an example, if we have two books, one book has 3 course with 10, 5, and 3 sections respectively ([10, 5, 3]), the other has 2 courses, one with 7 sections, the other with 6 sections ([7, 6]), then this function finds we need Course 1 to have 10 section slots, Course 2 to have 6, and Course 3 to have 3. ### process_book +Takes the preset information about a book and checks against all books so far to ensure that it does not already exist, otherwise it adds it to the master list. ### process_courses +Uses the prior get_max_courses function in order to create the headers and add them to the dataframe. ### process_sections +Uses the prior get_max_sections_list function in order to create the individual section headers and adds them to the dataframe. ### process_isbns +This function both finds the max number of ISBN columns required and puts the necessary columns into the dataframe. ### import_data +Imports all the data from the master book object list and imports it into the dataframe once it is ready to go. ## emails.py Handles compiling and creating Excel sheets for PowerAutomate emails. ### Book (Class) +This class is a very tiny version of the other class that just takes in some basic information for the purposes of emails. - Constructor +Takes the title, author, edition, year published, and access information. ### Instructor (Class) - Constructor +Takes in the instructor name, email, as well as course, section, and book information per section. - add_book +Adds books to a given course section for each professor. ### update_excel +Updates the main sheet with marking off what emails have been successfully created. ### write_to_excel +Writes the email data to an Excel sheet to be used with PowerAutomate. ### create_email_excel +Processes all the Instructor data to create the table to be used to write it into an Excel sheet. ## enrollment.py Handles pulling the relevant data from the enrollment csv file in order to get the maximum enrollment values. ### get_enrollment_data +Processes the format of the given CORE report enrollment file, returning campus and enrollment information. ## grabber.py Deals with setting up and handling pulling data from the Outlook browser to get emails for individual professors. @@ -141,8 +178,11 @@ Performs all of the functions related to creating new sheets from scratch. ## utilties.py This script hosts helpful functions that might be purposeful in multiple places around the various helper and main scripts. On top of this, it also helps to host hard coded data that is not necessary to keep in a configuration file (such as header values!). The organization is to help cut down on lines of code in other places, as well as keep information consistency so updating one variable does update it in all relevant places when needed. ### get_int +Takes a value, gets an integer out of it if it can, otherwise returns None. ### get_clean +Removes preset phrases and terms from the names of books and authors to reduce the number of duplicate listings. ### get_state +Takes strings of "True" or "False" and converts them to bool values. ### get_directory ### get_filepath ### get_letter @@ -158,7 +198,8 @@ This script hosts helpful functions that might be purposeful in multiple places ### get_row_info ### get_campus ### set_col_format - +A +--- ## CSV Storage In order to store all the data in a way that is accessible, quick, and aids in subsequent run times, all pulled data is compiled into `.csv` files, each with their own format. This makes it so we don't have to re-run the bookstore scraper, email grabber, or analytics scraper again every single time we wish to do something. ### analytics.csv @@ -178,7 +219,7 @@ Data is formatted as following: Instructor Name, Email ### enrollment.csv This is just a CORE report exported as a csv for all courses in the desired term. This must be done outside of the script itself. - +--- ## Config Files ### config.ini Primary settings for the script, though some of these are redundant / only used by one half of the script. diff --git a/helpers/analytics.py b/helpers/analytics.py index b0eefa4..37bccb2 100644 --- a/helpers/analytics.py +++ b/helpers/analytics.py @@ -223,53 +223,6 @@ def pull_data(driver, bib_section, bib_value, sql_key): return return_list -def pull_analytics(driver, isbn_list, state): - """Pulls the analytics information for the given ISBN list from the table.""" - return_list = [] - sql_section, sql_columns = get_columns(state) - sql_statement = setup_sql(sql_section, sql_columns) - - for isbn in isbn_list: - - sql_text = sql_statement.replace("%", f"%{isbn}%") - input_sql(driver, sql_text) - tr_list = get_table(driver) - - if tr_list == []: - return return_list - - cutoff = 3 + get_col_len(sql_columns) - tr_list = tr_list[cutoff:] - - # storing entries for empty portions - store_dict = {} - # storing order for indexing - store_list = [] - for dict in sql_columns: - for col in dict["Cols"]: - store_dict[col] = "" - store_list.append(col) - - for tr in tr_list: - td_list = tr.find_all("td") - for td in td_list: - try: - td_id = td["id"] - td_text = td.get_text() - id_list = td_id.split("_") - cat_id = get_int(id_list[5]) - column = store_list[cat_id] - store_dict[column] = td_text - except Exception as err: - print(err) - - return_list.append(store_dict.copy()) - - # print(f"{isbn_list} --> {return_list}") - - return return_list - - def input_sql(driver, text=""): """Helper function that inputs the given text into the SQL input box.""" click_element(driver, "td", "title", "Edit SQL, XML and other technical details") @@ -378,13 +331,6 @@ def process_new_isbn(driver, title, state): return None -# TODO -# TEST WITH 'CLIMATE CASINO' AS THE TITLE -# CANNOT DO DIGITAL INVENTORY WITH THE LICENSE TYPES DUE TO PHYSICAL -# need to work around the digital license problem first ! -# rework this to reprocess the digitial poritions first -# and then ensure it is checking for Book - Electronic first -# or something similar def process_analytics(analytics_driver, isbn): """Fully processes a given ISBN number and pulling all the data from Alma + processing it.""" year = None @@ -412,8 +358,6 @@ def process_analytics(analytics_driver, isbn): if mms_id.strip() == "": continue - # TODO - # double check implementation of this one search checker check_list = pull_one_search(analytics_driver, [mms_id]) if check_list[0] is False: print( @@ -482,8 +426,6 @@ def process_analytics(analytics_driver, isbn): if mms_id.strip() == "": continue - # TODO - # double check implementation of this one search checker check_list = pull_one_search(analytics_driver, [mms_id]) if check_list[0] is False: print(f"Missing from Primo (EBOOK); skipping. {mms_id}") diff --git a/helpers/bookstore.py b/helpers/bookstore.py index c1279b9..efd84c2 100644 --- a/helpers/bookstore.py +++ b/helpers/bookstore.py @@ -294,7 +294,7 @@ def set_term_val(value): if row_skip == 0: # if at some point it would be desirable to add in book pricing # this function doesn't work as is, but i stopped progress on it - # seeing has the compare site has rate limiting, which is fair + # seeing as the compare site has rate limiting, which is fair # otherwise, it would be best to simply link to the compare site # in the sheet if anything i think # row_text.append(STORE LINK HERE) diff --git a/helpers/classes.py b/helpers/classes.py index 4376bc5..01526c7 100644 --- a/helpers/classes.py +++ b/helpers/classes.py @@ -6,7 +6,7 @@ class Book: """Object that stores relevant book information to be used to format and configure a dataframe. Takes in a dictionary to construct the base - object. See main.py for the book_info dictionary structure.""" + object. See sheetmaker.py for the book_info dictionary structure.""" def __init__(self, info): # bookstore information @@ -120,7 +120,7 @@ def get_max_sections_list(book_list, course_amt): def process_book(book_list, book_dict): - """Processed incoming book information and crates / adds book + """Processed incoming book information and creates / adds book information to the main list of book objects.""" book = None for search_book in book_list: From c6635f8f8082fb30d1ff543b82bdd6706394aaed Mon Sep 17 00:00:00 2001 From: meows4h Date: Wed, 8 Jul 2026 14:48:55 -0700 Subject: [PATCH 05/17] started on unit test implementation --- .github/test.yml | 27 +++++ helpers/analytics.py | 12 +-- helpers/bookstore.py | 19 +++- helpers/test.py | 230 +++++++++++++++++++++++++++++++++++++++++++ main.py | 2 + runtest.py | 9 ++ 6 files changed, 288 insertions(+), 11 deletions(-) create mode 100644 .github/test.yml create mode 100644 helpers/test.py create mode 100644 runtest.py diff --git a/.github/test.yml b/.github/test.yml new file mode 100644 index 0000000..29b4752 --- /dev/null +++ b/.github/test.yml @@ -0,0 +1,27 @@ +name: Run Python Unit Tests + +on: + push: + branches: [ main, dev ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.14' + cache: 'pip' + + - name: Install dependencies + run: pip install -r helpers/requirements.txt + + - name: Run Unit Tests + run: python runtest.py diff --git a/helpers/analytics.py b/helpers/analytics.py index 37bccb2..8b3ba3b 100644 --- a/helpers/analytics.py +++ b/helpers/analytics.py @@ -133,12 +133,7 @@ def get_table(driver): # TODO -# this needs SEVERE checking -# first thought is the search link could possibly be incorrect? -# maybe searching via MMS ID is improper? -# patterns kristin told me: -# NONE of the Ebooks were caught, ONLY CDLs, but not ALL CDLs -# Print books were ONLY from our MAIN collection +# may need further testing def pull_one_search(driver, mms_list): """Opens a OneSearch tab to double check that the MMS ID exists within Primo. Takes a list and returns a list, though only used with single IDs right now.""" @@ -479,6 +474,11 @@ def process_analytics(analytics_driver, isbn): for section in process_list: if "user".upper() in section: users = get_int(section.split(" ")[0]) + + # adding a failstate to ensure these values are marked as unknown + # and can be easily removed via find & replace + if users == -1: + users = "?" if mms_id not in data: data[mms_id] = { diff --git a/helpers/bookstore.py b/helpers/bookstore.py index efd84c2..ab67f52 100644 --- a/helpers/bookstore.py +++ b/helpers/bookstore.py @@ -11,12 +11,21 @@ from helpers.helpergui import AddedGUI -def str_clean(str): +def str_clean(value): """Removes special characters and whitespace from a string.""" - new_str = str.replace(" ", "") - new_str = new_str.replace(" ", "") - new_str = new_str.replace("\t", "") - new_str = new_str.replace("\n", "") + original = value + new_str = "" + first = True + while new_str is not original: + if first: + first = False + else: + original = new_str + new_str = original.strip() + new_str = new_str.replace(" ", "") + new_str = new_str.replace(" ", " ") + new_str = new_str.replace("\t", "") + new_str = new_str.replace("\n", "") return new_str diff --git a/helpers/test.py b/helpers/test.py new file mode 100644 index 0000000..a5d426f --- /dev/null +++ b/helpers/test.py @@ -0,0 +1,230 @@ +import unittest +import helpers.analytics as alma +import helpers.bookstore as book +import helpers.classes as classes +import helpers.emails as emails +import helpers.enrollment as enroll +import helpers.grabber as outlook +import helpers.gui as gui +import helpers.helpergui as helpgui +import helpers.modes as modes +import helpers.output as output +import helpers.utilities as util + +# skipping selenium oriented functions as they require logging in via the interface +# this could be done in headless mode w/ more looking over the HTML of the log in +# pages, but focusing on the core logic functions for now + +# similarly, export and import testing implementations may be done later as they +# likely will not be changing soon and are currently functional + +class AnalyticsTest(unittest.TestCase): + """Testing the analytics.py file.""" + def setUp(self): + # error message setup + self.null_msg = "get_columns: Null % SQL Section." + self.empty_msg = "get_columns: Empty % SQL Section." + self.missing_msg = "get_columns: Missing Key or Column values in % SQL." + self.sql_statements = ["SELECT", "FROM", "WHERE", "LIKE"] + + # helper functions + + # checks the SQL columns for proper dictionary setup + def check_columns(self, columns): + check = True + for dict in columns: + if not dict["Key"] or not dict["Cols"]: + check = False + break + return check + + # functionizing the error messages + def get_sql_msg(self, value): + new_null = self.null_msg.replace("%", value) + new_empty = self.empty_msg.replace("%", value) + new_missing = self.missing_msg.replace("%", value) + result = {"Null": new_null, "Empty": new_empty, "Miss": new_missing} + return result + + # testing get_columns + def test_ebook_sql(self): + section, columns = alma.get_columns(key="ebook") + messages = self.get_sql_msg("Ebook") + + self.assertIsNotNone(section, messages["Null"]) + self.assertTrue(section, messages["Empty"]) + + col_check = self.check_columns(columns) + self.assertTrue(col_check, messages["Miss"]) + + def test_access_sql(self): + section, columns = alma.get_columns(key="access") + messages = self.get_sql_msg("Access") + + self.assertIsNotNone(section, messages["Null"]) + self.assertTrue(section, messages["Empty"]) + + col_check = self.check_columns(columns) + self.assertTrue(col_check, messages["Miss"]) + + def test_physical_sql(self): + section, columns = alma.get_columns(key="physical") + messages = self.get_sql_msg("Physical") + + self.assertIsNotNone(section, messages["Null"]) + self.assertTrue(section, messages["Empty"]) + + col_check = self.check_columns(columns) + self.assertTrue(col_check, messages["Miss"]) + + def test_einventory_sql(self): + section, columns = alma.get_columns(key="e-inventory") + messages = self.get_sql_msg("E-Inventory") + + self.assertIsNotNone(section, messages["Null"]) + self.assertTrue(section, messages["Empty"]) + + col_check = self.check_columns(columns) + self.assertTrue(col_check, messages["Miss"]) + + def test_public_sql(self): + section, columns = alma.get_columns(key="public") + messages = self.get_sql_msg("Public") + + self.assertIsNotNone(section, messages["Null"]) + self.assertTrue(section, messages["Empty"]) + + col_check = self.check_columns(columns) + self.assertTrue(col_check, messages["Miss"]) + + # testing get_col_len + def test_col_len(self): + # example from real code + # expects 14 columns + columns = [ + { + "Key": "Bibliographic Details", + "Cols": [ + "Author", + "Title", + "ISBN", + "Resource Type", + "MMS Id", + "Earliest Possible Publication Year", + ], + }, + { + "Key": "Physical Item Details", + "Cols": [ + "Material Type", + "Num of Items (In Repository)", + "Temporary Physical Location in Use", + ], + }, + { + "Key": "Holdings Details", + "Cols": ["Holdings Lifecycle", "Suppressed from Discovery"], + }, + {"Key": "Edition Simplified", "Cols": ["Edition Simplified (Num)"]}, + {"Key": "Location", "Cols": ["Location Name"]}, + {"Key": "Temporary Location", "Cols": ["Temporary Location Name"]}, + ] + num = alma.get_col_len(columns) + expected = 14 + self.assertEqual(num, expected, f"get_col_len: Miscounting column count; expected {expected}, got {num}") + + # selenium functions would go here + + # testing setup_sql + def test_setup_sql(self): + col_keys = ["ebook", "access", "physical", "e-inventory", "public"] + for key_val in col_keys: + section, columns = alma.get_columns(key=key_val) + sql = alma.setup_sql(section, columns) + for command in self.sql_statements: + self.assertTrue(command in sql, f"setup_sql: {key_val} SQL statement missing {command}") + + # export and import functions would go here + + +class BookstoreTest(unittest.TestCase): + """Testing the bookstore.py file.""" + def setUp(self): + self.clean_msg = "str_clean: Value mismatch from expected; % =/= ^" + + def get_clean_msg(self, res, exp): + out_msg = self.clean_msg.replace("%", res) + out_msg = out_msg.replace("^", exp) + return out_msg + + # testing str_clean + def test_clean_nl(self): + new_line = "\nsome text\n\n " + result = book.str_clean(new_line) + expected = "some text" + message = self.get_clean_msg(result, expected) + self.assertEqual(result, expected, message) + + def test_clean_tab(self): + tab_char = "\t\thello \t world" + result = book.str_clean(tab_char) + expected = "hello world" + message = self.get_clean_msg(result, expected) + self.assertEqual(result, expected, message) + + def test_clean_space(self): + space_text = " something a " + result = book.str_clean(space_text) + expected = "something a" + message = self.get_clean_msg(result, expected) + self.assertEqual(result, expected, message) + + # get_page_soup selenium function + + # testing get_link + + # get_prices selenium function + + # pull_textbook_data uses selenium + + # pull_data import function + + +class ClassesTest(unittest.TestCase): + """Testing the classes.py file.""" + + +class EmailsTest(unittest.TestCase): + """Testing the emails.py file.""" + + +class EnrollTest(unittest.TestCase): + """Testing the enrollment.py file.""" + + +class OutlookTest(unittest.TestCase): + """Testing the grabber.py file.""" + + +class GUITest(unittest.TestCase): + """Testing the gui.py file.""" + + +class HelperGUITest(unittest.TestCase): + """Testing the helpergui.py file.""" + + +class ModesTest(unittest.TestCase): + """Testing the modes.py file.""" + + +class OutputTest(unittest.TestCase): + """Testing the output.py file.""" + + +class SheetTest(unittest.TestCase): + """Testing the sheetmaker.py file.""" + + +class UtilTest(unittest.TestCase): + """Testing the utilities.py file.""" diff --git a/main.py b/main.py index b95093c..91a1afd 100644 --- a/main.py +++ b/main.py @@ -30,6 +30,8 @@ # not all of these are serious needs, requirements, or fixes # as much as they are probably spots i have left notes of # what COULD be done or what hasn't been fully tested +# - adding testcases for selenium functions using hidden github values +# could still login using headless functionality and secret key values config = configparser.ConfigParser() config.read("config.ini") diff --git a/runtest.py b/runtest.py new file mode 100644 index 0000000..619d676 --- /dev/null +++ b/runtest.py @@ -0,0 +1,9 @@ +from helpers.test import * +import unittest + +# this file just ensures that test.py is run within the correct scope of +# the helpers folder to allow all other files to properly reference +# their dependencies + +if __name__ == '__main__': + unittest.main() From d442dfc8c7932af6ebf48d5c9103d130ebe07a9c Mon Sep 17 00:00:00 2001 From: meows4h Date: Wed, 8 Jul 2026 14:49:45 -0700 Subject: [PATCH 06/17] placed gh action yml file in wrong location --- .github/{ => workflows}/test.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{ => workflows}/test.yml (100%) diff --git a/.github/test.yml b/.github/workflows/test.yml similarity index 100% rename from .github/test.yml rename to .github/workflows/test.yml From c812c7f6ee678849ac76d4e1a3ad11165f6afa58 Mon Sep 17 00:00:00 2001 From: meows4h Date: Wed, 8 Jul 2026 14:51:50 -0700 Subject: [PATCH 07/17] adding names to the workflow jobs --- .github/workflows/lint.yml | 2 ++ .github/workflows/test.yml | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 69f26d3..4a5bdfb 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,6 +11,7 @@ permissions: jobs: lint: + name: Lint Python Code runs-on: ubuntu-latest steps: @@ -21,6 +22,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: '3.14' + cache: 'pip' - name: Install dependencies run: pip install -r helpers/requirements.txt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 29b4752..609f658 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,6 +8,7 @@ on: jobs: test: + name: Run Unit Tests runs-on: ubuntu-latest steps: From b04332ad39e5f6cfd09bc1a71f81a677536c67aa Mon Sep 17 00:00:00 2001 From: meows4h Date: Thu, 9 Jul 2026 11:29:45 -0700 Subject: [PATCH 08/17] outlining more testing features --- documentation.md | 4 +- helpers/emails.py | 41 +++++++------ helpers/test.py | 89 +++++++++++++++++++++++++++++ helpers/testing/test_analytics.csv | 1 + helpers/testing/test_bookstore.csv | 1 + helpers/testing/test_emails.csv | 1 + helpers/testing/test_enrollment.csv | 5 ++ 7 files changed, 119 insertions(+), 23 deletions(-) create mode 100644 helpers/testing/test_analytics.csv create mode 100644 helpers/testing/test_bookstore.csv create mode 100644 helpers/testing/test_emails.csv create mode 100644 helpers/testing/test_enrollment.csv diff --git a/documentation.md b/documentation.md index 12985b7..874bf8b 100644 --- a/documentation.md +++ b/documentation.md @@ -96,10 +96,10 @@ Takes in the instructor name, email, as well as course, section, and book inform Adds books to a given course section for each professor. ### update_excel Updates the main sheet with marking off what emails have been successfully created. -### write_to_excel -Writes the email data to an Excel sheet to be used with PowerAutomate. ### create_email_excel Processes all the Instructor data to create the table to be used to write it into an Excel sheet. +### write_to_excel +Writes the email data to an Excel sheet to be used with PowerAutomate. ## enrollment.py Handles pulling the relevant data from the enrollment csv file in order to get the maximum enrollment values. diff --git a/helpers/emails.py b/helpers/emails.py index 50e310f..aeef366 100644 --- a/helpers/emails.py +++ b/helpers/emails.py @@ -60,22 +60,12 @@ def update_excel(directory, data, sheet_name): workbook.save(directory) -def write_to_excel(directory, export_data, sheetname): - """Exports given data to given directory with given sheetname.""" - dataframe = pd.DataFrame(data=export_data) - writer = pd.ExcelWriter(directory, engine="xlsxwriter") - dataframe.to_excel( - writer, sheet_name=sheetname, startrow=1, header=False, index=False - ) - worksheet = writer.sheets[sheetname] - max_row, max_col = dataframe.shape - column_settings = [] - for header in dataframe.columns: - column_settings.append({"header": header}) - worksheet.add_table(0, 0, max_row, max_col - 1, {"columns": column_settings}) - worksheet.set_column(0, max_col - 1, 12) - writer.close() +# the goal here should be to load book data into a book object, +# then for each instance of an instructor for that book, +# assign the book reference to them with the course + section +# if instructor already exists, just add the other information +# otherwise, make new instructor # TODO # rewrite this into smaller functions to break it apart a little @@ -409,9 +399,18 @@ def create_email_excel(input_sheet=None, file_name=""): print("Email sheet exported. Main sheet updated.") -# the goal here should be to load book data into a book object, -# then for each instance of an instructor for that book, -# assign the book reference to them with the course + section - -# if instructor already exists, just add the other information -# otherwise, make new instructor +def write_to_excel(directory, export_data, sheetname): + """Exports given data to given directory with given sheetname.""" + dataframe = pd.DataFrame(data=export_data) + writer = pd.ExcelWriter(directory, engine="xlsxwriter") + dataframe.to_excel( + writer, sheet_name=sheetname, startrow=1, header=False, index=False + ) + worksheet = writer.sheets[sheetname] + max_row, max_col = dataframe.shape + column_settings = [] + for header in dataframe.columns: + column_settings.append({"header": header}) + worksheet.add_table(0, 0, max_row, max_col - 1, {"columns": column_settings}) + worksheet.set_column(0, max_col - 1, 12) + writer.close() diff --git a/helpers/test.py b/helpers/test.py index a5d426f..7550f66 100644 --- a/helpers/test.py +++ b/helpers/test.py @@ -192,11 +192,100 @@ def test_clean_space(self): class ClassesTest(unittest.TestCase): """Testing the classes.py file.""" + def setUp(self): + analytics_data = {"Physical MMS Id": { + "Types": ["Physical"], + "Copies": [2], + "Users": [0], + "CDL": [False], + "Link": "https://www.example.com", + "Year": "2024", + "Location": "Valley Library BIB" + }, + "Ebook MMS Id": { + "Types": ["Electronic"], + "Copies": [0], + "Users": [4], + "CDL": [True], + "Link": "https://www.example2.com", + "Platform": "", + "Year": "2020" + }} + book_info = { + "Title": "Book Title", + "Author": "Book Author", + "Edition": "Book Edition", + "Instructor": "Example Name", + "Email": "Example Email", + "Course": "Example Course", + "Section": 1, + "Enroll": [1, "C"], + "ISBN": 1, + "Publisher": "Book Publisher", + "Req": "Optional", + "RequiDate": "Requisition Date", + "Comment": "Example Comment", + "Analytics": analytics_data, + } + self.book = classes.Book(book_info) + + # Book Class testing + + # add_course method + def test_add_course(self): + course_name = "Test Course" + section_num = "999" + instructor = "Test Name" + email = "Test Email" + enroll_data = [99, "D"] + + self.book.add_course(course_name, section_num, instructor, email, enroll_data) + self.assertEqual(len(self.book.courses), 2, "add_course: Number of courses is incorrect.") + self.assertEqual(len(self.book.sections), 2, "add_course: Number of sections is incorrect.") + self.assertEqual(self.book.sec_size[-1], 1, "add_course: Incorrect sec_size array number.") + self.assertEqual(self.book.total_enroll, 100, "add_course: Total enrollment count is incorrect.") + + # add_section method + + # add_isbn method + + # add_enroll method + + # add_required method + + # End Book Class testing + + # get_max_index testing + + # get_max_courses testing + + # get_max_sections_list testing + + # process_book testing + + # process_sections testing + + # process_isbns testing + + # import_data testing class EmailsTest(unittest.TestCase): """Testing the emails.py file.""" + # Book Class testing + + # Instructor Class testing + + # End Instructor Class testing + + # update_excel testing + # this needs a setup to run sheetmaker and import other information? + + # create_email_excel testing + + # write_to_excel testing -- this is an export function and should be fine + class EnrollTest(unittest.TestCase): """Testing the enrollment.py file.""" diff --git a/helpers/testing/test_analytics.csv b/helpers/testing/test_analytics.csv new file mode 100644 index 0000000..c9e55ff --- /dev/null +++ b/helpers/testing/test_analytics.csv @@ -0,0 +1 @@ +978111111, "{'11111': {'Types': ['Physical'], 'Copies': [3], 'Users': [0], 'CDL': [False], 'Link': 'https://www.example1.com', 'Year': '2022', 'Location': 'Valley Reserves'}, '11112': {'Types': ['Electronic'], 'Copies': [0], 'Users': ['unlimited'], 'CDL': [False], 'Link': 'https://www.example2.com', 'Platform': 'EBSCO (Ebooks-Purchased)', 'Year': '2022'}}" \ No newline at end of file diff --git a/helpers/testing/test_bookstore.csv b/helpers/testing/test_bookstore.csv new file mode 100644 index 0000000..712461f --- /dev/null +++ b/helpers/testing/test_bookstore.csv @@ -0,0 +1 @@ +2022-Summer,MTH : Mathematics,101,001,"LastName, FirstName", Intro to Math Textbook,11,Textbook Author,978-1-11-111,Textbook Publisher,Optional,1,Some Comment \ No newline at end of file diff --git a/helpers/testing/test_emails.csv b/helpers/testing/test_emails.csv new file mode 100644 index 0000000..ab44d03 --- /dev/null +++ b/helpers/testing/test_emails.csv @@ -0,0 +1 @@ +"LastName, FirstName", LastName.FirstName@example.edu \ No newline at end of file diff --git a/helpers/testing/test_enrollment.csv b/helpers/testing/test_enrollment.csv new file mode 100644 index 0000000..5d71893 --- /dev/null +++ b/helpers/testing/test_enrollment.csv @@ -0,0 +1,5 @@ +AcademicYear,AcademicPeriod,AS_OF,CourseLevelParameter,CampusParameter +Academic Year 2021-22,Summer 2022,2/22/2022,"100, 200, 300, 400, 500, 600, 700, 800","C- Oregon State - Corvallis, DZ-Ecampus-Distance Ed Internatl, DI-Ecampus-Distance Education-LD, DB-Ecampus-Distance Education-UD, L-LaGrande/EOU, N-Newport/HMSC, B-Oregon State - Cascades, PDX-Oregon State - Portland, H-Portland/OHSU" + +COLLEGE,COLLEGE_DESC,DEPARTMENT_DESC,ACADEMIC_PERIOD,SUBJECT,SUBJECT_DESC,COURSE_IDENTIFICATION,OFFERING_NUMBER,COURSE_REFERENCE_NUMBER,TITLE_SHORT_DESC,CAMPUS,MAX_CREDITS,SCHEDULE_DESC,MAXIMUM_ENROLLMENT,ACTUAL_ENROLLMENT,SEATS_AVAILABLE,SECTION_CROSS_LIST,CROSS_LIST_MAXIMUM_ENROLLMENT,CROSS_LIST_ENROLLMENT,CROSS_LIST_SEATS_AVAILABLE,GENERATED_CREDITS,PRIMARY_INSTRUCTOR,INTERNET_ADDRESS,PERCENTAGE_OF_RESPONSIBILITY,INSTRUCTOR_COUNT,MEETING_TIMES,LOCATION +1,College Desc Example,Department Desc Example,202300 - Summer 2022,MTH,Mathematics,MTH101,001,11111,Intro to Math,C,3,In-Person,100,1,99,,,,,300,"LastName, FirstName",FirstName.LastName@example.edu,100,1,, \ No newline at end of file From bcf04c8be31436902eb22bd88b99cec30b43972b Mon Sep 17 00:00:00 2001 From: meows4h Date: Thu, 9 Jul 2026 14:58:35 -0700 Subject: [PATCH 09/17] testing suite additions --- helpers/enrollment.py | 2 +- helpers/test.py | 83 ++++++++++++++++++++++++++++-- helpers/testing/test_bookstore.csv | 2 +- 3 files changed, 80 insertions(+), 7 deletions(-) diff --git a/helpers/enrollment.py b/helpers/enrollment.py index f8a8a80..b3ead43 100644 --- a/helpers/enrollment.py +++ b/helpers/enrollment.py @@ -26,7 +26,7 @@ def get_enrollment_data(dir, instructor_dict={}): elif section not in enrollment_dict[f"{course}"]: enrollment_dict[f"{course}"][f"{section}"] = [max_enroll, campus] else: - print("I don't think these cases exist; will write the logic") + print("I don't think these cases exist (Multiple enrollments for course and section)") # instructor things instructor = df["PRIMARY_INSTRUCTOR"][idx] diff --git a/helpers/test.py b/helpers/test.py index 7550f66..f167151 100644 --- a/helpers/test.py +++ b/helpers/test.py @@ -1,4 +1,5 @@ import unittest +import os import helpers.analytics as alma import helpers.bookstore as book import helpers.classes as classes @@ -15,9 +16,6 @@ # this could be done in headless mode w/ more looking over the HTML of the log in # pages, but focusing on the core logic functions for now -# similarly, export and import testing implementations may be done later as they -# likely will not be changing soon and are currently functional - class AnalyticsTest(unittest.TestCase): """Testing the analytics.py file.""" def setUp(self): @@ -179,6 +177,13 @@ def test_clean_space(self): message = self.get_clean_msg(result, expected) self.assertEqual(result, expected, message) + def test_clean(self): + text = " \t\n\n something \t t here \t " + result = book.str_clean(text) + expected = "something t here" + message = self.get_clean_msg(result, expected) + self.assertEqual(result, expected, message) + # get_page_soup selenium function # testing get_link @@ -187,7 +192,32 @@ def test_clean_space(self): # pull_textbook_data uses selenium - # pull_data import function + # pull_info import function + def test_pull_info(self): + directory = ["testing", "test_bookstore.csv"] + path = get_directory(directory) + table = book.pull_info(path) + + term = "2022-Summer" + subject = "MTH : Mathematics" + code = "101" + section = "001" + instructor = "LastName, FirstName" + title = "Intro to Math Textbook" + edition = "11" + author = "Textbook Author" + isbn = "978-1-11-111" + publisher = "Textbook Publisher" + req = "Optional" + sku = "1" + comments = "Some Comment" + requisition = "2/2/2022 2:22:22 PM" + + example_row = [term, subject, code, section, instructor, title, edition, author, isbn, publisher, req, sku, comments, requisition] + row = table[0] + + for idx, cell in enumerate(row): + self.assertEqual(cell, example_row[idx], f"pull_info: {cell} doesn't equal {example_row[idx]}") class ClassesTest(unittest.TestCase): @@ -289,7 +319,43 @@ class EmailsTest(unittest.TestCase): class EnrollTest(unittest.TestCase): """Testing the enrollment.py file.""" - + def setUp(self): + directory = ["testing", "test_enrollment.csv"] + self.enroll_dir = get_directory(directory) + self.instructor_dict = {"Name1 Name2": "Name@email.com"} + + def check_instructor_dict(self, instr_dict, name, email): + if name not in instr_dict: + return False + if email not in instr_dict[name]: + return False + return True + + def test_get_enrollment_without(self): + instr_dict, enroll_dict = enroll.get_enrollment_data(self.enroll_dir) + self.assertIn("MTH101", enroll_dict, "") + self.assertIn("1", enroll_dict["MTH101"], "") + + ex_name = "LastName, FirstName" + ex_email = "FirstName.LastName@example.edu" + check = self.check_instructor_dict(instr_dict, ex_name, ex_email) + self.assertTrue(check, "") + + def test_get_enrollment_with(self): + instr_dict, enroll_dict = enroll.get_enrollment_data(self.enroll_dir, self.instructor_dict) + self.assertIn("MTH101", enroll_dict, "") + self.assertIn("1", enroll_dict["MTH101"], "") + + ex_name = "LastName, FirstName" + ex_email = "FirstName.LastName@example.edu" + check = self.check_instructor_dict(instr_dict, ex_name, ex_email) + self.assertTrue(check, "") + + for name in self.instructor_dict: + email = self.instructor_dict[name] + check = self.check_instructor_dict(self.instructor_dict, name, email) + self.assertTrue(check, "") + class OutlookTest(unittest.TestCase): """Testing the grabber.py file.""" @@ -317,3 +383,10 @@ class SheetTest(unittest.TestCase): class UtilTest(unittest.TestCase): """Testing the utilities.py file.""" + + +def get_directory(folder_list): + curr_dir = os.path.dirname(__file__) + for file in folder_list: + curr_dir = os.path.join(curr_dir, file) + return curr_dir diff --git a/helpers/testing/test_bookstore.csv b/helpers/testing/test_bookstore.csv index 712461f..84d1e08 100644 --- a/helpers/testing/test_bookstore.csv +++ b/helpers/testing/test_bookstore.csv @@ -1 +1 @@ -2022-Summer,MTH : Mathematics,101,001,"LastName, FirstName", Intro to Math Textbook,11,Textbook Author,978-1-11-111,Textbook Publisher,Optional,1,Some Comment \ No newline at end of file +2022-Summer,MTH : Mathematics,101,001,"LastName, FirstName",Intro to Math Textbook,11,Textbook Author,978-1-11-111,Textbook Publisher,Optional,1,Some Comment,2/2/2022 2:22:22 PM \ No newline at end of file From 11242027fbd8fe0574e5d1f9c971f683e919a8f3 Mon Sep 17 00:00:00 2001 From: meows4h Date: Fri, 10 Jul 2026 12:59:05 -0700 Subject: [PATCH 10/17] started on automated book class testing cases --- helpers/classes.py | 11 ++- helpers/test.py | 170 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 174 insertions(+), 7 deletions(-) diff --git a/helpers/classes.py b/helpers/classes.py index 01526c7..d046976 100644 --- a/helpers/classes.py +++ b/helpers/classes.py @@ -57,7 +57,10 @@ def add_course(self, course, section, instructor, email, enroll): def add_section(self, course, section, instructor, email, enroll): """Adds section information to book object.""" - idx = self.courses.index(course) + try: + idx = self.courses.index(course) + except ValueError as err: + print(f"Course Index not found: {err}") self.sections[idx].append([section, instructor, email, enroll[0]]) self.sec_size[idx] += 1 self.add_enroll(enroll[1], enroll[0]) @@ -87,11 +90,13 @@ def add_required(self): def get_max_index(len_list): - """Gets the index of the largest course based on number of sections.""" + """Gets a list of indices of the largest courses in order based on number of sections.""" output = [] temp = len_list.copy() while max(temp) > 0: + # append the index of the largest course output.append(temp.index(max(temp))) + # set that course size to 0 before going to the next temp[output[-1]] = 0 return output @@ -105,7 +110,7 @@ def get_max_courses(book_list): def get_max_sections_list(book_list, course_amt): - """Gets the list of section counts by course for a book.""" + """Gets the values for how many section columns to put per course numbering.""" sec_len = [] for idx in range(course_amt): sec_len.append(0) diff --git a/helpers/test.py b/helpers/test.py index f167151..5ea7f80 100644 --- a/helpers/test.py +++ b/helpers/test.py @@ -1,5 +1,6 @@ import unittest import os +import random import helpers.analytics as alma import helpers.bookstore as book import helpers.classes as classes @@ -178,7 +179,7 @@ def test_clean_space(self): self.assertEqual(result, expected, message) def test_clean(self): - text = " \t\n\n something \t t here \t " + text = " \t\n\n something \t t here \t " result = book.str_clean(text) expected = "something t here" message = self.get_clean_msg(result, expected) @@ -223,13 +224,14 @@ def test_pull_info(self): class ClassesTest(unittest.TestCase): """Testing the classes.py file.""" def setUp(self): + # individiual preset testing analytics_data = {"Physical MMS Id": { "Types": ["Physical"], "Copies": [2], "Users": [0], "CDL": [False], "Link": "https://www.example.com", - "Year": "2024", + "Year": 2024, "Location": "Valley Library BIB" }, "Ebook MMS Id": { @@ -239,7 +241,7 @@ def setUp(self): "CDL": [True], "Link": "https://www.example2.com", "Platform": "", - "Year": "2020" + "Year": 2020 }} book_info = { "Title": "Book Title", @@ -259,12 +261,111 @@ def setUp(self): } self.book = classes.Book(book_info) + def generate_book_list(self): + # randomized testing + self.book_list = [] + self.course_list = [] + self.section_list = [] + + # course_templates = ["MTH", "BI", "WGSS", "CHEM", "GEO"] + section_templates = [1, 100, 200, 300, 400, 500] + campus_templates = ["C", "D", "N", "B", "Z", "L", "H", "PDX"] + platform_templates = ["OverDrive", "EBSCO", "Elsevier", "ProQuest", ""] + book_count = get_random(5, 100) + for num in range(1, book_count + 1): + base_course = "Course 0" + base_section = section_templates[get_random(0, len(section_templates) - 1)] + base_enroll = [get_random(0, 150), campus_templates[get_random(0, len(campus_templates ) - 1)]] + edition = get_random(0, 20) + # sometimes the edition value is left empty + if edition == 0: + edition = None + title = f"Book Title {num}" + author = f"Book Author {num}" + instructor = f"Example Name {num}" + email = f"Example Email {num}" + publisher = f"Book Publisher {num}" + req_check = get_random(1, 2) + if req_check == 2: + requirements = "Required" + else: + requirements = "Optional" + + base_analytics = {} + rand = get_random(1, 3) + if rand >= 2: + base_analytics[f"{num}Physical"] = { + "Types": ["Physical"], + "Copies": [get_random(1, 11)], + "Users": [0], + "CDL": [False], + "Link": "https://www.example3.com", + "Year": 2000 + get_random(0, 25), + "Location": "Valley Library BIB" + } + + rand = get_random(1, 3) + if rand >= 2: + rand = get_random(1, 2) + if rand >= 2: + is_cdl = True + else: + is_cdl = False + base_platform = platform_templates[get_random(0, len(platform_templates) - 1)] + base_analytics[f"{num}Electronic"] = { + "Types": ["Electronic"], + "Copies": [0], + "Users": [get_random(1, 11)], + "CDL": [is_cdl], + "Link": "https://www.example4.com", + "Platform": base_platform, + "Year": 2000 + get_random(0, 25) + } + + base_info = { + "Title": title, + "Author": author, + "Edition": edition, + "Instructor": instructor, + "Email": email, + "Course": base_course, + "Section": base_section, + "Enroll": base_enroll, + "ISBN": num, + "Publisher": publisher, + "Req": requirements, + "RequiDate": "2/2/2022 2:22:22 PM", + "Comment": "Example Comment", + "Analytics": base_analytics, + } + + new_book = classes.Book(base_info) + + course_count = get_random(1, 6) + for course in range(1, course_count + 1): + course_name = f"Course {course}" + course_section = section_templates[get_random(0, len(section_templates) - 1)] + course_enroll = [get_random(0, 150), campus_templates[get_random(0, len(campus_templates ) - 1)]] + new_book.add_course(course_name, course_section, instructor, email, course_enroll) + + section_count = get_random(1, 30) + for sec in range(1, section_count + 1): + sec_num = course_section + sec + sec_enroll = [get_random(0, 150), campus_templates[get_random(0, len(campus_templates ) - 1)]] + new_book.add_section(course_name, sec_num, instructor, email, sec_enroll) + + self.section_list.append(section_count) + + # need to add one for the initial set + self.course_list.append(course_count + 1) + self.book_list.append(new_book) + # Book Class testing # add_course method def test_add_course(self): course_name = "Test Course" - section_num = "999" + section_num = 999 instructor = "Test Name" email = "Test Email" enroll_data = [99, "D"] @@ -276,20 +377,77 @@ def test_add_course(self): self.assertEqual(self.book.total_enroll, 100, "add_course: Total enrollment count is incorrect.") # add_section method + def test_add_section(self): + course_name = "Example Course" + section_num = 999 + instructor = "Test Name" + email = "Test Email" + enroll_data = [99, "D"] + + self.book.add_section(course_name, section_num, instructor, email, enroll_data) + self.assertEqual(len(self.book.courses), 1, "add_section: Number of courses is incorrect.") + # adding the 0 index as book.sections is a list of lists of sections, with the index + # corresponding to the index of the course index + self.assertEqual(len(self.book.sections[0]), 2, "add_section: Number of sections is incorrect.") + self.assertEqual(self.book.sec_size[-1], 2, "add_section: Incorrect sec_size array number.") + self.assertEqual(self.book.total_enroll, 100, "add_section: Total enrollment count is incorrect.") # add_isbn method + def test_add_isbn(self): + isbn = 2 + self.book.add_isbn(isbn) + self.assertEqual(len(self.book.isbns), 2, "add_isbn: Number of ISBNs is incorrect.") + self.assertIn(2, self.book.isbns, "add_isbn: Missing ISBN number.") # add_enroll method + def test_add_enroll(self): + enroll_data = [99, "D"] + self.book.add_enroll(enroll_data[1], enroll_data[0]) + self.assertEqual(self.book.total_enroll, 100, "add_enroll: Total enrollment count is incorrect.") + self.assertIn("Ecampus", self.book.campuses, "add_enroll: Missing one of the correct campuses.") # add_required method + def test_add_required(self): + self.book.add_required() + self.assertEqual("Required", self.book.requirement, "add_required: Value did not update properly.") # End Book Class testing # get_max_index testing + def test_max_index(self): + # setup + for num in range(1, 5): + course_name = f"Test Course {num}" + base_section = 1 + base_enroll = [99, "D"] + instructor = "Test Name" + email = "Test Email" + self.book.add_course(course_name, base_section, instructor, email, base_enroll) + for sec in range(1, num): + sec_num = base_section + sec + sec_enroll = [sec + num, "C"] + self.book.add_section(course_name, sec_num, instructor, email, sec_enroll) + + max_list = classes.get_max_index(self.book.sec_size) + # it finds the first index first, meaning the matching sizes are done by lowest index + expected = [4, 3, 2, 0, 1] + self.assertEqual(expected, max_list, "get_max_index: Course maximum section count order does not match expected values.") # get_max_courses testing + def test_max_courses(self): + self.generate_book_list() + expected_num = max(self.course_list) + most_courses = classes.get_max_courses(self.book_list) + self.assertEqual(most_courses, expected_num, "") # get_max_sections_list testing + def test_max_sections_list(self): + self.generate_book_list() + expected_size = max(self.course_list) + + most_courses = classes.get_max_courses(self.book_list) + section_size_list = classes.get_max_sections_list(self.book_list, most_courses) + # TODO # process_book testing @@ -390,3 +548,7 @@ def get_directory(folder_list): for file in folder_list: curr_dir = os.path.join(curr_dir, file) return curr_dir + + +def get_random(low, high): + return random.randint(low, high) From a22b38137e064c0c01cc1ae03a71a65c3ae0dac0 Mon Sep 17 00:00:00 2001 From: meows4h Date: Sun, 12 Jul 2026 17:57:40 -0700 Subject: [PATCH 11/17] fixing some issues with random generation and max sections --- helpers/test.py | 56 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/helpers/test.py b/helpers/test.py index 5ea7f80..d55d4c7 100644 --- a/helpers/test.py +++ b/helpers/test.py @@ -225,7 +225,7 @@ class ClassesTest(unittest.TestCase): """Testing the classes.py file.""" def setUp(self): # individiual preset testing - analytics_data = {"Physical MMS Id": { + self.analytics_data = {"Physical MMS Id": { "Types": ["Physical"], "Copies": [2], "Users": [0], @@ -243,7 +243,7 @@ def setUp(self): "Platform": "", "Year": 2020 }} - book_info = { + self.book_info = { "Title": "Book Title", "Author": "Book Author", "Edition": "Book Edition", @@ -257,9 +257,9 @@ def setUp(self): "Req": "Optional", "RequiDate": "Requisition Date", "Comment": "Example Comment", - "Analytics": analytics_data, + "Analytics": self.analytics_data, } - self.book = classes.Book(book_info) + self.book = classes.Book(self.book_info) def generate_book_list(self): # randomized testing @@ -273,7 +273,7 @@ def generate_book_list(self): platform_templates = ["OverDrive", "EBSCO", "Elsevier", "ProQuest", ""] book_count = get_random(5, 100) for num in range(1, book_count + 1): - base_course = "Course 0" + base_course = f"Course 0" base_section = section_templates[get_random(0, len(section_templates) - 1)] base_enroll = [get_random(0, 150), campus_templates[get_random(0, len(campus_templates ) - 1)]] edition = get_random(0, 20) @@ -341,11 +341,24 @@ def generate_book_list(self): new_book = classes.Book(base_info) + book_sections = [] + + # generating sections for the already existing base section + section_count = get_random(1, 30) + for sec in range(1, section_count + 1): + course_section = section_templates[get_random(0, len(section_templates) - 1)] + sec_num = course_section + sec + sec_enroll = [get_random(0, 150), campus_templates[get_random(0, len(campus_templates) - 1)]] + new_book.add_section(base_course, sec_num, instructor, email, sec_enroll) + + book_sections.append(section_count + 1) + + # generate remaining courses + sections course_count = get_random(1, 6) for course in range(1, course_count + 1): course_name = f"Course {course}" course_section = section_templates[get_random(0, len(section_templates) - 1)] - course_enroll = [get_random(0, 150), campus_templates[get_random(0, len(campus_templates ) - 1)]] + course_enroll = [get_random(0, 150), campus_templates[get_random(0, len(campus_templates) - 1)]] new_book.add_course(course_name, course_section, instructor, email, course_enroll) section_count = get_random(1, 30) @@ -354,7 +367,10 @@ def generate_book_list(self): sec_enroll = [get_random(0, 150), campus_templates[get_random(0, len(campus_templates ) - 1)]] new_book.add_section(course_name, sec_num, instructor, email, sec_enroll) - self.section_list.append(section_count) + book_sections.append(section_count + 1) + + book_sections = sorted(book_sections, reverse=True) + self.section_list.append(book_sections) # need to add one for the initial set self.course_list.append(course_count + 1) @@ -447,11 +463,35 @@ def test_max_sections_list(self): most_courses = classes.get_max_courses(self.book_list) section_size_list = classes.get_max_sections_list(self.book_list, most_courses) - # TODO + + max_list_size = 0 + for sec_list in self.section_list: + max_list_size = max(len(sec_list), max_list_size) + + expected_list = [] + for idx in range(0, max_list_size + 1): + max_idx = 0 + for sec_list in self.section_list: + + if len(sec_list) - 1 >= idx: + max_idx = max(sec_list[idx], max_idx) + expected_list.append(max_idx) + + for idx, sec_size in enumerate(section_size_list): + self.assertGreater(expected_size, idx) + self.assertEqual(sec_size, expected_list[idx]) # process_book testing + def test_process_book(self): + self.generate_book_list() + local_list = classes.process_book(self.book_list, self.book_info) + self.book_list.append(self.book) + for book_idx in range(0, len(local_list)): + self.assertEqual(local_list[book_idx], self.book_list[book_idx]) # process_sections testing + def test_process_sections(self): + self.assertTrue(True) # process_isbns testing From a037bf31e255334b80da72bda22d0ed4f66aa0fd Mon Sep 17 00:00:00 2001 From: meows4h Date: Sun, 12 Jul 2026 21:56:04 -0700 Subject: [PATCH 12/17] completed classes testing suite --- helpers/test.py | 130 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 126 insertions(+), 4 deletions(-) diff --git a/helpers/test.py b/helpers/test.py index d55d4c7..c97c945 100644 --- a/helpers/test.py +++ b/helpers/test.py @@ -1,6 +1,8 @@ import unittest import os import random +import pandas as pd + import helpers.analytics as alma import helpers.bookstore as book import helpers.classes as classes @@ -13,6 +15,8 @@ import helpers.output as output import helpers.utilities as util +from warnings import simplefilter + # skipping selenium oriented functions as they require logging in via the interface # this could be done in headless mode w/ more looking over the HTML of the log in # pages, but focusing on the core logic functions for now @@ -472,7 +476,6 @@ def test_max_sections_list(self): for idx in range(0, max_list_size + 1): max_idx = 0 for sec_list in self.section_list: - if len(sec_list) - 1 >= idx: max_idx = max(sec_list[idx], max_idx) expected_list.append(max_idx) @@ -489,22 +492,141 @@ def test_process_book(self): for book_idx in range(0, len(local_list)): self.assertEqual(local_list[book_idx], self.book_list[book_idx]) + # process_courses testing + def test_process_courses(self): + simplefilter(action="ignore", category=pd.errors.PerformanceWarning) + simplefilter(action="ignore", category=FutureWarning) + + self.generate_book_list() + dataframe = pd.DataFrame() + head_names, main_headers = util.get_sheet_headers() + total_cols = 0 + for header in main_headers: + dataframe[f"{header}"] = [] + total_cols += 1 + format_headers = util.get_format_headers() + + max_len = 0 + for book in self.book_list: + max_len = max(len(book.courses), max_len) + total_cols += max_len + + max_courses = classes.process_courses(self.book_list, format_headers, dataframe) + self.assertEqual(max_len, max_courses) + self.assertEqual(total_cols, len(dataframe.columns)) + # process_sections testing def test_process_sections(self): - self.assertTrue(True) + simplefilter(action="ignore", category=pd.errors.PerformanceWarning) + simplefilter(action="ignore", category=FutureWarning) + + self.generate_book_list() + dataframe = pd.DataFrame() + head_names, main_headers = util.get_sheet_headers() + total_cols = 0 + for header in main_headers: + dataframe[f"{header}"] = [] + total_cols += 1 + format_headers = util.get_format_headers() + + max_len = 0 + for book in self.book_list: + max_len = max(len(book.courses), max_len) + total_cols += max_len + + most_courses = classes.get_max_courses(self.book_list) + section_size_list = classes.get_max_sections_list(self.book_list, most_courses) + for sec_size in section_size_list: + total_cols += sec_size * 4 + + max_courses = classes.process_courses(self.book_list, format_headers, dataframe) + classes.process_sections(self.book_list, format_headers, max_courses, dataframe) + + self.assertEqual(total_cols, len(dataframe.columns)) # process_isbns testing + def test_process_isbns(self): + simplefilter(action="ignore", category=pd.errors.PerformanceWarning) + simplefilter(action="ignore", category=FutureWarning) + + self.generate_book_list() + dataframe = pd.DataFrame() + head_names, main_headers = util.get_sheet_headers() + total_cols = 0 + for header in main_headers: + dataframe[f"{header}"] = [] + total_cols += 1 + format_headers = util.get_format_headers() + + max_len = 0 + max_isbn = 0 + for book in self.book_list: + max_len = max(len(book.courses), max_len) + max_isbn = max(len(book.isbns), max_isbn) + total_cols += max_len + + most_courses = classes.get_max_courses(self.book_list) + section_size_list = classes.get_max_sections_list(self.book_list, most_courses) + for sec_size in section_size_list: + total_cols += sec_size * 4 + + max_courses = classes.process_courses(self.book_list, format_headers, dataframe) + classes.process_sections(self.book_list, format_headers, max_courses, dataframe) + classes.process_isbns(self.book_list, head_names, main_headers, dataframe) + + self.assertEqual(total_cols, len(dataframe.columns)) # import_data testing + def test_import_data(self): + simplefilter(action="ignore", category=pd.errors.PerformanceWarning) + simplefilter(action="ignore", category=FutureWarning) + + self.generate_book_list() + dataframe = pd.DataFrame() + head_names, main_headers = util.get_sheet_headers() + for header in main_headers: + dataframe[f"{header}"] = [] + format_headers = util.get_format_headers() + + max_courses = classes.process_courses(self.book_list, format_headers, dataframe) + classes.process_sections(self.book_list, format_headers, max_courses, dataframe) + classes.process_isbns(self.book_list, head_names, main_headers, dataframe) + dataframe = classes.import_data(self.book_list, format_headers, head_names, dataframe) + self.assertEqual(len(self.book_list), len(dataframe)) class EmailsTest(unittest.TestCase): """Testing the emails.py file.""" - - # Book Class testing + def setUp(self): + book_title = "" + book_author = "" + book_edition = "" + book_year = "" + book_access = {} + self.emails_book = emails.Book(book_title, book_author, book_edition, book_year, book_access) + + inst_name = "" + inst_email = "" + inst_course = "" + inst_section = "" + self.emails_instructor = emails.Instructor(inst_name, inst_email, inst_course, inst_section, self.emails_book) # Instructor Class testing + # add_book method + def test_add_book(self): + title = "" + author = "" + edition = "" + year = "" + access = {} + local_book = emails.Book(title, author, edition, year, access) + + course = "" + section = "" + self.emails_instructor.add_book(course, section, local_book) + + # End Instructor Class testing # update_excel testing From e6ad4e18333820e1eae1e4e0f6cd1187e800a0ba Mon Sep 17 00:00:00 2001 From: meows4h Date: Mon, 13 Jul 2026 10:58:34 -0700 Subject: [PATCH 13/17] email test cases implementations --- .gitignore | 3 +- helpers/emails.py | 1 + helpers/test.py | 66 +++++++++++++++++++++++++++++++ helpers/testing/test_output.xlsx | Bin 0 -> 6608 bytes 4 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 helpers/testing/test_output.xlsx diff --git a/.gitignore b/.gitignore index b9e3cbd..8906a3d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ __pycache__ -*.xlsx \ No newline at end of file +*.xlsx +!test_output.xlsx \ No newline at end of file diff --git a/helpers/emails.py b/helpers/emails.py index aeef366..c6c7d2c 100644 --- a/helpers/emails.py +++ b/helpers/emails.py @@ -58,6 +58,7 @@ def update_excel(directory, data, sheet_name): cell.value = datetime.datetime.now() break workbook.save(directory) + workbook.close() # the goal here should be to load book data into a book object, diff --git a/helpers/test.py b/helpers/test.py index c97c945..b4cd346 100644 --- a/helpers/test.py +++ b/helpers/test.py @@ -2,6 +2,8 @@ import os import random import pandas as pd +import openpyxl +import datetime import helpers.analytics as alma import helpers.bookstore as book @@ -631,6 +633,70 @@ def test_add_book(self): # update_excel testing # this needs a setup to run sheetmaker and import other information? + def test_update_excel(self): + bookstore_dir = get_directory(["testing", "test_bookstore.csv"]) + output_dir = get_directory(["testing", "test_output.xlsx"]) + + workbook = openpyxl.load_workbook(output_dir) + sheet_name = "Test" + worksheet = workbook[sheet_name] + + header_dict, header_list = util.get_sheet_headers() + + # generate test data + email_sent_list = [] + email_ready_list = [] + test_time = datetime.datetime.now() + for idx, row in enumerate(worksheet.iter_rows(min_row=2)): + if idx >= 151: + break + prior_check = False + for cell in row: + col = cell.column_letter + if worksheet[f"{col}1"].value == header_dict["ReadingList"]: + num = get_random(1, 2) + if num == 1: + cell.value = True + prior_check = True + email_ready_list.append(idx) + else: + cell.value = False + elif worksheet[f"{col}1"].value == header_dict["EmailDate"]: + num = get_random(1, 2) + if num == 1 or prior_check == False: + cell.value = None + else: + email_ready_list.pop() + email_sent_list.append(idx) + cell.value = test_time + + workbook.save(output_dir) + workbook.close() + + emails.update_excel(output_dir, email_ready_list, sheet_name) + + final_email_list = sorted(email_ready_list + email_sent_list) + + workbook = openpyxl.load_workbook(output_dir) + worksheet = workbook[sheet_name] + for idx, row in enumerate(worksheet.iter_rows(min_row=2)): + if idx >= 151: + break + reading_val = None + email_val = None + for cell in row: + col = cell.column_letter + if worksheet[f"{col}1"].value == header_dict["ReadingList"]: + reading_val = cell.value + elif worksheet[f"{col}1"].value == header_dict["EmailDate"]: + email_val = cell.value + if idx in final_email_list: + self.assertTrue(reading_val) + self.assertTrue(email_val) + else: + self.assertFalse(reading_val) + self.assertFalse(email_val) + # create_email_excel testing diff --git a/helpers/testing/test_output.xlsx b/helpers/testing/test_output.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..79c322affd52d2d4f6d28404cb564e3567ef029e GIT binary patch literal 6608 zcmZ{I1z1#D+x7s0fOJYqDXN_1w>1>sjlGeXA>>Js<%90GNOqonJ<>2HiNJ$W|TlBtV{4u9oT^ zu5Lgsb2m3mkh7CYyvm~+hG(D2-m6|G}kYlE_*Q1pf^^GC^X8V-bM(%XOLX;rjW=oHG>jMp0B}GB z00{qkyq2yW)_(>JO@6J`OGnb*In?uw%x=n=-WY!hU@o4!6#NxY{)zvlu)YYvMQSc+mV)yO4(1CQNmODx!|~srpheeSR-IzcKpCtwGM| z^Z5_CE(&_3%C4hK|9Ckw3mb;=Ap>snkgHkMa|0OY$T|zN0zCqQ#BQHkg;jQjjPAI} zCnl}Q6{AWi03}WxU#BG(b(&8|$8~)JS61tjkB6$4ahkU;EL{It(5hHRUmbB-&X$1$ z2+njA7J@766EwX4Ama zXY0SOJb!HXOMfJWN|29B(2Nkb={6B2;u)e3KyFig~Jss#WTK6T27E z1mPA~=EI?M=gA`jtocY~y^I55iu5KFxo?76a*d8bZ@b+TPrEWq97T zd%TnvN}l(i(CJgSfY>TlTA}f4*$1W?J%$hw%w6<=INqXf+B>$DF4e`~IR_t7(}g~I zJoG{!w}&w5CFg_3xWFQsJolcbDw3nYlO8ALyC-}8Csdy&h1_d zO{6xSsI_Pthl=`N9$$B-96juWhzEdfzi-K`F8b+82z)p(xjvXNT@9?hxfnjTNnJG~l))niw=(u#wBpMPsMy*=DuL~l+576e)PCz~h5;$#!;v-TDy7HssCV<03s-TEK@=dD@Yq(sj2XP*Fbtan%zkMQ!1^Bj4~nZHW1_x%k(khG-Z? zW@+NXU;>J7j49kG+iwObtl!1nyj49XL0`b_NoA7v3McWReV8~CaU~GCJM!V=IV5D7 zxX3O8Ue1i7;3YPCh}q>-NBBn5FZz@2=2;wU??xo!CI^1JYQN0Qqp_*Bg5GyC=jx%LXffttPg1Qm-l=@!9|C4t&e~$^wXkUpnwB6`zaNEx#1A zct*@T^I*N84cjFfO=H%h=am$PkYZP=kL5esK2LK$0WO7wD4-+L3=G zZJ)@slU{y0)4?G=(_nd}t`{w2I>&TFuuUM-wS~%Q)IdsDH+o^V4U4yZ*fuEPiLPz> z#P3^h-#+Hr?=p`Jthb&S#-r&1!y1O2wth5%4IQ=S0v$oUUPkp3$HJoepY~BY$G%^z&%`9Ub_6M;_3u6+RKR5z?T%0YGvC6G0h8Ap>A@rPqH1v5EMpt2G$*G?%sf>6>!X$<4_zO-G=b3mAHv+L?BO zP;G}a#AI=ulz(wXv;~$o`CjU+Hb2>!Deqh;-!1~T391}5ir*#dam*z6fmL#CCT>7w zYFi?Uai^=6JA2!tZR>TmJS5>L3suiT#y{gS&F zE-;4GG>ssv7IT+n5T@-2*AesC>W{-J;Il6ci+eT0d#&ELE{&PgD=RLamXu%lJPC$x zn*Vlhajda%PRkQ__V&j)y(Lh-qtg=%_51C3mZ3l6vuY}ai14D#dX=EE-i_DUMFQ*&#JYj~O94BXs;3C^z9F z$&wwB8_}p#ajt>IIA)400l&p8X0NPeL);kthHSbZ8%BKkOT{Fm-$uM?xsm2YseMdi z%<&h#=~{}fRel>KXXZu{vcR}sN4;ZZiRF16&kwBR>2FQ99HB;}ZWJXOdt~T=Ld}276WJ# zEyVYyH-l%i?+E$FoVb?J9nO)h+YKM|WQ;qF17Q}-iffSdHT5TPSb z$4<;yxQVFf2*?ln;s}=8f9*SvYkMoYLhkpO>ll{NeUr?Yc%EFg3eHt+vfS z7jiJ;MBC&g0aBz72P2Gvy*RnSC^h0zhnIZZX()4@0K^~3O|I{!;Xh5SUU1WaLoObr zcLFXRA@z81b_wh9*piy4=|IQC**9x?PHI9=YAi$BsO)jz{@{G=? ziz7BY_jk{nyZSFILuX=l$$cQ$mGoAwLf*Vp%%bf)*qd8!ohEb4}M|JBf0F z-cRi4LdWD4-q~~x{XDH$w`OCV*w>}N1#2%I(lHy{V%M0fZN;t-Pb}*`o?rs>8*w_!9vZi-fO}-=_yJ1LY zdmh(nCckm<$j-`5bSd&mDO|pO>%$1mqCVPuI!(2`f-@kvE!OR+IAYUPuOMf7o%g;d zgfH)VQQ44VRU*)?YgD1?g>wCY+cT-J^iJ!EA9jr5MIs%`Yu%qo_0#WeBocT$>mA7V zV2*s7B88@@D(v@ph&g{%JpwGH*F|sseLOfcMOD59?dcEdp;{m&LWt>_nv%eCGV!xF zHfbb=QTFsV4j0bkT2DKPDFvs4yWV07 z^Sz;^^zxNpd+i)CB2_4ie=d!c`S2~|81g-nZJLJC&1W((-N1ZXJ)%Tveu|!cXFRwp zg>oFu*6IeblUPT-VDYEqem+iTq%8WAgX#w(6w&zaMP6AIS3+K2ly0J}yw3}eI7W(T zagxvtxdoRRp)#1GXhdL0&!aKWqiRH8Nf)vaq(6Io7mSly!WN{cEJ<8|qZTzJLr+vf zU|lNtVh0=dW9Ga4%9B;%YhYcVo{M*icHk;h-LKmmMPN4xu`3-zs~;curJE_}kD(i+pPw*q&>$K98!?>PW~m(9#qzV*@TXVp#OucT7?D);E}1N` zv%{rZ23+jSazjj>k3OVhQi>9M!3Or|=TqouOLGT#3)1yM!)#qJ z%uhu_6^cciKf1mC_)|z0tAk5t&6~<+OesfDz4c-7u1O+G-$i0c#rX5eG=824hnB{a z(GtS|6!e?(+c%4%zxahlHs{tVa4DhsSjkrbJHB7^owB#kF{Ft{1V_iOOgG!QFs&A|hTB^z#P`VMVhQrc|0fefFsC@oC#?teVx z&dgSH7hNG4OFHZVzcUlSJ7Nqr7fyee9#`MvdHWGWW+#f*8(;`ZLU*W_aL#^@KiGy} zve7)ayOHo>IGIVME_SnJE{uBEFtcZFEaC!#EFJIJjY0L{av%E-4;nLnzcR^M`9sC8 z3{W6$l(?%|lJ)I!pJx~9B6_8_#A*#1KfGCiXkHs8SQ+NdG|c_LVZyY~KHBPOcmfz) z%c+pefgI0;m^=|qh|5<(7}^1fRB}<8x2Oc29gn z;VULev_xw!@#|$#Q%u?88v;#xgPG4L@=eSM_M=IX)zcib{E?Fq(uJ_3t$qm1Tqip3VW)Ly#=*9a@SOWMmT@!=heOoDmOj&S&8;EIgz4JkoW@LTWi z3KRJZdSsb0gHr`G#bBEL)5NLm8cT)-Kf|9F?sjjmk{we+P81(GQ6#tXr+~L`b#?p`65^7UU3$22!cB{Z&??cT!wKLvc4+ns;}UK( zlxYS(J}Mevi{32h%p`i(vu{z$jSgCEHSrII7_i87C6xM({ptX9IUg|F0p0{ zChJ`SsoC%~!-;}phoTkuiha&Ijn$XIPhofSZ31H^5Xr5jc)RzGygUm1Q)N+mVYDN% z99gO~8XAK-!Z}P~%^0F-UpMUp)?%2{R+GoA8C1?yLtd$$X{KpyzSaFM=dYdOFQxKb zH~IT=TdSnM%LaGcIhI_cOR3md%N@S2E=hXt5WEpZNz4zw71vJcoi;(zdko&XNQviU zkWLLThIM7Nt{du>HNkQVaR`|m~gYuhy@iQ$DISE7ap4&YZWlR{TE)??=+KtEz6 zB7#<9H*cx+L8FUTSq<;*1BwhAqB%!5xYZoUE7$cS zFxn?O90-!1wZ5L`vx4kp6l9CuN_l!M+XuY7`SkJtpS#`oXPJx@t0F1o59J=aCV_6v z^74C#0&CS&8@7DO1epkS8?1xp11d$$vV+DnF2Y% zF|@z=LqjQ77f)*!Pt!NPZq`8KKl`jIMMbrjn*cdGYeQyE4!yh*Dzit%ZYY*&lohur zwg!`ne5wnIc|@e)DwUv>_Ay9W01{N~hpHAd44~=;dLf1D%q9lK z#uP(>vtf9cdN(ns{F#3$Vur85!?g^w?MtO{ppwyLp)VTO19MQMDCAcpnuB&Do}}(b zm$m@M&1@Ms_0*#)-Wpxdxb66hNGQrm)g`?%DYkt7vr0nPpv!MzeTYJ8krG|Fv}5A~ zr6p=v9U7f2V#1S0g$2SLJJ$B?y4wbOU`Pd+sq9DBhZAr^X@W9R;T+zTKK+#`*gv76f}k4 zV*dZw^FPRg`&#Z7G5^sLjM9M&VaQ?pQ_|dr-jD15K;cCHit_h?_v7S0z)>V6elKd? x2jBPdf50lFe}VrGpTDo?z9apkhnEZh_+Q*gT?rlIj}R8}_X-)|1IYih{vY=O6 Date: Mon, 13 Jul 2026 14:59:20 -0700 Subject: [PATCH 14/17] more email script test cases --- helpers/emails.py | 6 ++-- helpers/test.py | 52 +++++++++++++++++++++++++++++++ helpers/testing/test_output.xlsx | Bin 6608 -> 29397 bytes 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/helpers/emails.py b/helpers/emails.py index c6c7d2c..355f3c9 100644 --- a/helpers/emails.py +++ b/helpers/emails.py @@ -290,10 +290,10 @@ def create_email_excel(input_sheet=None, file_name=""): book_str += "
    " - print(book.access) + # print(book.access) for access_data in book.access: link = book.access[access_data]["Link"] - print(book.access[access_data]) + # print(book.access[access_data]) if access_data == "Ebook": user_num = book.access[access_data]["Number"] @@ -397,7 +397,7 @@ def create_email_excel(input_sheet=None, file_name=""): write_to_excel(output_path, final_data, "Email List") update_excel(input_path, book_idx, process_sheet) - print("Email sheet exported. Main sheet updated.") + # print("Email sheet exported. Main sheet updated.") def write_to_excel(directory, export_data, sheetname): diff --git a/helpers/test.py b/helpers/test.py index b4cd346..e63fb23 100644 --- a/helpers/test.py +++ b/helpers/test.py @@ -699,6 +699,58 @@ def test_update_excel(self): # create_email_excel testing + def test_create_emails(self): + output_dir = get_directory(["testing", "test_output.xlsx"]) + + workbook = openpyxl.load_workbook(output_dir) + sheet_name = "Test" + worksheet = workbook[sheet_name] + + header_dict, header_list = util.get_sheet_headers() + + # generate test data + email_sent_list = [] + email_ready_list = [] + test_time = datetime.datetime.now() + for idx, row in enumerate(worksheet.iter_rows(min_row=2)): + if idx >= 151: + break + prior_check = False + for cell in row: + col = cell.column_letter + if worksheet[f"{col}1"].value == header_dict["ReadingList"]: + num = get_random(1, 2) + if num == 1: + cell.value = True + prior_check = True + email_ready_list.append(idx) + else: + cell.value = False + elif worksheet[f"{col}1"].value == header_dict["EmailDate"]: + num = get_random(1, 2) + if num == 1 or prior_check == False: + cell.value = None + else: + email_ready_list.pop() + email_sent_list.append(idx) + cell.value = test_time + + workbook.save(output_dir) + workbook.close() + emails.create_email_excel("Test", output_dir) + + final_email_list = sorted(email_ready_list + email_sent_list) + email_dir = get_directory(["..", "email_output.xlsx"]) + + email_workbook = openpyxl.load_workbook(email_dir) + email_worksheet = email_workbook["Email List"] + + # change this implementation to check each row, each cell, check header like normal + # split the name cell into the number on its own and then verify the number in the list + # for number in email_ready_list: + # for row in email_worksheet.iter_rows(min_row=2): + # self.assertEqual(row["Name"], f"Example Name {number}") + # write_to_excel testing -- this is an export function and should be fine diff --git a/helpers/testing/test_output.xlsx b/helpers/testing/test_output.xlsx index 79c322affd52d2d4f6d28404cb564e3567ef029e..15500f5ebfaa4d18690cb35c5ebb1f7f15dad6b2 100644 GIT binary patch literal 29397 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 delta 3766 zcmZXXcUTi!x5g=l2pEbq1yMvP0@6i9KspFW5e_{Pdans3)B%yAbRvf;h%~7I>CGS* zYUov36i6sh5b6Df``mM%bG~o>*)#k7?X~x__slzMP2kCgCBgdIlz*_0k&#i8of;>D zD}htvpOEB{y$EJczp)rrd*0~Nf~3$#U(m* z!;k(jUEk+vegW7>v%8@<`L?cV=90H7=86bB&vryO-6Q|d{!OmE#1DPc!b~5Zsz0eN zc2zgIcZVyVn2J*~t*evk0Uiv#*bvM+e7wp=B{YeS8+u{HKAHc0aI>hxuW^eXkgWD= zGoFzC=T6p~le2@EF80!qqa-Zw5*Znz3>n!qGNh|F2T+(r^q5s=REMe!Q>w}a;X(Cb zJnkbD0*pLP#}@U@7BM`{Ozy>=P=kgHC-%_Tfn3wt-i!$Y{+fQ7=0Nv)sDGmGyzZw8 zldSpOooe^og+Ygw+3vu$={Dz_b)EgRV}};Y+WGH$*7ZR%ld&zQYQ%}BLU>0a1}6Cy z`V-g(oMq82IkOWmLtsYNZ-}^SbBsF0^JcFg(Hfw|A2(%E3MDor1ex5P0v5@#*<3 zQ|T9hzg_n;%qIMCPgm#gpoH{?TFKhKArfi1(bffTx_E|o6AlB?4YibXenV0+f!ydTd{{B1 zm{$UPapFn|Qt)!|?&fsc0Z!tZrGk{_u$p^e&hXM8euaCUdDy2;5rN-rFSxiFDg&Vp+uQggAhPH}=vC#GtsvfA-8AYXI-hqsZl?Ww4f(6c=-XBJUg!zhy0putJ zm8~O5u{822G#T-NB#3?k_99oIcK5|@0pW+2)6ryymq~$qMcN;kh&!@c>KS8(6od$< zj$PD?U6jqBAlA&N^}H;-hIR^F+IVJ!g+>ygR75By+lcgpAt7giB>nY1zzR3B77>~= zW2wNzE!E3GKHolLha6O6GYjzuV$cQ4r7xpbS(7CK*-2^_X%dvS3uzFLATJ@ zm`uK4`07?!93NURbVgaL_RvT@7y4Zt&Ga@jb!6QHGxS9Bl>@qOb+2sEUQl3U(}bN4 zFaqYbZt9x=7f3bRXQ#>_`zWxnk2epe9|fqGwsTv5 z8xDJc)P2%K$-m6mR|Tgg%J7_UaxnUgCN=uevPOd-+2ddDd5EM``8X+#X&R=Y07PWF z3Ok6wIx;%W$3k-T3mXGXe6)nzO%B$8_LtO;B>%k0;TxbRsN^0sppyTH;Xsup^U}+l zot$r(g5x(hy!=O#(#@^c^rMPYrpLIs*N4MOkQ~E!!DcUnlj0(2hS^o+%{(wVTAhmP zSLGiA?5`P8WgpuWmgguPejrdT0IBbS6?aISp$N;ZKCiG35Q^K@IaT|c1%S#*4=^e_+Ou#XU3F(^Pi2|X}4C+ zDgXJ${s~FU%j`KNWju`ZWMp)ttOwyH3ik#X<2U~hL^@J zs@>{(#;72L#4@YrXPA^J}`d`@6Cqw#n+dyfF6q zTHX;M4(#f%NVQP176hPN1ceO*gk!1=+Y&R)4GkVx@gVLeF5PE(+3VHzMx&awh{kXI z?tox6opyLoh~Q6~KvqzKhzalWNdDti=Edh`py=z|9+`Y`ll>o7%teKGHTrmbF8`HI z%^}D5i5A{rmd9>tq$(B)>xY|83sm}8x!q$qsRo1w@*^BQK-PP5FEk5f-HW}HidSXS zY1%}Mzx!SHAJTp+t>1j9@P}m*U)Nz$QQ5F?d76}XluJ_`aCo#b;fl0u z<1Qf6wO*lp_5;VCZ^X5xm3n(DHAnILz3;h9XKvt6=$n67_;$@3Ba344c$dX$>33B$ zmiXEg?BR3FGeq2bI+wk7*zWvzb-(RR4g1xKB39Ry01LXKYsYSOxeB)Yz5{yK2lov0 zuO63CO=IvL=`CU*cj<~r7mxnLiW!67Tn?@aR;> z5t^nVIQuO!JEn%B4nb_`^mk9qE4SvmgqC49tbzhdl&hX>X??kkfz!n(cv>afpUrpq zv{TGdmHR2;P?S>m22G}EBdm0JgtrP$zt|rxBhY;3*LM7oi2zU~qM34!w4g)zCrsdQ zk1toSl)uPTQVLow<<4u2jLEacpOa51kpJ^*L7up-&|RdrJqawT2BcC!5BbaO8ZhtC zAqI|w7WPq=Q>jKW;vJkQoo9v>y>4)%nJ*V>L1^Vp=Zq(k{Az?&I5h@)SJakH@XK?& z&FtWxigt%t+SV_!A#LWTqds(_So;~X|e)N_nq5&kA_J!c|;Cn2O9xi3r z{cd&i;|E~IQI;4v?|E&w=Kh5uNCXBp|0a@*R{UY=r zDKwr5`NNz2`M-{y3ZRx`r70aEM$Kdnm-{tH?!VX okl+K-yTkFX!8)u+4-ZacC=7ftXB>=^LV^uh3gZO@U%lY|2b+Q~W&i*H From c4aaace5326993504b69d51e124f7eb1e8b19908 Mon Sep 17 00:00:00 2001 From: meows4h Date: Tue, 14 Jul 2026 08:36:52 -0700 Subject: [PATCH 15/17] fixing dataframe dtype for gh actions --- helpers/test.py | 1 + helpers/testing/test_output.xlsx | Bin 29397 -> 29417 bytes 2 files changed, 1 insertion(+) diff --git a/helpers/test.py b/helpers/test.py index e63fb23..1e850ff 100644 --- a/helpers/test.py +++ b/helpers/test.py @@ -588,6 +588,7 @@ def test_import_data(self): head_names, main_headers = util.get_sheet_headers() for header in main_headers: dataframe[f"{header}"] = [] + dataframe[f"{header}"] = dataframe[f"{header}"].astype(object) format_headers = util.get_format_headers() max_courses = classes.process_courses(self.book_list, format_headers, dataframe) diff --git a/helpers/testing/test_output.xlsx b/helpers/testing/test_output.xlsx index 15500f5ebfaa4d18690cb35c5ebb1f7f15dad6b2..141f9e86aaaf62ef507b7fdd406af20a246d9895 100644 GIT binary patch literal 29417 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

    {%wpPOQF$0|V1l1EGXX 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 literal 29397 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 From 12ed806ff0ad395b93e4e87b22da92a43132274c Mon Sep 17 00:00:00 2001 From: meows4h Date: Tue, 14 Jul 2026 10:59:25 -0700 Subject: [PATCH 16/17] utilities testing suite + remaining notes --- helpers/test.py | 167 +++++++++++++++++++++++++++++-- helpers/testing/test_config.ini | 3 + helpers/testing/test_output.xlsx | Bin 29417 -> 29413 bytes helpers/utilities.py | 30 +++++- 4 files changed, 192 insertions(+), 8 deletions(-) create mode 100644 helpers/testing/test_config.ini diff --git a/helpers/test.py b/helpers/test.py index 1e850ff..d67bf66 100644 --- a/helpers/test.py +++ b/helpers/test.py @@ -4,6 +4,7 @@ import pandas as pd import openpyxl import datetime +import configparser import helpers.analytics as alma import helpers.bookstore as book @@ -746,12 +747,14 @@ def test_create_emails(self): email_workbook = openpyxl.load_workbook(email_dir) email_worksheet = email_workbook["Email List"] - # change this implementation to check each row, each cell, check header like normal - # split the name cell into the number on its own and then verify the number in the list - # for number in email_ready_list: - # for row in email_worksheet.iter_rows(min_row=2): - # self.assertEqual(row["Name"], f"Example Name {number}") - + for row in email_worksheet.iter_rows(min_row=2): + for cell in row: + col = cell.column_letter + if email_worksheet[f"{col}1"] == "Name": + name_col_list = cell.value.split(" ") + number = int(name_col_list[2]) + self.assertIn(number, final_email_list) + break # write_to_excel testing -- this is an export function and should be fine @@ -798,31 +801,183 @@ def test_get_enrollment_with(self): class OutlookTest(unittest.TestCase): """Testing the grabber.py file.""" + # every function aside from the importer and exporter deal with selenium webdrivers class GUITest(unittest.TestCase): """Testing the gui.py file.""" + # automated gui testing falls under a similar circumstance to testing selenium + # there are ways and methods to do so, but for now, not enough time class HelperGUITest(unittest.TestCase): """Testing the helpergui.py file.""" + # see above GUI notes, automated testing gui not within scope as of right now + # additionally, it is easy to manually test and verify results class ModesTest(unittest.TestCase): """Testing the modes.py file.""" + # functions to primarily test here would be the excel sheet updaters + + # enrollment_update testing + + # analytics_update testing + + # emails_update testing class OutputTest(unittest.TestCase): """Testing the output.py file.""" + # this file hosts a single function for the pure reason of handling + # formatting and finishing out the export for an excel file + # it should be fine to leave out of testing for now class SheetTest(unittest.TestCase): """Testing the sheetmaker.py file.""" + # only testing the version of the function with no gui, no cli interaction + + # make_excel_sheet testing class UtilTest(unittest.TestCase): """Testing the utilities.py file.""" + # get_int testing + def test_get_int(self): + values = [" 12 4 5 ", "93478", None, "", " ", 65.435343, 109] + expected = [1245, 93478, None, None, None, 65, 109] + for idx, value in enumerate(values): + result = util.get_int(value) + self.assertEqual(expected[idx], result, f"get_int: result {result} does not match {expected[idx]}.") + + # get_clean testing + def test_get_clean(self): + test_cleaner = ["Example", "WORDS", "something"] + test_names = [ + "a book that has examples and wOrdS SOMEthing to do with an example e-reading", + "what other EXAMPLE case isn't here", + "'what about words appearing' or something elsewhere" + ] + expected = [ + "A Book That Has S And To Do With An E-Reading", + "What Other Case Isn't Here", + "'What About Appearing' Or Elsewhere" + ] + for idx, name in enumerate(test_names): + result = util.get_clean(test_cleaner, name) + self.assertEqual(expected[idx], result) + + # get_state testing + def test_get_state(self): + cases = ["true", "hmmm", "anything", "false", "FALSE", "False", "True"] + expected = [True, True, True, True, True, False, True] + for idx, case in enumerate(cases): + result = bool(util.get_state(case)) + self.assertEqual(expected[idx], result) + + # get_directory testing + def test_get_directory(self): + test_config = configparser.ConfigParser() + test_config.read("helpers/testing/test_config.ini") + test_config = test_config["test"] + result = util.get_directory("Test", test_config) + expected = get_directory(["testing", "test_config.csv"]) + self.assertEqual(expected, result, f"get_directory: result {result} does not match {expected}.") + + # get_filepath testing + def test_get_filepath(self): + cases = [] + expected = [] + for idx, case in enumerate(cases): + result = util + self.assertEqual(expected[idx], result) + + # get_letter testing + def test_get_letter(self): + cases = [] + expected = [] + for idx, case in enumerate(cases): + result = util + self.assertEqual(expected[idx], result) + + # get_edition_string testing + def test_get_edition_string(self): + cases = [] + expected = [] + for idx, case in enumerate(cases): + result = util + self.assertEqual(expected[idx], result) + + # get_format_headers testing + def test_get_format_headers(self): + cases = [] + expected = [] + for idx, case in enumerate(cases): + result = util + self.assertEqual(expected[idx], result) + + # get_replace_header testing + def test_get_replace_header(self): + cases = [] + expected = [] + for idx, case in enumerate(cases): + result = util + self.assertEqual(expected[idx], result) + + # get_split_course testing + def test_get_split_course(self): + cases = [] + expected = [] + for idx, case in enumerate(cases): + result = util + self.assertEqual(expected[idx], result) + + # skipping get_input and get_enabled as it is an input oriented function + + # get_sheet_headers testing + def test_get_sheet_headers(self): + cases = [] + expected = [] + for idx, case in enumerate(cases): + result = util + self.assertEqual(expected[idx], result) + + # get_config_headers testing + def test_get_config_headers(self): + cases = [] + expected = [] + for idx, case in enumerate(cases): + result = util + self.assertEqual(expected[idx], result) + + # get_string_cleaners testing + def test_get_string_cleaners(self): + cases = [] + expected = [] + for idx, case in enumerate(cases): + result = util + self.assertEqual(expected[idx], result) + + # get_row_info testing + def test_get_row_info(self): + cases = [] + expected = [] + for idx, case in enumerate(cases): + result = util + self.assertEqual(expected[idx], result) + + # get_campus testing + def test_get_campus(self): + cases = [] + expected = [] + for idx, case in enumerate(cases): + result = util + self.assertEqual(expected[idx], result) + + # skipping set_col_format as it is likely to fluctuate and change + def get_directory(folder_list): curr_dir = os.path.dirname(__file__) diff --git a/helpers/testing/test_config.ini b/helpers/testing/test_config.ini new file mode 100644 index 0000000..4cb09a2 --- /dev/null +++ b/helpers/testing/test_config.ini @@ -0,0 +1,3 @@ +[test] +TestDir = testing +TestFile = test_config.csv \ No newline at end of file diff --git a/helpers/testing/test_output.xlsx b/helpers/testing/test_output.xlsx index 141f9e86aaaf62ef507b7fdd406af20a246d9895..d3ffd7276015424366e0f77673dbcedefd4b12f9 100644 GIT binary patch delta 21271 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 LkRjX|`tJV)zh1+y delta 21399 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 wkZt4ZH0c(%K0xeWf9!zs@;(IJ?U}a(`z2h3_W_ Date: Tue, 14 Jul 2026 15:03:47 -0700 Subject: [PATCH 17/17] wrapping up major test cases + comments --- helpers/bookstore.py | 2 + helpers/emails.py | 7 +- helpers/grabber.py | 6 +- helpers/test.py | 154 +++++++++++++++++-------------- helpers/testing/test_output.xlsx | Bin 29413 -> 29406 bytes helpers/utilities.py | 29 +++++- main.py | 13 +-- 7 files changed, 129 insertions(+), 82 deletions(-) diff --git a/helpers/bookstore.py b/helpers/bookstore.py index ab67f52..c0f849d 100644 --- a/helpers/bookstore.py +++ b/helpers/bookstore.py @@ -125,6 +125,8 @@ def get_prices(driver, table): main_div = material_dict[isbn] main_div.click() # TODO + # this has been partially implemented but it cannot handle the number of + # requests this script would like to make, check other TODO or documentation # this needs to pull the used and new price points from the beaverstore page # https://osubeaverstore.verbacompare.com/comparison?id=2026-Summer__AEC__411__400 html = main_div.get_attribute("innerHTML") diff --git a/helpers/emails.py b/helpers/emails.py index 355f3c9..1a7fe17 100644 --- a/helpers/emails.py +++ b/helpers/emails.py @@ -30,6 +30,9 @@ def __init__(self, name, email, course, section, book): self.data = {course: {section: [book]}} def add_book(self, course, section, book): + # if we have the course & section, add book to it + # if we have course, but not section, make new section with book + # if neither, make new course and section dictionary if course in self.data and section in self.data[course]: self.data[course][section].append(book) elif course in self.data: @@ -70,6 +73,7 @@ def update_excel(directory, data, sheet_name): # TODO # rewrite this into smaller functions to break it apart a little +# could easily be much more functionized, making it easier to work with def create_email_excel(input_sheet=None, file_name=""): """Creates an excel sheet to be formatted for usage with PowerAutomate function.""" full_config = configparser.ConfigParser() @@ -267,7 +271,8 @@ def create_email_excel(input_sheet=None, file_name=""): # course[1]}.

      ' # TODO - # combining class codes ? + # combining class codes or just restructuring this whole thing to make it + # cleaner and easier to work with in the future # book_dict = {book_str1: [section1, section2], # book_str2: [section1], etc.} diff --git a/helpers/grabber.py b/helpers/grabber.py index 9a60006..df3895b 100644 --- a/helpers/grabber.py +++ b/helpers/grabber.py @@ -280,7 +280,11 @@ def run_get_email(name): while True: time.sleep(0.7) try: - # TODO trying to implement webdriverwait + # TODO + # implementing webdriverwait to speed up checking times + # and make this whole process a little faster and more precise + # otherwise, leaving this alone for now as it seems to cause more + # issues that it aids in fixing # suggestion_box = WebDriverWait(driver, 3).until( # EC.presence_of_element_located((By.CLASS_NAME, "ms-FloatingSuggestionsList-container")) # ) diff --git a/helpers/test.py b/helpers/test.py index d67bf66..9e36e44 100644 --- a/helpers/test.py +++ b/helpers/test.py @@ -602,33 +602,65 @@ def test_import_data(self): class EmailsTest(unittest.TestCase): """Testing the emails.py file.""" def setUp(self): - book_title = "" - book_author = "" - book_edition = "" - book_year = "" - book_access = {} + book_title = "Book Title" + book_author = "Book Author" + book_edition = "Book Edition" + book_year = "Book Year" + book_access = { + "Ebook": { + "Number": 1, + "Link": "ebook cdl link", + "CDL": True + }, + "Print1": { + "Number": 2, + "Link": "print 1 link" + }, + "Print2": { + "Number": 4, + "Link": "print 2 link" + }, + "Audio": { + "Link": "audio link" + } + } self.emails_book = emails.Book(book_title, book_author, book_edition, book_year, book_access) - inst_name = "" - inst_email = "" - inst_course = "" - inst_section = "" + inst_name = "Instructor Name" + inst_email = "Instructor Email" + inst_course = "Course 1" + inst_section = "101" self.emails_instructor = emails.Instructor(inst_name, inst_email, inst_course, inst_section, self.emails_book) # Instructor Class testing # add_book method def test_add_book(self): - title = "" - author = "" - edition = "" - year = "" - access = {} + title = "Book 2" + author = "Author 2" + edition = "Edition 2" + year = "Year 2" + access = {"Ebook": { + "Number": 7, + "Link": "ebook link", + "CDL": False + }} local_book = emails.Book(title, author, edition, year, access) - course = "" - section = "" - self.emails_instructor.add_book(course, section, local_book) + case_courses = ["Course 1", "Course 1", "Course 2"] + case_sections = ["101", "102", "101"] + for idx, course in enumerate(case_courses): + self.emails_instructor.add_book(course, case_sections[idx], local_book) + + self.assertEqual(2, len(self.emails_instructor.data)) + self.assertEqual(2, len(self.emails_instructor.data["Course 1"])) + self.assertEqual(2, len(self.emails_instructor.data["Course 1"]["101"])) + self.assertEqual(1, len(self.emails_instructor.data["Course 1"]["102"])) + self.assertEqual(1, len(self.emails_instructor.data["Course 2"])) + self.assertEqual(1, len(self.emails_instructor.data["Course 2"]["101"])) + + # could add in more tests beyond just the length checking but the data issues would + # be apparent when generating emails as well # End Instructor Class testing @@ -888,96 +920,76 @@ def test_get_directory(self): # get_filepath testing def test_get_filepath(self): - cases = [] - expected = [] + cases = ["testing"] + expected = [get_directory(["..", "testing"])] + false_expected = [get_directory(["testing"])] for idx, case in enumerate(cases): - result = util + result = util.get_filepath(case) + false_result = util.get_filepath(case, home=False) self.assertEqual(expected[idx], result) + self.assertEqual(false_expected[idx], false_result) # get_letter testing def test_get_letter(self): - cases = [] - expected = [] + cases = [1, 27, 4, 53, 55] + expected = ["A", "AA", "D", "BA", "BC"] for idx, case in enumerate(cases): - result = util + result = util.get_letter(case) self.assertEqual(expected[idx], result) # get_edition_string testing def test_get_edition_string(self): - cases = [] - expected = [] + cases = [None, "", "5", 29, 10, "1", 11, 21, 22, 13] + expected = [None, None, "5th", "29th", "10th", "1st", "11th", "21st", "22nd", "13th"] for idx, case in enumerate(cases): - result = util + result = util.get_edition_string(case) self.assertEqual(expected[idx], result) - # get_format_headers testing - def test_get_format_headers(self): - cases = [] - expected = [] - for idx, case in enumerate(cases): - result = util - self.assertEqual(expected[idx], result) + # get_format_headers just grabs a set of information + # as it is supposed to be the central point of modification for those strings # get_replace_header testing def test_get_replace_header(self): - cases = [] - expected = [] + format_headers = util.get_format_headers() + cases = [["TestNum", "TestSection", "TestEmailInfo"]] for idx, case in enumerate(cases): - result = util - self.assertEqual(expected[idx], result) + result = util.get_replace_header(format_headers[2], case[0], case[1], case[2]) + for val in case: + self.assertIn(val, result) # get_split_course testing def test_get_split_course(self): - cases = [] - expected = [] + cases = ["MTH251", "BIO305", "WR221Z"] + expected = [["MTH", "251"], ["BIO", "305"], ["WR", "221Z"]] for idx, case in enumerate(cases): - result = util + result = util.get_split_course(case) self.assertEqual(expected[idx], result) # skipping get_input and get_enabled as it is an input oriented function - # get_sheet_headers testing - def test_get_sheet_headers(self): - cases = [] - expected = [] - for idx, case in enumerate(cases): - result = util - self.assertEqual(expected[idx], result) - - # get_config_headers testing - def test_get_config_headers(self): - cases = [] - expected = [] - for idx, case in enumerate(cases): - result = util - self.assertEqual(expected[idx], result) - - # get_string_cleaners testing - def test_get_string_cleaners(self): - cases = [] - expected = [] - for idx, case in enumerate(cases): - result = util - self.assertEqual(expected[idx], result) + # get_sheet_headers, get_config_headers, and get_string_cleaners + # are all also is information grabbing functions # get_row_info testing def test_get_row_info(self): - cases = [] - expected = [] - for idx, case in enumerate(cases): - result = util - self.assertEqual(expected[idx], result) + # an implementation can be made for row data to be tested, but there is + # only one spot this gets used in, so it is not relevant to time to get done + pass # get_campus testing def test_get_campus(self): - cases = [] - expected = [] + # this is breakable via any string starting with D and then + # filling the rest with non-existent values, but this usually does + # not occur in the natural data this script will be managing + cases = ["C", "D", "Z", "L", "N", "B", "H", "PDX", "DC", "23432", "CR"] + expected = ["Corvallis", "Ecampus", "International", "LaGrande", "Newport", "Cascades", "Portland", "Portland", "Ecampus", None, None] for idx, case in enumerate(cases): - result = util + result = util.get_campus(case) self.assertEqual(expected[idx], result) # skipping set_col_format as it is likely to fluctuate and change +# some helper functions to carry out testing various features def get_directory(folder_list): curr_dir = os.path.dirname(__file__) diff --git a/helpers/testing/test_output.xlsx b/helpers/testing/test_output.xlsx index d3ffd7276015424366e0f77673dbcedefd4b12f9..523f5e59c97057d6e0f510bf4554ce24c5fc1810 100644 GIT binary patch literal 29406 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 z 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 P5(Qp6f!dxF3jXzfcA<>` literal 29413 zcmcG#cRbboA3uD|6p|EiLM2j!C?{l;P>QJRoxNooA|paF8dj2>QP#=k*e9}A_8!@e zRmeEp@6S1LUEllh_+5JZ?#KN{*LC94@$wqa@p`|H!W|OQV=x$u0%oK#uPLowLFEbl z_67Vo0{$3S>nqq=+t~5y+1T(nSz5>i$x&AF9l5z9J3U~fw0P>$srH27T9;c0!Pf?o zNbjwu4=P^uT^r;JC^}xJ`jyS`iqF?%H`-bmSRSP`M)Lb7Mj3V{Q&kp;rYHV%J;q

      aG;$~|%U{ny8`>ZmsELyx4`Nkdb* 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 1: + if edition_num[-2] == "1": + edition_num += "th" + else: + edition_num += "st" + + else: + edition_num += "st" + elif edition_num[-1] == "2": - edition_num += "nd" + if len(edition_num) > 1: + if edition_num[-2] == "1": + edition_num += "th" + else: + edition_num += "nd" + else: + edition_num += "nd" + elif edition_num[-1] == "3": - edition_num += "rd" + if len(edition_num) > 1: + if edition_num[-2] == "1": + edition_num += "th" + else: + edition_num += "rd" + else: + edition_num += "rd" + elif edition_num[-1] in th_list: edition_num += "th" diff --git a/main.py b/main.py index 91a1afd..5cb9079 100644 --- a/main.py +++ b/main.py @@ -5,14 +5,9 @@ from helpers.gui import start_app from helpers.sheetmaker import make_excel_sheet -# General TODO: -# finish isbn updating from the bookstore (i.e. isbn differs from what we own) -# fully finish documentation.md -# add images to readme - # TODO # This is a list of improvements that I am aware may be able to be added -# However, due to time constraints or lack of information are left as is +# However, due to time constraints or lack of information are left for later # - try / except blocks around data entry code # things like analytics.py could use more error checking to prevent @@ -32,6 +27,12 @@ # what COULD be done or what hasn't been fully tested # - adding testcases for selenium functions using hidden github values # could still login using headless functionality and secret key values +# - adding in cost comparison that works without querying the site too many times +# - improving documentation, comments, and overall code structure +# - cross referencing our own ISBN values from the bookstore ISBN values +# sometimes we own a different book than what the bookstore has per class +# but it is the same book, in which case this could use an automatic cross +# referencing function to automatically pull those values via titles config = configparser.ConfigParser() config.read("config.ini")