Exemple #1
0
    def patch(self, **kwargs):
        """
        PATCH list with search params or with list of candidate ids
        Input data:
            json body having following keys
            "name": Name with which smart list will be created
            "search_params": search parameters for smart list in dictionary format
                or  "candidate_ids": if not search_params then candidate_ids should be present
        :return: smartlist id
        """
        auth_user = request.user

        list_id = kwargs.get('id')
        data = request.get_json(silent=True)

        if not data:
            raise InvalidUsage("Received empty request body")

        if not list_id or not is_number(list_id):
            raise InvalidUsage(
                "Either List ID is not provided or It's not an integer")

        smart_list = Smartlist.query.get(list_id)

        if not smart_list:
            raise InvalidUsage("No SmartList exists with id: %s" % list_id)

        # request data must pass through this function, as this will create data in desired format
        data = validate_and_format_smartlist_patch_data(data, auth_user)

        if data.get('name'):
            smart_list.name = data.get('name')

        if data.get('talent_pipeline_id'):
            smart_list.talent_pipeline_id = data.get('talent_pipeline_id')

        if data.get('search_params'):
            smart_list.search_params = data.get('search_params')
        elif data.get('remove_candidate_ids'):
            SmartlistCandidate.query.filter(
                SmartlistCandidate.smartlist_id == smart_list.id,
                SmartlistCandidate.candidate_id.in_(
                    data.get('remove_candidate_ids'))).delete()
        elif data.get('add_candidate_ids'):
            for candidate_id in data.get('add_candidate_ids'):
                row = SmartlistCandidate(smartlist_id=smart_list.id,
                                         candidate_id=candidate_id)
                db.session.add(row)

        db.session.commit()

        candidate_ids = data.get('remove_candidate_ids', []) + data.get(
            'add_candidate_ids', [])
        update_candidates_on_cloudsearch(request.oauth_token, candidate_ids)

        return {'smartlist': {'id': smart_list.id}}, 201
Exemple #2
0
def validate_candidate_ids(candidate_ids, user):

    if not isinstance(candidate_ids, list):
        raise InvalidUsage("`candidate_ids` should be in list format.")

    if filter(lambda x: not isinstance(x, (int, long)), candidate_ids):
        raise InvalidUsage("`candidate_ids` should be list of whole numbers")

    # Remove duplicate ids, in case user accidentally added duplicates
    candidate_ids = list(set(candidate_ids))

    # Check if provided candidate ids are present in our database and also belongs to auth user's domain
    if not validate_candidate_ids_belongs_to_user_domain(candidate_ids, user):
        raise ForbiddenError(
            "Provided list of candidates does not belong to user's domain")

    return candidate_ids
Exemple #3
0
def get_candidates_of_talent_pipeline(talent_pipeline,
                                      oauth_token=None,
                                      request_params=None):
    """
    Fetch all candidates of a talent-pipeline
    :param talent_pipeline: TalentPipeline Object
    :param oauth_token: Authorization Token
    :param request_params: Request parameters
    :return: A dict containing info of all candidates according to query parameters
    """

    if request_params is None:
        request_params = dict()

    try:
        if talent_pipeline.search_params:
            request_params.update(json.loads(talent_pipeline.search_params))

    except Exception as e:
        raise InvalidUsage(
            error_message=
            "Search params of talent pipeline or its smartlists are in bad format "
            "because: %s" % e.message)

    request_params['talent_pool_id'] = talent_pipeline.talent_pool_id

    request_params = dict((k, v) for k, v in request_params.iteritems() if v)

    request_params['talent_pipelines'] = str(talent_pipeline.id)

    # Query Candidate Search API to get all candidates of a given talent-pipeline
    is_successful, response = get_candidates_from_search_api(
        request_params,
        generate_jwt_header(oauth_token, talent_pipeline.user_id))

    if not is_successful:
        raise NotFoundError(
            "Couldn't get candidates from candidates search service because: %s"
            % response)
    else:
        return response
Exemple #4
0
    def delete(self, **kwargs):
        """
        Deletes (hides) the smartlist

        :return: Id of deleted smartlist.
        """
        list_id = kwargs.get('id')
        if not list_id:
            return InvalidUsage("List id is required for deleting a list")

        smartlist = Smartlist.query.get(list_id)
        if not smartlist or smartlist.is_hidden:
            raise NotFoundError("List id does not exists")

        if request.user.role.name == 'USER' and smartlist.user.id != request.user.id:
            raise ForbiddenError(
                "Logged-in user doesn't have appropriate permissions to delete this smartlist"
            )

        # check whether smartlist belongs to user's domain
        if request.user.role.name != 'TALENT_ADMIN' and smartlist.user.domain_id != request.user.domain_id:
            raise ForbiddenError(
                "Logged-in user doesn't have appropriate permissions to delete this smartlist"
            )

        smartlist_candidate_ids = SmartlistCandidate.query.with_entities(
            SmartlistCandidate.candidate_id).filter(
                SmartlistCandidate.smartlist_id == list_id).all()
        smartlist_candidate_ids = [
            smartlist_candidate_id[0]
            for smartlist_candidate_id in smartlist_candidate_ids
        ]
        smartlist.delete()
        if smartlist_candidate_ids:
            logger.info(
                "Candidates %s of SmartList %s are going to be updated in Amazon Cloud "
                "Search" % (smartlist_candidate_ids, list_id))
            update_candidates_on_cloudsearch(request.oauth_token,
                                             smartlist_candidate_ids)
        return {'smartlist': {'id': smartlist.id}}
Exemple #5
0
def get_smartlist_candidates(smartlist, oauth_token=None, request_params=None):
    """
    This endpoint will be used to get smartlist candidates for given duration
    :param search_params: search parameters dict
    :param oauth_token: OAuth Token value
    :return:
    """
    if request_params is None:
        request_params = dict()
    if smartlist.talent_pipeline:
        if smartlist.talent_pipeline.search_params:
            try:
                request_params.update(
                    json.loads(smartlist.talent_pipeline.search_params))
            except Exception as e:
                raise InvalidUsage(
                    "Search params of talent pipeline are in bad format because: %s"
                    % e.message)

        request_params[
            'talent_pool_id'] = smartlist.talent_pipeline.talent_pool_id
        request_params['talent_pipelines'] = str(smartlist.talent_pipeline.id)

    if smartlist.search_params:
        request_params['smartlist_ids'] = smartlist.id
    else:
        request_params['dumb_list_ids'] = smartlist.id

    request_params = dict((k, v) for k, v in request_params.iteritems() if v)

    is_successful, response = get_candidates_from_search_api(
        request_params, generate_jwt_header(oauth_token, smartlist.user_id))
    if not is_successful:
        raise NotFoundError("Couldn't get candidates for smartlist %s" %
                            smartlist.id)
    else:
        return response
Exemple #6
0
 def post(self):
     """
     Creates list with search params or with list of candidate ids
     Input data:
         json body having following keys
         "name": Name with which smart list will be created
         "search_params": search parameters for smart list in dictionary format
             or  "candidate_ids": if not search_params then candidate_ids should be present
     :return: smartlist id
     """
     auth_user = request.user
     data = request.get_json(silent=True)
     if not data:
         raise InvalidUsage("Received empty request body")
     # request data must pass through this function, as this will create data in desired format
     data = validate_and_format_smartlist_post_data(data, auth_user)
     smartlist = save_smartlist(
         user_id=auth_user.id,
         name=data.get('name'),
         talent_pipeline_id=data.get('talent_pipeline_id'),
         search_params=data.get('search_params'),
         candidate_ids=data.get('candidate_ids'),
         access_token=request.oauth_token)
     return {'smartlist': {'id': smartlist.id}}, 201
Exemple #7
0
def get_stats_generic_function(container_object,
                               container_name,
                               user=None,
                               from_date_string='',
                               to_date_string='',
                               interval=1,
                               is_update=False,
                               offset=0):
    """
    This method will be used to get stats for talent-pools, talent-pipelines or smartlists.
    :param container_object: TalentPipeline, TalentPool or SmartList Object
    :param container_name:
    :param user: Logged-in user
    :param from_date_string: From Date String (In Client's Local TimeZone)
    :param to_date_string: To Date String (In Client's Local TimeZone)
    :param interval: Interval in days
    :param is_update: Either stats update process is in progress
    :param offset: Timezone offset from utc i.e. if client's lagging 4 hours behind utc, offset value should be -4
    :return:
    """
    if not container_object:
        raise NotFoundError("%s doesn't exist in database" % container_name)

    if user and container_object.user.domain_id != user.domain_id:
        raise ForbiddenError(
            "Logged-in user %s is unauthorized to get stats of %s: %s" %
            (user.id, container_name, container_object.id))

    if not is_number(offset) or math.ceil(abs(float(offset))) > 12:
        raise InvalidUsage(
            "Value of offset should be an integer and less than or equal to 12"
        )

    offset = int(math.ceil(float(offset)))

    current_date_time = datetime.utcnow()

    ninety_days_old_date_time = current_date_time - timedelta(days=90)

    try:
        # To convert UTC time to any other time zone we can use `offset_date_time` with inverted value of offset
        from_date = parse(from_date_string).replace(tzinfo=None) if from_date_string \
            else offset_date_time(ninety_days_old_date_time, -1 * offset)
        to_date = parse(to_date_string).replace(tzinfo=None) if to_date_string else \
            offset_date_time(current_date_time, -1 * offset)
    except Exception as e:
        raise InvalidUsage(
            "Either 'from_date' or 'to_date' is invalid because: %s" %
            e.message)

    if offset_date_time(from_date, offset) < container_object.added_time:
        from_date = offset_date_time(container_object.added_time, -1 * offset)

    if offset_date_time(from_date, offset) < ninety_days_old_date_time:
        raise InvalidUsage(
            "`Stats data older than 90 days cannot be retrieved`")

    if from_date > to_date:
        raise InvalidUsage("`to_date` cannot come before `from_date`")

    if offset_date_time(to_date, offset) > current_date_time:
        raise InvalidUsage("`to_date` cannot be in future")

    if not is_number(interval):
        raise InvalidUsage("Interval '%s' should be integer" % interval)

    interval = int(interval)
    if interval < 1:
        raise InvalidUsage(
            "Interval's value should be greater than or equal to 1 day")

    if container_name == 'TalentPipeline':
        get_stats_for_given_day = get_talent_pipeline_stat_for_given_day
    elif container_name == 'TalentPool':
        get_stats_for_given_day = get_talent_pool_stat_for_a_given_day
    elif container_name == 'SmartList':
        get_stats_for_given_day = get_smartlist_stat_for_a_given_day
    else:
        raise Exception("Container %s is not supported for this method" %
                        container_name)

    list_of_stats_dicts = []

    to_date = to_date.replace(hour=23, minute=59, second=59)
    from_date = from_date.replace(hour=23, minute=59, second=59)

    if is_update:
        to_date -= timedelta(days=interval)

    while to_date.date() >= from_date.date():
        list_of_stats_dicts.append({
            'total_number_of_candidates':
            get_stats_for_given_day(container_object,
                                    offset_date_time(to_date, offset)),
            'added_datetime':
            to_date.date().isoformat(),
        })
        to_date -= timedelta(days=interval)

    reference_stat = get_stats_for_given_day(container_object,
                                             offset_date_time(to_date, offset))
    for index, stat_dict in enumerate(list_of_stats_dicts):
        stat_dict['number_of_candidates_added'] = stat_dict[
            'total_number_of_candidates'] - (
                list_of_stats_dicts[index + 1]['total_number_of_candidates']
                if index + 1 < len(list_of_stats_dicts) else reference_stat)

    return list_of_stats_dicts
Exemple #8
0
    def get(self, **kwargs):
        """Retrieve list information
        List must belong to auth user's domain
        Call this resource from url:
            /v1/smartlists?page=1&page_size=10 :: to retrieve all the smartlists in user's domain
            /v1/smartlists/<int:id> :: to get single smartlist

        example: http://localhost:8008/v1/smartlists/2
        Returns: List in following json format
            {
              "smartlist": {
                "total_found": 3,
                "user_id": 1,
                "id": 1,
                "name": "my list"
                "search_params": {"location": "San Jose, CA"}
              }
            }
        """
        list_id = kwargs.get('id')
        candidate_count = request.args.get('candidate-count', False)

        if not is_number(candidate_count) or int(candidate_count) not in (
                True, False):
            raise InvalidUsage("`candidate_count` field value can be 0 or 1")

        auth_user = request.user
        if list_id:
            smartlist = Smartlist.query.get(list_id)
            if not smartlist or smartlist.is_hidden:
                raise NotFoundError("List id does not exists")
            # check whether smartlist belongs to user's domain
            if request.user.role.name != 'TALENT_ADMIN' and smartlist.user.domain_id != auth_user.domain_id:
                raise ForbiddenError("List does not belong to user's domain")
            return {
                'smartlist':
                create_smartlist_dict(smartlist, request.oauth_token,
                                      int(candidate_count))
            }
        else:
            # Return all smartlists from user's domain
            page = request.args.get('page', DEFAULT_PAGE)
            per_page = request.args.get('per_page', DEFAULT_PAGE_SIZE)
            total_number_of_smartlists = Smartlist.query.join(
                Smartlist.user).filter(User.domain_id == auth_user.domain_id,
                                       Smartlist.is_hidden == 0).count()

            if not is_number(page) or not is_number(
                    per_page) or int(page) < 1 or int(per_page) < 1:
                raise InvalidUsage(
                    "page and per_page should be positive integers")

            page = int(page)
            per_page = int(per_page)

            headers = generate_pagination_headers(total_number_of_smartlists,
                                                  per_page, page)

            response = {
                'smartlists':
                get_all_smartlists(auth_user, request.oauth_token, int(page),
                                   int(per_page), candidate_count),
                'page_number':
                page,
                'smartlists_per_page':
                per_page,
                'total_number_of_smartlists':
                total_number_of_smartlists
            }
            return ApiResponse(response=response, headers=headers, status=200)
Exemple #9
0
def validate_and_format_smartlist_patch_data(smart_list, data, user):
    """Validates request.form data against required parameters
    strips unwanted whitespaces (if present)
    creates list of candidate ids (if present)
    returns list of candidate ids or search params
    """
    smartlist_name = data.get('name', '').strip()
    candidate_ids = data.get('candidate_ids')  # comma separated ids
    search_params = data.get('search_params')
    talent_pipeline_id = data.get('talent_pipeline_id')

    # Both the parameters will create a confusion, so only one parameter should be present. Its better to notify user.
    if candidate_ids and search_params:
        raise InvalidUsage(
            "Bad input: `search_params` and `candidate_ids` both are present. Service accepts only one"
        )
    if search_params and not isinstance(search_params, dict):
        raise InvalidUsage("`search_params` should in dictionary format.")

    talent_pipeline = TalentPipeline.query.get(
        talent_pipeline_id) if talent_pipeline_id else None

    formatted_request_data = {
        'name': smartlist_name,
        'candidate_ids': None,
        'search_params': None,
        'talent_pipeline_id': talent_pipeline_id if talent_pipeline else None
    }

    if candidate_ids:
        if not isinstance(candidate_ids, dict):
            raise InvalidUsage("`candidate_ids` should be in dict format.")

        add_candidate_ids = candidate_ids.get('add', [])
        remove_candidate_ids = candidate_ids.get('remove', [])

        if add_candidate_ids:
            formatted_request_data[
                'add_candidate_ids'] = validate_candidate_ids(
                    add_candidate_ids, user)
            if SmartlistCandidate(
                    SmartlistCandidate.smartlist_id == smart_list.id,
                    SmartlistCandidate.candidate_id.in_(
                        formatted_request_data['add_candidate_ids'])).count():
                raise InvalidUsage(
                    "Some candidates to be added are already the part of smart_list"
                )

        if remove_candidate_ids:
            formatted_request_data[
                'remove_candidate_ids'] = validate_candidate_ids(
                    remove_candidate_ids, user)
            if SmartlistCandidate.query.filter(
                    SmartlistCandidate.smartlist_id == smart_list.id,
                    SmartlistCandidate.candidate_id.in_(
                        formatted_request_data['remove_candidate_ids'])
            ).count() != len(formatted_request_data['remove_candidate_ids']):
                raise InvalidUsage(
                    "Some of candidates to be removed are not the part of smart_list"
                )
    else:
        formatted_request_data['search_params'] = json.dumps(search_params)

    return {
        key: value
        for key, value in formatted_request_data.items() if value
    }
Exemple #10
0
def validate_and_format_smartlist_post_data(data, user):
    """Validates request.form data against required parameters
    strips unwanted whitespaces (if present)
    creates list of candidate ids (if present)
    returns list of candidate ids or search params
    """
    smartlist_name = data.get('name')
    candidate_ids = data.get('candidate_ids')  # comma separated ids
    search_params = data.get('search_params')
    talent_pipeline_id = data.get('talent_pipeline_id')

    if not smartlist_name or not smartlist_name.strip():
        raise InvalidUsage(
            error_message="Missing input: `name` is required for creating list"
        )
    # any of the parameters "search_params" or "candidate_ids" should be present
    if not candidate_ids and not search_params:
        raise InvalidUsage(
            error_message=
            "Missing input: Either `search_params` or `candidate_ids` are required"
        )
    # Both the parameters will create a confusion, so only one parameter should be present. Its better to notify user.
    if candidate_ids and search_params:
        raise InvalidUsage(
            error_message=
            "Bad input: `search_params` and `candidate_ids` both are present. Service accepts only one"
        )
    if search_params:
        # validate if search_params in valid dict format.
        if not isinstance(search_params, dict):
            raise InvalidUsage("`search_params` should in dictionary format.")

    talent_pipeline = TalentPipeline.query.get(
        talent_pipeline_id) if talent_pipeline_id else None

    if not talent_pipeline:
        raise InvalidUsage(
            "Valid talent-pipeline-id is required to create new smartlist")

    smartlist_name = smartlist_name.strip()
    formatted_request_data = {
        'name': smartlist_name,
        'candidate_ids': None,
        'search_params': None,
        'talent_pipeline_id': talent_pipeline.id
    }
    if candidate_ids:
        if not isinstance(candidate_ids, list):
            raise InvalidUsage("`candidate_ids` should be in list format.")
        if filter(lambda x: not isinstance(x, (int, long)), candidate_ids):
            raise InvalidUsage(
                "`candidate_ids` should be list of whole numbers")
        # Remove duplicate ids, in case user accidentally added duplicates
        candidate_ids = list(set(candidate_ids))
        # Check if provided candidate ids are present in our database and also belongs to auth user's domain
        if not validate_candidate_ids_belongs_to_user_domain(
                candidate_ids, user):
            raise ForbiddenError(
                "Provided list of candidates does not belong to user's domain")
        formatted_request_data['candidate_ids'] = candidate_ids
    else:  # if not candidate_ids then it is search_params
        formatted_request_data['search_params'] = json.dumps(search_params)
    return formatted_request_data