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()