From e1dd3559ec7340d085bf9b7f9b91bd4a85b268a9 Mon Sep 17 00:00:00 2001 From: "David A. Desrosiers" Date: Mon, 22 Mar 2021 17:33:17 -0400 Subject: [PATCH 01/13] Put credentials into separate file, not env, added additional cities in AMER, EMEA, clean up formatting with black and isort. --- post-job.py | 277 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 177 insertions(+), 100 deletions(-) diff --git a/post-job.py b/post-job.py index ddc815d..d16f086 100755 --- a/post-job.py +++ b/post-job.py @@ -1,94 +1,122 @@ #!/usr/bin/env python3 import argparse -import contextlib +import json import os import time +from appdirs import user_data_dir import selenium +import selenium.webdriver.support.ui as ui from selenium import webdriver -from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys -import selenium.webdriver.support.ui as ui - -SSO_EMAIL = os.getenv('SSO_EMAIL') -SSO_PASSWORD = os.getenv('SSO_PASSWORD') -NO_AUTH = not (SSO_EMAIL and SSO_PASSWORD) - -SITE = 'https://canonical.greenhouse.io' -JOB_BOARD = 'Canonical - Jobs' +SITE = "https://canonical.greenhouse.io" +JOB_BOARD = "Canonical - Jobs" REGIONS = { - 'americas': [ + "americas": [ # United States - 'Home based - Americas, Atlanta', - 'Home based - Americas, Austin', - 'Home based - Americas, Boston', - 'Home based - Americas, Chicago', - 'Home based - Americas, Colorado', - 'Home based - Americas, Dallas', - 'Home based - Americas, Los Angeles', - 'Home based - Americas, New York', - 'Home based - Americas, Raleigh', - 'Home based - Americas, San Francisco', - 'Home based - Americas, Seattle', + "Home based - Americas, Atlanta", + "Home based - Americas, Austin", + "Home based - Americas, Boston", + "Home based - Americas, Charlotte", + "Home based - Americas, Chicago", + "Home based - Americas, Colorado", + "Home based - Americas, Dallas", + "Home based - Americas, Denver", + "Home based - Americas, Los Angeles", + "Home based - Americas, Miami", + "Home based - Americas, New York", + "Home based - Americas, Portland", + "Home based - Americas, Raleigh", + "Home based - Americas, San Francisco", + "Home based - Americas, Seattle", # Canada - 'Home based - Americas, Calgary', - 'Home based - Americas, Montreal', - 'Home based - Americas, Ottawa', - 'Home based - Americas, Toronto', - 'Home based - Americas, Vancouver', - 'Home based - Americas, Victoria', + "Home based - Americas, Calgary", + "Home based - Americas, Montreal", + "Home based - Americas, Ottawa", + "Home based - Americas, Toronto", + "Home based - Americas, Vancouver", + "Home based - Americas, Victoria", # South America - 'Home based - Americas, Buenos Aires', - 'Home based - Americas, Mexico City', - 'Home based - Americas, Santiago', - 'Home based - Americas, São Paulo', + "Home based - Americas, Buenos Aires", + "Home based - Americas, Mexico City", + "Home based - Americas, Santiago", + "Home based - Americas, São Paulo", + "Home based - Americas, Rio de Janeiro", + "Home based - Americas, Santiago", + "Home based - Americas, Montevideo", + ], + "emea": [ + "Home based - Europe, Amsterdam", + "Home based - Europe, Barcelona", + "Home based - Europe, Berlin", + "Home based - Europe, Copenhagen", + "Home based - Europe, Dublin", + "Home based - Europe, Eindhoven", + "Home based - Europe, Hamburg", + "Home based - Europe, Helsinki", + "Home based - Europe, Istanbul", + "Home based - Europe, London", + "Home based - Europe, Munich", + "Home based - Europe, Rotterdam", + "Home based - Europe, Stockholm", + "Home based - Europe, Stuttgart", ], - 'emea': [ - 'Home based - Europe, Amsterdam', - 'Home based - Europe, Barcelona', - 'Home based - Europe, Berlin', - 'Home based - Europe, Copenhagen', - 'Home based - Europe, Dublin', - 'Home based - Europe, Eindhoven', - 'Home based - Europe, Hamburg', - 'Home based - Europe, Helsinki', - 'Home based - Europe, Istanbul', - 'Home based - Europe, London', - 'Home based - Europe, Munich', - 'Home based - Europe, Rotterdam', - 'Home based - Europe, Stockholm', - 'Home based - Europe, Stuttgart', + "apac": [ + "Home based - Asia Pacific, Auckland", + "Home based - Asia Pacific, Bangalore", + "Home based - Asia Pacific, Beijing", + "Home based - Asia Pacific, Hong Kong", + "Home based - Asia Pacific, Hyderabad", + "Home based - Asia Pacific, Seoul", + "Home based - Asia Pacific, Shanghai", + "Home based - Asia Pacific, Singapore", + "Home based - Asia Pacific, Sydney", + "Home based - Asia Pacific, Taipei", + "Home based - Asia Pacific, Tokyo", ], - 'apac': [ - 'Home based - Asia Pacific, Auckland', - 'Home based - Asia Pacific, Bangalore', - 'Home based - Asia Pacific, Beijing', - 'Home based - Asia Pacific, Hong Kong', - 'Home based - Asia Pacific, Hyderabad', - 'Home based - Asia Pacific, Seoul', - 'Home based - Asia Pacific, Shanghai', - 'Home based - Asia Pacific, Singapore', - 'Home based - Asia Pacific, Sydney', - 'Home based - Asia Pacific, Taipei', - 'Home based - Asia Pacific, Tokyo', - ] } +def parse_credentials(): + # Read configuration from secured file in $HOME/.config/ + creds = os.path.join(user_data_dir("greenhouse"), "login.tokens") + + with open(os.path.expanduser(creds), "r") as auth: + try: + creds = json.load(auth) + ghsso_user = creds["username"] + ghsso_pass = creds["password"] + return (ghsso_user, ghsso_pass) + except FileNotFoundError: + print("file {} does not exist".format(creds)) + + def parse_args(): - parser = argparse.ArgumentParser(description='Duplicate Greenhouse job postings to multiple locations.') + parser = argparse.ArgumentParser( + description="Duplicate Greenhouse job postings to multiple locations." + ) parser.add_argument( - 'job_ids', nargs='+', - help='The numeric Greenhouse job id (the number in the URL when on the Job Dashboard)') + "job_ids", + nargs="+", + help="The numeric Greenhouse job id (the number in the URL when on the Job Dashboard)", + ) parser.add_argument( - '--region', dest='regions', nargs='+', choices=['americas', 'emea', 'apac'], - help='The regions in which to create job postings') + "--region", + dest="regions", + nargs="+", + choices=["americas", "emea", "apac"], + help="The regions in which to create job postings", + ) parser.add_argument( - '--browser', dest='browser', choices=['chrome', 'firefox'], default='chrome', - help='The browser to use (default is chrome)') + "--browser", + dest="browser", + choices=["chrome", "firefox"], + default="chrome", + help="The browser to use (default is chrome)", + ) return parser.parse_args() @@ -96,7 +124,7 @@ def parse_args(): def main(): args = parse_args() - if args.browser == 'firefox': + if args.browser == "firefox": browser = webdriver.Firefox() else: browser = webdriver.Chrome() @@ -104,24 +132,28 @@ def main(): new_browser = True + (ghsso_user, ghsso_pass) = parse_credentials() + for job_id in args.job_ids: - job_posts_page_url = f'{SITE}/plans/{job_id}/jobapp' + job_posts_page_url = f"{SITE}/plans/{job_id}/jobapp" browser.get(job_posts_page_url) if new_browser and not NO_AUTH: # click Accept Cookies button - accept_cookies_btn = browser.find_elements_by_xpath('//*[@id="cookie-policy-button-accept"]') + accept_cookies_btn = browser.find_elements_by_xpath( + '//*[@id="cookie-policy-button-accept"]' + ) if accept_cookies_btn: accept_cookies_btn[0].click() # enter Ubuntu SSO email and password email_txt = browser.find_element_by_id("id_email") if email_txt: - email_txt.send_keys(SSO_EMAIL) + email_txt.send_keys(ghsso_user) password_txt = browser.find_element_by_id("id_password") if password_txt: - password_txt.send_keys(SSO_PASSWORD) + password_txt.send_keys(ghsso_pass) continue_btn = browser.find_elements_by_xpath('//button[@name="continue"]') if continue_btn: @@ -129,8 +161,12 @@ def main(): new_browser = False # pause 60 seconds for 2-factor auth by user - wait = ui.WebDriverWait(browser, 60) # timeout after 60 seconds - results = wait.until(lambda browser: browser.find_elements_by_class_name('job-application__offices')) + wait = ui.WebDriverWait(browser, 60) # timeout after 60 seconds + results = wait.until( + lambda browser: browser.find_elements_by_class_name( + "job-application__offices" + ) + ) # minimize trays so they don't obstruct clicks trays = browser.find_elements_by_xpath('//div[@data-provides="tray-close"]') @@ -138,7 +174,9 @@ def main(): tray.click() # accept cookies so the popup doesn't obstruct clicks - cookie_accept_btn = browser.find_elements_by_css_selector('#inform-cookies button') + cookie_accept_btn = browser.find_elements_by_css_selector( + "#inform-cookies button" + ) for btn in cookie_accept_btn: try: # click can raise if element exists but is in a hidden block @@ -150,12 +188,16 @@ def main(): existing_locations = [] while True: # gather all existing job post locations from each page of results - existing_locations += [result.text.strip('()') for result in results] - next_page = browser.find_elements_by_css_selector('a.next_page') + existing_locations += [result.text.strip("()") for result in results] + next_page = browser.find_elements_by_css_selector("a.next_page") if next_page: multipage = True next_page[0].click() - results = wait.until(lambda browser: browser.find_elements_by_class_name('job-application__offices')) + results = wait.until( + lambda browser: browser.find_elements_by_class_name( + "job-application__offices" + ) + ) else: break @@ -167,61 +209,96 @@ def main(): region_locations = REGIONS[region] new_locations = set(region_locations) - set(existing_locations) for location_text in sorted(new_locations): - publish_location_text = location_text.split(',')[-1].strip() + publish_location_text = location_text.split(",")[-1].strip() - duplicate_link = wait.until(lambda browser: browser.find_elements_by_xpath('//*[@id="job_applications"]//tr[1]//a[text()="Duplicate"]')) - page = duplicate_link[0].get_attribute('href') + duplicate_link = wait.until( + lambda browser: browser.find_elements_by_xpath( + '//*[@id="job_applications"]//tr[1]//a[text()="Duplicate"]' + ) + ) + page = duplicate_link[0].get_attribute("href") browser.get(page) - job_name_txt = browser.find_elements_by_xpath('//input[../label="Job Name"]')[0] - job_name = job_name_txt.get_attribute('value').replace('Copy of ', '').strip() + job_name_txt = browser.find_elements_by_xpath( + '//input[../label="Job Name"]' + )[0] + job_name = ( + job_name_txt.get_attribute("value").replace("Copy of ", "").strip() + ) job_name_txt.clear() job_name_txt.send_keys(job_name) - post_to = browser.find_elements_by_xpath('//label[text()="Post To"]/..//input[1]')[0] + post_to = browser.find_elements_by_xpath( + '//label[text()="Post To"]/..//input[1]' + )[0] post_to.send_keys(JOB_BOARD) post_to.send_keys(Keys.ENTER) - location = browser.find_elements_by_xpath('//label[text()="Location"]/..//input[1]')[0] + location = browser.find_elements_by_xpath( + '//label[text()="Location"]/..//input[1]' + )[0] location.send_keys(location_text) - browser.find_elements_by_xpath('//label[text()="Glassdoor"]/input[1]')[0].click() - browser.find_elements_by_xpath('//label[text()="Indeed"]/input[1]')[0].click() - browser.find_elements_by_xpath('//label[text()="Remote"]/input[1]')[0].click() - publish_location = browser.find_elements_by_xpath('//input[@placeholder="Select location"]')[0] + browser.find_elements_by_xpath('//label[text()="Glassdoor"]/input[1]')[ + 0 + ].click() + browser.find_elements_by_xpath('//label[text()="Indeed"]/input[1]')[ + 0 + ].click() + browser.find_elements_by_xpath('//label[text()="Remote"]/input[1]')[ + 0 + ].click() + publish_location = browser.find_elements_by_xpath( + '//input[@placeholder="Select location"]' + )[0] publish_location.send_keys(publish_location_text) popup_menu_xpath = ( f'//ul[contains(@class, "ui-menu")]' f'/li[contains(@class, "ui-menu-item")]' f'/div[contains(text(), "{publish_location_text}")]' ) - location_choices = wait.until(lambda browser: browser.find_elements_by_xpath(popup_menu_xpath)) + location_choices = wait.until( + lambda browser: browser.find_elements_by_xpath(popup_menu_xpath) + ) publish_location.send_keys(Keys.DOWN) publish_location.send_keys(Keys.TAB) - time.sleep(.5) + time.sleep(0.5) # click the Save button save_btn = browser.find_elements_by_xpath('//a[text()="Save"]')[0] save_btn.click() - #publish_btns = wait.until(lambda browser: browser.find_elements_by_css_selector('tr.job-application.draft img.publish-application-button')) - wait.until(lambda browser: browser.find_elements_by_class_name('job-application__offices')) - publish_btns = browser.find_elements_by_css_selector('tr.job-application.draft img.publish-application-button') + # publish_btns = wait.until(lambda browser: browser.find_elements_by_css_selector('tr.job-application.draft img.publish-application-button')) + wait.until( + lambda browser: browser.find_elements_by_class_name( + "job-application__offices" + ) + ) + publish_btns = browser.find_elements_by_css_selector( + "tr.job-application.draft img.publish-application-button" + ) for btn in publish_btns: btn.click() - time.sleep(.2) + time.sleep(0.2) while True: - results = wait.until(lambda browser: browser.find_elements_by_class_name('job-application__offices')) - publish_btns = browser.find_elements_by_css_selector('tr.job-application.draft img.publish-application-button') + results = wait.until( + lambda browser: browser.find_elements_by_class_name( + "job-application__offices" + ) + ) + publish_btns = browser.find_elements_by_css_selector( + "tr.job-application.draft img.publish-application-button" + ) for btn in publish_btns: btn.click() - time.sleep(.2) - next_page = browser.find_elements_by_css_selector('a.next_page') + time.sleep(0.2) + next_page = browser.find_elements_by_css_selector("a.next_page") if next_page: next_page[0].click() else: break -if __name__ == '__main__': + +if __name__ == "__main__": main() From 1d9fe26afd67a81b052e62aab85e0a3e98b115d1 Mon Sep 17 00:00:00 2001 From: "David A. Desrosiers" Date: Mon, 28 Jun 2021 10:13:21 -0400 Subject: [PATCH 02/13] Numerous fixes, expanded cities, read credentials from local json, support for sub-jobs in a single role, cleaned up with black/isort --- post-job.py | 108 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 92 insertions(+), 16 deletions(-) diff --git a/post-job.py b/post-job.py index d16f086..9738fe4 100755 --- a/post-job.py +++ b/post-job.py @@ -4,15 +4,18 @@ import json import os import time -from appdirs import user_data_dir +from re import search import selenium import selenium.webdriver.support.ui as ui +from appdirs import user_data_dir from selenium import webdriver +from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.keys import Keys SITE = "https://canonical.greenhouse.io" JOB_BOARD = "Canonical - Jobs" +NO_AUTH = "" REGIONS = { "americas": [ @@ -24,7 +27,6 @@ "Home based - Americas, Chicago", "Home based - Americas, Colorado", "Home based - Americas, Dallas", - "Home based - Americas, Denver", "Home based - Americas, Los Angeles", "Home based - Americas, Miami", "Home based - Americas, New York", @@ -42,13 +44,12 @@ # South America "Home based - Americas, Buenos Aires", "Home based - Americas, Mexico City", - "Home based - Americas, Santiago", - "Home based - Americas, São Paulo", + "Home based - Americas, Montevideo", "Home based - Americas, Rio de Janeiro", "Home based - Americas, Santiago", - "Home based - Americas, Montevideo", + "Home based - Americas, São Paulo", ], - "emea": [ + "eu": [ "Home based - Europe, Amsterdam", "Home based - Europe, Barcelona", "Home based - Europe, Berlin", @@ -58,14 +59,70 @@ "Home based - Europe, Hamburg", "Home based - Europe, Helsinki", "Home based - Europe, Istanbul", - "Home based - Europe, London", "Home based - Europe, Munich", "Home based - Europe, Rotterdam", "Home based - Europe, Stockholm", "Home based - Europe, Stuttgart", ], + "brasil": [ + "Home based - Americas, Santiago", + "Home based - Americas, Rio de Janeiro", + "Home based - Americas, Belo Horizonte", + "Home based - Americas, Porto Alegre", + "Home based - Americas, Salvador", + "Home based - Americas, Resife", + "Home based - Americas, Fortaleza", + ], + "emea": [ + "Home based - Africa, Cairo", + "Home based - Africa, Cape Town", + "Home based - Africa, Lagos", + "Home based - Africa, Nairobi", + "Home based - Europe, Amsterdam", + "Home based - Europe, Ankara", + "Home based - Europe, Athens", + "Home based - Europe, Barcelona", + "Home based - Europe, Berlin", + "Home based - Europe, Bratislava", + "Home based - Europe, Brno", + "Home based - Europe, Brussels", + "Home based - Europe, Bucharest", + "Home based - Europe, Budapest", + "Home based - Europe, Cluj-Napoca", + "Home based - Europe, Dublin", + "Home based - Europe, Edinburgh", + "Home based - Europe, Frankfurt", + "Home based - Europe, Glasgow", + "Home based - Europe, Helsinki", + "Home based - Europe, Istanbul", + "Home based - Europe, Kraków", + "Home based - Europe, Lisbon", + "Home based - Europe, Ljubljana", + "Home based - Europe, Lyon", + "Home based - Europe, Madrid", + "Home based - Europe, Manchester", + "Home based - Europe, Milan", + "Home based - Europe, Moscow", + "Home based - Europe, Munich", + "Home based - Europe, Oslo", + "Home based - Europe, Paris", + "Home based - Europe, Plovdiv", + "Home based - Europe, Prague", + "Home based - Europe, Riga", + "Home based - Europe, Rome", + "Home based - Europe, Sofia", + "Home based - Europe, St. Petersburg", + "Home based - Europe, Stockholm", + "Home based - Europe, Tallinn", + "Home based - Europe, Timișoara", + "Home based - Europe, Vienna", + "Home based - Europe, Vilnius", + "Home based - Europe, Warsaw", + "Home based - Europe, Wrocław", + "Home based - Europe, Zagreb", + ], "apac": [ - "Home based - Asia Pacific, Auckland", + "Home based - Asia Pacific, Auckland, Auckland", "Home based - Asia Pacific, Bangalore", "Home based - Asia Pacific, Beijing", "Home based - Asia Pacific, Hong Kong", @@ -107,7 +164,7 @@ def parse_args(): "--region", dest="regions", nargs="+", - choices=["americas", "emea", "apac"], + choices=["americas", "eu", "brasil", "emea", "apac"], help="The regions in which to create job postings", ) parser.add_argument( @@ -124,10 +181,13 @@ def parse_args(): def main(): args = parse_args() + options = Options() + # options.add_argument("--headless") + if args.browser == "firefox": browser = webdriver.Firefox() else: - browser = webdriver.Chrome() + browser = webdriver.Chrome(options=options) browser.maximize_window() new_browser = True @@ -168,11 +228,6 @@ def main(): ) ) - # minimize trays so they don't obstruct clicks - trays = browser.find_elements_by_xpath('//div[@data-provides="tray-close"]') - for tray in trays: - tray.click() - # accept cookies so the popup doesn't obstruct clicks cookie_accept_btn = browser.find_elements_by_css_selector( "#inform-cookies button" @@ -184,6 +239,16 @@ def main(): except selenium.common.exceptions.ElementNotInteractableException: pass + # click "Got it" button for new tips + got_it_btn = browser.find_elements_by_xpath('//a[text()="Got it"]') + if got_it_btn: + got_it_btn[0].click() + + # minimize trays so they don't obstruct clicks + trays = browser.find_elements_by_xpath('//div[@data-provides="tray-close"]') + for tray in trays: + tray.click() + multipage = False existing_locations = [] while True: @@ -217,7 +282,14 @@ def main(): ) ) page = duplicate_link[0].get_attribute("href") - browser.get(page) + + # This is a quick workaround for roles with multiple + # _different_ job posts within it + if search('12345', page): + browser.get(page) + else: + print(page) + continue job_name_txt = browser.find_elements_by_xpath( '//input[../label="Job Name"]' @@ -225,6 +297,9 @@ def main(): job_name = ( job_name_txt.get_attribute("value").replace("Copy of ", "").strip() ) + + print(job_name) + job_name_txt.clear() job_name_txt.send_keys(job_name) @@ -251,6 +326,7 @@ def main(): publish_location = browser.find_elements_by_xpath( '//input[@placeholder="Select location"]' )[0] + publish_location.clear() publish_location.send_keys(publish_location_text) popup_menu_xpath = ( f'//ul[contains(@class, "ui-menu")]' From c1bf6f0ef09def072e865fb7f7abc91cfb074762 Mon Sep 17 00:00:00 2001 From: "David A. Desrosiers" Date: Mon, 28 Jun 2021 10:28:53 -0400 Subject: [PATCH 03/13] Updated README.md to reflect changes in _this_ forked version of the repository --- README.md | 67 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 739ba2a..1746a86 100644 --- a/README.md +++ b/README.md @@ -3,19 +3,48 @@ ## Install ```bash -git clone git@github.com:tvansteenburgh/greenhouse.git +git clone https://github.com/desrod/greenhouse cd greenhouse -python3 -m venv .venv -source .venv/bin/activate -pip install selenium +python3 -m venv .env +source .env/bin/activate +pip install selenium appdirs ``` If you are using Chrome you'll need to install the appropriate version of ChromeDriver for your browser, see https://chromedriver.chromium.org/downloads. -## Duplicate job posts to different locations +You'll also need to put this in your default `$PATH`, or export a new `$PATH` environment variable to include this location, eg: + +```bash +export PATH=$PATH:/opt/bin +``` + +## Adding your credentials to be used for the cloning/duplication +--- + The former automation this was forked from used environment variables to hodl the SSO_EMAIL and SSO_PASSWORD. This forked version puts these in a managed file in `~/.local/share/greenhouse/` called `login.tokens`, with the following format: + +``` bash +{ + "username": "your.name@canonical.com", + "password": "So0perSe3kre7P4ssw0rd" +} +``` +Keep in mind that any 'foreign' characters you may have in your password may need to be escaped in this file, so it will be parsed correctly. For example, if you have a forward-slash '/' in your password, you'd need to escape that as follows: + +``` bash + "password": "So0per\\/Se3kre7\\/P4ssw0rd" +``` + +The same rule applies for backslashes: +``` bash + "password": "So0per\\\\Se3kre7\\\\P4ssw0rd" +``` +Protect this file with your standard operating system permissions. + +## Duplicate job posts to different locations +--- Start with a Greenhouse job with one job posting (the one that will be duplicated). @@ -31,38 +60,44 @@ Now run the script and tell it which job id(s) you want to update with new job posts, and which regions you want to post in: ```bash -SSO_EMAIL=myemail@gmail.com \ -SSO_PASSWORD=mysecretpassword \ ./post-job.py 1592880 1726996 --region americas emea ``` +> Note: If you're cloning to multiple regions, you **must** provide them at the time of cloning. You cannot clone to 'emea' and then in a second pass, clone to 'americas', as this will fail. + The browser will open and things will happen: -- Script will log you in to Ubuntu SSO and then pause for you to 2fa. If - you don't define SSO env vars, the script will pause for you to auth - manually. If you don't auth within 60 seconds the script will time out. +- Script will log you in to Ubuntu SSO and then pause for you to 2FA. - New posts will be created for each city in the region that doesn't - already have a post. -- The new posts (and any others that are OFF) will be turned ON (made - live on the 'Canonical - Jobs' board). + already have an existing post in that location. +- The new posts (and any others that are currently 'OFF') will be turned ON (made + live on the 'Canonical - Jobs' board). + + > Note: Please make sure you review the posts when complete, so any that you intended to remain off, are disabled after cloning. If the script fails partway through you can safely rerun it, since it won't create a duplicate job post for cities that already have one. -### Browsers +> Note: If the script does fail, it may leave lingering 'chromedriver' processes running. You can kill those off easily with the following: +``` bash +kill -9 $(pgrep -f chromedriver) +``` + +### Browsers +--- Default browser is Chrome but you can alternatively pass the `--browser firefox` option. I have only tested on Chrome. ### Regions -The available regions are `americas`, `emea`, and `apac`. You can +The available regions are `americas`, `emea`, `eu`, `brasil`, and `apac`. You can view/update the lists of cities in those regions directly in the source file. ### Troubleshooting - +--- #### The script crashes with an error about `element click intercepted`. - The script is trying to 'click' on something, but another element on From 76465eb623ef80f87a30b88bfb65149b03197c7a Mon Sep 17 00:00:00 2001 From: "David A. Desrosiers" Date: Wed, 30 Jun 2021 10:25:00 -0400 Subject: [PATCH 04/13] Adding additional cities to help increase velocity in finding candidates --- post-job.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/post-job.py b/post-job.py index 9738fe4..7207bdf 100755 --- a/post-job.py +++ b/post-job.py @@ -26,10 +26,13 @@ "Home based - Americas, Charlotte", "Home based - Americas, Chicago", "Home based - Americas, Colorado", + "Home based - Americas, Columbus", "Home based - Americas, Dallas", + "Home based - Americas, Houston", "Home based - Americas, Los Angeles", "Home based - Americas, Miami", "Home based - Americas, New York", + "Home based - Americas, Philadelphia", "Home based - Americas, Portland", "Home based - Americas, Raleigh", "Home based - Americas, San Francisco", @@ -122,11 +125,15 @@ "Home based - Europe, Zagreb", ], "apac": [ - "Home based - Asia Pacific, Auckland, Auckland", + "Home based - Asia Pacific, Adelaide", + "Home based - Asia Pacific, Auckland", "Home based - Asia Pacific, Bangalore", + "Home based - Asia Pacific, Brisbane", "Home based - Asia Pacific, Beijing", + "Home based - Asia Pacific, Canberra", "Home based - Asia Pacific, Hong Kong", "Home based - Asia Pacific, Hyderabad", + "Home based - Asia Pacific, Melbourne", "Home based - Asia Pacific, Seoul", "Home based - Asia Pacific, Shanghai", "Home based - Asia Pacific, Singapore", From 1b57324394c76b4d8df3fb93d71f1eab9dd38d46 Mon Sep 17 00:00:00 2001 From: "David A. Desrosiers" Date: Fri, 2 Jul 2021 11:22:02 -0400 Subject: [PATCH 05/13] Added a filter for multiple roles in a single job req, updated README with more clarity --- README.md | 59 ++++++++++++++++++++++++++++------------------------- post-job.py | 18 +++++++++++----- 2 files changed, 44 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 1746a86..ef33a6f 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,23 @@ pip install selenium appdirs If you are using Chrome you'll need to install the appropriate version of ChromeDriver for your browser, see -https://chromedriver.chromium.org/downloads. +https://chromedriver.chromium.org/downloads -You'll also need to put this in your default `$PATH`, or export a new `$PATH` environment variable to include this location, eg: +Make sure you choose the version that is in the same major version as your installed version of Chrome. + +``` bash +$ google-chrome --version +Google Chrome 91.0.4472.114 +``` + +You can download the matching version, and unpack it into somewhere in your $PATH, or place it in a location you can append to $PATH later. I've put it into `/opt/bin` here: + +``` bash +$ /opt/bin/chromedriver --version +ChromeDriver 91.0.4472.101 (af52a90bf87030dd1523486a1cd3ae25c5d76c9b-refs/branch-heads/4472@{#1462}) +``` + +You will also need to put this in your default `$PATH`, or export a new `$PATH` environment variable to include this location, eg: ```bash export PATH=$PATH:/opt/bin @@ -22,9 +36,9 @@ export PATH=$PATH:/opt/bin ## Adding your credentials to be used for the cloning/duplication --- - The former automation this was forked from used environment variables to hodl the SSO_EMAIL and SSO_PASSWORD. This forked version puts these in a managed file in `~/.local/share/greenhouse/` called `login.tokens`, with the following format: + The [oroginal automation](https://github.com/tvansteenburgh/greenhouse) this repo was forked from used environment variables to hodl the SSO_EMAIL and SSO_PASSWORD. This forked version puts these in a managed file in `~/.local/share/greenhouse/` called `login.tokens`, with the following format: -``` bash +``` json { "username": "your.name@canonical.com", "password": "So0perSe3kre7P4ssw0rd" @@ -32,34 +46,30 @@ export PATH=$PATH:/opt/bin ``` Keep in mind that any 'foreign' characters you may have in your password may need to be escaped in this file, so it will be parsed correctly. For example, if you have a forward-slash '/' in your password, you'd need to escape that as follows: -``` bash +``` yaml "password": "So0per\\/Se3kre7\\/P4ssw0rd" ``` The same rule applies for backslashes: -``` bash +``` yaml "password": "So0per\\\\Se3kre7\\\\P4ssw0rd" ``` -Protect this file with your standard operating system permissions. +Protect this file with your standard operating system permissions. `chmod 0400` should be sufficient to secure it against any unintentional snooping. ## Duplicate job posts to different locations --- -Start with a Greenhouse job with one job posting (the one that will be -duplicated). +Start with a Greenhouse job with one job posting (the one that will be duplicated). -Get the numeric id for the job. You can get this by going to the job -dashboard and copying the id out of the url. It will look something like -this: +Get the numeric id for the job. You can get this by going to the job dashboard and copying the id out of the url. It will look something like this: `https://canonical.greenhouse.io/sdash/1592880` That number at the end of the url is the job id. -Now run the script and tell it which job id(s) you want to update with -new job posts, and which regions you want to post in: +Now run the script and tell it which job id(s) you want to update with new job posts, and which regions you want to post in: -```bash +``` bash ./post-job.py 1592880 1726996 --region americas emea ``` @@ -68,15 +78,12 @@ new job posts, and which regions you want to post in: The browser will open and things will happen: - Script will log you in to Ubuntu SSO and then pause for you to 2FA. -- New posts will be created for each city in the region that doesn't - already have an existing post in that location. -- The new posts (and any others that are currently 'OFF') will be turned ON (made - live on the 'Canonical - Jobs' board). +- New posts will be created for each city in the region that doesn't already have an existing post in that location. +- The new posts (and any others that are currently 'OFF') will be turned ON (made live on the 'Canonical - Jobs' board). > Note: Please make sure you review the posts when complete, so any that you intended to remain off, are disabled after cloning. -If the script fails partway through you can safely rerun it, since it won't -create a duplicate job post for cities that already have one. +If the script fails partway through you can safely rerun it, since it won't create a duplicate job post for cities that already have one. > Note: If the script does fail, it may leave lingering 'chromedriver' processes running. You can kill those off easily with the following: @@ -92,16 +99,12 @@ I have only tested on Chrome. ### Regions -The available regions are `americas`, `emea`, `eu`, `brasil`, and `apac`. You can -view/update the lists of cities in those regions directly in the source -file. +The available regions are `americas`, `emea`, `eu`, `brasil`, and `apac`. You can view/update the lists of cities in those regions directly in the source file. ### Troubleshooting --- #### The script crashes with an error about `element click intercepted`. -- The script is trying to 'click' on something, but another element on - the page is blocking the click. -- Try making your browser bigger (especially wider). This is most likely - the quickest workaround. +- The script is trying to 'click' on something, but another element on the page is blocking the click. +- Try making your browser bigger (especially wider). This is most likely the quickest workaround. - Create an issue with the full error message and I'll fix it if I can. diff --git a/post-job.py b/post-job.py index 7207bdf..7901299 100755 --- a/post-job.py +++ b/post-job.py @@ -182,6 +182,12 @@ def parse_args(): help="The browser to use (default is chrome)", ) + parser.add_argument( + "--limit", + dest="limit", + help="The specific job post to clone inside a REQ", + ) + return parser.parse_args() @@ -292,11 +298,13 @@ def main(): # This is a quick workaround for roles with multiple # _different_ job posts within it - if search('12345', page): - browser.get(page) - else: - print(page) - continue + if args.limit: + print(f"Limited to: {args.limit}") + if search(args.limit, page): + browser.get(page) + else: + print(page) + # continue job_name_txt = browser.find_elements_by_xpath( '//input[../label="Job Name"]' From 3683599c66a088337e3b384ca8f8f46bc207fea9 Mon Sep 17 00:00:00 2001 From: "David A. Desrosiers" Date: Thu, 8 Jul 2021 13:47:18 -0400 Subject: [PATCH 06/13] Adding additional US/West cities, initial fixes for merged roles (not yet working) --- post-job.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/post-job.py b/post-job.py index 7901299..4a16476 100755 --- a/post-job.py +++ b/post-job.py @@ -20,23 +20,34 @@ REGIONS = { "americas": [ # United States + "Home based - Americas, Anchorage", "Home based - Americas, Atlanta", "Home based - Americas, Austin", + "Home based - Americas, Boise", "Home based - Americas, Boston", "Home based - Americas, Charlotte", "Home based - Americas, Chicago", "Home based - Americas, Colorado", "Home based - Americas, Columbus", "Home based - Americas, Dallas", + "Home based - Americas, Fresno", "Home based - Americas, Houston", + "Home based - Americas, Las Vegas", "Home based - Americas, Los Angeles", "Home based - Americas, Miami", "Home based - Americas, New York", "Home based - Americas, Philadelphia", + "Home based - Americas, Phoenix", "Home based - Americas, Portland", "Home based - Americas, Raleigh", + "Home based - Americas, Sacramento", + "Home based - Americas, Salt Lake City", + "Home based - Americas, San Bernadino", + "Home based - Americas, San Diego", "Home based - Americas, San Francisco", "Home based - Americas, Seattle", + "Home based - Americas, Spokane", + "Home based - Americas, Tacoma", # Canada "Home based - Americas, Calgary", "Home based - Americas, Montreal", @@ -183,9 +194,9 @@ def parse_args(): ) parser.add_argument( - "--limit", - dest="limit", - help="The specific job post to clone inside a REQ", + "--limit", + dest="limit", + help="The specific job post to clone inside a REQ" ) return parser.parse_args() @@ -264,13 +275,22 @@ def main(): multipage = False existing_locations = [] + # job_locations = set() while True: # gather all existing job post locations from each page of results existing_locations += [result.text.strip("()") for result in results] + + # for result in results: + # result = result.text.strip("()") + # job_locations.add(result) + # existing_locations = list(job_locations) + + print("DEBUG existing_locations:", *existing_locations, sep="\n") next_page = browser.find_elements_by_css_selector("a.next_page") if next_page: multipage = True next_page[0].click() + print(browser.find_elements_by_class_name("job-application__offices")) results = wait.until( lambda browser: browser.find_elements_by_class_name( "job-application__offices" @@ -313,8 +333,6 @@ def main(): job_name_txt.get_attribute("value").replace("Copy of ", "").strip() ) - print(job_name) - job_name_txt.clear() job_name_txt.send_keys(job_name) @@ -327,6 +345,7 @@ def main(): location = browser.find_elements_by_xpath( '//label[text()="Location"]/..//input[1]' )[0] + location.clear() location.send_keys(location_text) browser.find_elements_by_xpath('//label[text()="Glassdoor"]/input[1]')[ From 0b7326bd93873b9e1f11461ee2434b5b92f7b774 Mon Sep 17 00:00:00 2001 From: "David A. Desrosiers" Date: Wed, 21 Jul 2021 22:56:51 -0400 Subject: [PATCH 07/13] This is the beginning of a rework, closing a few bugs/issues. Added '--headless' option with 2FA support, also '--reset' for deleting posts prior to re-cloning, also fixed the multi-page update for missed cities, moving code around to separate functions, removed an unnecessary nested auth loop, and other performance tweaks. More to come. --- post-job.py | 226 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 140 insertions(+), 86 deletions(-) diff --git a/post-job.py b/post-job.py index 4a16476..8d7feaf 100755 --- a/post-job.py +++ b/post-job.py @@ -10,12 +10,13 @@ import selenium.webdriver.support.ui as ui from appdirs import user_data_dir from selenium import webdriver +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver.common.by import By from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.keys import Keys -SITE = "https://canonical.greenhouse.io" +gh_url = "https://canonical.greenhouse.io" JOB_BOARD = "Canonical - Jobs" -NO_AUTH = "" REGIONS = { "americas": [ @@ -42,9 +43,9 @@ "Home based - Americas, Raleigh", "Home based - Americas, Sacramento", "Home based - Americas, Salt Lake City", - "Home based - Americas, San Bernadino", - "Home based - Americas, San Diego", - "Home based - Americas, San Francisco", + "Home based - Americas, San Bernadino, California", + "Home based - Americas, San Diego, California", + "Home based - Americas, San Francisco, California", "Home based - Americas, Seattle", "Home based - Americas, Spokane", "Home based - Americas, Tacoma", @@ -154,7 +155,7 @@ ], } - +############################################################### def parse_credentials(): # Read configuration from secured file in $HOME/.config/ creds = os.path.join(user_data_dir("greenhouse"), "login.tokens") @@ -169,6 +170,78 @@ def parse_credentials(): print("file {} does not exist".format(creds)) +############################################################### +def sso_authenticate(browser, args): + (ghsso_user, ghsso_pass) = parse_credentials() + + browser.get(gh_url) + # click Accept Cookies button + accept_cookies_btn = browser.find_elements_by_xpath('//*[@id="cookie-policy-button-accept"]') + if accept_cookies_btn: + accept_cookies_btn[0].click() + + # enter Ubuntu SSO email and password + email_txt = browser.find_element_by_id("id_email") + if email_txt: + email_txt.send_keys(ghsso_user) + + password_txt = browser.find_element_by_id("id_password") + if password_txt: + password_txt.send_keys(ghsso_pass) + + continue_btn = browser.find_elements_by_xpath('//button[@name="continue"]') + if continue_btn: + continue_btn[0].click() + + # accept cookies so the popup doesn't obstruct clicks + cookie_accept_btn = browser.find_elements_by_css_selector("#inform-cookies button") + for btn in cookie_accept_btn: + try: + # click can raise if element exists but is in a hidden block + btn.click() + except selenium.common.exceptions.ElementNotInteractableException: + pass + + # click "Got it" button for new tips + got_it_btn = browser.find_elements_by_xpath('//a[text()="Got it"]') + if got_it_btn: + got_it_btn[0].click() + + # minimize trays so they don't obstruct clicks + trays = browser.find_elements_by_xpath('//div[@data-provides="tray-close"]') + for tray in trays: + tray.click() + + if args.headless: + mfa_token = input("Enter your 2FA token: ") + time.sleep(0.2) + mfa_txt = browser.find_element_by_xpath('//*[@id="id_oath_token"]') + mfa_txt.send_keys(mfa_token) + auth_button = browser.find_elements_by_xpath('//*[@id="login-form"]/button')[0] + auth_button.click() + + +############################################################### +def delete_posts(browser, wait, job_id): + browser.get(f"{gh_url}/plans/{job_id}/jobapp") + job_posts = len(wait.until(lambda browser: browser.find_elements_by_xpath('//*[@id="job_applications"]/tbody/tr'))) + + for i in range(job_posts, 0, -1): + browser.refresh() + + if i > 1: + active_post = browser.find_element(By.CSS_SELECTOR,".job-application:nth-child(2) .unpublish-application-button",).click() + browser.find_element(By.LINK_TEXT, "Unpublish").click() + + # Click options menu (Delete/Duplicate) + browser.find_elements_by_xpath('//*[@id="job_applications"]/tbody/tr[2]/td[3]/div/div[1]')[0].click() + print(f"Deleting post {i} from job {job_id} ...") + browser.find_elements_by_xpath('//*[@id="job_applications"]/tbody/tr[2]/td[3]/div/div[2]/span/a')[0].click() + browser.find_elements_by_xpath('//*[@id="confirm-delete-post"]')[0].click() + time.sleep(0.2) + + +############################################################### def parse_args(): parser = argparse.ArgumentParser( description="Duplicate Greenhouse job postings to multiple locations." @@ -194,108 +267,89 @@ def parse_args(): ) parser.add_argument( - "--limit", - dest="limit", + "--reset", + action="store_true", + help="Delete all posts under a given job_id" + ) + + parser.add_argument( + "--headless", + action="store_true", + help="Run the automation without the GUI" + ) + + parser.add_argument( + "--limit", + dest="limit", help="The specific job post to clone inside a REQ" ) return parser.parse_args() +############################################################### def main(): args = parse_args() options = Options() - # options.add_argument("--headless") + + prefs = { + "profile.default_content_setting_values": { + "plugins": 2, + "popups": 2, + "geolocation": 2, + "notifications": 2, + "fullscreen": 2, + "ssl_cert_decisions": 2, + "site_engagement": 2, + "durable_storage": 2, + } + } + + if args.headless: + options.add_argument("--headless") + options.add_argument("--window-size=1920,1080") + else: + options.add_experimental_option("prefs", prefs) + options.add_argument("disable-infobars") + options.add_argument("--disable-extensions") if args.browser == "firefox": browser = webdriver.Firefox() else: - browser = webdriver.Chrome(options=options) + browser = webdriver.Chrome( + options=options, executable_path="/opt/bin/chromedriver" + ) browser.maximize_window() - new_browser = True - - (ghsso_user, ghsso_pass) = parse_credentials() + sso_authenticate(browser, args) for job_id in args.job_ids: - job_posts_page_url = f"{SITE}/plans/{job_id}/jobapp" + job_posts_page_url = f"{gh_url}/plans/{job_id}/jobapp" browser.get(job_posts_page_url) + wait = ui.WebDriverWait(browser, 60) # timeout after 60 seconds - if new_browser and not NO_AUTH: - # click Accept Cookies button - accept_cookies_btn = browser.find_elements_by_xpath( - '//*[@id="cookie-policy-button-accept"]' - ) - if accept_cookies_btn: - accept_cookies_btn[0].click() - - # enter Ubuntu SSO email and password - email_txt = browser.find_element_by_id("id_email") - if email_txt: - email_txt.send_keys(ghsso_user) - - password_txt = browser.find_element_by_id("id_password") - if password_txt: - password_txt.send_keys(ghsso_pass) - - continue_btn = browser.find_elements_by_xpath('//button[@name="continue"]') - if continue_btn: - continue_btn[0].click() - new_browser = False - - # pause 60 seconds for 2-factor auth by user - wait = ui.WebDriverWait(browser, 60) # timeout after 60 seconds - results = wait.until( - lambda browser: browser.find_elements_by_class_name( - "job-application__offices" - ) - ) + if args.reset: + delete_posts(browser, wait, job_id) + exit() - # accept cookies so the popup doesn't obstruct clicks - cookie_accept_btn = browser.find_elements_by_css_selector( - "#inform-cookies button" - ) - for btn in cookie_accept_btn: - try: - # click can raise if element exists but is in a hidden block - btn.click() - except selenium.common.exceptions.ElementNotInteractableException: - pass - - # click "Got it" button for new tips - got_it_btn = browser.find_elements_by_xpath('//a[text()="Got it"]') - if got_it_btn: - got_it_btn[0].click() - - # minimize trays so they don't obstruct clicks - trays = browser.find_elements_by_xpath('//div[@data-provides="tray-close"]') - for tray in trays: - tray.click() + job_locations = wait.until(lambda browser: browser.find_elements_by_class_name("job-application__offices")) multipage = False existing_locations = [] - # job_locations = set() - while True: - # gather all existing job post locations from each page of results - existing_locations += [result.text.strip("()") for result in results] - # for result in results: - # result = result.text.strip("()") - # job_locations.add(result) - # existing_locations = list(job_locations) + # gather all existing job post locations from each page of results + existing_locations += [result.text.strip("()") for result in job_locations] + next_page = browser.find_elements_by_class_name('next_page') - print("DEBUG existing_locations:", *existing_locations, sep="\n") - next_page = browser.find_elements_by_css_selector("a.next_page") + while True: if next_page: multipage = True next_page[0].click() - print(browser.find_elements_by_class_name("job-application__offices")) - results = wait.until( - lambda browser: browser.find_elements_by_class_name( - "job-application__offices" - ) - ) + time.sleep(2.5) + more_jobs = browser.find_elements_by_class_name("job-application__offices") + for s in more_jobs: + existing_locations.append(s.text.strip("()")) else: break @@ -306,6 +360,7 @@ def main(): for region in args.regions: region_locations = REGIONS[region] new_locations = set(region_locations) - set(existing_locations) + for location_text in sorted(new_locations): publish_location_text = location_text.split(",")[-1].strip() @@ -324,6 +379,8 @@ def main(): browser.get(page) else: print(page) + else: + browser.get(page) # continue job_name_txt = browser.find_elements_by_xpath( @@ -347,6 +404,7 @@ def main(): )[0] location.clear() location.send_keys(location_text) + print(f"Publishing job {job_id} to {location_text}...") browser.find_elements_by_xpath('//label[text()="Glassdoor"]/input[1]')[ 0 @@ -389,20 +447,16 @@ def main(): ) for btn in publish_btns: btn.click() - time.sleep(0.2) + time.sleep(0.5) while True: - results = wait.until( - lambda browser: browser.find_elements_by_class_name( - "job-application__offices" - ) - ) + results = browser.find_elements_by_class_name("job-application__offices") publish_btns = browser.find_elements_by_css_selector( "tr.job-application.draft img.publish-application-button" ) for btn in publish_btns: btn.click() - time.sleep(0.2) + time.sleep(0.5) next_page = browser.find_elements_by_css_selector("a.next_page") if next_page: next_page[0].click() From eb3644a19323dda123d2f731d2d1f796a65224dc Mon Sep 17 00:00:00 2001 From: "David A. Desrosiers" Date: Wed, 21 Jul 2021 23:01:59 -0400 Subject: [PATCH 08/13] Oops, some more cleanup from the detritus left after running black, removed unused modules --- post-job.py | 36 ++++++++++-------------------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/post-job.py b/post-job.py index 8d7feaf..b1e684a 100755 --- a/post-job.py +++ b/post-job.py @@ -10,9 +10,8 @@ import selenium.webdriver.support.ui as ui from appdirs import user_data_dir from selenium import webdriver -from selenium.common.exceptions import NoSuchElementException -from selenium.webdriver.common.by import By from selenium.webdriver.chrome.options import Options +from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys gh_url = "https://canonical.greenhouse.io" @@ -230,7 +229,7 @@ def delete_posts(browser, wait, job_id): browser.refresh() if i > 1: - active_post = browser.find_element(By.CSS_SELECTOR,".job-application:nth-child(2) .unpublish-application-button",).click() + browser.find_element(By.CSS_SELECTOR,".job-application:nth-child(2) .unpublish-application-button",).click() browser.find_element(By.LINK_TEXT, "Unpublish").click() # Click options menu (Delete/Duplicate) @@ -386,16 +385,12 @@ def main(): job_name_txt = browser.find_elements_by_xpath( '//input[../label="Job Name"]' )[0] - job_name = ( - job_name_txt.get_attribute("value").replace("Copy of ", "").strip() - ) + job_name = (job_name_txt.get_attribute("value").replace("Copy of ", "").strip()) job_name_txt.clear() job_name_txt.send_keys(job_name) - post_to = browser.find_elements_by_xpath( - '//label[text()="Post To"]/..//input[1]' - )[0] + post_to = browser.find_elements_by_xpath('//label[text()="Post To"]/..//input[1]')[0] post_to.send_keys(JOB_BOARD) post_to.send_keys(Keys.ENTER) @@ -406,18 +401,10 @@ def main(): location.send_keys(location_text) print(f"Publishing job {job_id} to {location_text}...") - browser.find_elements_by_xpath('//label[text()="Glassdoor"]/input[1]')[ - 0 - ].click() - browser.find_elements_by_xpath('//label[text()="Indeed"]/input[1]')[ - 0 - ].click() - browser.find_elements_by_xpath('//label[text()="Remote"]/input[1]')[ - 0 - ].click() - publish_location = browser.find_elements_by_xpath( - '//input[@placeholder="Select location"]' - )[0] + browser.find_elements_by_xpath('//label[text()="Glassdoor"]/input[1]')[0].click() + browser.find_elements_by_xpath('//label[text()="Indeed"]/input[1]')[0].click() + browser.find_elements_by_xpath('//label[text()="Remote"]/input[1]')[0].click() + publish_location = browser.find_elements_by_xpath('//input[@placeholder="Select location"]')[0] publish_location.clear() publish_location.send_keys(publish_location_text) popup_menu_xpath = ( @@ -436,21 +423,18 @@ def main(): save_btn = browser.find_elements_by_xpath('//a[text()="Save"]')[0] save_btn.click() - # publish_btns = wait.until(lambda browser: browser.find_elements_by_css_selector('tr.job-application.draft img.publish-application-button')) wait.until( lambda browser: browser.find_elements_by_class_name( "job-application__offices" ) ) - publish_btns = browser.find_elements_by_css_selector( - "tr.job-application.draft img.publish-application-button" - ) + publish_btns = browser.find_elements_by_css_selector("tr.job-application.draft img.publish-application-button") for btn in publish_btns: btn.click() time.sleep(0.5) while True: - results = browser.find_elements_by_class_name("job-application__offices") + job_locations = browser.find_elements_by_class_name("job-application__offices") publish_btns = browser.find_elements_by_css_selector( "tr.job-application.draft img.publish-application-button" ) From fe516420a053089099c5a636a00a203424211ae4 Mon Sep 17 00:00:00 2001 From: "David A. Desrosiers" Date: Thu, 29 Jul 2021 18:23:10 -0400 Subject: [PATCH 09/13] Added a quick fix for cloning a single post on a job with multiple, discrete posts --- post-job.py | 42 +++++++++++++++--------------------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/post-job.py b/post-job.py index b1e684a..8761699 100755 --- a/post-job.py +++ b/post-job.py @@ -339,7 +339,7 @@ def main(): # gather all existing job post locations from each page of results existing_locations += [result.text.strip("()") for result in job_locations] - next_page = browser.find_elements_by_class_name('next_page') + next_page = browser.find_elements_by_class_name("next_page") while True: if next_page: @@ -356,6 +356,9 @@ def main(): if multipage: browser.get(job_posts_page_url) + if args.limit: + print(f"Filtered cloning to post_id: {args.limit}") + for region in args.regions: region_locations = REGIONS[region] new_locations = set(region_locations) - set(existing_locations) @@ -363,28 +366,22 @@ def main(): for location_text in sorted(new_locations): publish_location_text = location_text.split(",")[-1].strip() - duplicate_link = wait.until( - lambda browser: browser.find_elements_by_xpath( - '//*[@id="job_applications"]//tr[1]//a[text()="Duplicate"]' - ) - ) - page = duplicate_link[0].get_attribute("href") + duplicate_link = [] + duplicate_link = wait.until(lambda browser: browser.find_elements_by_xpath('//*[@id="job_applications"]//tr//a[text()="Duplicate"]')) # This is a quick workaround for roles with multiple # _different_ job posts within it if args.limit: - print(f"Limited to: {args.limit}") + result = list(filter(lambda u: args.limit in u.get_attribute("href"),duplicate_link,)) + page = result[0].get_attribute("href") if search(args.limit, page): browser.get(page) - else: - print(page) else: browser.get(page) # continue - job_name_txt = browser.find_elements_by_xpath( - '//input[../label="Job Name"]' - )[0] + job_name_txt = browser.find_elements_by_xpath('//input[../label="Job Name"]')[0] + job_name = (job_name_txt.get_attribute("value").replace("Copy of ", "").strip()) job_name_txt.clear() @@ -394,9 +391,7 @@ def main(): post_to.send_keys(JOB_BOARD) post_to.send_keys(Keys.ENTER) - location = browser.find_elements_by_xpath( - '//label[text()="Location"]/..//input[1]' - )[0] + location = browser.find_elements_by_xpath('//label[text()="Location"]/..//input[1]')[0] location.clear() location.send_keys(location_text) print(f"Publishing job {job_id} to {location_text}...") @@ -412,9 +407,7 @@ def main(): f'/li[contains(@class, "ui-menu-item")]' f'/div[contains(text(), "{publish_location_text}")]' ) - location_choices = wait.until( - lambda browser: browser.find_elements_by_xpath(popup_menu_xpath) - ) + location_choices = wait.until(lambda browser: browser.find_elements_by_xpath(popup_menu_xpath)) publish_location.send_keys(Keys.DOWN) publish_location.send_keys(Keys.TAB) time.sleep(0.5) @@ -423,11 +416,8 @@ def main(): save_btn = browser.find_elements_by_xpath('//a[text()="Save"]')[0] save_btn.click() - wait.until( - lambda browser: browser.find_elements_by_class_name( - "job-application__offices" - ) - ) + wait.until(lambda browser: browser.find_elements_by_class_name("job-application__offices")) + publish_btns = browser.find_elements_by_css_selector("tr.job-application.draft img.publish-application-button") for btn in publish_btns: btn.click() @@ -435,9 +425,7 @@ def main(): while True: job_locations = browser.find_elements_by_class_name("job-application__offices") - publish_btns = browser.find_elements_by_css_selector( - "tr.job-application.draft img.publish-application-button" - ) + publish_btns = browser.find_elements_by_css_selector("tr.job-application.draft img.publish-application-button") for btn in publish_btns: btn.click() time.sleep(0.5) From 2a158f7ada035874c55b34d7423e20e01bb69fb1 Mon Sep 17 00:00:00 2001 From: "David A. Desrosiers" Date: Mon, 9 Aug 2021 18:12:46 -0400 Subject: [PATCH 10/13] More code refactor to support multiple posts in a single role, cleaning out unreachable code --- post-job.py | 116 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 79 insertions(+), 37 deletions(-) diff --git a/post-job.py b/post-job.py index 8761699..644079b 100755 --- a/post-job.py +++ b/post-job.py @@ -10,12 +10,14 @@ import selenium.webdriver.support.ui as ui from appdirs import user_data_dir from selenium import webdriver +# from selenium.common.exceptions import WebDriverException from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys gh_url = "https://canonical.greenhouse.io" JOB_BOARD = "Canonical - Jobs" +# JOB_BOARD = "INTERNAL" REGIONS = { "americas": [ @@ -77,6 +79,25 @@ "Home based - Europe, Rotterdam", "Home based - Europe, Stockholm", "Home based - Europe, Stuttgart", + "Home based - Europe, Gothenburg", + "Home based - Europe, Malmö", + "Home based - Europe, Uppsala", + "Home based - Europe, Västerås", + "Home based - Europe, Örebro", + "Home based - Europe, Linköping", + "Home based - Europe, Helsingborg", + "Home based - Europe, Jönköping", + ], + "austin": [ + "Home based - Americas, Austin, Texas", + "Home based - Americas, Bastrop, Texas", + "Home based - Americas, Buda, Texas", + "Home based - Americas, Fredricksberg, Texas", + "Home based - Americas, San Marcos, Texas", + "Home based - Americas, Dripping Springs, Texas", + "Home based - Americas, Kyle, Texas", + "Home based - Americas, Wimberly, Texas", + "Home based - Americas, Lockhart, Texas" ], "brasil": [ "Home based - Americas, Santiago", @@ -156,6 +177,8 @@ ############################################################### def parse_credentials(): + print("Inside: parse_credentials()") + # Read configuration from secured file in $HOME/.config/ creds = os.path.join(user_data_dir("greenhouse"), "login.tokens") @@ -171,6 +194,7 @@ def parse_credentials(): ############################################################### def sso_authenticate(browser, args): + print("Inside: sso_authenticate()") (ghsso_user, ghsso_pass) = parse_credentials() browser.get(gh_url) @@ -216,8 +240,7 @@ def sso_authenticate(browser, args): time.sleep(0.2) mfa_txt = browser.find_element_by_xpath('//*[@id="id_oath_token"]') mfa_txt.send_keys(mfa_token) - auth_button = browser.find_elements_by_xpath('//*[@id="login-form"]/button')[0] - auth_button.click() + auth_button = browser.find_elements_by_xpath('//*[@id="login-form"]/button')[0].click() ############################################################### @@ -242,6 +265,7 @@ def delete_posts(browser, wait, job_id): ############################################################### def parse_args(): + print("Inside: parse_args()") parser = argparse.ArgumentParser( description="Duplicate Greenhouse job postings to multiple locations." ) @@ -254,7 +278,7 @@ def parse_args(): "--region", dest="regions", nargs="+", - choices=["americas", "eu", "brasil", "emea", "apac"], + choices=["americas", "eu", "brasil", "austin", "emea", "apac"], help="The regions in which to create job postings", ) parser.add_argument( @@ -317,7 +341,7 @@ def main(): browser = webdriver.Firefox() else: browser = webdriver.Chrome( - options=options, executable_path="/opt/bin/chromedriver" + options=options ) browser.maximize_window() @@ -332,32 +356,37 @@ def main(): delete_posts(browser, wait, job_id) exit() - job_locations = wait.until(lambda browser: browser.find_elements_by_class_name("job-application__offices")) - multipage = False existing_locations = [] - # gather all existing job post locations from each page of results - existing_locations += [result.text.strip("()") for result in job_locations] - next_page = browser.find_elements_by_class_name("next_page") - while True: - if next_page: + # Ensure page navigation and job locations have had sufficient time to load + next_page = wait.until(lambda browser: browser.find_elements_by_class_name("next_page")) + job_locations = wait.until(lambda browser: browser.find_elements_by_class_name("job-application__offices")) + + # gather all existing job post locations from each page of results + existing_locations += [result.text.strip("()") for result in job_locations] + + print(*existing_locations, sep = "\n") + + if not next_page: + print("ERROR: 'next_page' element not found. GH page formatting changed?") + return + + if "disabled" not in next_page[0].get_attribute("class"): + print(next_page[0].get_attribute("class")) multipage = True next_page[0].click() - time.sleep(2.5) - more_jobs = browser.find_elements_by_class_name("job-application__offices") - for s in more_jobs: - existing_locations.append(s.text.strip("()")) else: break + + time.sleep(1.5) # return to first page of job posts if multipage: browser.get(job_posts_page_url) - if args.limit: - print(f"Filtered cloning to post_id: {args.limit}") + time.sleep(1.5) for region in args.regions: region_locations = REGIONS[region] @@ -366,21 +395,29 @@ def main(): for location_text in sorted(new_locations): publish_location_text = location_text.split(",")[-1].strip() - duplicate_link = [] - duplicate_link = wait.until(lambda browser: browser.find_elements_by_xpath('//*[@id="job_applications"]//tr//a[text()="Duplicate"]')) + duplicate_link = wait.until(lambda browser: browser.find_elements_by_xpath( + '//*[@id="job_applications"]//tr//a[text()="Duplicate"]' + ) + ) # This is a quick workaround for roles with multiple # _different_ job posts within it if args.limit: result = list(filter(lambda u: args.limit in u.get_attribute("href"),duplicate_link,)) page = result[0].get_attribute("href") + print(page) if search(args.limit, page): browser.get(page) else: + page = duplicate_link[0].get_attribute("href") + print(page) browser.get(page) # continue - job_name_txt = browser.find_elements_by_xpath('//input[../label="Job Name"]')[0] + time.sleep(2.5) + browser.refresh() + job_name_txt = browser.find_elements_by_xpath('//input[contains(@class, "Input__InputElem-ipbxf8-0")]')[0] + print(job_name_txt) job_name = (job_name_txt.get_attribute("value").replace("Copy of ", "").strip()) @@ -396,9 +433,17 @@ def main(): location.send_keys(location_text) print(f"Publishing job {job_id} to {location_text}...") - browser.find_elements_by_xpath('//label[text()="Glassdoor"]/input[1]')[0].click() - browser.find_elements_by_xpath('//label[text()="Indeed"]/input[1]')[0].click() - browser.find_elements_by_xpath('//label[text()="Remote"]/input[1]')[0].click() + ## Publish the posts out to our external partner sites + try: + browser.find_elements_by_xpath('//label[text()="Glassdoor"]/input[1]')[0].click() + except: + print("Glassdoor board not available at the moment") + + try: + browser.find_elements_by_xpath('//label[text()="Indeed"]/input[1]')[0].click() + except: + print("Indeed board not available at the moment") + publish_location = browser.find_elements_by_xpath('//input[@placeholder="Select location"]')[0] publish_location.clear() publish_location.send_keys(publish_location_text) @@ -407,7 +452,10 @@ def main(): f'/li[contains(@class, "ui-menu-item")]' f'/div[contains(text(), "{publish_location_text}")]' ) - location_choices = wait.until(lambda browser: browser.find_elements_by_xpath(popup_menu_xpath)) + + location_choices = wait.until( + lambda browser: browser.find_elements_by_xpath(popup_menu_xpath) + ) publish_location.send_keys(Keys.DOWN) publish_location.send_keys(Keys.TAB) time.sleep(0.5) @@ -416,24 +464,18 @@ def main(): save_btn = browser.find_elements_by_xpath('//a[text()="Save"]')[0] save_btn.click() - wait.until(lambda browser: browser.find_elements_by_class_name("job-application__offices")) - + wait.until( + lambda browser: browser.find_elements_by_class_name( + "job-application__offices" + ) + ) + ## Click the "Enable" button on each new post created, to make it live publish_btns = browser.find_elements_by_css_selector("tr.job-application.draft img.publish-application-button") for btn in publish_btns: btn.click() time.sleep(0.5) - while True: - job_locations = browser.find_elements_by_class_name("job-application__offices") - publish_btns = browser.find_elements_by_css_selector("tr.job-application.draft img.publish-application-button") - for btn in publish_btns: - btn.click() - time.sleep(0.5) - next_page = browser.find_elements_by_css_selector("a.next_page") - if next_page: - next_page[0].click() - else: - break + print("All done! Now go bring those candidates through to offers!") if __name__ == "__main__": From fbc85e872469cfcba172b99da7e233431c30a2c3 Mon Sep 17 00:00:00 2001 From: Dan Hill Date: Tue, 10 Aug 2021 13:49:32 -0600 Subject: [PATCH 11/13] Enhanced support for multiple posts within a single role Fixed several bugs due to GH changing element names/structure. Fixed marking posts live due to GH changing how tables get sorted. Applied consistent sleep/wait conditions after each page update. Disabled "--reset" until a fix can be addressed for multiple posts. Reworked tool workflow so each post gets processed when no limit is passed. ``` $ ./post-job.py 1787567 --region americas --headless Enter your 2FA token: 818970 [Harvesting job details] -> Processing page 1 -> Processing page 2 -> Processing page 3 -> Processing page 4 [Creating posts for "Cloud Engineering - open source, remote"] -> Processing americas --> All locations already exist. [Creating posts for "Linux Engineering - open source, remote"] -> Processing americas --> All locations already exist. [Marking all job posts live] -> Processing page 1 -> Processing page 2 -> Processing page 3 -> Processing page 4 All done! Now go bring those candidates through to offers! ``` --- post-job.py | 219 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 125 insertions(+), 94 deletions(-) diff --git a/post-job.py b/post-job.py index 644079b..9c2b70f 100755 --- a/post-job.py +++ b/post-job.py @@ -177,7 +177,7 @@ ############################################################### def parse_credentials(): - print("Inside: parse_credentials()") + #print("Inside: parse_credentials()") # Read configuration from secured file in $HOME/.config/ creds = os.path.join(user_data_dir("greenhouse"), "login.tokens") @@ -194,7 +194,7 @@ def parse_credentials(): ############################################################### def sso_authenticate(browser, args): - print("Inside: sso_authenticate()") + #print("Inside: sso_authenticate()") (ghsso_user, ghsso_pass) = parse_credentials() browser.get(gh_url) @@ -265,7 +265,7 @@ def delete_posts(browser, wait, job_id): ############################################################### def parse_args(): - print("Inside: parse_args()") + #print("Inside: parse_args()") parser = argparse.ArgumentParser( description="Duplicate Greenhouse job postings to multiple locations." ) @@ -353,127 +353,158 @@ def main(): wait = ui.WebDriverWait(browser, 60) # timeout after 60 seconds if args.reset: - delete_posts(browser, wait, job_id) + print("[Disabled] The reset function must be updated to support multiple Canonical jobs.") + #delete_posts(browser, wait, job_id) exit() multipage = False + page = 1 + existing_ids = [] + existing_types = [] + existing_names = [] existing_locations = [] + print(f"[Harvesting job details]") while True: - # Ensure page navigation and job locations have had sufficient time to load + print(f"-> Processing page {page}") + time.sleep(1.5) + + # Ensure page navigation and job details have had sufficient time to load next_page = wait.until(lambda browser: browser.find_elements_by_class_name("next_page")) job_locations = wait.until(lambda browser: browser.find_elements_by_class_name("job-application__offices")) - - # gather all existing job post locations from each page of results + job_names = browser.find_elements_by_class_name("job-application__name") + job_ids = browser.find_elements_by_class_name("job-edit-pencil") + job_types = browser.find_elements_by_class_name("board-column") + + # harvest job details from each page of results + existing_types += [result.text for result in job_types] + existing_ids += [result.get_attribute("href").split("/")[4] for result in job_ids] + existing_names += [result.text.split("\n")[0] for result in job_names] existing_locations += [result.text.strip("()") for result in job_locations] - print(*existing_locations, sep = "\n") - if not next_page: print("ERROR: 'next_page' element not found. GH page formatting changed?") return if "disabled" not in next_page[0].get_attribute("class"): - print(next_page[0].get_attribute("class")) multipage = True + page += 1 next_page[0].click() else: break - time.sleep(1.5) - # return to first page of job posts if multipage: browser.get(job_posts_page_url) + time.sleep(1.5) - time.sleep(1.5) + # Process updates for each `Canonical` job unless a limit arg is passed + if args.limit: + canonical_list = [args.limit] + else: + canonical_list = [existing_ids[i] for i,x in enumerate(existing_types) if "Canonical" == x] + + for canonical_job_id in canonical_list: + canonical_job_name = [existing_names[i] for i,x in enumerate(existing_ids) if canonical_job_id == x][0] + limited_locations = [existing_locations[i] for i,x in enumerate(existing_names) if canonical_job_name in x] + + print(f"[Creating posts for \"{canonical_job_name}\"]") + for region in args.regions: + print(f"-> Processing {region}") + region_locations = REGIONS[region] + new_locations = set(region_locations) - set(limited_locations) + + if not new_locations: + print(f"--> All locations already exist.") + continue + + for location_text in sorted(new_locations): + print(f"--> Processing {location_text}") + break + publish_location_text = location_text.split(",")[-1].strip() + + browser.get(f"{job_posts_page_url}s/new?from=duplicate&greenhouse_job_application_id={canonical_job_id}") + time.sleep(2.5) + + browser.refresh() + job_name_txt = browser.find_elements_by_xpath('//input[contains(@class, "Input__InputElem-ipbxf8-0")]')[0] + + job_name = (job_name_txt.get_attribute("value").replace("Copy of ", "").strip()) + + job_name_txt.clear() + job_name_txt.send_keys(job_name) + + post_to = browser.find_elements_by_xpath('//label[text()="Post To"]/..//input[1]')[0] + post_to.send_keys(JOB_BOARD) + post_to.send_keys(Keys.ENTER) + + location = browser.find_elements_by_xpath('//label[text()="Location"]/..//input[1]')[0] + location.clear() + location.send_keys(location_text) + print(f"Publishing job {job_id} to {location_text}...") + + ## Publish the posts out to our external partner sites + try: + browser.find_elements_by_xpath('//label[text()="Glassdoor"]/input[1]')[0].click() + except: + print("Glassdoor board not available at the moment") + + try: + browser.find_elements_by_xpath('//label[text()="Indeed"]/input[1]')[0].click() + except: + print("Indeed board not available at the moment") + + publish_location = browser.find_elements_by_xpath('//input[@placeholder="Select location"]')[0] + publish_location.clear() + publish_location.send_keys(publish_location_text) + popup_menu_xpath = ( + f'//ul[contains(@class, "ui-menu")]' + f'/li[contains(@class, "ui-menu-item")]' + f'/div[contains(text(), "{publish_location_text}")]' + ) - for region in args.regions: - region_locations = REGIONS[region] - new_locations = set(region_locations) - set(existing_locations) + location_choices = wait.until( + lambda browser: browser.find_elements_by_xpath(popup_menu_xpath) + ) + publish_location.send_keys(Keys.DOWN) + publish_location.send_keys(Keys.TAB) + time.sleep(0.5) - for location_text in sorted(new_locations): - publish_location_text = location_text.split(",")[-1].strip() + # click the Save button + save_btn = browser.find_elements_by_xpath('//a[text()="Save"]')[0] + save_btn.click() - duplicate_link = wait.until(lambda browser: browser.find_elements_by_xpath( - '//*[@id="job_applications"]//tr//a[text()="Duplicate"]' + wait.until( + lambda browser: browser.find_elements_by_class_name( + "job-application__offices" + ) ) - ) - - # This is a quick workaround for roles with multiple - # _different_ job posts within it - if args.limit: - result = list(filter(lambda u: args.limit in u.get_attribute("href"),duplicate_link,)) - page = result[0].get_attribute("href") - print(page) - if search(args.limit, page): - browser.get(page) - else: - page = duplicate_link[0].get_attribute("href") - print(page) - browser.get(page) - # continue - - time.sleep(2.5) - browser.refresh() - job_name_txt = browser.find_elements_by_xpath('//input[contains(@class, "Input__InputElem-ipbxf8-0")]')[0] - print(job_name_txt) - - job_name = (job_name_txt.get_attribute("value").replace("Copy of ", "").strip()) - - job_name_txt.clear() - job_name_txt.send_keys(job_name) - - post_to = browser.find_elements_by_xpath('//label[text()="Post To"]/..//input[1]')[0] - post_to.send_keys(JOB_BOARD) - post_to.send_keys(Keys.ENTER) - - location = browser.find_elements_by_xpath('//label[text()="Location"]/..//input[1]')[0] - location.clear() - location.send_keys(location_text) - print(f"Publishing job {job_id} to {location_text}...") - - ## Publish the posts out to our external partner sites - try: - browser.find_elements_by_xpath('//label[text()="Glassdoor"]/input[1]')[0].click() - except: - print("Glassdoor board not available at the moment") - - try: - browser.find_elements_by_xpath('//label[text()="Indeed"]/input[1]')[0].click() - except: - print("Indeed board not available at the moment") - - publish_location = browser.find_elements_by_xpath('//input[@placeholder="Select location"]')[0] - publish_location.clear() - publish_location.send_keys(publish_location_text) - popup_menu_xpath = ( - f'//ul[contains(@class, "ui-menu")]' - f'/li[contains(@class, "ui-menu-item")]' - f'/div[contains(text(), "{publish_location_text}")]' - ) - - location_choices = wait.until( - lambda browser: browser.find_elements_by_xpath(popup_menu_xpath) - ) - publish_location.send_keys(Keys.DOWN) - publish_location.send_keys(Keys.TAB) - time.sleep(0.5) - # click the Save button - save_btn = browser.find_elements_by_xpath('//a[text()="Save"]')[0] - save_btn.click() + print(f"[Marking all job posts live]") + browser.get(job_posts_page_url) + page = 1 - wait.until( - lambda browser: browser.find_elements_by_class_name( - "job-application__offices" - ) - ) - ## Click the "Enable" button on each new post created, to make it live - publish_btns = browser.find_elements_by_css_selector("tr.job-application.draft img.publish-application-button") - for btn in publish_btns: - btn.click() - time.sleep(0.5) + while True: + print(f"-> Processing page {page}") + time.sleep(1.5) + + # Ensure page navigation and job details have had sufficient time to load + next_page = wait.until(lambda browser: browser.find_elements_by_class_name("next_page")) + if not next_page: + print("ERROR: 'next_page' element not found. GH page formatting changed?") + return + + ## Click the "Enable" button on each new post created, to make it live + publish_btns = browser.find_elements_by_xpath('//tr[@class="job-application draft external"]//img[@class="publish-application-button"]') + for btn in publish_btns: + btn.click() + time.sleep(0.5) + + if "disabled" not in next_page[0].get_attribute("class"): + next_page[0].click() + page += 1 + else: + break print("All done! Now go bring those candidates through to offers!") From 5b729354289bab5a45adcc9364869e73c55f8ee5 Mon Sep 17 00:00:00 2001 From: Dan Hill Date: Tue, 10 Aug 2021 15:51:01 -0600 Subject: [PATCH 12/13] Handle listings without page navigation elements Fixed several bugs with single page listings. Added support for testing Demo Jobs on the INTERNAL board. Removed debug code that should not have been pushed. --- post-job.py | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/post-job.py b/post-job.py index 9c2b70f..9e4fabe 100755 --- a/post-job.py +++ b/post-job.py @@ -370,7 +370,6 @@ def main(): time.sleep(1.5) # Ensure page navigation and job details have had sufficient time to load - next_page = wait.until(lambda browser: browser.find_elements_by_class_name("next_page")) job_locations = wait.until(lambda browser: browser.find_elements_by_class_name("job-application__offices")) job_names = browser.find_elements_by_class_name("job-application__name") job_ids = browser.find_elements_by_class_name("job-edit-pencil") @@ -382,9 +381,9 @@ def main(): existing_names += [result.text.split("\n")[0] for result in job_names] existing_locations += [result.text.strip("()") for result in job_locations] + next_page = browser.find_elements_by_class_name("next_page") if not next_page: - print("ERROR: 'next_page' element not found. GH page formatting changed?") - return + break if "disabled" not in next_page[0].get_attribute("class"): multipage = True @@ -402,7 +401,7 @@ def main(): if args.limit: canonical_list = [args.limit] else: - canonical_list = [existing_ids[i] for i,x in enumerate(existing_types) if "Canonical" == x] + canonical_list = [existing_ids[i] for i,x in enumerate(existing_types) if "Canonical" == x or "INTERNAL" == x] for canonical_job_id in canonical_list: canonical_job_name = [existing_names[i] for i,x in enumerate(existing_ids) if canonical_job_id == x][0] @@ -420,7 +419,6 @@ def main(): for location_text in sorted(new_locations): print(f"--> Processing {location_text}") - break publish_location_text = location_text.split(",")[-1].strip() browser.get(f"{job_posts_page_url}s/new?from=duplicate&greenhouse_job_application_id={canonical_job_id}") @@ -441,18 +439,17 @@ def main(): location = browser.find_elements_by_xpath('//label[text()="Location"]/..//input[1]')[0] location.clear() location.send_keys(location_text) - print(f"Publishing job {job_id} to {location_text}...") ## Publish the posts out to our external partner sites try: browser.find_elements_by_xpath('//label[text()="Glassdoor"]/input[1]')[0].click() except: - print("Glassdoor board not available at the moment") + print("INFO: Glassdoor board not available at the moment") try: browser.find_elements_by_xpath('//label[text()="Indeed"]/input[1]')[0].click() except: - print("Indeed board not available at the moment") + print("INFO: Indeed board not available at the moment") publish_location = browser.find_elements_by_xpath('//input[@placeholder="Select location"]')[0] publish_location.clear() @@ -474,11 +471,7 @@ def main(): save_btn = browser.find_elements_by_xpath('//a[text()="Save"]')[0] save_btn.click() - wait.until( - lambda browser: browser.find_elements_by_class_name( - "job-application__offices" - ) - ) + wait.until(lambda browser: browser.find_elements_by_class_name("job-application__offices")) print(f"[Marking all job posts live]") browser.get(job_posts_page_url) @@ -489,17 +482,18 @@ def main(): time.sleep(1.5) # Ensure page navigation and job details have had sufficient time to load - next_page = wait.until(lambda browser: browser.find_elements_by_class_name("next_page")) - if not next_page: - print("ERROR: 'next_page' element not found. GH page formatting changed?") - return - + wait.until(lambda browser: browser.find_elements_by_class_name("job-application__offices")) + ## Click the "Enable" button on each new post created, to make it live publish_btns = browser.find_elements_by_xpath('//tr[@class="job-application draft external"]//img[@class="publish-application-button"]') for btn in publish_btns: btn.click() time.sleep(0.5) + next_page = browser.find_elements_by_class_name("next_page") + if not next_page: + break + if "disabled" not in next_page[0].get_attribute("class"): next_page[0].click() page += 1 From f481514ded636579b7d3f6ed9bddb7d784333714 Mon Sep 17 00:00:00 2001 From: Richard Zack Date: Mon, 16 Aug 2021 13:10:38 -0400 Subject: [PATCH 13/13] adding several more AMER cities --- post-job.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/post-job.py b/post-job.py index 9e4fabe..3fe05f9 100755 --- a/post-job.py +++ b/post-job.py @@ -22,35 +22,57 @@ REGIONS = { "americas": [ # United States + "Home based - Americas, Albany", "Home based - Americas, Anchorage", "Home based - Americas, Atlanta", "Home based - Americas, Austin", + "Home based - Amercias, Baltimore", "Home based - Americas, Boise", "Home based - Americas, Boston", + "Home based - Americas, Buffalo", "Home based - Americas, Charlotte", "Home based - Americas, Chicago", + "Home based - Americas, Cincinnati", + "Home based - Americas, Cleveland", "Home based - Americas, Colorado", "Home based - Americas, Columbus", "Home based - Americas, Dallas", + "Home based - Americas, Dayton", + "Home based - Americas, Detroit", "Home based - Americas, Fresno", "Home based - Americas, Houston", + "Home based - Americas, Honolulu", + "Home based - Americas, Jacksonville, Florida", + "Home based - Americas, Kansas City", "Home based - Americas, Las Vegas", "Home based - Americas, Los Angeles", "Home based - Americas, Miami", + "Home based - Americas, Milwaukee", + "Home based - Americas, Minneapolis", + "Home based - Americas, Nashville, Tennessee", "Home based - Americas, New York", + "Home based - Americas, Oklahoma City", + "Home based - Americas, Omaha" "Home based - Americas, Philadelphia", "Home based - Americas, Phoenix", "Home based - Americas, Portland", "Home based - Americas, Raleigh", + "Home based - Amercias, Rochester", "Home based - Americas, Sacramento", "Home based - Americas, Salt Lake City", - "Home based - Americas, San Bernadino, California", + "Home based - Americas, San Antonio", + "Home based - Americas, San Bernadino, California", "Home based - Americas, San Diego, California", "Home based - Americas, San Francisco, California", + "Home based - Americas, San Jose, California", "Home based - Americas, Seattle", "Home based - Americas, Spokane", + "Home based - Americas, Syracuse", "Home based - Americas, Tacoma", - # Canada + "Home based - Americas, Tucson", + "Home based - Americas, Tulsa", + "Home based - Americas, Wichita", + # Canada "Home based - Americas, Calgary", "Home based - Americas, Montreal", "Home based - Americas, Ottawa",