def play_selected_bot_thread(self): '''run the bot selected from the dropdown menu.''' job_site_bot_name = self.ui.jobsite_select.currentText() + '_Bot' CommonFuncs.log( self, 'attempting to play selected: %s' % job_site_bot_name) with CommonFuncs.get_db() as db: try: bot = CommonFuncs.get_bot(job_site_bot_name) bot.is_running = True except: bot = JobSiteAccount() bot.site_bot_name = job_site_bot_name bot.is_running = True db.add(bot) db.commit() jobsiteaccount = CommonFuncs.get_bot( job_site_bot_name ) if jobsiteaccount.username is None or jobsiteaccount.password is None: CommonFuncs.log(self, 'no valid login creds available') CommonFuncs.log(self, 'playing of bot canceled') return if bot_threads[job_site_bot_name]['applier'] is None or not bot_threads[job_site_bot_name]['applier'].isRunning(): bot_threads[job_site_bot_name]['applier'] = BotThread(job_site_bot_name) # only build thread, if it doesn't exist bot_threads[job_site_bot_name]['applier'].started.connect(self.bot_thread_started) bot_threads[job_site_bot_name]['applier'].finished.connect(self.bot_thread_finished) bot_threads[job_site_bot_name]['applier'].start() CommonFuncs.log(self, 'playing of %s successful!' % job_site_bot_name) else: CommonFuncs.log(self, 'playing of %s unsuccessful!' % job_site_bot_name)
def job_site_account_select(self): '''load user's account creds for the selected site.''' job_site_bot_name = self.ui.jobsite_select.currentText() + '_Bot' CommonFuncs.log(self, 'starting to find the account creds and stats for the user after job site account select') todo_count = 0 applied_count = 0 try: with CommonFuncs.get_db() as db: todo_count = len(db.query(UnprocessedJob).filter(UnprocessedJob.bot_type == job_site_bot_name).all()) applied_count = len(db.query(Job).filter( and_( Job.job_site == JOB_SITE_LINKS[self.ui.jobsite_select.currentText()]['job_site'], Job.applied == True )).all()) except: pass self.ui.todoforsite_btn.setText(str(todo_count)) self.ui.appliedforsite_btn.setText(str(applied_count)) jobsiteaccount = None jobsiteaccount = CommonFuncs.get_bot(job_site_bot_name) if not jobsiteaccount: self.ui.jobsiteusername_box.setText('') self.ui.jobsitepassword_box.setText('') else: self.ui.jobsiteusername_box.setText(jobsiteaccount.username) self.ui.jobsitepassword_box.setText(jobsiteaccount.password) if jobsiteaccount.is_running: self.ui.playload_lbl.show() else: self.ui.playload_lbl.hide() self.ui.jobsiteusername_box.setStyleSheet( 'background-color: white' ) self.ui.jobsitepassword_box.setStyleSheet( 'background-color: white' ) self.ui.verify_btn.setIcon( QtGui.QIcon( STATIC_FILES[ 'checked' ] ) ) CommonFuncs.log(self, 'finished finding the account creds and stats for the user after job site account select')
def delete_selected_job_site(self): msg = QMessageBox() # show error message msg.setIcon(QMessageBox.Critical) msg.setText("Your login creds and unprocessed jobs will be deleted for this site.") msg.setInformativeText( "Are you sure you want to continue?" ) msg.setWindowTitle("Warning About Deletion: Irreversible") msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) reply = msg.exec() if reply == QMessageBox.Ok: self.ui.deleteload_lbl.show() job_site = self.ui.jobsite_select.currentText() + '_Bot' with CommonFuncs.get_db() as db: # DELETE ACCOUNT jobsiteaccount = CommonFuncs.get_bot( job_site ) if not jobsiteaccount is None: db.delete(jobsiteaccount) db.commit() CommonFuncs.log(self, 'successfully deleted account for: ' + job_site) # DELETE ANY UNPROCESSED JOBS db.query(UnprocessedJob).filter(UnprocessedJob.bot_type == job_site).delete(synchronize_session=False) db.commit() CommonFuncs.log(self, 'successfully deleted all unprocessed jobs for account: ' + job_site) self.job_site_account_select() # refresh job site account section of gui self.ui.deleteload_lbl.hide()
def file_select(self): '''upload a file path to the user's job profile in the db on click in cell.''' CommonFuncs.log(self,'attempting to update a user job file') for currentTableWidgetItem in self.ui.jobprofile_table.selectedItems(): current_row = currentTableWidgetItem.row() cell_text = self.ui.jobprofile_table.verticalHeaderItem(currentTableWidgetItem.row()).text() dlg = None if cell_text == 'resume' or cell_text == 'cover_letter': dlg = QFileDialog() dlg.setFileMode(QFileDialog.AnyFile) elif cell_text == 'supporting_docs': dlg = QFileDialog() dlg.setFileMode(QFileDialog.ExistingFiles) if dlg and dlg.exec_(): filenames = dlg.selectedFiles() with CommonFuncs.get_db() as db: job_profile = db.query(JobProfile).one() setattr(job_profile, cell_text, str(filenames)) db.add(job_profile) db.commit() CommonFuncs.log(self,'successfully committed doc to job profile: %s' % str(filenames)) self.ui.jobprofile_table.setItem( current_row-1, 1, QTableWidgetItem(str(filenames))) break
def job_profile_search_results_finished(self): CommonFuncs.log(self, 'completed query for jobs matching the user job profile') if not self.threads['job_profile_search_results'].isRunning(): # multiple edits may lead to multiple threads self.ui.matchupload_lbl.hide() if self.threads['job_profile_search_results'].results: self.ui.matchup_table.setRowCount(len(self.threads['job_profile_search_results'].results)) i=0 for result in self.threads['job_profile_search_results'].results: self.ui.matchup_table.setItem(i, 0, QTableWidgetItem(result['jobtitle'])) i+=1 else: self.ui.matchup_table.clear() self.ui.matchup_table.setHorizontalHeaderItem(0, QTableWidgetItem('Top Results'))
def job_profile_table_edited(self): '''commit the change to the database and update the results returned from jobs sites for the new profile version.''' jobprofile = None try: with CommonFuncs.get_db() as db: jobprofile = db.query(JobProfile).one() except: pass if not jobprofile: jobprofile = JobProfile() job_profile_fields = JobProfile.__table__.columns.keys() # get the column headers for the Job Profile table i=0 for field in job_profile_fields: if not field == 'id': # skip the id value, which is autoincremented try: cell_text = self.ui.jobprofile_table.item(i,0).text() setattr(jobprofile, field, cell_text) except: pass else: # reset the index, so we don't skip a row i-=1 i+=1 with CommonFuncs.get_db() as db: db.add(jobprofile) db.commit() # if the user has checked that they want to delete unprocessed jobs on job profile edit, delete them if not self.init_process: with CommonFuncs.get_db() as db: try: settings = db.query(JobbybotSettings).one() except: pass if settings.delete_ujobs_on_jprofile_edit == True: db.query(UnprocessedJob).delete(synchronize_session=False) db.commit() CommonFuncs.log(self, 'committed update to job profile for user') CommonFuncs.log(self, 'starting query thread to find jobs related to their profile') self.threads['job_profile_search_results'] = JobProfileResultsThread() self.threads['job_profile_search_results'].started.connect(self.job_profile_search_results_started) self.threads['job_profile_search_results'].finished.connect(self.job_profile_search_results_finished) self.threads['job_profile_search_results'].start()
def verify_job_site_account_thread_finished(self): CommonFuncs.log(self, 'completed verification process of account creds') self.ui.jobsiteusername_box.setEnabled(True) self.ui.jobsitepassword_box.setEnabled(True) self.ui.jobsite_select.setEnabled(True) self.ui.jobsiteaccountcancel_btn.setEnabled(True) self.ui.verifyload_lbl.hide() self.ui.jobsiteaccountcancel_btn.setIcon(QIcon(STATIC_FILES['revert'])) if self.threads['verify_job_site_account'].error == True: self.ui.jobsiteusername_box.setStyleSheet('background-color: rgb(247, 126, 74)') self.ui.jobsitepassword_box.setStyleSheet('background-color: rgb(247, 126, 74)') self.ui.verify_btn.setIcon( QtGui.QIcon(STATIC_FILES['submit'])) # show the site creds need to be verified msg = QMessageBox() # show error message msg.setIcon(QMessageBox.Critical) msg.setText("Job Site Account Verification Failed") msg.setInformativeText("Please correct your username and password and try again.") msg.setWindowTitle("Job Site Login Failed") msg.setStandardButtons(QMessageBox.Ok) msg.exec() else: # COMMIT THE JOB SITE ACCOUNT CREDS jobsitestring = str(self.ui.jobsite_select.currentText()) + '_Bot' jobsiteaccount = None try: jobsiteaccount = CommonFuncs.get_bot(jobsitestring) except: pass if not jobsiteaccount: jobsiteaccount = JobSiteAccount() jobsiteaccount.site_bot_name = jobsitestring jobsiteaccount.username = self.ui.jobsiteusername_box.text() jobsiteaccount.password = self.ui.jobsitepassword_box.text() with CommonFuncs.get_db() as db: db.add(jobsiteaccount) db.commit() CommonFuncs.log(self, 'successfully stored valid account creds') self.ui.verify_btn.setIcon( QtGui.QIcon(STATIC_FILES['checked'])) # show the site creds have been verified self.ui.jobsiteusername_box.setStyleSheet('background-color: rgb(70, 188, 128)') self.ui.jobsitepassword_box.setStyleSheet('background-color: rgb(70, 188, 128)')
def verify_jobsiteaccount_btn_clicked(self): # GET LOGIN CREDS FROM THE GUI jobsitestring = str(self.ui.jobsite_select.currentText()) + '_Bot' jobsiteaccount = JobSiteAccount() jobsiteaccount.site_bot_name = jobsitestring jobsiteaccount.username = self.ui.jobsiteusername_box.text() jobsiteaccount.password = self.ui.jobsitepassword_box.text() CommonFuncs.log(self, 'building thread to verify the new login creds for the selected job site') self.threads['verify_job_site_account'] = VerifyLoginThread(jobsiteaccount=jobsiteaccount) # open thread # SHOW PROGRESS BAR WHILE VERIFYING LOGIN self.threads['verify_job_site_account'].started.connect(self.verify_job_site_account_thread_started) # HIDE PROGRESS BAR WHEN LOGIN VERIFICATION TERMINATES self.threads['verify_job_site_account'].finished.connect(self.verify_job_site_account_thread_finished) # RUN THE THREAD TO VERIFY THE ACCOUNT self.threads['verify_job_site_account'].start() CommonFuncs.log(self, 'finished building thread to verify the new login creds for the selected job site')
def update_stats(self, stats): '''update stats whenever they are emitted by the Stats thread''' CommonFuncs.log('updating stats on gui from db') stats_dict = eval(stats) if self.ui.applied_btn.text().isdigit(): # if the number of applied jobs changes, play the happy popping sound if int(stats_dict['applied']) > int(self.ui.applied_btn.text()): t = Thread(target=CommonFuncs.play_sound, args=(STATIC_FILES['job_applied_pop'],)) t.daemon = True t.start() job_site_bot_name = self.ui.jobsite_select.currentText() + '_Bot' self.ui.todoforsite_btn.setText(str(stats_dict[job_site_bot_name + '_todo'])) self.ui.todoforsite_btn.repaint() self.ui.appliedforsite_btn.setText(str(stats_dict[job_site_bot_name + '_applied'])) self.ui.appliedforsite_btn.repaint() self.ui.todo_btn.setText(str(stats_dict['todo'])) self.ui.todo_btn.repaint() self.ui.processed_btn.setText(str(stats_dict['processed'])) self.ui.processed_btn.repaint() self.ui.applied_btn.setText(str(stats_dict['applied'])) self.ui.applied_btn.repaint()
def pause_selected_bot_thread(self): job_site_bot_name = self.ui.jobsite_select.currentText() + '_Bot' CommonFuncs.log(self, 'attempting to send pause signal to bot: %s' % job_site_bot_name ) with CommonFuncs.get_db() as db: try: bot = CommonFuncs.get_bot(job_site_bot_name) if bot.is_running: self.ui.pauseload_lbl.show() # only show loading gif if there is bot to pause bot.is_running = False db.add(bot) db.commit() except: CommonFuncs.log(self, 'problem sending pause signal for bot: %s' % job_site_bot_name, level='debug') pass CommonFuncs.log(self,'pause signal for %s successfully sent' % job_site_bot_name)
def search_table_btn_clicked(self): '''run query of jobs in db and return results.''' CommonFuncs.log('running query on jobs in db') try: search_string = self.ui.search_box.text() results={ 'jobs':[], } self.ui.jobs_table.clear() job_fields = Job.__table__.columns.keys() with CommonFuncs.get_db() as db: for field in job_fields: field_results = db.query(Job).filter(getattr(Job,field).contains(search_string)).all() if field_results: results['jobs'] += field_results results['jobs'] = list(set(results['jobs'])) # remove duplicates self.ui.jobs_table.setRowCount(len(job_fields)) header = self.ui.jobprofile_table.verticalHeader() header.setSectionResizeMode(QHeaderView.Stretch) i = 0 for field in job_fields: # build headers self.ui.jobs_table.setVerticalHeaderItem(i, QTableWidgetItem(field)) i += 1 results['jobs'].sort(key=lambda x: x.app_date and x.link_to_job, reverse=True) # sort by app_date if results['jobs']: col = 0 self.ui.jobs_table.setColumnCount(len(results['jobs'])) for result in results['jobs']: row = 0 for field in job_fields: cell_val = getattr(result,field) if cell_val is None: cell_val = '' self.ui.jobs_table.setItem(row,col,QTableWidgetItem(str(cell_val))) row+=1 if row>len(job_fields)-1: break col+=1 else: self.ui.jobs_table.setColumnCount(0) except: CommonFuncs.log(self,'query unsuccessful',level='debug') pass CommonFuncs.log('query of jobs in db successful')
def open_job_site_link(self): '''open browser tab to goto the corresponding job site.''' job_site_name = self.ui.jobsite_select.currentText() webbrowser.open( url=JOB_SITE_LINKS[job_site_name]['applied_jobs'], new=2 ) CommonFuncs.log(self, 'opened link to job site for user: '******'applied_jobs'] )
def initialize_gui(self): '''initialize numerous gui settings before launching the application''' # JOB SITE ACCOUNT SECTION self.setup_job_site_account_section() # JOB PROFILE TABLE jobprofile = None try: with CommonFuncs.get_db() as db: jobprofile = db.query(JobProfile).one() except: pass if not jobprofile: jobprofile = JobProfile() job_profile_fields = JobProfile.__table__.columns.keys() self.ui.jobprofile_table.doubleClicked.connect(self.file_select) self.ui.jobprofile_table.setColumnCount(1) self.ui.jobprofile_table.setRowCount(len(job_profile_fields) - 1) self.ui.jobprofile_table.setEditTriggers(QAbstractItemView.DoubleClicked | QAbstractItemView.SelectedClicked) header = self.ui.jobprofile_table.horizontalHeader() header.setSectionResizeMode(QHeaderView.Stretch) # JOB PROFILE TABLE HEADERS i = 0 for field in job_profile_fields: # build headers if not field == 'id': # don't show the id self.ui.jobprofile_table.setVerticalHeaderItem(i, QTableWidgetItem(field)) else: i -= 1 i += 1 # LOAD JOB PROFILE FROM DB i = -1 # when setting table widget values, the index starts at -1 for field in job_profile_fields: if not field == 'id': # don't show the id self.ui.jobprofile_table.setItem(i, 1, QTableWidgetItem(getattr(jobprofile, field))) else: # return to previous row, to avoid skipping a row's header i -= 1 i += 1 CommonFuncs.log(self,'finished loading job profile from db') self.ui.jobprofile_table.setHorizontalHeaderItem(0, QTableWidgetItem('values')) self.ui.jobprofile_table.cellChanged.connect(self.job_profile_table_edited) # JOB PROFILE SEARCH RESULTS self.set_progress_bar_gif(self.ui.matchupload_lbl, static_key='hourglass_big') self.ui.matchupload_lbl.setStyleSheet('background-color: rgb(225,225,225)') self.ui.matchupload_lbl.setAlignment(Qt.AlignCenter) self.ui.matchupload_lbl.hide() self.ui.matchup_table.setColumnCount(1) self.ui.matchup_table.setHorizontalHeaderItem(0, QTableWidgetItem('Top Results')) header = self.ui.matchup_table.horizontalHeader() header.setSectionResizeMode(QHeaderView.Stretch) # DELETE ON JOB PROFILE EDIT -- this must be initialized after table edit function run with CommonFuncs.get_db() as db: try: settings = db.query(JobbybotSettings).one() except: pass if settings.delete_ujobs_on_jprofile_edit == True: self.ui.delete_ujobs_on_jprofile_edit_check.setChecked(True) # SEARCH INTERFACE SETUP self.ui.search_btn.setIcon(QIcon(STATIC_FILES['search'])) self.ui.search_btn.setIconSize(QSize(28, 28)) # STATS THREADS self.threads['stats'] = StatsUpdateThread() self.threads['stats'].stats.connect(self.update_stats) # SEARCH TABLE AND BOX self.ui.tabs.setCurrentIndex(0) # select jobs tab self.ui.search_btn.clicked.connect(self.search_table_btn_clicked) self.ui.search_box.returnPressed.connect(self.search_table_btn_clicked) self.threads['database_tables'] = DatabaseTablesThread() self.threads['database_tables'].update_tables.connect(self.search_table_btn_clicked) self.search_table_btn_clicked()
def __init__(self): self.init_process = True # some processes in functions disabled during initialization if os.path.isfile(LOG_FILE_PATH): os.remove(LOG_FILE_PATH) # delete the log from the last session CommonFuncs.log(self, 'Jobbybot session started') self.user_settings = None # store login creds, job profile, etc self.threads = THREADS_DICT # RESET ALL BOTS TO NOT IS_RUNNING for j_site in JOB_SITE_LINKS: site_bot_name = j_site + '_Bot' with CommonFuncs.get_db() as db: try: bot = CommonFuncs.get_bot(site_bot_name) bot.is_running = False except: bot = JobSiteAccount() bot.is_running = False bot.site_bot_name = site_bot_name db.add(bot) db.commit() CommonFuncs.log(self,'reset %s to not running in db' % site_bot_name) # CHECK FOR SETTINGS OBJECT - create if it does not exist settings = None with CommonFuncs.get_db() as db: try: settings = db.query(JobbybotSettings).one() except: pass if not settings: new_settings = JobbybotSettings() new_settings.connect_to_gsheets = False new_settings.delete_ujobs_on_jprofile_edit = True db.add(new_settings) db.commit() # add settings object to database # START GUI SETUP app = QApplication(sys.argv) self.MainWindow = QtWidgets.QMainWindow() self.ui = Ui_MainWindow() self.ui.setupUi(self.MainWindow) QApplication.setStyle(QStyleFactory.create('Fusion')) self.MainWindow.setWindowIcon(QIcon(STATIC_FILES['logo'])) self.MainWindow.setGeometry(0,60,778,629) self.initialize_gui() CommonFuncs.log(self,'finished initializing gui') CommonFuncs.log(self,'Launching Jobbybot!') self.threads['stats'].start() self.threads['database_tables'].start() # OPEN AND RUN THE GUI self.init_process = False self.MainWindow.show() self.job_profile_table_edited() # initial population of the results for the job profile sys.exit(app.exec_())
def run(self): self.isRunning() self.set_error(False) # reset error Bot_Class = eval(self.site_bot_name) site_name = self.site_bot_name.split('_Bot')[0] spider_name = '_' + site_name.lower() + '_' + 'webcrawler.py' cached_username = '' cached_password = '' logged_in = False # APPLY LOOP bot = CommonFuncs.get_bot(self.site_bot_name) new_links = [''] with CommonFuncs.get_driver(visible=WEB_DRIVERS_VISIBLE, headless=WEB_DRIVERS_HEADLESS) as driver: bot_inst = Bot_Class(driver) while bot.is_running and len(new_links)>0: if cached_username != bot.username or cached_password != bot.password: # if the username or password changed, attempt new login cached_username = bot.username cached_password = bot.password logged_in = bot_inst.login(bot) if logged_in: # if logged in and bot is running, apply to a job with CommonFuncs.get_db() as db: try: new_to_db = False while not new_to_db: unprocessed_job = db.query(UnprocessedJob).filter( UnprocessedJob.bot_type == self.site_bot_name).all() new_link = unprocessed_job[0].job db.delete(unprocessed_job[0]) db.commit() db_results = db.query(Job).filter(Job.link_to_job == new_link).all() if db_results is None or db_results == []: new_to_db = True except: new_link = None pass if not new_link is None: CommonFuncs.log(self, 'attempting to apply to: ' + new_link) new_job = bot_inst.apply(new_link) # goto page and apply if new_job != False and isinstance(new_job, Job): # only add the job to database, if it is an instance with CommonFuncs.get_db() as db: # save job object to db try: db.add(new_job) db.commit() except Exception as e: print(e) else: CommonFuncs.log('applier taking a timeout as it waits for more job links') Jobbybot.run_bot_job_link_webcrawler( spider_name=spider_name ) # start the webcrawler for this bot sleep_count = 5 for i in range(sleep_count): # wait for more results, check to make sure the bot is still running if CommonFuncs.is_bot_running(self.site_bot_name): sleep(1) else: break bot = CommonFuncs.get_bot(self.site_bot_name) sleep(0.1) self.isFinished()