"""Blueprint for abacus state API.""" from http import HTTPStatus from abacus_common_logic.views.item_view import ItemView from abacus_common_logic.views.list_view import ListView from abacus_common_logic.views.validations import validated_request_body from common_apispec import doc, marshal_with, use_kwargs from common_apispec.views import MethodResource from flask import Blueprint, request from marshmallow import ValidationError from owsrequest import flask_request from owsrequest.constants.headers import ORCHARD_PROFILE_TYPE from owsresponse import response from owsresponse.adaptors.flask import flaskify from abacus_state.config import authorization_backend from abacus_state.constants import constants, error from abacus_state.logic import abacus_state as logic from abacus_state.models.abacus_state import AbacusState from abacus_state.schemas.abacus_state import ( AbacusStateBulkPutSchema, AbacusStateBulkQueryParamsSchema, AbacusStateBulkQueryResponseSchema, AbacusStateBulkQuerySchema, AbacusStateDetailSchema, AbacusStateIdsPostBodySchema, AbacusStatePostSchema, AbacusStatePutSchema, ) from abacus_state.utils.authorization import ( AbacusStateResourceGetter, authorize_dataloader_states, get_action, get_resource_type, ) from abacus_state.utils.format_error import validation_error from abacus_state.utils.permissions import ( check_create_permissions_access_checks, check_update_permissions_access_checks, ) from abacus_state.utils.request import get_optional_numeric_list_from_params abacus_state_api = Blueprint('abacus_state_api', __name__) abacus_state_resource_getter = AbacusStateResourceGetter() class AbacusStateItemView(ItemView, MethodResource): """View for fetching a abacus state by ID.""" model_class = AbacusState object_detail_schema = AbacusStateDetailSchema() put_schema = AbacusStatePutSchema() @doc( summary='Update action status of specified abacus state', params={ 'object_id': { 'description': 'ID of abacus state', }, }, ) @use_kwargs(put_schema, location='json', required=True, apply=False) @marshal_with( object_detail_schema, code=HTTPStatus.OK, description=HTTPStatus.OK.phrase, ) def put(self, object_id, **kwargs): """Override put method in ItemView.""" access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=403, ) ) return super().put(object_id, **kwargs) def update_handler(self, obj, **params): """Update abacus state.""" profile_type = request.headers.get(ORCHARD_PROFILE_TYPE) check_update_permissions_access_checks(obj, params, profile_type) return logic.update_abacus_state(obj, **params) class AbacusStateDataloader(ListView, MethodResource): """View for states dataloader.""" model_class = AbacusState list_entry_schema = AbacusStateDetailSchema() @doc( summary='Dataload states by state ids', ) @use_kwargs( AbacusStateIdsPostBodySchema, location='json', required=True, apply=False ) @marshal_with( AbacusStateDetailSchema(many=True), code=HTTPStatus.OK, description=HTTPStatus.OK.phrase, ) @marshal_with( None, code=HTTPStatus.BAD_REQUEST, description='Bad data received in payload', ) def post(self): """Get states by ids. NOTE: This endpoint uses a POST method to allow for a larger list of query args. """ try: state_ids = get_optional_numeric_list_from_params() if not state_ids: return flaskify( validation_error( error.ERROR_INVALID_IDS.format(object='AbacusState') ) ) except ValueError: return flaskify( validation_error(error.ERROR_INVALID_IDS.format(object='AbacusState')) ) states = logic.dataload_states_by_ids(state_ids) access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: if not authorize_dataloader_states( authorization_backend, states.message.get('items', []), ): return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=403, ) ) return flaskify(states) abacus_state_api.add_url_rule( '/abacus-state/', methods=['PUT'], view_func=AbacusStateItemView.as_view('abacus_state'), ) abacus_state_api.add_url_rule( '/abacus-state/dataloader', methods=['POST'], view_func=AbacusStateDataloader.as_view('abacus_state_dataloader'), ) @abacus_state_api.route('/abacus-states/query', methods=['POST']) @doc( summary='Bulk query abacus states with filters and pagination', ) @use_kwargs(AbacusStateBulkQueryParamsSchema, location='query', required=False) @use_kwargs(AbacusStateBulkQuerySchema, location='json', required=False) @marshal_with( AbacusStateBulkQueryResponseSchema, code=HTTPStatus.OK, description=HTTPStatus.OK.phrase, ) def bulk_query_states(**params): """Bulk query abacus states with filters and pagination. NOTE: This endpoint uses a POST method to allow for a larger list of parent_table_ids. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=403, ) ) try: result = logic.bulk_query_states( parent_table_name=params.get('parent_table_name'), parent_table_ids=params.get('parent_table_ids'), action_name=params.get('action_name'), limit=params.get('limit'), offset=params.get('offset'), ) return flaskify(result) except ValidationError as e: return flaskify(validation_error(str(e))) @abacus_state_api.route( '/abacus-state//dataloader', methods=['POST'] ) def dataload_states_by_target(parent_table_name): """Batch states for one parent table across many parent ids. POST so a large id list fits in the body. Body is a raw JSON array of parent ids; the response is one entry per requested id in order (the /dataloader convention). Route-level access via verify_rules_access_standalone, matching the sibling /abacus-states/query route. """ parent_table_name = parent_table_name.replace('-', '_') access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=403, ) ) try: parent_table_ids = get_optional_numeric_list_from_params() if not parent_table_ids: return flaskify( validation_error(error.ERROR_INVALID_IDS.format(object='AbacusState')) ) except (ValueError, TypeError): return flaskify( validation_error(error.ERROR_INVALID_IDS.format(object='AbacusState')) ) if len(parent_table_ids) > constants.DATALOADER_BATCH_LIMIT: return flaskify( validation_error( error.ERROR_TOO_MANY_DATALOADER_IDS.format( limit=constants.DATALOADER_BATCH_LIMIT ) ) ) return flaskify( logic.dataload_states_by_target(parent_table_name, parent_table_ids) ) @abacus_state_api.route('/abacus-states', methods=['POST']) @doc( summary='Create abacus states', ) @use_kwargs( AbacusStatePostSchema(many=True), location='json', required=True, apply=False ) @marshal_with( AbacusStateDetailSchema, code=HTTPStatus.CREATED, description=HTTPStatus.OK.phrase, ) def create_abacus_states(): """Create abacus states.""" access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=403, ) ) post_schema = AbacusStatePostSchema(many=True) params = validated_request_body(post_schema) profile_type = request.headers.get(ORCHARD_PROFILE_TYPE) check_create_permissions_access_checks(params, profile_type) return flaskify(logic.create_abacus_states(params)) @doc( summary='Create abacus_state records for specified parent table. No request body.', params={ 'parent_table_name': { 'description': 'Name of the royalty_accounting datatable', }, 'parent_table_id': { 'description': 'ID of the particular record in the parent table for which to create states', }, }, ) @marshal_with( AbacusStateDetailSchema(many=True), code=HTTPStatus.CREATED, description=HTTPStatus.OK.phrase, ) @abacus_state_api.route( '/abacus-states///', methods=['POST'] ) def create_abacus_states_by_parent_table(parent_table_name, parent_table_id): """Create all abacus state records for the specified parent_table_name.""" access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=403, ) ) formatted_parent_table = parent_table_name.replace('-', '_').lower() contract_type = request.args.get('contract_type') return flaskify( logic.create_abacus_states_by_parent_table( formatted_parent_table, parent_table_id, contract_type ) ) @doc( summary='Retrieves abacus state actions details', params={ 'parent_table_name': { 'description': 'Name of the royalty_accounting datatable', }, 'parent_table_id': { 'description': 'An ID of a record in the parent_table', }, }, ) @marshal_with( AbacusStateDetailSchema(many=True), code=HTTPStatus.OK, description=HTTPStatus.OK.phrase, ) @abacus_state_api.route( '/abacus-state//', methods=['GET'] ) def get_abacus_state_action_list(parent_table_name, parent_table_id): """Endpoint to GET state actions list.""" parent_table_name = parent_table_name.replace('-', '_') access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: is_authorized = authorization_backend.is_authorized( get_action(parent_table_name, 'view'), f'{parent_table_name}_{parent_table_id}', get_resource_type(parent_table_name), abacus_state_resource_getter, parent_table_name=parent_table_name, parent_table_id=parent_table_id, ) if not is_authorized: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=403, ) ) message = logic.get_action_status_list( parent_table_name=parent_table_name, parent_table_id=parent_table_id ) return flaskify(response.Response(message=message)) @doc( summary='Update action status of abacus states', params={ 'parent_table_name': { 'description': 'Name of the royalty_accounting datatable', }, }, ) @use_kwargs( AbacusStateBulkPutSchema(many=True), location='json', required=True, apply=False ) @marshal_with( AbacusStateDetailSchema(many=True), code=HTTPStatus.OK, description=HTTPStatus.OK.phrase, ) @abacus_state_api.route('/abacus-states/', methods=['PUT']) def bulk_update_abacus_states_by_parent_table(parent_table_name): """Bulk updating abacus states for a specified parent_table_name.""" access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=403, ) ) formatted_parent_table = parent_table_name.replace('-', '_').lower() put_schema = AbacusStateBulkPutSchema(many=True) states = validated_request_body(put_schema) return flaskify( logic.bulk_update_abacus_states_by_parent_table(formatted_parent_table, states) ) @abacus_state_api.route( '/abacus-state///reset', methods=['PUT'], ) @doc( summary='Reset abacus state actions', params={ 'parent_table_name': { 'description': 'Name of the royalty_accounting datatable', }, 'parent_table_id': { 'description': 'An ID of a record in the parent_table', }, }, ) @marshal_with( AbacusStateDetailSchema(many=True), code=HTTPStatus.OK, description=HTTPStatus.OK.phrase, ) def reset_abacus_state_actions(parent_table_name: str, parent_table_id: int): """Endpoint to reset all action states for specified parent table name and id.""" parent_table_name = parent_table_name.replace('-', '_') access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: is_authorized = authorization_backend.is_authorized( get_action(parent_table_name, 'update'), f'{parent_table_name}_{parent_table_id}', get_resource_type(parent_table_name), abacus_state_resource_getter, parent_table_name=parent_table_name, parent_table_id=parent_table_id, ) if not is_authorized: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=403, ) ) return flaskify( logic.reset_abacus_state_actions( parent_table_name=parent_table_name, parent_table_id=parent_table_id ) )