"""Manual Adjustment general logic.""" import uuid from flask import g import requests import validictory from manualadjustment import config from manualadjustment import constants from manualadjustment.logic import file_upload from manualadjustment.logic.result import Result from manualadjustment.models import manual_adj_persister as persister from manualadjustment.validation_schema import post_schema from manualadjustment.validation_schema import put_schema ID_VALIDATION_MAP = { 'parent_id': persister.vendor_id_exists, 'adjust_for_period_id': persister.period_id_exists, 'apply_to_period_id': persister.period_id_exists, 'category_id': persister.category_id_exists, 'created_by': persister.orchadmin_users_id_exists, 'currencies_id': persister.currencies_id_exists} def validate_params( parent_id, parent_type, page_offset, page_limit): """ Validate manual adjustment input params. Args: parent_id (int): parent id parent_type (str): parent type page_offset (int) page_limit (int) Returns: If valid: Empty result If invalid: Result object containing error info """ # TODO Use a validation library in the future # check if parent_id and parent_type are set if not parent_id or not parent_type: missing_params = [] if not parent_id: missing_params.append('parent_id') if not parent_type: missing_params.append('parent_type') error_detail = 'missing fields {}'.format(','.join(missing_params)) return Result( error='missing required parameters', error_detail=error_detail, status=400) elif page_offset < 0: error_detail = 'Requested page_offset of {}. Must be >= 0.' error_detail = error_detail.format(page_offset) return Result( error='invalid page_offset', error_detail=error_detail, status=400) elif page_limit < 0 or page_limit > constants.PAGE_LIMIT_MAX_ACCEPTABLE: error_detail = 'Requested page_limit of {}. Must be between [{}, {}].' error_detail = error_detail.format( page_limit, 0, constants.PAGE_LIMIT_MAX_ACCEPTABLE) return Result( error='invalid page_limit', error_detail=error_detail, status=400) else: # inputs are valid return Result() def create(attachment, request_data): """Create logic.""" # validate json schema try: validictory.validate(request_data, post_schema.schema) except Exception as e: return Result( error='schema validation error', error_detail=str(e), status=400) # verify that ids exist in db all_ids_found, err_detail = _check_existence_of_ids_for_create( request_data['parent_id'], request_data['adjust_for_period_id'], request_data['apply_to_period_id'], request_data['category_id'], request_data['created_by'], request_data.get('currencies_id')) if not all_ids_found: return Result( error='id(s) not found in database', error_detail=err_detail, status=400) attachment_validation = _validate_attachment(attachment, request_data) if not attachment_validation.success: return attachment_validation # validated, proceed with insert key_path = None try: if attachment: key_path = _put_attachment_to_s3(attachment, False) elif 'attachment_url' in request_data: key_path = _put_attachment_to_s3( request_data['attachment_url'], True) except Exception as e: return Result( error='could not upload to s3', error_detail=str(e), status=500) attachment_location = None if key_path: attachment_location = '{}/{}'.format(config.bucket_name, key_path) manual_adj = persister.insert( attachment_location=attachment_location, **request_data) result = Result(data=manual_adj) if manual_adj: result.status = 201 else: # rollback s3 upload if db insert fails g.log.debug('could not insert manual adjustment into database') file_upload.delete_from_s3(key_path) g.log.debug('deleted uploaded file from S3') result.status = 500 result.error = 'Could not create manual adjustment' result.error_detail = request_data return result def get( parent_id, parent_type, category_id, apply_to_period_id, page_offset, page_limit): """Get manual adjustment logic. Validates parameters and then retrieves manual adjustment object Args: parent_id (int): parent id parent_type (str): parent type category_id (int): category_id apply_to_period_id (int): apply_to_period_id page_offset (int): page_offset page_limit (int): page_limit Returns: If valid: Result object with manual adjustment information If invalid: Result object containing error info """ validation_res = validate_params( parent_id, parent_type, page_offset, page_limit) if not validation_res.success: return validation_res manual_adjustments, page_count = persister.get_manual_adj_from_db( parent_id, parent_type, category_id, apply_to_period_id, page_offset, page_limit) return Result( data={ 'manual_adjustments': manual_adjustments, 'page_count': page_count, 'page_offset': page_offset, 'page_limit': page_limit}, status=200) def update(adjustment_id, params, attachment=None): """ Update an existing manual adjustment. This involves updating database columns and ingesting the attachment, if one is given. Args: adjustment_id (int): Unique key of the adjustment to update params (dict): key-value pairs of properties to update attachment (werkzeug.datastructures.FileStorage): optional attachment Returns: Result object containing model object or errors """ # @todo: validate associations (category_id, created_by, etc.) # @todo: deal with attachment try: validictory.validate(params, put_schema.schema) except Exception as e: return Result( error='schema validation error', error_detail=str(e), status=400) id_params = {} for param_name in params: if param_name in ID_VALIDATION_MAP: id_params[param_name] = params[param_name] all_ids_found, error_detail = _check_existence_of_ids(**id_params) if not all_ids_found: return Result( error='id(s) not found in database', error_detail=error_detail, status=400) attachment_validation = _validate_attachment(attachment, params) if not attachment_validation.success: return attachment_validation # flag indicating user has requested that prior file # be deleted and not replaced with a new file is_file_deletion = 'attachment_url' in params \ and params['attachment_url'] is None # save new attachment to S3 new_key_path = None prior_rec = persister.get_manual_adj_from_db_by_id(adjustment_id) try: if (attachment is not None): new_key_path = _put_attachment_to_s3(attachment, False) elif 'attachment_url' in params and not is_file_deletion: new_key_path = _put_attachment_to_s3( params['attachment_url'], True) except Exception as e: return Result( error='could not upload to s3', error_detail=str(e), status=500) if new_key_path is not None: params['attachment_location'] = '{}/{}'.format( config.bucket_name, new_key_path) if is_file_deletion: params['attachment_location'] = None manual_adj = persister.update(adjustment_id, params) if (attachment is not None or 'attachment_url' in params): # delete prior attachment from S3 if len(prior_rec) < 1: # man adj id not found, raise error return Result( error='Manual adjustment not found', error_detail=adjustment_id, status=404) else: prior_attach_loc = prior_rec[0].attachment_location if prior_attach_loc is not None: # parse the key path out of attachment location # key path is everything after the first forward slash prior_key_path = '/'.join(prior_attach_loc.split('/')[1:]) if prior_key_path: msg = "deleting prior file '{}' from S3".format( prior_key_path) g.log.info(msg) file_upload.delete_from_s3(prior_key_path) result = Result(data=manual_adj) if manual_adj: result.status = 200 else: result.status = 404 result.error = 'Manual adjustment not found' result.error_detail = adjustment_id return result def _check_existence_of_ids_for_create( parent_id, adjust_for_period_id, apply_to_period_id, category_id, created_by, currencies_id=None): """ Check whether various ids exist in the database as part of validation. Args: parent_id (int) adjust_for_period_id (int) apply_to_period_id (int) category_id (int) created_by (int) currencies_id (int): optional field Returns: 1. True if all ids found else False (bool) 2. error detail (string or None) """ parent_id_exists = persister.vendor_id_exists(parent_id) adjust_for_period_id_exists = persister.period_id_exists( adjust_for_period_id) apply_to_period_id_exists = persister.period_id_exists( apply_to_period_id) category_id_exists = persister.category_id_exists( category_id) created_by_exists = persister.orchadmin_users_id_exists( created_by) currencies_id_exists = True if currencies_id is not None: currencies_id_exists = persister.currencies_id_exists( currencies_id) if not all( # if any id is missing [parent_id_exists, adjust_for_period_id_exists, apply_to_period_id_exists, category_id_exists, created_by_exists, currencies_id_exists]): error_detail = 'missing ' if not parent_id_exists: error_detail += 'parent_id={};'.format(parent_id) if not adjust_for_period_id_exists: error_detail += 'adjust_for_period_id={};'.format( adjust_for_period_id) if not apply_to_period_id_exists: error_detail += 'apply_to_period_id={};'.format(apply_to_period_id) if not category_id_exists: error_detail += 'category_id={};'.format(category_id) if not created_by_exists: error_detail += 'created_by={};'.format(created_by) if not currencies_id_exists: error_detail += 'currencies_id={};'.format(currencies_id) return False, error_detail else: # else all ids found return True, None def _put_attachment_to_s3(attachment, is_stream): """ Take the attachment file or read a remote file's contents, then send to S3. Args: attachment (string): Either the attachment file or attachment url is_stream (boolean): Whether or not the attachment url was provided Returns: The S3 key path of the file that was uploaded """ if is_stream: # parse filename from url filename = attachment.rsplit('/', 1) key_path = '{}/{}/{}'.format( config.prefix, uuid.uuid4(), filename[1]) requests_object = requests.get(attachment) requests_object.raise_for_status() attachment = requests_object.content else: key_path = '{}/{}/{}'.format( config.prefix, uuid.uuid4(), attachment.filename) file_upload.upload_to_s3(attachment, key_path, is_stream) return key_path def _validate_attachment(attachment, request_data): """ Error if both a url to a file and a file are included in the request. Args: attachment (string): File included separately in the request request_data (dict): Request data included in the request body Returns: The S3 key path of the file that was uploaded """ if attachment and 'attachment_url' in request_data: err_detail = 'attachment url and file included, must include only one' return Result( error='attachment invalid', error_detail=err_detail, status=400) else: return Result() # Validation for create could be refactored to use this function def _check_existence_of_ids(**kwargs): """Check whether various IDs exist in the database as part of validation. Args: **kwargs: some set of ID params to validate Returns: 1.) True if all IDs found, False otherwise 2.) Error detail (string or None) """ error_detail = 'missing ' success = True for id_param in kwargs: id_value = kwargs[id_param] validator = ID_VALIDATION_MAP[id_param] if not validator(id_value): success = False error_detail += '{}={};'.format(id_param, id_value) if success: error_detail = None return success, error_detail