def delete_ats_account(user_id, ats_account_id): """ Remove an ATS account and all of its candidates. :param int ats_account_id: id of the ATS account. :rtype: None """ # First, verify the user and account account = ATSAccount.get(ats_account_id) if not account: raise NotFoundError('delete_ats_account: No such account {}'.format(ats_account_id)) user = User.get(user_id) if not user: raise NotFoundError('delete_ats_account: No such user {}'.format(user_id)) # Next remove all candidates and candidate attributes from the account candidate_list = ATSCandidate.query.filter(ATSCandidate.ats_account_id == ats_account_id).all() for candidate in candidate_list: profile = ATSCandidateProfile.get(candidate.profile_id) ATSCandidateProfile.delete(profile) # Then remove the account credentials credentials = ATSCredential.get(account.ats_credential_id) ATSCredential.delete(credentials) # Remove the account ATSAccount.delete(account) # If this is the only ATS account for this user, mark the user as not ATS enabled all_accounts = ATSAccount.query.filter(ATSAccount.user_id == user_id).all() if not all_accounts: update_dict = {'ats_enabled': False} User.query.filter(User.id == user_id).update(update_dict) db.session.commit()
def get(self, account_id): """ GET /v1/ats-accounts/:account_id Retrieve an ATS accoun. :param account_id: int, id of the ATS account. :rtype string, JSON describing the account. """ ats_service.app.logger.info("{} {} {} {}".format( request.method, request.path, request.user.email, request.user.id)) account = ATSAccount.get_by_id(account_id) if not account: raise NotFoundError( 'Account id not found', additional_error_info=dict(account_id=account_id)) account_dict = account.to_dict() credentials = ATSCredential.get_by_id(account.ats_credential_id) ats = ATS.get_by_id(account.ats_id) account_dict.update({ 'credentials': credentials.credentials_json, 'ats_name': ats.name, 'ats_homepage': ats.homepage_url, 'ats_login': ats.login_url }) response = json.dumps(account_dict) headers = dict(Location=ATSServiceApiUrl.ACCOUNT % account_id) return ApiResponse(response, headers=headers, status=codes.OK)
def post(self, account_id): """ POST /v1/ats-candidates/:account_id Create a new ATS candidate. :param account_id: int, id of the ATS account. :rtype string, JSON indicating success. """ ats_service.app.logger.info("{} {} {} {}".format( request.method, request.path, request.user.email, request.user.id)) data = get_valid_json_data(request) # Validate data fields validate_ats_candidate_data(data) # Validate ATS Account account = ATSAccount.get_by_id(account_id) if not account: raise NotFoundError( 'Account id not found', additional_error_info=dict(account_id=account_id)) # Create the candidate. No attempt to determine if duplicate. candidate = new_ats_candidate(account_id, data) response = json.dumps( dict(id=candidate.id, message="ATS candidate successfully created.")) headers = dict(Location=ATSServiceApiUrl.CANDIDATES % candidate.id) return ApiResponse(response, headers=headers, status=codes.CREATED)
def get(self, account_id): """ GET /v1/ats-candidates/:account_id Retrieve all ATS candidates stored locally associated with an ATS account :param account_id: int, id of the ATS account. :rtype string, JSON with all candidates. """ ats_service.app.logger.info("{} {} {} {}".format( request.method, request.path, request.user.email, request.user.id)) account = ATSAccount.get_by_id(account_id) if not account: raise NotFoundError( 'Account id not found', additional_error_info=dict(account_id=account_id)) candidates = ATSCandidate.get_all(account_id) if candidates: # TODO: Consider adding the ATS-particular entry. response = json.dumps(dict(candidate_list=candidates)) headers = dict(Location=ATSServiceApiUrl.CANDIDATES % account_id) return ApiResponse(response, headers=headers, status=codes.OK) ats_service.app.logger.info( "No candidates in ATS account {}".format(account_id)) response = json.dumps( dict(account_id=account_id, message="No candidates in ATS account {}".format(account_id))) headers = dict(Location=ATSServiceApiUrl.CANDIDATES % account_id) return ApiResponse(response, headers=headers, status=codes.NOT_FOUND)
def match_ats_and_gt_candidates(logger, account_id, method, link=False): """ Search for getTalent condidates which appear to match ATS candidates using a particular method. :param object logger: object to use for logging. :param int account_id: ATS account to search within. :param string method: matching technique to use. :param boolean link: Whether to link the candidates matched or not. :rtype int: Number of matches found. """ if method not in MATCH_DICT: raise UnprocessableEntity("match_ats_and_gt_candidates: Invalid match method", additional_error_info=dict(unsupported_method=method)) match_method = MATCH_DICT[method] account = ATSAccount.get(account_id) if not account: raise NotFoundError('ATS Account id not found', additional_error_info=dict(account_id=account_id)) # Now get the user that owns this account user = User.get(account.user_id) # Get all candidates from user's domain gt_candidate_list = db.session.query(Candidate).join(User).filter(User.domain_id == user.domain_id).all() # Get a list of our ATS candidates ats_candidate_list = ATSCandidate.get_all(account_id) matches = 0 for gt_candidate in gt_candidate_list: for ats_candidate in ats_candidate_list: if match_method(gt_candidate, ats_candidate): matches += 1 if link: link_ats_candidate(gt_candidate.id, ats_candidate.id) return matches
def emails_match(gt_candidate, ats_candidate): """ Determine if there are matching email addresses between a GT candidate and a Workday individual. Workday individuals have only one, but this returns a list so that all ATS may have the same method signature. :param Candidate gt_candidate: getTalent candidate. :param ATSCandidate ats_candidate: Workday individual. :rtype boolean: """ if gt_candidate.is_archived: return False # Get the ATS candidate email address with an ATS-specific static method ats_email_list = ATS_CONSTRUCTORS[ATSAccount.get(ats_candidate.ats_id).name].get_individual_contact_email_addresses(ats_candidate) if not ats_email_list: return False # Compare to GT candidate email address(es) if not gt_candidate.emails: return False # Compare. This makes a 4-deep for loop, but we expect the lists to be very small. For Workday, there'll be only one email address. for gt_email in candidate.emails: for ats_email in ats_email_list: if gt_email == ats_email: return True return False
def fetch_auth_data(account_id): """ Return the values needed to authenticate to an ATS account. :param int account_id: Primary key of the account. :rtype string: ATS name. :rtype string: Login URL. :rtype: tuple[str] | tuple[str] | tuple[int] | tuple[ATSCredential]: """ # Validate ATS Account account = ATSAccount.get(account_id) if not account: raise InvalidUsage("ATS account {} not found.".format(account_id)) if not account.active: return None, None, None, None ats = ATS.get(account.ats_id) if not ats: raise UnprocessableEntity("Invalid ats id", additional_error_info=dict(id=account.ats_id)) credentials = ATSCredential.get(account.ats_credential_id) if not credentials: raise UnprocessableEntity("No credentials for account", additional_error_info=dict(id=account.ats_id)) return ats.name, ats.login_url, account.user_id, credentials
def new_ats_candidate(account_id, data): """ Register an ATS candidate with an ATS account. :param obj account: an ATS account object. :param dict data: keys and values describing the candidate. :rtype: ATSCandidate """ gt_candidate_id = None if 'gt_candidate_id' in data: gt_candidate_id = data.get('gt_candidate_id', None) account = ATSAccount.get(account_id) if not account: raise InvalidUsage("ATS account {} not found.".format(account_id)) profile = ATSCandidateProfile(active=True, profile_json=data['profile_json'], ats_id=account.ats_id) profile.save() candidate = ATSCandidate(ats_account_id=account.id, ats_remote_id=data['ats_remote_id'], gt_candidate_id=gt_candidate_id, profile_id=profile.id) if 'ats_table_id' in data: candidate.ats_table_id = data.get('ats_table_id', None) candidate.save() update_dict = {} now = datetime.datetime.utcnow() update_dict = {'updated_at' : now} account.update(**update_dict) return candidate
def emails_and_phones_match(gt_candidate, ats_candidate): """ Determine if there are matching email addresses and phone numbers between a GT candidate and a Workday individual. Workday indidviduals have only one, but this returns a list so that all ATS may have the same method signature. :param Candidate gt_candidate: getTalent candidate. :param ATSCandidate ats_candidate: Workday individual. :rtype boolean: """ if gt_candidate.is_archived: return False # Get the ATS candidate email address with an ATS-specific static method ats_phone_list = ATS_CONSTRUCTORS[ATSAccount.get(ats_candidate.ats_id).name].get_individual_contact_phone_numbers(ats_candidate) if not ats_phone_list: return False # Compare to GT candidate email address(es) if not gt_candidate.phones: return False # Get the ATS candidate email address with an ATS-specific static method ats_email_list = ATS_CONSTRUCTORS[ATSAccount.get(ats_candidate.ats_id).name].get_individual_contact_email_addresses(ats_candidate) if not ats_email_list: return False # Compare to GT candidate email address(es) if not gt_candidate.emails: return False # Compare emails. for gt_email in candidate.emails: for ats_email in ats_email_list: if gt_email == ats_email: email_match = True # Compare phones. phone_match = normalized_phones_match(candidate.phones, ats_phone_list) return email_match and phone_match
def get(self): """ GET /v1/ats-accounts Retrieve all ATS accounts of a user. :rtype string, JSON describing account. """ ats_service.app.logger.info("{} {} {} {}".format( request.method, request.path, request.user.email, request.user.id)) account_list = dict( ats_accounts=ATSAccount.get_user_accounts(request.user.id)) response = json.dumps(account_list) headers = dict(Location=ATSServiceApiUrl.ACCOUNTS) return ApiResponse(response, headers=headers, status=codes.OK)
def update_ats_account(account_id, new_data): """ Update the values of an ATS account. :param int account_id: primary key of the account. :param dict new_data: New values for the account. :rtype: None """ # Search for the account, complain if it doesn't exist account = ATSAccount.get(account_id) if not account: raise UnprocessableEntity("Invalid ats account id", additional_error_info=dict(id=account_id)) ats = ATS.get(account.ats_id) if not ats: raise UnprocessableEntity("Invalid ats id", additional_error_info=dict(id=account.ats_id)) # Update the ATS info update_dict = {} if 'ats_name' in new_data: update_dict['name'] = new_data['ats_name'] if 'ats_homepage' in new_data: update_dict['homepage_url'] = new_data['ats_homepage'] if 'ats_login' in new_data: update_dict['login_url'] = new_data['ats_login'] if 'ats_auth_type' in new_data: update_dict['auth_type'] = new_data['ats_auth_type'] if len(update_dict) > 0: ats.update(**update_dict) update_dict = {} now = datetime.datetime.utcnow() update_dict = {'updated_at' : now} if 'active' in new_data: if new_data['active'] == "False": update_dict['active'] = False else: update_dict['active'] = True account.update(**update_dict) # If they're changing credentials, find those. Presumably auth_type won't change. if 'ats_credentials' in new_data: credentials = ATSCredential.get(account.ats_credential_id) update_dict = {'credentials_json' : new_data['ats_credentials']} credentials.update(**update_dict)
def post(self): """ POST /v1/ats-accounts/ Register an ATS account for a user. :rtype string, JSON indicating success. """ ats_service.app.logger.info("{} {} {} {}".format( request.method, request.path, request.user.email, request.user.id)) authenticated_user = request.user # data['active'] = True data = get_valid_json_data(request) # Validate data fields validate_ats_account_data(data) # Search for this ATS account entry already existing account = ATSAccount.get_account(authenticated_user.id, data['ats_name']) if account: raise NotFoundError( 'Account id not found', additional_error_info=dict(account_id=account_id)) # Search for ATS entry, create if absent ats = ATS.get_by_name(data['ats_name']) if not ats: ats = new_ats(data) ats_service.app.logger.info( "Adding new ATS account {}, id {}".format(ats.name, ats.id)) # Create ATS account for user account = new_ats_account(authenticated_user.id, ats.id, data) ats_service.app.logger.info("Added new ATS account {}.".format( account.id)) response = json.dumps( dict(id=account.id, message="ATS account successfully created.")) headers = dict(Location=ATSServiceApiUrl.ACCOUNT % account.id) return ApiResponse(response, headers=headers, status=codes.CREATED)
def delete_ats_candidate(candidate_id): """ Remove an ATS candidate from the database. :param int candidate_id: The id of the candidate. :rtype: None """ candidate = ATSCandidate.get(candidate_id) if not candidate: raise InvalidUsage('delete_ats_candidate: No such candidate {}'.format(candidate_id)) profile = ATSCandidateProfile.get(candidate.profile_id) if profile: ATSCandidateProfile.delete(profile) account = ATSAccount.get(candidate.ats_account_id) if not account: raise InvalidUsage("ATS account {} not found.".format(candidate.ats_account_id)) now = datetime.datetime.utcnow() update_dict = {'updated_at' : now} account.update(**update_dict)
def update_ats_candidate(account_id, candidate_id, new_data): """ Update the profile of an ATS candidate. :param int account_id: primary key of the account the candidate belongs to. :param int candidate_id: primary key of the candidate. :param dict new_data: values to update for the candidate. :rtype: None """ if 'profile_json' not in new_data: raise InvalidUsage("profile_json not found.") # Validate ATS Account account = ATSAccount.get(account_id) if not account: raise InvalidUsage("ATS account {} not found.".format(account_id)) # Validate candidate id candidate = ATSCandidate.get(candidate_id) if not candidate: raise UnprocessableEntity("Invalid candidate id", additional_error_info=dict(id=account.candidate_id)) # Validate profile id profile = ATSCandidateProfile.get(candidate.profile_id) if not profile: raise UnprocessableEntity("Invalid candidate profile id", additional_error_info=dict(id=candidate.profile_id)) if 'ats_table_id' in new_data: candidate.ats_table_id = new_data.get('ats_table_id', None) now = datetime.datetime.utcnow() update_dict = {'profile_json' : new_data['profile_json'], 'updated_at' : now} profile.update(**update_dict) update_dict = {'updated_at' : now} candidate.update(**update_dict) account.update(**update_dict) return candidate
def new_ats_account(user_id, ats_id, data): """ Register an ATS account for a user. :param int user_id: id of the user to associate the account with. :param int ats_id: id of the ATS system. :param dict data: keys and values describing the account. :rtype: ATS """ # Create account and credential entries account = ATSAccount(active=True, ats_id=ats_id, user_id=user_id, ats_credential_id=0) account.save() credentials = ATSCredential(ats_account_id=0, auth_type=data['ats_auth_type'], credentials_json=data['ats_credentials']) credentials.save() # Now make the two rows point to each other update_dict = {'ats_credential_id': credentials.id} account.update(**update_dict) update_dict = {'ats_account_id': account.id} credentials.update(**update_dict) update_dict = {'ats_enabled': True} User.get(user_id).update(**update_dict) return account