def merge_degrees(self, existing_degrees, new_degrees): """ This method compares degrees of existing education with degrees of new education EducationDegree objects looks like this { 'start_month': 11, 'start_year': 2002, 'end_year': 2006, 'degree_type': u'ms', 'end_month': 12, 'end_time': datetime.datetime(2006, 12, 1, 0, 0), 'degree_title': u'M.Sc', 'gpa_num': 1.5 } We have list of education degrees dict (GtDict) objects that end-user want to add or update in existing degrees of a specific candidate. We will loop over new degrees and will compare each new degree object with all existing candidate's degree objects and if it matches, we will update existing degree object with new degree object (dict) data and will append a tuple (new_degree, existing degree) in degrees list and if it will not match, we will append tuple (new_degree, None) in degrees list. """ degrees = [] for new_degree in new_degrees or []: is_same_degree = False for existing_degree_obj in existing_degrees or []: start_year = new_degree.start_year end_year = new_degree.end_year # If start year needs to be updated, it cannot be greater than existing end year if start_year and not end_year and ( start_year > existing_degree_obj.end_year): raise InvalidUsage( 'Start year ({}) cannot be greater than end year ({})'. format(start_year, existing_degree_obj.end_year)) # If end year needs to be updated, it cannot be less than existing start year if end_year and not start_year and ( end_year < existing_degree_obj.start_year): raise InvalidUsage( 'End year ({}) cannot be less than start year ({})'. format(end_year, existing_degree_obj.start_year)) if existing_degree_obj == new_degree: is_same_degree = True degrees.append((new_degree, existing_degree_obj)) track_edits(update_dict=new_degree, table_name='candidate_education_degree', candidate_id=self.existing_candidate.id, user_id=self.existing_candidate.user_id, query_obj=existing_degree_obj) existing_degree_obj.update(**new_degree) break if not is_same_degree: degrees.append((new_degree, None)) return degrees
def get_json_if_exist(_request): """ Function will ensure data's content-type is JSON, and it isn't empty :type _request: request """ if "application/json" not in _request.content_type: raise InvalidUsage("Request body must be a JSON object", custom_error.INVALID_INPUT) if not _request.get_data(): raise InvalidUsage("Request body cannot be empty", custom_error.MISSING_INPUT) return _request.get_json()
def validate_id_list(key, values): if ',' in values or isinstance(values, list): values = values.split(',') if ',' in values else map(str, values) for value in values: if not value.strip().isdigit(): raise InvalidUsage("`%s` must be comma separated ids" % key) # if multiple values then return as list else single value. return values[0] if values.__len__() == 1 else values else: if not values.strip().isdigit(): raise InvalidUsage("`%s` must be comma separated ids()" % key) return values.strip()
def patch(self, **kwargs): """ Function will update candidate's tag(s) Endpoints: i. PATCH /v1/candidates/:candidate_id/tags ii. PATCH /v1/candidates/:candidate_id/tags/:id Example: >>> url = 'host/v1/candidates/4/tags' or 'host/v1/candidates/4/tags/57' >>> headers = {'Authorization': 'Bearer edo9rdSKN8hYuc1zBWMfLXpXFd4ZbE'} >>> data = {"tags": [{"description": "minority"}, {"description": "remote"}]} >>> requests.patch(url=url, headers=headers, data=json.dumps(data)) """ # Get json data if exists and validate its schema body_dict = get_json_data_if_validated(request, tag_schema, False) # Description is a required field (must not be empty) tags, tag = body_dict.get('tags'), body_dict.get('tag') for tag in tags: tag['name'] = tag['name'].strip().lower( ) # remove whitespaces while validating if not tag['name']: raise InvalidUsage('Tag name is a required field', custom_error.MISSING_INPUT) # Authenticated user, candidate ID, and tag ID authed_user, candidate_id, tag_id = request.user, kwargs[ 'candidate_id'], kwargs.get('id') # If tag_id is provided in the url, it is assumed that only one candidate-tag needs to be updated if tag_id and len(tags) > 1: raise InvalidUsage( "Updating multiple Tags via this resource is not permitted.", custom_error.INVALID_USAGE) # Check for candidate's existence and web-hidden status get_candidate_if_validated(authed_user, candidate_id) # If tag_id is provided in the url, update only one record if tag_id: return { 'updated_tag': update_candidate_tag(candidate_id, tag_id, tag['name'].strip()) } # Update tag(s) updated_tag_ids = update_candidate_tags(candidate_id=candidate_id, tags=tags) # Update cloud search upload_candidate_documents.delay([candidate_id]) return {'updated_tags': [{'id': tag_id} for tag_id in updated_tag_ids]}
def _add_reference(candidate_id, reference_dict): """ Function will insert a record in CandidateReference. Function will check db to prevent adding duplicate records. :rtype: int """ reference_name = reference_dict.get('person_name') duplicate_reference_note = CandidateReference.query.filter_by( candidate_id=candidate_id, person_name=reference_name, comments=reference_dict.get('comments')).first() if not duplicate_reference_note: candidate_reference = CandidateReference(**reference_dict) db.session.add(candidate_reference) db.session.flush() return candidate_reference.id else: raise InvalidUsage( error_message="Reference already exists for candidate", error_code=custom_error.REFERENCE_EXISTS, additional_error_info={ 'reference_id': duplicate_reference_note.id, 'candidate_id': candidate_id })
def update_candidate_tag(candidate_id, tag_id, tag_name): """ Function will update candidate's tag :type candidate_id: int | long :type tag_id: int | long :type tag_name str :param tag_name name of the tag, e.g. 'diligent', 'minority' :rtype dict[int|long] """ tag_obj_from_id = Tag.get(tag_id) if not tag_obj_from_id: raise NotFoundError("Tag ID: {} is not recognized".format(tag_id), custom_error.TAG_NOT_FOUND) # Candidate must already be associated with provided tag_id candidate_tag_query = CandidateTag.query.filter_by( candidate_id=candidate_id, tag_id=tag_id) candidate_tag_object = candidate_tag_query.first() if not candidate_tag_object: raise InvalidUsage( 'Candidate (id = {}) is not associated with Tag (id = {})'.format( candidate_id, tag_id), custom_error.INVALID_USAGE) # If Tag is not found, create it tag_object = Tag.get_by_name(tag_name) if not tag_object: tag_object = Tag(name=tag_name) db.session.add(tag_object) db.session.flush() # Update candidate_tag_query.update( dict(candidate_id=candidate_id, tag_id=tag_object.id)) db.session.commit() return {'id': tag_object.id}
def validate_encoded_json(value): """ This function will validate and decodes a encoded json string """ try: return json.loads(value) except Exception as e: raise InvalidUsage( error_message="Encoded JSON %s couldn't be decoded because: %s" % (value, e.message))
def get(self): """ Search candidates based on the given filter criteria """ # Authenticated user authed_user = request.user body_dict = request.get_json(silent=True) if body_dict: # In case req-body is empty try: validate(instance=body_dict, schema=candidates_resource_schema_get) except ValidationError as e: raise InvalidUsage(error_message=e.message, error_code=custom_error.INVALID_INPUT) candidate_ids = body_dict.get('candidate_ids') # Candidate IDs must belong to user's domain if not do_candidates_belong_to_users_domain( authed_user, candidate_ids): raise ForbiddenError('Not authorized', custom_error.CANDIDATE_FORBIDDEN) retrieved_candidates = [] for candidate_id in candidate_ids: # Check for candidate's existence and web-hidden status candidate = get_candidate_if_exists(candidate_id) retrieved_candidates.append( fetch_candidate_info(candidate=candidate)) return {'candidates': retrieved_candidates} else: request_vars = validate_and_format_data(request.args) if 'smartlist_ids' in request_vars: request_vars['search_params_list'] = [] smartlist_search_params_list = get_search_params_of_smartlists( request_vars.get('smartlist_ids')) for search_params in smartlist_search_params_list: request_vars['search_params_list'].append( validate_and_format_data(search_params)) # Get domain_id from auth_user domain_id = request.user.domain_id limit = request_vars.get('limit') search_limit = int(limit) if limit else 15 count_only = True if 'count_only' in request.args.get( 'fields', '') else False facets = request.args.get('facets', '') # If limit is not requested then the Search limit would be taken as 15, the default value candidate_search_results = search_candidates( domain_id, request_vars, facets, search_limit, count_only) return candidate_search_results
def validate_sort_by(key, value): # If sorting is present, modify it according to cloudsearch sorting variables. try: sort_by = SORTING_FIELDS_AND_CORRESPONDING_VALUES_IN_CLOUDSEARCH[value] except KeyError: raise InvalidUsage( error_message="sort_by `%s` is not correct input for sorting." % value, error_code=400) return sort_by
def update_candidate_tags(candidate_id, tags): """ Function will update candidate's tag(s) :type candidate_id: int | long :type tags: list[dict] :rtype: list[int|long] """ created_tag_ids, updated_tag_ids = [], [] for tag in tags: tag_name, tag_id = tag['name'], tag.get('id') tag_object = Tag.get_by_name(tag_name) # If Tag is not found, create it if not tag_object: tag_object = Tag(name=tag_name) db.session.add(tag_object) db.session.flush() # Data for updating candidate's tag update_dict = dict(candidate_id=candidate_id, tag_id=tag_object.id) if not tag_id: raise InvalidUsage('Tag ID is required for updating', custom_error.INVALID_USAGE) tag_obj_from_id = Tag.get(tag_id) if not tag_obj_from_id: raise NotFoundError("Tag ID: {} is not recognized".format(tag_id), custom_error.TAG_NOT_FOUND) # Candidate must already be associated with provided tag_id candidate_tag_query = CandidateTag.query.filter_by( candidate_id=candidate_id, tag_id=tag_id) if not candidate_tag_query.first(): raise InvalidUsage( 'Candidate (id = {}) is not associated with Tag (id = {})'. format(candidate_id, tag_id), custom_error.INVALID_USAGE) # Update candidate_tag_query.update(update_dict) updated_tag_ids.append(tag_object.id) db.session.commit() return updated_tag_ids
def update_reference_phone(reference_id, reference_phone_dict): """ Function will update Reference Phone """ reference_phone_query = ReferencePhone.query.filter_by( reference_id=reference_id) if not reference_phone_query.first(): raise InvalidUsage("Unable to update. Reference phone does not exist.") reference_phone_dict.update(reference_id=reference_id) reference_phone_query.update(reference_phone_dict) return
def post(self): """ Upload Candidate Documents to Amazon Cloud Search """ requested_data = request.get_json(silent=True) if not requested_data or 'candidate_ids' not in requested_data: raise InvalidUsage( error_message="Request body is empty or invalid") upload_candidate_documents.delay(requested_data.get('candidate_ids')) return '', 204
def validate_fields(key, value): # If `fields` are present, validate and modify `fields` values according to cloudsearch supported return field names. fields = [field.strip() for field in value.split(',') if field.strip()] try: fields = ','.join([ RETURN_FIELDS_AND_CORRESPONDING_VALUES_IN_CLOUDSEARCH[field] for field in fields ]) except KeyError: raise InvalidUsage( error_message="Field name `%s` is not correct `return field` name" % fields, error_code=400) return fields
def delete(self): """ Delete Candidate Documents from Amazon Cloud Search """ requested_data = request.get_json(silent=True) if not requested_data or 'candidate_ids' not in requested_data: raise InvalidUsage( error_message="Request body is empty or invalid") delete_candidate_documents( candidate_ids=requested_data.get('candidate_ids')) return '', 204
def update_reference_email(reference_id, reference_email_dict): """ Function will update reference-email's info """ # Reference Email must already exist reference_email_query = ReferenceEmail.query.filter_by( reference_id=reference_id) if not reference_email_query.first(): raise InvalidUsage("Unable to update. Reference email does not exist.", custom_error.REFERENCE_NOT_FOUND) reference_email_dict.update(reference_id=reference_id) reference_email_query.update(reference_email_dict) return
def update_reference_web_address(reference_id, reference_web_address_dict): """ Function will update Reference Web Address """ # ReferenceWebAddress must already exist reference_web_query = ReferenceWebAddress.query.filter_by( reference_id=reference_id) if not reference_web_query.first(): raise InvalidUsage( "Unable to update. Reference web address does not exist.") reference_web_address_dict.update(reference_id=reference_id) reference_web_query.update(reference_web_address_dict) return
def get_json_data_if_validated(request_body, json_schema, format_checker=True): """ Function will compare requested json data with provided json schema :type request_body: request :type json_schema: dict :param format_checker: If True, specified formats will need to be validated, e.g. datetime :return: JSON data if validation passes """ try: body_dict = get_json_if_exist(request_body) if format_checker: validate(instance=body_dict, schema=json_schema, format_checker=FormatChecker()) else: validate(instance=body_dict, schema=json_schema) except ValidationError as e: raise InvalidUsage('JSON schema validation error: {}'.format(e), custom_error.INVALID_INPUT) return body_dict
def update_reference(candidate_id, reference_id, reference_dict): """ Function will validate and update Candidate Reference """ candidate_reference_query = CandidateReference.query.filter_by( id=reference_id) candidate_reference_obj = candidate_reference_query.first() # Reference ID must be recognized if not candidate_reference_obj: raise InvalidUsage( "Reference ID ({}) not recognized".format(reference_id)) # CandidateReference must belong to specified candidate if candidate_reference_obj.candidate_id != candidate_id: raise ForbiddenError( "Reference (id={}) does not belong to candidate (id={})".format( reference_id, candidate_id)) candidate_reference_query.update(reference_dict) return
def post(self, **kwargs): """ Function will create tags Note: "description" is a required field Endpoint: POST /v1/candidates/:candidate_id/tags Docs: http://docs.candidatetags.apiary.io/#reference/tags/tags-collections-resource/create-tags Example: >>> url = 'host/v1/candidates/4/tags' >>> headers = {'Authorization': 'Bearer edo9rdSKN8hYuc1zBWMfLXpXFd4ZbE'} >>> data = {"tags": [{"description": "python"}]} >>> requests.post(url=url, headers=headers, data=json.dumps(data)) :return: {'tags': [{'id': int}, {'id': int}, ...]} """ # Get json data if exists and validate its schema body_dict = get_json_data_if_validated(request, tag_schema, False) # Description is a required field (must not be empty) for tag in body_dict['tags']: tag['name'] = tag['name'].strip().lower( ) # remove whitespaces while validating if not tag['name']: raise InvalidUsage('Tag name is a required field', custom_error.MISSING_INPUT) # Authenticated user & candidate ID authed_user, candidate_id = request.user, kwargs['candidate_id'] # Check for candidate's existence and web-hidden status get_candidate_if_validated(authed_user, candidate_id) # Create tags created_tag_ids = create_tags(candidate_id=candidate_id, tags=body_dict['tags']) # Update cloud search upload_candidate_documents.delay([candidate_id]) return {'tags': [{'id': tag_id} for tag_id in created_tag_ids]}, 201
def convert_date(key, value): """ Convert the given date into cloudsearch's desired format and return. Raise error if input date string is not matching the "MM/DD/YYYY" format. """ if value: try: formatted_date = parse(value) # If only date is given without any time (21-06-2016 or 21/06/2016) if re.match(r'\d+[-/]\d+[-/]\d+$', value): if key == "date_from": formatted_date = formatted_date.replace(hour=0, minute=0, second=0) else: formatted_date = formatted_date.replace(hour=23, minute=59, second=59) except ValueError: raise InvalidUsage( "Field `%s` contains incorrect date format. " "Date format should be MM/DD/YYYY (eg. 06/10/2016)" % key) return formatted_date.isoformat( ) + 'Z' # format it as required by cloudsearch.
def add_notes(candidate_id, user_id, data): """ Function will insert candidate notes into the db and return their IDs Notes must have a comment. :type candidate_id: int|long :type data: list[dict] :rtype: list[int] """ created_note_ids = [] for note in data: # Normalize value title = normalize_value(note.get('title')) if note.get('title') else None comments = normalize_value(note.get('comment')) # Notes must have a comment if not comments: raise InvalidUsage('Note must have a comment', custom_error.INVALID_USAGE) notes_dict = dict( candidate_id=candidate_id, owner_user_id=user_id, title=title, comment=comments, added_time=datetime.utcnow() ) notes_dict = dict((k, v) for k, v in notes_dict.iteritems() if v is not None) note_obj = CandidateTextComment(**notes_dict) db.session.add(note_obj) db.session.flush() created_note_ids.append(note_obj.id) db.session.commit() return created_note_ids
def create_or_update_references(candidate_id, references, is_creating=False, is_updating=False, reference_id_from_url=None): """ Function will insert candidate's references' information into db. References' information must include: comments. References' information may include: reference-email dict & reference-phone dict. Empty data will not be added to db Duplicate records will not be added to db :type candidate_id: int|long :type references: list[dict] :type is_creating: bool :type is_updating: bool :type reference_id_from_url: int | long :rtype: list[int] """ created_or_updated_reference_ids = [] for reference in references: candidate_reference_dict = dict( person_name=reference.get('name'), position_title=reference.get('position_title'), comments=reference.get('comments')) # Strip each value & remove keys with empty values candidate_reference_dict = purge_dict(candidate_reference_dict) # Prevent inserting empty records in db if not candidate_reference_dict: continue reference_id = reference_id_from_url or reference.get('id') if not reference_id and is_updating: raise InvalidUsage("Reference ID is required for updating", custom_error.INVALID_USAGE) candidate_reference_dict.update(resume_id=candidate_id, candidate_id=candidate_id) if is_creating: # Add reference_id = _add_reference(candidate_id, candidate_reference_dict) elif is_updating: # Update update_reference(candidate_id, reference_id, candidate_reference_dict) reference_email = reference.get('reference_email') reference_phone = reference.get('reference_phone') reference_web_address = reference.get('reference_web_address') if reference_email: # add reference's email info default_label = EmailLabel.PRIMARY_DESCRIPTION email_label = default_label if not reference_email.get( 'label') else reference_email['label'].strip().title() value = reference_email['address'].strip() if reference_email.get( 'address') else None reference_email_dict = dict( email_label_id=EmailLabel.email_label_id_from_email_label( email_label) if value else None, is_default=reference_email.get('is_default') or True if value else None, value=value) # Remove keys with empty values reference_email_dict = purge_dict(reference_email_dict, strip=False) if reference_email_dict and is_creating: # Add add_reference_email(reference_id, reference_email_dict) elif reference_email_dict and is_updating: # Update update_reference_email(reference_id, reference_email_dict) if reference_phone: # add reference's phone info if provided default_label = PhoneLabel.DEFAULT_LABEL phone_label = default_label if not reference_phone.get( 'label') else reference_phone['label'].strip().title() value = reference_phone['value'].strip() if reference_phone.get( 'value') else None phone_number_dict = format_phone_number(value) if value else None reference_phone_dict = dict( phone_label_id=PhoneLabel.phone_label_id_from_phone_label( phone_label) if phone_number_dict else None, is_default=reference_phone.get('is_default') or True if phone_number_dict else None, value=phone_number_dict.get('formatted_number') if phone_number_dict else None, extension=phone_number_dict.get('extension') if phone_number_dict else None) # Remove keys with empty values reference_phone_dict = purge_dict(reference_phone_dict, strip=False) if reference_phone_dict and is_creating: # Add add_reference_phone(reference_id, reference_phone_dict) elif reference_phone_dict and is_updating: # Update update_reference_phone(reference_id, reference_phone_dict) if reference_web_address: reference_web_address_dict = dict( url=reference_web_address.get('url'), description=reference_web_address.get('description'), ) # Remove keys with empty values & strip each value reference_web_address_dict = purge_dict(reference_web_address_dict) if reference_web_address_dict and is_creating: # Add add_reference_web_address(reference_id, reference_web_address_dict) elif reference_web_address_dict and is_updating: # Update update_reference_web_address(reference_id, reference_web_address_dict) db.session.commit() # Commit transactions to db created_or_updated_reference_ids.append(reference_id) return created_or_updated_reference_ids
def get(self, **kwargs): """ Function will return Pipelines for which given candidate is part of. :rtype: dict[list[dict]] Usage: >>> requests.get('host/v1/candidates/:candidate_id/pipelines') <Response [200]> """ # Authenticated user & candidate ID authed_user, candidate_id = request.user, kwargs['candidate_id'] # Ensure candidate exists and belongs to user's domain get_candidate_if_validated(user=authed_user, candidate_id=candidate_id) # Maximum number of Talent Pipeline objects used for searching. # This is to prevent client from waiting too long for a response max_requests = request.args.get('max_requests', 30) is_hidden = request.args.get('is_hidden', 0) if not is_number(is_hidden) or int(is_hidden) not in (0, 1): raise InvalidUsage('`is_hidden` can be either 0 or 1') # Candidate's talent pool ID candidate_talent_pool_ids = [ tp.talent_pool_id for tp in TalentPoolCandidate.query.filter_by( candidate_id=candidate_id).all() ] added_pipelines = TalentPipelineIncludedCandidates.query.filter_by( candidate_id=candidate_id).all() added_pipelines = map(lambda x: x.talent_pipeline, added_pipelines) logger.info('added_pipelines are:{}. candidate_id:{}'.format( len(added_pipelines), candidate_id)) removed_pipeline_ids = map( lambda x: x[0], TalentPipelineExcludedCandidates.query.with_entities( TalentPipelineExcludedCandidates.talent_pipeline_id).filter_by( candidate_id=candidate_id).all()) # Get User-domain's 30 most recent talent pipelines in order of added time talent_pipelines = TalentPipeline.query.join(User).filter( TalentPipeline.is_hidden == is_hidden, TalentPipeline.talent_pool_id.in_(candidate_talent_pool_ids), TalentPipeline.id.notin_(removed_pipeline_ids)).order_by( TalentPipeline.added_time.desc()).limit(max_requests).all() logger.info( 'Going for CS for {} talent_pipelines for candidate_id:{}'.format( len(talent_pipelines), candidate_id)) # Use Search API to retrieve candidate's domain-pipeline inclusion found_talent_pipelines = [] futures = [] for talent_pipeline in talent_pipelines: search_params = talent_pipeline.search_params if search_params: search_future = search_candidates_from_params( search_params=format_search_params( talent_pipeline.search_params), access_token=request.oauth_token, url_args='?id={}&talent_pool_id={}'.format( candidate_id, talent_pipeline.talent_pool_id), facets='none') search_future.talent_pipeline = talent_pipeline search_future.search_params = search_params futures.append(search_future) # Wait for all the futures to complete completed_futures = wait(futures) for completed_future in completed_futures[0]: if completed_future._result.ok: search_response = completed_future._result.json() if search_response.get('candidates'): found_talent_pipelines.append( completed_future.talent_pipeline) logger.info( "\ncandidate_id: {}\ntalent_pipeline_id: {}\nsearch_params: {}\nsearch_response: {}" .format(candidate_id, completed_future.talent_pipeline.id, completed_future.search_params, search_response)) else: logger.error( "Couldn't get candidates from Search API because %s" % completed_future._result.text) result = [] found_talent_pipelines += added_pipelines found_talent_pipelines = list(set(found_talent_pipelines)) found_talent_pipelines = sorted( found_talent_pipelines, key=lambda talent_pipeline: talent_pipeline.added_time, reverse=True) logger.info("\nFound {} talent_pipelines:{}".format( len(found_talent_pipelines), found_talent_pipelines)) if found_talent_pipelines: pipeline_engagements = top_most_engaged_pipelines_of_candidate( candidate_id) for talent_pipeline in found_talent_pipelines: result.append({ "id": talent_pipeline.id, "candidate_id": candidate_id, "name": talent_pipeline.name, "description": talent_pipeline.description, "open_positions": talent_pipeline.positions, "pipeline_engagement": pipeline_engagements.get(int(talent_pipeline.id), None), "datetime_needed": str(talent_pipeline.date_needed), 'is_hidden': talent_pipeline.is_hidden, "user_id": talent_pipeline.user_id, "added_datetime": str(talent_pipeline.added_time) }) return {'candidate_pipelines': result}
def validate_is_digit(key, value): if not value.isdigit(): raise InvalidUsage("`%s` should be a whole number" % key, 400) return value
def validate_is_number(key, value): if not is_number(value): raise InvalidUsage("`%s` should be a numeric value" % key, 400) return value
def post(self, **kwargs): """ Endpoints: POST /v1/candidates/:candidate_id/custom_fields Usage: >>> headers = {"Authorization": "Bearer access_token", "content-type": "application/json"} >>> data = {"candidate_custom_fields": [{"custom_field_id": 547, "value": "scripted"}]} >>> requests.post(url="hots/v1/candidates/4/custom_fields", headers=headers, data=json.dumps(data)) <Response [201]> :return {'candidate_custom_fields': [{'id': int}, {'id': int}, ...]} """ # Validate data body_dict = get_json_data_if_validated(request, ccf_schema) # Get authenticated user and candidate ID authed_user, candidate_id = request.user, kwargs['candidate_id'] # Candidate must exists and must belong to user's domain candidate = get_candidate_if_validated(authed_user, candidate_id) created_candidate_custom_field_ids = [ ] # aggregate created CandidateCustomField IDs candidate_custom_fields = body_dict['candidate_custom_fields'] for candidate_custom_field in candidate_custom_fields: # Custom field value(s) must not be empty values = filter(None, [value.strip() for value in (candidate_custom_field.get('values') or []) if value]) \ or [candidate_custom_field['value'].strip()] if not values: raise InvalidUsage("Custom field value must be provided.", custom_error.INVALID_USAGE) # Custom Field must be recognized custom_field_id = candidate_custom_field['custom_field_id'] custom_field = CustomField.get_by_id(custom_field_id) if not custom_field: raise NotFoundError( "Custom field ID ({}) not recognized".format( custom_field_id), custom_error.CUSTOM_FIELD_NOT_FOUND) # Custom Field must belong to user's domain if custom_field.domain_id != candidate.user.domain_id: raise ForbiddenError( "Custom field ID ({}) does not belong to user ({})".format( custom_field_id, authed_user.id), custom_error.CUSTOM_FIELD_FORBIDDEN) custom_field_dict = dict(values=values, custom_field_id=custom_field_id) for value in custom_field_dict.get('values'): custom_field_id = candidate_custom_field.get('custom_field_id') # Prevent duplicate insertions if not does_candidate_cf_exist(candidate, custom_field_id, value): added_time = datetime.datetime.utcnow() candidate_custom_field = CandidateCustomField( candidate_id=candidate_id, custom_field_id=custom_field_id, value=value, added_time=added_time) db.session.add(candidate_custom_field) db.session.flush() created_candidate_custom_field_ids.append( candidate_custom_field.id) db.session.commit() upload_candidate_documents.delay([candidate_id]) return { 'candidate_custom_fields': [{ 'id': custom_field_id } for custom_field_id in created_candidate_custom_field_ids] }, http_status_codes.CREATED