def validate_query(self, key, value, op): """ Validate parameters passed to any endpoint for filtering. For filtering by delete the parameter should be a boolean For filtering by date the date should be a valid date The filter operator should be valid delete Example ============== Correct: http://127.0.0.1:5000/api/v1/asset-categories/stats?where= deleted,eq,true Incorrect and will throw an error and return a response: http://127.0.0.1:5000/api/v1/asset-categories/stats?where= deleted,eq,Invalid_delete_value date example ============= Correct: http://127.0.0.1:5000/api/v1/asset-categories/stats?where= created_at,eq,218-06-19 12:06:43.339809 Incorrect and will throw an error and return a response: http://127.0.0.1:5000/api/v1/asset-categories/stats?where= created_at,eq,invalid_date Invalid operator example ======================== http://127.0.0.1:5000/api/v1/asset-categories/stats?where= deleted,invalid_operator,true The essence of this is to validate parameters before making a call to the database, if the parameters are invalid then we dont make any database query which therefore improves perfomance """ fields = [ 'deleted', 'created_at', 'updated_at', 'deleted_at', 'warranty' ] if op not in self.mapper: raise ValidationError( dict(message=filter_errors['INVALID_OPERATOR'])) try: if key in fields: if key in ('created_at', 'updated_at', 'deleted_at', 'warranty'): datetime.strptime(value, "%Y-%m-%d") elif key == 'deleted' and value not in ('true', 'false'): raise ValidationError( dict( message=filter_errors['INVALID_DELETE_ATTRIBUTE'])) except ValueError: raise ValidationError( dict(message=filter_errors['INVALID_DATE'].format(value)))
def post(self): """ Endpoint to create new Task""" data = request.get_json() title = data.get('title') formatted_date = convert_date_to_date_time(data['due_date']) schema = TaskSchema() project_id = data['projectId'] project = Project.get_or_404(project_id) task_exist = Task.find_by_title_and_project_id(title=title, project_id=project.id) if task_exist: raise ValidationError( { "message": serialization_messages['exists'].format('Task with title') }, 409) if not check_date_difference(formatted_date, project.due_date): raise ValidationError( { "message": "Tasks cannot be created under this project because of dat difference" }, 401) assignee_list = check_assignee(data.get('task_assignees'), project.assignees) if assignee_list is not None: user_list = assign_user(assignee_list) data['task_assignees'] = user_list else: data['task_assignees'] = [] assignee_ids = data['task_assignees'] if data[ 'task_assignees'] is not None else [] del data['task_assignees'] task_data = schema.load_object_into_schema(data) task = Task() task.title = data['title'] task.description = data['description'] task.due_date = data['due_date'] task.task_assignees = assignee_ids task.project_id = project.id task.save() task.save() return response('success', message=success_messages['created'].format('Task'), data=schema.dump(task).data, status_code=201)
def check_id_valid(**kwargs): for key in kwargs: if key.endswith("_id") and not is_id_valid(kwargs.get(key, None)): raise ValidationError({ "status": "error", "message": ERROR_MSG["invalid_id"] }, 400)
def post(self): """ Endpoint to create new project""" data = request.get_json() title = data.get('title') user = get_jwt_identity() project_exist = Project.find_by_title_and_user(title=title, user_id=user.get('id')) if project_exist: raise ValidationError( { "message": serialization_messages['exists'].format( 'Project with title') }, 409) data['createdBy'] = user.get("id") project = Project() user_list = assign_user(data.get('assignees')) convert_date_to_date_time(data['due_date']) data['assignees'] = user_list if user_list is not None else [] assignee_ids = data['assignees'] del data['assignees'] schema = ProjectSchema() project_data = schema.load_object_into_schema(data) project.created_by = data['createdBy'] project.title = data['title'] project.description = data['description'] project.due_date = data['due_date'] project.assignees = assignee_ids project.save() return response('success', message=success_messages['created'].format('Project'), data=schema.dump(project).data, status_code=201)
def post(self): """login endpoint """ mapper = { 'testing': self.get_user_testing, 'development': self.get_user, 'production': self.get_user } request_data = request.get_json() user_schema = UserSchema(only=["username", "password"]) user_data = user_schema.load_object_into_schema(request_data) user = mapper[FLASK_ENV](user_data["username"]) token, error = generate_token(user, user_data) if error: return ValidationError(error).to_dict(), 401 return { "status": "success", "message": SUCCESS_MSG["login"], "token": token }, 200
def validate_pagination_args(arg_value, arg_name): """ Validates if the query strings are valid. Arguments: arg_value (string): Query string value arg_name (string): Query string name Raises: ValidationError: Use to raise exception if any error occur Returns: (int) -- Returns True or False """ if arg_name == 'limit' and arg_value == 'None': return 10 # Defaults limit to 10 if not provided if arg_name == 'page' and arg_value == 'None': return 1 # Defaults page to 1 if not provided # Checks if the arg is >= 1 if arg_value.isdigit() and int(arg_value) > 0: return int(arg_value) else: raise ValidationError({ 'message': serialization_errors['invalid_query_strings'].format( arg_name, arg_value) })
def validate_duplicate(model, **kwargs): """ Checks if model instance already exists in database Parameters: model(object): model to run validation on kwargs(dict): keyword arguments containing fields to filter query by """ record_id = kwargs.get('id') kwargs.pop('id', None) # remove id from kwargs if found or return None query = dict(deleted=False, **kwargs) if record_id: result = model.query.filter_by(**query).filter( model.id == record_id).first( ) # selects the first query object for model records if result: return None # return None if query object is found result = model.query.filter_by(**query).first() if result: raise ValidationError( { 'message': serialization_errors['exists'].format( f'{re.sub(r"(?<=[a-z])[A-Z]+",lambda x: f" {x.group(0).lower()}" , model.__name__)}' ) }, 409)
def load_object_into_schema(self, data, partial=False): """Helper function to load python objects into schema""" data, errors = self.load(data, partial=partial) if errors: raise ValidationError( dict(errors=errors, message='An error occurred'), 400) return data
def get(cls, id): """ return entries by id """ value = cls.query.filter_by(id=id, deleted=False).first() if value is None: raise ValidationError({'message': f'{cls.__name__} not found'}) return value
def get_all(cls): """ return all entries """ value = cls.query.filter_by(deleted=False).order_by( cls.due_date.asc()).all() if value is None: raise ValidationError({'message': f'{cls.__name__} not found'}) return value
def get_username_or_404(cls, username): """Get user by username or return 404 """ record = cls.query.filter_by(username=username).first() if not record: raise ValidationError( {"message": ERROR_MSG["not_found"].format("User")}, 404) return record
def filter_query(self, args): """ Returns filtered database entries. An example of filter_condition is: User._query('name,like,john'). Apart from 'like', other comparators are eq(equal to), ne(not equal to), lt(less than), le(less than or equal to) gt(greater than), ge(greater than or equal to) :param filter_condition: :return: an array of filtered records """ raw_filters = args.getlist('where') result = self.query for raw in raw_filters: try: key, op, value = raw.split(',', 3) except ValueError: raise ValidationError( dict(message=filter_errors['INVALID_FILTER_FORMAT'].format( raw))) self.validate_query(key, value, op) column = getattr(self.model, key, None) json_field = getattr(self.model, 'custom_attributes', None) db_filter = self.mapper.get(op) if not column and not json_field: raise ValidationError( dict(message=filter_errors['INVALID_COLUMN'].format(key))) elif not column and json_field: result = result.filter( db_filter( self.model.custom_attributes[key].astext.cast(Unicode), value)) elif str(column.type) == 'DATETIME': result = result.filter(db_filter(func.date(column), value)) else: result = result.filter(db_filter(column, value)) return result
def decorated_function(*args, **kwargs): """Function with decorated function mutations.""" for key in kwargs: if key.endswith('_id') and not is_valid_id(kwargs.get(key, None)): raise ValidationError( { 'status': 'error', 'message': serialization_errors['invalid_id'] }, 400) return func(*args, **kwargs)
def generate_token(user, user_data): """ This method generates a jwt token on login it authenticates/validate the user returns token """ password = user.password candidate_password = user_data['password'] if sha256_crypt.verify(candidate_password, password): exp_time = datetime.datetime.utcnow() + datetime.timedelta(minutes=30) token = jwt.encode( { 'id': user.id, 'username': user.username, 'exp': exp_time }, os.getenv("SECRET")) return token.decode(CHARSET) else: error = ValidationError({'message': ERROR_MESSAGES['AUTH_ERROR']}, 401) return error.to_dict()
def delete(self, task_id): """ Delete a single task :param task_id: :return: """ user = get_jwt_identity() task_exists = Task.find_by_id(task_id) if task_exists is None: raise ValidationError({'message': 'Task not found'}) Project.delete_item(task_exists) return {'status': 'success', 'message': 'Task deleted successfully'}
def delete(self, project_id): """ Delete a single project :param project_id: :return: """ user = get_jwt_identity() project_exists = Project.find_by_id_and_user(project_id, user.get('id')) if project_exists is None: raise ValidationError({'message': 'Project thing not found'}) Project.delete_item(project_exists) return {'status': 'success', 'message': 'Project deleted successfully'}
def get(self): """ Get all tasks :param None: :return: Tasks List """ # user = get_jwt_identity() schema = TaskSchema(many=True) tasks = Task.get_all() if tasks is None: raise ValidationError({'message': 'No Task Found'}) return response('success', success_messages['retrieved'].format('Tasks'), schema.dump(tasks).data)
def get_or_404(cls, id): """Return entries by id """ record = cls.query.get(id) if not record: raise ValidationError( { 'message': f'{re.sub(r"(?<=[a-z])[A-Z]+",lambda x: f" {x.group(0).lower()}" , cls.__name__)} not found' }, 404) return record
def get(self): """ Get all Project :param None: :return: Project object """ # user = get_jwt_identity() schema = ProjectSchema(many=True) projects = Project.get_all() if projects is None: raise ValidationError({'message': 'No Project Found'}) projects_list = schema.dump(projects).data return response('success', success_messages['retrieved'].format('Projects'), projects_list)
def get(self, task_id): """ Get a single task :param task_id: :return: Task object """ user = get_jwt_identity() schema = TaskSchema() task = Task.get(task_id) if task is None: raise ValidationError({'message': 'Task not found'}) return response('success', success_messages['retrieved'].format('Task'), schema.dump(task).data)
def get_or_404(cls, instance_id): """ Gets an instance by id or returns 404 :param instance_id: the id of instance to get :return: return instance or 404 """ instance = cls.query.filter_by(id=instance_id).first() if not instance: raise ValidationError( { 'message': f'{re.sub(r"(?<=[a-z])[A-Z]+", lambda x: f" {x.group(0).lower()}", cls.__name__)} not found' # noqa }, 404) return instance
def get(self, project_id): """ Get a single project :param project_id: :return: Project object """ user = get_jwt_identity() schema = ProjectSchema() project = Project.get(project_id) if project is None: raise ValidationError({'message': 'Project not found'}) project = schema.dump(project).data return response('success', success_messages['retrieved'].format('Project'), project)
def validate_duplicate(model, **kwargs): """ Checks if model instance already exists in database Parameters: model(object): model to run validation on kwargs(dict): keyword arguments containing fields to filter query by """ result = model.query.filter_by(deleted=False, **kwargs).first() if result: raise ValidationError({ 'message': serialization_errors['exists'].format( f'{re.sub(r"(?<=[a-z])[A-Z]+",lambda x: f" {x.group(0).lower()}" , model.__name__)}' ) }, 409)
def post(self): """ An endpoint to authenticate user :return: dict(user data) """ request_data = request.get_json() email = request_data.get('email') password = request_data.get('password') user = User.find_by_email(email) if not user or not bcrypt.check_password_hash(user.password, password): raise ValidationError({'message': serialization_messages['invalid_user_data']}, 400) token = generate_token(user) data = { 'token': token, 'user': schema.dump(user).data } return { 'status': 'success', 'message': success_messages['retrieved'].format('User'), 'data': data }, 200
def post(self): """ An endpoint to register a user """ request_data = request.get_json() user_data = schema.load_object_into_schema(request_data) email = request_data.get('email') user_exist = User.find_by_email(email) if user_exist: raise ValidationError({ "message": serialization_messages['exists'].format('User') }, 409) user = User(**user_data) user.save() token = generate_token(user) data = { 'token': token, 'user': schema.dump(user).data } return response('success', message=success_messages['created'].format('User'), data=data, status_code=201)
def patch(self, project_id): """ Endpoint to update project""" request_data = request.get_json() user = get_jwt_identity() project = Project.get(project_id) if user.get('id') != project.created_by: raise ValidationError({ 'message': 'Unauthorized user, you cannot perform this operation' }) schema = ProjectSchema(context={'id': project_id}) if 'assignees' in request_data: user_list = assign_user(request_data.get('assignees')) assignees = user_list if user_list is not None else [] del request_data['assignees'] data = schema.load_object_into_schema(request_data, partial=True) data['assignees'] = assignees else: data = schema.load_object_into_schema(request_data, partial=True) project.update_(**data) return response('success', message=success_messages['updated'].format('Project'), data=schema.dump(project).data, status_code=200)
def get(self): """ Search Asset by date and warranty """ qry_keys = ('start', 'end', 'warranty_start', 'warranty_end') qry_dict = dict(request.args) for key in qry_dict: if key not in qry_keys: raise ValidationError( dict(message=filter_errors['INVALID_COLUMN'].format(key))) start = request.args.get('start') end = request.args.get('end') warranty_start = request.args.get('warranty_start') warranty_end = request.args.get('warranty_end') qry_list = [] if start: qry_list.append(('where', f'created_at,ge,{start}')) if end: qry_list.append(('where', f'created_at,le,{end}')) if warranty_start: qry_list.append(('where', f'warranty,ge,{warranty_start}')) if warranty_end: qry_list.append(('where', f'warranty,le,{warranty_end}')) args = ImmutableMultiDict(qry_list) assets = Asset._query(args) asset_schema = AssetSchema(many=True, exclude=EXCLUDED_FIELDS) return { 'status': 'success', 'data': asset_schema.dump(assets).data }, 200
def pagination_helper(model, schema, extra_query=None, exclude=EXCLUDED_FIELDS, only=None): """ Paginates records of a model. Arguments: model (class): Model to be paginated schema (class) -- Schema to be used for serilization Keyword Arguments: extra_query (dict): Contains extra query to be performed on the model (default: {None}) example: {"asset_category_id": "-LHYVNP2yx8oIOJFzXS4", "deleted": False} Returns: (tuple): Returns a tuple containing the paginated data and the paginated meta object or returns a tuple of None depending on whether the limit and page object is provided """ # Validates if the query strings are digits and the digits are >= 1 limit = validate_pagination_args(request.args.get('limit', 'None'), 'limit') current_page_count = validate_pagination_args( request.args.get('page', 'None'), 'page' ) # assign the page query string to the variable current_page_count query = model.query_(request.args) # Removes the trailing / at the end of the root url root_url = request.url_root[:-1] # Removes the trailing / at the begining of the url path url_path = request.path[1:] base_url = f'{root_url}/{url_path}' current_page_url = request.url records_query = query.filter_by(deleted=False) # Checks if they are extra queries to perform on the model if extra_query and isinstance(extra_query, dict): try: records_query = records_query.filter_by(**extra_query) except: # Raise a validation error if the keys in the extra queries are not part of the models fields raise ValidationError( {'message': serialization_errors['invalid_field']}) records_count = records_query.count() first_page = f'{base_url}?page=1&limit={limit}' pages_count = ceil(records_count / limit) # when there are no records the default page_count should still be 1 if pages_count == 0: pages_count = 1 meta_message = None if current_page_count > pages_count: # If current_page_count > pages_count set current_page_count to pages_count current_page_count = pages_count first_page = f'{base_url}?page=1&limit={limit}' current_page_url = f'{base_url}?page={pages_count}&limit={limit}' meta_message = serialization_errors['last_page_returned'] offset = (current_page_count - 1) * limit records = records_query.offset(offset).limit(limit) # pagination meta object pagination_object = { "firstPage": first_page, "currentPage": current_page_url, "nextPage": "", "previousPage": "", "page": current_page_count, "pagesCount": pages_count, "totalCount": records_count } previous_page_count = current_page_count - 1 next_page_count = current_page_count + 1 next_page_url = f'{base_url}?page={next_page_count}&limit={limit}' previous_page_url = f'{base_url}?page={previous_page_count}&limit={limit}' # noqa if current_page_count > 1: # if current_page_count > 1 there should be a previous page url pagination_object['previousPage'] = previous_page_url if pages_count >= next_page_count: # if pages_count >= next_page_count there should be a next page url pagination_object['nextPage'] = next_page_url if meta_message: pagination_object['message'] = meta_message data = schema(many=True, exclude=exclude, only=only).dump(records).data return data, pagination_object
def create_asset_category(self, data): """Return asset category object after successful loading of data""" result = AssetCategory.query.filter_by(name=data['name']).first() if not result: return AssetCategory(**data) raise ValidationError({'message': 'Asset Category already exist'}, 409)