204 lines
8.0 KiB
Python
204 lines
8.0 KiB
Python
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]
|
|
student_name = row.find_element(By.XPATH, './td[contains(@class, "cdk-column-Customer")]').get_attribute("innerText")
|
|
reservation_date = row.find_element(By.XPATH, './td[contains(@class, "cdk-column-date")]').get_attribute("innerText")
|
|
print('adding reservation comment for student ' + student_name + ' on ' + reservation_date)
|
|
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()
|
|
comment_text = "c172 " + today.strftime('%d %b, %H:%M')
|
|
print('comment text: ' + comment_text)
|
|
textarea.send_keys(comment_text)
|
|
|
|
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() |