diff --git a/make_reservations.py b/make_reservations.py new file mode 100644 index 0000000..e948059 --- /dev/null +++ b/make_reservations.py @@ -0,0 +1,199 @@ +from selenium import webdriver +from selenium.webdriver.remote import webelement +from selenium.webdriver.support.ui import WebDriverWait +from selenium.common.exceptions import TimeoutException +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.common.by import By +from selenium.webdriver.firefox.service import Service as FirefoxService +from selenium.webdriver.firefox.firefox_profile import FirefoxProfile +from webdriver_manager.firefox import GeckoDriverManager +import configparser +import os +import sys +import datetime +import argparse + +# TODO factor out common code (defaults, login, etc) from progress_reports script +GECKO_DRIVER = 'gecko' +CHROME_DRIVER = 'chrome' +DEFAULT_CONFIG_PATH = './config.ini' + +DEFAULT_WORKING_DIRECTORY = './tmp' +DEFAULT_APP_URL = 'https://app.flightschedulepro.com' +DEFAULT_TIMEOUT = 10 # seconds +DEFAULT_DAY_LOOKAHEAD = 2 + + +parser = argparse.ArgumentParser( + prog='FSP Make Reservations', + description='Reserves flights in advance', + epilog='Kill urself') +parser.add_argument('-c', '--config', default=DEFAULT_CONFIG_PATH) +parser.add_argument('-s', '--show_browser', help='Show browser window', action='store_true') +parser.add_argument('-d', '--days', help='Number of days in advance to place reservations for', default=DEFAULT_DAY_LOOKAHEAD) +parser.add_argument('-D', '--dry_run', help='Disables saving of comments', action='store_true') + +args = parser.parse_args() + +config = configparser.ConfigParser() + +config.read(args.config) +working_dir = config['main'].get('working_directory', DEFAULT_WORKING_DIRECTORY) +app_url = config['main'].get('app_url', DEFAULT_APP_URL) +timeout = config['main'].get('timeout', DEFAULT_TIMEOUT) +tenant_id = config['main'].get('tenant_id') +if tenant_id is None or tenant_id == "": + print('No Tenant ID configured!') + sys.exit(1) + +# HACK maybe Ubuntu specific +if config['main'].getboolean('use_working_dir_for_tmp', False): + os.environ["TMPDIR"] = working_dir + + +driver_type = config['main']['driver'] +# TODO only firefox for now +if driver_type == GECKO_DRIVER: + opt = webdriver.FirefoxOptions() + if config['firefox'].get('binary_path') is not None: + opt.binary_location = config['firefox'].get('binary_path') + if not args.show_browser: + opt.add_argument('-headless') + # Force window size to avoid issues with responsive design + opt.add_argument("--width=1920") + opt.add_argument("--height=1080") + # Disable print dialog. Causes race conditions with window switching + firefox_profile = FirefoxProfile() + firefox_profile.set_preference("print.enabled", False) + opt.profile = firefox_profile + driver = webdriver.Firefox(options=opt, service=FirefoxService(GeckoDriverManager().install())) +else: + print(f'unsupported driver type "{driver_type}"') + sys.exit(1) + +# optionally prints a message and cleans up before exiting +def die(message=None): + if message is not None: + print(message) + driver.quit() + sys.exit(0) + +login_url = app_url + '/Account/Login?company='+ tenant_id +driver.get(login_url) + +# wait for stupid cookie modal to load and accept all ¯\_(ツ)_/¯ +try: + WebDriverWait(driver, timeout).until( + EC.presence_of_all_elements_located((By.ID, 'onetrust-accept-btn-handler')) + ) + cookie_btn = driver.find_element(By.ID, 'onetrust-accept-btn-handler') + cookie_btn.click() +except: + pass + +# Login +form = driver.find_element(By.CLASS_NAME, 'account-form') +username = driver.find_element(By.ID, 'username') +username.send_keys(config['main']['username']) +username.submit() + +try: + WebDriverWait(driver, timeout).until( + EC.presence_of_all_elements_located((By.ID, 'password')) + ) +except TimeoutException: + if len(driver.find_elements(By.XPATH, '//div[@id="alerts"]')) > 0: + die('Bad username') + die('unknown authentication error') + +password = driver.find_element(By.ID, 'password') +password.send_keys(config['main']['password']) + +password.submit() + +# Wait for app to load and naviagte to Reservations "page" +try: + WebDriverWait(driver, timeout).until( + EC.presence_of_all_elements_located((By.CLASS_NAME, 'fsp-main-drawer-content')) + ) +except TimeoutException: + if len(driver.find_elements(By.XPATH, '//div[@id="alerts"]')) > 0: + die('Bad password') + die('unknown authentication error') + +today = datetime.datetime.today() +def get_reservation_rows(): + driver.get(app_url + '/App/Reservations') + # Wait for "Future" button and click + WebDriverWait(driver, timeout).until( + EC.presence_of_all_elements_located((By.XPATH, '//a[@id="mat-tab-link-1"]')) + ) + driver.find_element(By.XPATH, '//a[@id="mat-tab-link-1"]').click() + + #Wait for rows to load + WebDriverWait(driver, timeout).until( + EC.presence_of_all_elements_located((By.XPATH, '//tr[contains(@class, "cdk-row")]')) + ) + reservation_rows = driver.find_elements(By.XPATH, '//tr[contains(@class, "cdk-row")]') + + reservation_rows = list(filter(filter_row_by_date, reservation_rows)) + return reservation_rows + +def filter_row_by_date(reservation_row: webelement.WebElement) -> bool: + instructor = reservation_row.find_element(By.XPATH, './td[contains(@class, "cdk-column-instructor")]').get_attribute("innerText") + # Double check we're only commenting on your reservations. Out of caution abort if we see any other instructors + if "Jason Vitale" not in instructor: + die("WARNING: retrieved reservation for another instructor. Aborting...") + return False + date_str = reservation_row.find_element(By.XPATH, './td[contains(@class, "cdk-column-date")]').get_attribute("innerText") + date = datetime.datetime.strptime(date_str, '%a, %b %d, %Y') + if date.date() == (today + datetime.timedelta(days=args.days)).date(): + return True + return False + +rows = get_reservation_rows() + +print('found ' + str(len(rows)) + ' reservations to add reservation comment to') +for i in range(0, len(rows)): + row = rows[i] + row.find_element(By.XPATH, './/button[contains(@class, "fsp-button")]').click() + + WebDriverWait(driver, timeout).until( + EC.presence_of_all_elements_located((By.ID, 'ReservationModal-StandardViewCtrl')), + ) + modal = driver.find_element(By.ID, 'ReservationModal-StandardViewCtrl') + #Wait for modal/comments sections to load + WebDriverWait(driver, timeout).until( + EC.presence_of_all_elements_located((By.XPATH, '//div[contains(@ng-model, "reservationDetails.comments")]')), + ) + WebDriverWait(driver, timeout).until( + EC.presence_of_all_elements_located((By.XPATH, '//a[contains(@ng-click, "editComment()")]')), + ) + + comments = modal.find_element(By.XPATH, './/div[contains(@ng-model, "reservationDetails.comments")]') + edit_btn = comments.find_element(By.XPATH, './/a[contains(@ng-click, "editComment()")]') + WebDriverWait(driver, timeout).until( + EC.element_to_be_clickable(edit_btn) + ) + edit_btn.click() + #Wait for modal/comments sections to load + WebDriverWait(driver, timeout).until( + EC.presence_of_all_elements_located((By.ID, 'comments')) + ) + textarea = comments.find_element(By.ID, 'comments') + + textarea.clear() + textarea.send_keys("c172 " + today.strftime('%d %b, %H:%S')) + + if args.dry_run is False: + save_btn = comments.find_element(By.XPATH, './/button[contains(@ng-click, "save()")]') + WebDriverWait(driver, timeout).until( + EC.element_to_be_clickable(save_btn) + ) + print("saving reservation comment") + save_btn.click() + + driver.find_element(By.XPATH, '//button[contains(@ng-click, "dismissModal()")]').click() + rows = get_reservation_rows() + +die() \ No newline at end of file