diff --git a/.vscode/settings.json b/.vscode/settings.json index 4df75ca2..9d62f42c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,7 @@ }, "editor.formatOnSave": false, "python.testing.pytestArgs": [ + "tests_apply", "tests" ], "python.testing.unittestEnabled": false, diff --git a/app.py b/app.py index 143b3b78..7c1bb161 100644 --- a/app.py +++ b/app.py @@ -3,6 +3,7 @@ import connexion from config import Config from connexion import FlaskApp +from connexion.resolver import MethodResolver from connexion.resolver import MethodViewResolver from fsd_utils import init_sentry from fsd_utils.healthchecks.checkers import DbChecker @@ -20,6 +21,13 @@ def create_app() -> FlaskApp: get_bundled_specs("/openapi/api.yml"), validate_responses=True, resolver=MethodViewResolver("api"), + base_path="/assess", + ) + connexion_app.add_api( + get_bundled_specs("/openapi/apply_api.yml"), + validate_responses=True, + resolver=MethodResolver("api"), + base_path="/apply", ) flask_app = connexion_app.app diff --git a/apply/api/routes/application/routes.py b/apply/api/routes/application/routes.py index d259f706..267f4c46 100644 --- a/apply/api/routes/application/routes.py +++ b/apply/api/routes/application/routes.py @@ -109,17 +109,6 @@ def get_applications_statuses_report( fund_id: Optional[list] = [], format: Optional[str] = "csv", ): - print("hello") - current_app.logger.warning("hello") - return jsonify( - { - "metrics": [ - {"fund_id": "asdf", "rounds": [{"round_id": "123", "application_statuses": {"NOT_STARTED": 12}}]} - ] - } - ) - print("hello") - current_app.logger.warning("hello") if not round_id and not fund_id: print("No params") return jsonify({"code": 404, "message": "No funds or rounds specified"}) diff --git a/config/envs/default.py b/config/envs/default.py index f0d13294..176c586f 100644 --- a/config/envs/default.py +++ b/config/envs/default.py @@ -21,7 +21,7 @@ class DefaultConfig: FLASK_ROOT = str(Path(__file__).parent.parent.parent) FLASK_ENV = CommonConfig.FLASK_ENV FORCE_HTTPS = CommonConfig.FORCE_HTTPS - FSD_LOG_LEVEL = CommonConfig.FSD_LOG_LEVEL + FSD_LOG_LEVEL = "DEBUG" # --------------- # APIs Config: contains api hosts (set in manifest.yml) @@ -71,6 +71,7 @@ class DefaultConfig: # S3 Config # --------------- AWS_MSG_BUCKET_NAME = environ.get("AWS_MSG_BUCKET_NAME") + AWS_BUCKET_NAME = environ.get("AWS_BUCKET_NAME") # --------------- # Task Executor Config # --------------- diff --git a/config/envs/unit_testing.py b/config/envs/unit_testing.py index 473c13e0..a96a6055 100644 --- a/config/envs/unit_testing.py +++ b/config/envs/unit_testing.py @@ -22,7 +22,7 @@ class UnitTestingConfig(DefaultConfig): WARN_IF_QUERIES_OVER_MS = 5 - SQLALCHEMY_DATABASE_URI = DefaultConfig.SQLALCHEMY_DATABASE_URI + "_UNIT_TEST" + SQLALCHEMY_DATABASE_URI = DefaultConfig.SQLALCHEMY_DATABASE_URI + "_unit_test" # --------------- # Task Executor Config @@ -46,3 +46,5 @@ class UnitTestingConfig(DefaultConfig): SQS_BATCH_SIZE = 10 # MaxNumber Of Messages to process SQS_VISIBILITY_TIME = 1 # time for message to temporarily invisible to others (in sec) SQS_RECEIVE_MESSAGE_CYCLE_TIME = 5 # Run the job every 'x' seconds + + USE_LOCAL_DATA = True diff --git a/openapi/api.yml b/openapi/api.yml index 60a172c0..af80dd3d 100644 --- a/openapi/api.yml +++ b/openapi/api.yml @@ -10,8 +10,6 @@ tags: description: Score operations - name: flags description: Flag operations - - name: application-store - description: Application store operations paths: '/application_overviews/{fund_id}/{round_id}': @@ -1522,607 +1520,3 @@ paths: type: array items: $ref: 'components.yml#/components/schemas/UserAssociation' - -# ---------------------------------------------------- -# Imported from application-store -# ---------------------------------------------------- - - /applications/reporting/applications_statuses_data: - get: - tags: - - reporting - summary: Get report on started and submitted applications - description: Get report on started and submitted applications - operationId: apply.api.ApplicationsView.get_applications_statuses_report - parameters: - - name: round_id - in: query - description: Optional round ID to filter by - schema: - type: array - items: - type: string - - name: fund_id - in: query - description: Optional fund ID to filter by - schema: - type: array - items: - type: string - - name: format - in: query - description: Optional format specifier, csv or json - schema: - type: string - enum: [csv,json] - responses: - 200: - description: SUCCESS - Here is the status report on applications - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/StatusReport' - 404: - description: ERROR - Could not get report - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/Error' - - /applications/reporting/key_application_metrics/{application_id}: - get: - tags: - - reporting - summary: Get the key data report on an application - description: Get the key data report on an application - operationId: apply.api.ApplicationsView.get_key_application_data_report - responses: - 200: - description: SUCCESS - Here is the report on requested application - content: - text/csv: {} - 404: - description: ERROR - Could not get report - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/Error' - parameters: - - name: application_id - in: path - required: true - schema: - type: string - format: path - - /applications/reporting/key_application_metrics: - get: - tags: - - reporting - summary: Get the key data report on applications - description: Get the key data report on applications - operationId: apply.api.ApplicationsView.get_key_applications_data_report - parameters: - - name: status - in: query - required: false - schema: - type: string - - name: round_id - in: query - description: Optional round ID to filter by - schema: - type: string - - name: fund_id - in: query - description: Optional fund ID to filter by - schema: - type: string - responses: - 200: - description: SUCCESS - Here is the report on applications - content: - text/csv: {} - 404: - description: ERROR - Could not get report - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/Error' - - /applications/forms: - put: - requestBody: - description: Update application with new forms state - required: true - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/PutForms' - tags: - - applications - summary: Update an application with new forms state - description: Updates the form state of an application - operationId: apply.api.ApplicationsView.put - responses: - 201: - description: SUCCESS - Application updated - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/UpdatedForms' - 404: - description: ERROR - Form cannot be updated - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/Error' - example: - code: 404 - status: 'error' - message: 'Form is not updated' - - /applications/get_all_feedbacks_and_survey_report: - get: - tags: - - feedback - - survey - summary: Retrive all section feedbacks & survey - description: Retrive all section feedbacks & survey - operationId: apply.api.ApplicationsView.get_all_feedbacks_and_survey_report - responses: - 200: - description: SUCCESS - Here is the report on requested applicants feedback & survey - content: - application/vnd.ms-excel: {} - 404: - description: ERROR - Could not get report - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/Error' - parameters: - - in: query - name: fund_id - style: form - schema: - type: string - required: true - explode: false - - in: query - name: round_id - style: form - schema: - type: string - required: true - explode: false - - in: query - name: status_only - style: form - schema: - type: string - required: false - explode: false - - /applications: - get: - tags: - - applications - summary: Search applications - description: List all applications - operationId: apply.api.ApplicationsView.get - responses: - 200: - description: SUCCESS - A list of applications - content: - application/json: - schema: - type: array - items: - $ref: 'apply_components.yml#/components/schemas/Application' - parameters: - - in: query - name: application_id - style: form - schema: - type: string - required: false - explode: false - - in: query - name: account_id - style: form - schema: - type: string - required: false - explode: false - - in: query - name: fund_id - style: form - schema: - type: string - required: false - explode: false - - in: query - name: round_id - style: form - schema: - type: string - required: false - explode: false - - in: query - name: status_only - style: form - schema: - type: array - items: - type: string - required: false - explode: true - # - filtering applications - - in: query - name: order_by - style: form - schema: - type: string - required: false - explode: false - - in: query - name: order_rev - style: form - schema: - type: string - required: false - explode: false - - in: query - name: forms - style: form - schema: - type: boolean - required: false - explode: false - post: - tags: - - applications - summary: Post a new application - description: Registers a new application for a user - operationId: apply.api.ApplicationsView.post - requestBody: - description: Application creation parameters - required: true - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/PostApplication' - example: - account_id: 'usera' - fund_id: '47aef2f5-3fcb-4d45-acb5-f0152b5f03c4' - round_id: 'c603d114-5364-4474-a0c4-c41cbf4d3bbd' - language: 'en' - responses: - 201: - description: SUCCESS - Application created - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/CreatedApplication' - 401: - description: ERROR - Could not create application - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/Error' - example: - code: 401 - status: 'error' - message: 'An assessment for this application already exists' - - /applications/{application_id}: - get: - tags: - - applications - summary: Get a specific application - description: Get a specific application by application id - operationId: apply.api.ApplicationsView.get_by_id - responses: - 200: - description: SUCCESS - An applicaton by id - content: - application/json: - schema: - type: object - items: - $ref: 'apply_components.yml#/components/schemas/ReturnedApplication' - 404: - description: ERROR - Application cannot be found. - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/Error' - example: - code: 404 - status: 'error' - message: 'Application corresponding to id not found.' - parameters: - - name: application_id - in: path - required: true - schema: - type: string - format: path - - in: query - name: with_questions_file - style: form - schema: - type: boolean - required: false - explode: false - - /applications/{application_id}/submit: - post: - tags: - - applications - summary: Submit an application - description: Application is submitted and cannot be changed from frontend - operationId: apply.api.ApplicationsView.submit - parameters: - - name: application_id - in: path - required: true - schema: - type: string - format: path - - name: dont_send_email - in: query - required: false - description: Whether to send an email notification for the submitted application - schema: - type: boolean - responses: - 201: - description: Application has been submitted successfully - content: - application/json: - schema: - type: object - items: - $ref: 'apply_components.yml#/components/schemas/SubmittedApplication' - 404: - description: ERROR - Application cannot be submitted. - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/Error' - example: - code: 404 - status: 'error' - message: 'Application corresponding to id not found.' - - /application/feedback: - post: - tags: - - feedback - summary: Post a new feedback - description: Create a new feedback entry - operationId: apply.api.ApplicationsView.post_feedback - requestBody: - description: Feedback creation parameters - required: true - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/PostFeedback' - example: - application_id: '47aef2f5-3fcb-4d45-acb5-f0152b5f03c4' - fund_id: '47aef2f5-3fcb-4d45-acb5-f0152b5f03c4' - round_id: 'c603d114-5364-4474-a0c4-c41cbf4d3bbd' - section_id: 'section_1' - feedback_json: {"comment": "Great work!", "rating": 5} - status: "NOT_STARTED" - responses: - 201: - description: SUCCESS - Feedback created - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/Feedback' - 401: - description: ERROR - Could not create feedback - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/Error' - example: - code: 401 - status: 'error' - message: 'Could not create feedback' - - get: - tags: - - feedback - summary: Search feedback entries - description: List all feedback entries - operationId: apply.api.ApplicationsView.get_feedback_for_section - responses: - 200: - description: SUCCESS - A list of feedback entries - content: - application/json: - schema: - type: object - items: - $ref: 'apply_components.yml#/components/schemas/Feedback' - parameters: - - in: query - name: application_id - style: form - schema: - type: string - required: true - explode: false - - in: query - name: section_id - style: form - schema: - type: string - required: true - explode: false - - in: query - name: fund_id - style: form - schema: - type: string - required: false - explode: false - - in: query - name: round_id - style: form - schema: - type: string - required: false - explode: false - - /application/end_of_application_survey_data: - post: - tags: - - survey - summary: Post end of application survey data - operationId: apply.api.ApplicationsView.post_end_of_application_survey_data - requestBody: - required: true - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/PostEndOfApplicationSurveyDataRequest' - responses: - 201: - description: SUCCESS - Survey data saved - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/GetEndOfApplicationSurveyDataResponse' - 400: - description: ERROR - Bad request - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/Error' - - get: - tags: - - survey - summary: Get end of application survey data - operationId: apply.api.ApplicationsView.get_end_of_application_survey_data - parameters: - - in: query - name: application_id - required: true - schema: - type: string - description: ID of the application - - in: query - name: page_number - required: true - schema: - type: integer - description: Page number of the survey data - responses: - 200: - description: SUCCESS - Survey data retrieved - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/GetEndOfApplicationSurveyDataResponse' - 404: - description: ERROR - Survey data not found - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/Error' - - /application/research: - post: - tags: - - survey - summary: Post contact details for research survey - operationId: apply.api.ApplicationsView.post_research_survey_data - requestBody: - required: true - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/PostResearchSurveyDataRequest' - responses: - 201: - description: SUCCESS - Survey data saved - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/GetResearchSurveyDataResponse' - 400: - description: ERROR - Bad request - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/Error' - get: - tags: - - survey - summary: Get contact details for research survey - operationId: apply.api.ApplicationsView.get_research_survey_data - parameters: - - in: query - name: application_id - required: true - schema: - type: string - description: ID of the application - responses: - 200: - description: SUCCESS - Survey data retrieved - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/GetResearchSurveyDataResponse' - 404: - description: ERROR - Survey data not found - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/Error' - - /queue_for_assessment/{application_id}: - post: - tags: - - SQS (queues) - summary: post an application (if it has the status submitted) to the assessment import queue - description: post an application (if it has the status submitted) to assessment import queue - operationId: apply.api.QueueView.post_submitted_application_to_assessment - parameters: - - name: application_id - in: path - required: true - schema: - type: string - format: path - responses: - 200: - description: SUCCESS - message sent - content: - application/json: - schema: - type: object - items: - type: object - properties: - sent: - type: boolean - 500: - description: ERROR - Application cannot be found. - content: - application/json: - schema: - $ref: 'apply_components.yml#/components/schemas/Error' - example: - code: 500 - status: 'error' - message: 'Message could not be staged' diff --git a/openapi/apply_api.yml b/openapi/apply_api.yml new file mode 100644 index 00000000..aaa62e5b --- /dev/null +++ b/openapi/apply_api.yml @@ -0,0 +1,612 @@ +# ---------------------------------------------------- +# Imported from application-store +# ---------------------------------------------------- +openapi: "3.0.0" +info: + description: Assesplication poc API + version: "0.0.1" + title: Funding Service - Assesplication Store +tags: + - name: application-store + description: Application store operations + +paths: + /applications/reporting/applications_statuses_data: + get: + tags: + - reporting + summary: Get report on started and submitted applications + description: Get report on started and submitted applications + operationId: apply.api.ApplicationsView.get_applications_statuses_report + parameters: + - name: round_id + in: query + description: Optional round ID to filter by + schema: + type: array + items: + type: string + - name: fund_id + in: query + description: Optional fund ID to filter by + schema: + type: array + items: + type: string + - name: format + in: query + description: Optional format specifier, csv or json + schema: + type: string + enum: [csv,json] + responses: + 200: + description: SUCCESS - Here is the status report on applications + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/StatusReport' + 404: + description: ERROR - Could not get report + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/Error' + + /applications/reporting/key_application_metrics/{application_id}: + get: + tags: + - reporting + summary: Get the key data report on an application + description: Get the key data report on an application + operationId: apply.api.ApplicationsView.get_key_application_data_report + responses: + 200: + description: SUCCESS - Here is the report on requested application + content: + text/csv: {} + 404: + description: ERROR - Could not get report + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/Error' + parameters: + - name: application_id + in: path + required: true + schema: + type: string + format: path + + /applications/reporting/key_application_metrics: + get: + tags: + - reporting + summary: Get the key data report on applications + description: Get the key data report on applications + operationId: apply.api.ApplicationsView.get_key_applications_data_report + parameters: + - name: status + in: query + required: false + schema: + type: string + - name: round_id + in: query + description: Optional round ID to filter by + schema: + type: string + - name: fund_id + in: query + description: Optional fund ID to filter by + schema: + type: string + responses: + 200: + description: SUCCESS - Here is the report on applications + content: + text/csv: {} + 404: + description: ERROR - Could not get report + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/Error' + + /applications/forms: + put: + requestBody: + description: Update application with new forms state + required: true + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/PutForms' + tags: + - applications + summary: Update an application with new forms state + description: Updates the form state of an application + operationId: apply.api.ApplicationsView.put + responses: + 201: + description: SUCCESS - Application updated + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/UpdatedForms' + 404: + description: ERROR - Form cannot be updated + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/Error' + example: + code: 404 + status: 'error' + message: 'Form is not updated' + + /applications/get_all_feedbacks_and_survey_report: + get: + tags: + - feedback + - survey + summary: Retrive all section feedbacks & survey + description: Retrive all section feedbacks & survey + operationId: apply.api.ApplicationsView.get_all_feedbacks_and_survey_report + responses: + 200: + description: SUCCESS - Here is the report on requested applicants feedback & survey + content: + application/vnd.ms-excel: {} + 404: + description: ERROR - Could not get report + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/Error' + parameters: + - in: query + name: fund_id + style: form + schema: + type: string + required: true + explode: false + - in: query + name: round_id + style: form + schema: + type: string + required: true + explode: false + - in: query + name: status_only + style: form + schema: + type: string + required: false + explode: false + + /applications: + get: + tags: + - applications + summary: Search applications + description: List all applications + operationId: apply.api.ApplicationsView.get + responses: + 200: + description: SUCCESS - A list of applications + content: + application/json: + schema: + type: array + items: + $ref: 'apply_components.yml#/components/schemas/Application' + parameters: + - in: query + name: application_id + style: form + schema: + type: string + required: false + explode: false + - in: query + name: account_id + style: form + schema: + type: string + required: false + explode: false + - in: query + name: fund_id + style: form + schema: + type: string + required: false + explode: false + - in: query + name: round_id + style: form + schema: + type: string + required: false + explode: false + - in: query + name: status_only + style: form + schema: + type: array + items: + type: string + required: false + explode: true + # - filtering applications + - in: query + name: order_by + style: form + schema: + type: string + required: false + explode: false + - in: query + name: order_rev + style: form + schema: + type: string + required: false + explode: false + - in: query + name: forms + style: form + schema: + type: boolean + required: false + explode: false + post: + tags: + - applications + summary: Post a new application + description: Registers a new application for a user + operationId: apply.api.ApplicationsView.post + requestBody: + description: Application creation parameters + required: true + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/PostApplication' + example: + account_id: 'usera' + fund_id: '47aef2f5-3fcb-4d45-acb5-f0152b5f03c4' + round_id: 'c603d114-5364-4474-a0c4-c41cbf4d3bbd' + language: 'en' + responses: + 201: + description: SUCCESS - Application created + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/CreatedApplication' + 401: + description: ERROR - Could not create application + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/Error' + example: + code: 401 + status: 'error' + message: 'An assessment for this application already exists' + + /applications/{application_id}: + get: + tags: + - applications + summary: Get a specific application + description: Get a specific application by application id + operationId: apply.api.ApplicationsView.get_by_id + responses: + 200: + description: SUCCESS - An applicaton by id + content: + application/json: + schema: + type: object + items: + $ref: 'apply_components.yml#/components/schemas/ReturnedApplication' + 404: + description: ERROR - Application cannot be found. + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/Error' + example: + code: 404 + status: 'error' + message: 'Application corresponding to id not found.' + parameters: + - name: application_id + in: path + required: true + schema: + type: string + format: path + - in: query + name: with_questions_file + style: form + schema: + type: boolean + required: false + explode: false + + /applications/{application_id}/submit: + post: + tags: + - applications + summary: Submit an application + description: Application is submitted and cannot be changed from frontend + operationId: apply.api.ApplicationsView.submit + parameters: + - name: application_id + in: path + required: true + schema: + type: string + format: path + - name: dont_send_email + in: query + required: false + description: Whether to send an email notification for the submitted application + schema: + type: boolean + responses: + 201: + description: Application has been submitted successfully + content: + application/json: + schema: + type: object + items: + $ref: 'apply_components.yml#/components/schemas/SubmittedApplication' + 404: + description: ERROR - Application cannot be submitted. + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/Error' + example: + code: 404 + status: 'error' + message: 'Application corresponding to id not found.' + + /application/feedback: + post: + tags: + - feedback + summary: Post a new feedback + description: Create a new feedback entry + operationId: apply.api.ApplicationsView.post_feedback + requestBody: + description: Feedback creation parameters + required: true + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/PostFeedback' + example: + application_id: '47aef2f5-3fcb-4d45-acb5-f0152b5f03c4' + fund_id: '47aef2f5-3fcb-4d45-acb5-f0152b5f03c4' + round_id: 'c603d114-5364-4474-a0c4-c41cbf4d3bbd' + section_id: 'section_1' + feedback_json: {"comment": "Great work!", "rating": 5} + status: "NOT_STARTED" + responses: + 201: + description: SUCCESS - Feedback created + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/Feedback' + 401: + description: ERROR - Could not create feedback + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/Error' + example: + code: 401 + status: 'error' + message: 'Could not create feedback' + + get: + tags: + - feedback + summary: Search feedback entries + description: List all feedback entries + operationId: apply.api.ApplicationsView.get_feedback_for_section + responses: + 200: + description: SUCCESS - A list of feedback entries + content: + application/json: + schema: + type: object + items: + $ref: 'apply_components.yml#/components/schemas/Feedback' + parameters: + - in: query + name: application_id + style: form + schema: + type: string + required: true + explode: false + - in: query + name: section_id + style: form + schema: + type: string + required: true + explode: false + - in: query + name: fund_id + style: form + schema: + type: string + required: false + explode: false + - in: query + name: round_id + style: form + schema: + type: string + required: false + explode: false + + /application/end_of_application_survey_data: + post: + tags: + - survey + summary: Post end of application survey data + operationId: apply.api.ApplicationsView.post_end_of_application_survey_data + requestBody: + required: true + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/PostEndOfApplicationSurveyDataRequest' + responses: + 201: + description: SUCCESS - Survey data saved + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/GetEndOfApplicationSurveyDataResponse' + 400: + description: ERROR - Bad request + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/Error' + + get: + tags: + - survey + summary: Get end of application survey data + operationId: apply.api.ApplicationsView.get_end_of_application_survey_data + parameters: + - in: query + name: application_id + required: true + schema: + type: string + description: ID of the application + - in: query + name: page_number + required: true + schema: + type: integer + description: Page number of the survey data + responses: + 200: + description: SUCCESS - Survey data retrieved + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/GetEndOfApplicationSurveyDataResponse' + 404: + description: ERROR - Survey data not found + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/Error' + + /application/research: + post: + tags: + - survey + summary: Post contact details for research survey + operationId: apply.api.ApplicationsView.post_research_survey_data + requestBody: + required: true + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/PostResearchSurveyDataRequest' + responses: + 201: + description: SUCCESS - Survey data saved + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/GetResearchSurveyDataResponse' + 400: + description: ERROR - Bad request + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/Error' + get: + tags: + - survey + summary: Get contact details for research survey + operationId: apply.api.ApplicationsView.get_research_survey_data + parameters: + - in: query + name: application_id + required: true + schema: + type: string + description: ID of the application + responses: + 200: + description: SUCCESS - Survey data retrieved + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/GetResearchSurveyDataResponse' + 404: + description: ERROR - Survey data not found + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/Error' + + /queue_for_assessment/{application_id}: + post: + tags: + - SQS (queues) + summary: post an application (if it has the status submitted) to the assessment import queue + description: post an application (if it has the status submitted) to assessment import queue + operationId: apply.api.QueueView.post_submitted_application_to_assessment + parameters: + - name: application_id + in: path + required: true + schema: + type: string + format: path + responses: + 200: + description: SUCCESS - message sent + content: + application/json: + schema: + type: object + items: + type: object + properties: + sent: + type: boolean + 500: + description: ERROR - Application cannot be found. + content: + application/json: + schema: + $ref: 'apply_components.yml#/components/schemas/Error' + example: + code: 500 + status: 'error' + message: 'Message could not be staged' diff --git a/pytest.ini b/pytest.ini index 9e863c69..9a6e625e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -4,6 +4,7 @@ env = FLASK_DEBUG=1 # pragma: allowlist nextline secret D:DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5432/fsd_assess_store + GITHUB_SHA=abc123 mocked-sessions=db.db.session markers = diff --git a/requirements-dev.txt b/requirements-dev.txt index 54cfae18..b6096ec5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,62 +6,62 @@ # a2wsgi==1.10.6 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # connexion alembic==1.13.2 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # alembic-utils # flask-migrate alembic-utils==0.8.3 - # via -r requirements.txt + # via -r /assesplication-store/requirements.txt anyio==4.4.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # httpx # starlette # watchfiles apscheduler==3.10.4 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # flask-apscheduler asgiref==3.8.1 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # connexion # flask async-timeout==4.0.3 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # redis attrs==23.2.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # jsonschema # referencing babel==2.15.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # flask-babel beautifulsoup4==4.12.3 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # funding-service-design-utils black==24.4.2 # via -r requirements-dev.in blinker==1.8.2 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # flask # sentry-sdk boto3==1.35.18 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # funding-service-design-utils # moto botocore==1.35.18 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # boto3 # moto # s3transfer @@ -69,28 +69,28 @@ build==1.2.1 # via pip-tools certifi==2024.6.2 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # httpcore # httpx # requests # sentry-sdk cffi==1.16.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # cryptography cfgv==3.4.0 # via pre-commit chardet==5.2.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # prance charset-normalizer==3.3.2 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # requests click==8.1.7 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # black # flask # pip-tools @@ -99,30 +99,34 @@ colored==2.2.4 # via -r requirements-dev.in commonmark==0.9.1 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # rich connexion[flask,swagger-ui,uvicorn]==3.1.0 - # via -r requirements.txt + # via -r /assesplication-store/requirements.txt cryptography==42.0.8 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # moto # pyjwt dataclass-dict-convert==1.7.3 - # via -r requirements.txt + # via -r /assesplication-store/requirements.txt debugpy==1.8.2 # via -r requirements-dev.in decorator==5.1.1 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # jsonpath-rw deepdiff==7.0.1 # via -r requirements-dev.in distlib==0.3.8 # via virtualenv +et-xmlfile==2.0.0 + # via + # -r /assesplication-store/requirements.txt + # openpyxl exceptiongroup==1.2.1 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # anyio # pytest filelock==3.15.4 @@ -133,7 +137,7 @@ flake8-pyproject==1.2.3 # via -r requirements-dev.in flask[async]==3.0.3 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # connexion # flask-apscheduler # flask-babel @@ -144,62 +148,66 @@ flask[async]==3.0.3 # pytest-flask # sentry-sdk flask-apscheduler==1.13.1 - # via -r requirements.txt + # via -r /assesplication-store/requirements.txt flask-babel==4.0.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # funding-service-design-utils flask-migrate==4.0.7 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # funding-service-design-utils flask-redis==0.4.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # funding-service-design-utils flask-sqlalchemy==3.1.1 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # flask-migrate # funding-service-design-utils flupy==1.2.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # alembic-utils funding-service-design-utils==5.0.8 - # via -r requirements.txt + # via -r /assesplication-store/requirements.txt +greenlet==3.1.1 + # via + # -r /assesplication-store/requirements.txt + # sqlalchemy gunicorn==22.0.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # funding-service-design-utils h11==0.14.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # httpcore # uvicorn httpcore==1.0.5 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # httpx httptools==0.6.1 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # uvicorn httpx==0.27.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # connexion identify==2.5.36 # via pre-commit idna==3.7 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # anyio # httpx # requests inflection==0.5.1 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # connexion iniconfig==2.0.0 # via pytest @@ -207,11 +215,11 @@ invoke==2.2.0 # via -r requirements-dev.in itsdangerous==2.2.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # flask jinja2==3.1.4 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # connexion # flask # flask-babel @@ -219,53 +227,53 @@ jinja2==3.1.4 # swagger-ui-bundle jmespath==1.0.1 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # boto3 # botocore json2html==1.3.0 # via -r requirements-dev.in jsonpath-rw==1.4.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # jsonpath-rw-ext jsonpath-rw-ext==1.2.2 - # via -r requirements.txt + # via -r /assesplication-store/requirements.txt jsonschema==4.22.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # connexion # openapi-schema-validator # openapi-spec-validator jsonschema-path==0.3.3 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # openapi-spec-validator jsonschema-specifications==2023.12.1 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # jsonschema # openapi-schema-validator lazy-object-proxy==1.10.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # openapi-spec-validator mako==1.3.5 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # alembic markupsafe==2.1.5 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # jinja2 # mako # sentry-sdk # werkzeug marshmallow==3.21.3 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # marshmallow-sqlalchemy marshmallow-sqlalchemy==1.0.0 - # via -r requirements.txt + # via -r /assesplication-store/requirements.txt mccabe==0.7.0 # via flake8 moto[s3,sqs]==5.0.10 @@ -274,36 +282,44 @@ mypy-extensions==1.0.0 # via black nodeenv==1.9.1 # via pre-commit +numpy==2.1.3 + # via + # -r /assesplication-store/requirements.txt + # pandas openapi-schema-validator==0.6.2 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # openapi-spec-validator openapi-spec-validator==0.7.1 - # via -r requirements.txt + # via -r /assesplication-store/requirements.txt +openpyxl==3.1.5 + # via -r /assesplication-store/requirements.txt ordered-set==4.1.0 # via deepdiff packaging==24.1 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # black # build # gunicorn # marshmallow # prance # pytest +pandas==2.2.3 + # via -r /assesplication-store/requirements.txt parse==1.20.2 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # alembic-utils pathable==0.4.3 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # jsonschema-path pathspec==0.12.1 # via black pbr==6.0.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # jsonpath-rw-ext pip-tools==7.4.1 # via -r requirements-dev.in @@ -315,31 +331,31 @@ pluggy==1.5.0 # via pytest ply==3.11 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # jsonpath-rw prance==23.6.21.0 - # via -r requirements.txt + # via -r /assesplication-store/requirements.txt pre-commit==4.0.1 # via -r requirements-dev.in psycopg2-binary==2.9.9 - # via -r requirements.txt + # via -r /assesplication-store/requirements.txt py-partiql-parser==0.5.5 # via moto pycodestyle==2.12.0 # via flake8 pycparser==2.22 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # cffi pyflakes==3.2.0 # via flake8 pygments==2.18.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # rich pyjwt[crypto]==2.8.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # funding-service-design-utils pyproject-hooks==1.1.0 # via @@ -359,33 +375,35 @@ pytest-mock==3.14.0 # via -r requirements-dev.in python-dateutil==2.9.0.post0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # botocore # dataclass-dict-convert # flask-apscheduler # moto + # pandas python-dotenv==1.0.1 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # funding-service-design-utils # uvicorn python-json-logger==2.0.7 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # funding-service-design-utils python-multipart==0.0.9 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # connexion pytz==2024.1 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # apscheduler # flask-babel # funding-service-design-utils + # pandas pyyaml==6.0.1 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # connexion # funding-service-design-utils # jsonschema-path @@ -395,17 +413,17 @@ pyyaml==6.0.1 # uvicorn redis==4.6.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # flask-redis referencing==0.35.1 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # jsonschema # jsonschema-path # jsonschema-specifications requests==2.32.3 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # connexion # funding-service-design-utils # jsonschema-path @@ -416,36 +434,36 @@ responses==0.25.3 # via moto rfc3339-validator==0.1.4 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # openapi-schema-validator rich==12.6.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # funding-service-design-utils rpds-py==0.18.1 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # jsonschema # referencing ruamel-yaml==0.18.6 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # prance ruamel-yaml-clib==0.2.8 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # ruamel-yaml s3transfer==0.10.2 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # boto3 sentry-sdk[flask]==2.9.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # funding-service-design-utils six==1.16.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # apscheduler # jsonpath-rw # prance @@ -453,36 +471,39 @@ six==1.16.0 # rfc3339-validator sniffio==1.3.1 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # anyio # httpx soupsieve==2.5 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # beautifulsoup4 sqlalchemy==2.0.31 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # alembic # alembic-utils # flask-sqlalchemy # marshmallow-sqlalchemy + # sqlalchemy-json # sqlalchemy-utils +sqlalchemy-json==0.7.0 + # via -r /assesplication-store/requirements.txt sqlalchemy-utils==0.41.2 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # funding-service-design-utils starlette==0.37.2 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # connexion stringcase==1.2.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # dataclass-dict-convert swagger-ui-bundle==1.1.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # connexion tomli==2.0.1 # via @@ -494,7 +515,7 @@ tomli==2.0.1 # pytest-env typing-extensions==4.12.2 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # a2wsgi # alembic # alembic-utils @@ -505,38 +526,42 @@ typing-extensions==4.12.2 # flupy # sqlalchemy # uvicorn +tzdata==2024.2 + # via + # -r /assesplication-store/requirements.txt + # pandas tzlocal==5.2 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # apscheduler urllib3==2.2.2 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # botocore # requests # responses # sentry-sdk uvicorn[standard]==0.30.1 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # connexion uvloop==0.19.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # uvicorn virtualenv==20.26.3 # via pre-commit watchfiles==0.22.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # uvicorn websockets==12.0 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # uvicorn werkzeug==3.0.3 # via - # -r requirements.txt + # -r /assesplication-store/requirements.txt # connexion # flask # moto diff --git a/requirements.in b/requirements.in index aaf7ddb5..8e2e63a9 100644 --- a/requirements.in +++ b/requirements.in @@ -35,3 +35,6 @@ prance==23.6.21.0 dataclass_dict_convert==1.7.3 jsonpath-rw-ext==1.2.2 + +pandas +openpyxl diff --git a/requirements.txt b/requirements.txt index 94affb87..7c32a32e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -69,6 +69,8 @@ dataclass-dict-convert==1.7.3 # via -r requirements.in decorator==5.1.1 # via jsonpath-rw +et-xmlfile==2.0.0 + # via openpyxl exceptiongroup==1.2.1 # via anyio flask[async]==3.0.3 @@ -101,6 +103,8 @@ flupy==1.2.0 # via alembic-utils funding-service-design-utils==5.0.8 # via -r requirements.in +greenlet==3.1.1 + # via sqlalchemy gunicorn==22.0.0 # via funding-service-design-utils h11==0.14.0 @@ -161,15 +165,21 @@ marshmallow==3.21.3 # via marshmallow-sqlalchemy marshmallow-sqlalchemy==1.0.0 # via -r requirements.in +numpy==2.1.3 + # via pandas openapi-schema-validator==0.6.2 # via openapi-spec-validator openapi-spec-validator==0.7.1 # via -r requirements.in +openpyxl==3.1.5 + # via -r requirements.in packaging==24.1 # via # gunicorn # marshmallow # prance +pandas==2.2.3 + # via -r requirements.in parse==1.20.2 # via alembic-utils pathable==0.4.3 @@ -187,12 +197,15 @@ pycparser==2.22 pygments==2.18.0 # via rich pyjwt[crypto]==2.8.0 - # via funding-service-design-utils + # via + # funding-service-design-utils + # pyjwt python-dateutil==2.9.0.post0 # via # botocore # dataclass-dict-convert # flask-apscheduler + # pandas python-dotenv==1.0.1 # via # funding-service-design-utils @@ -206,6 +219,7 @@ pytz==2024.1 # apscheduler # flask-babel # funding-service-design-utils + # pandas pyyaml==6.0.1 # via # connexion @@ -262,7 +276,10 @@ sqlalchemy==2.0.31 # alembic-utils # flask-sqlalchemy # marshmallow-sqlalchemy + # sqlalchemy-json # sqlalchemy-utils +sqlalchemy-json==0.7.0 + # via -r requirements.in sqlalchemy-utils==0.41.2 # via # -r requirements.in @@ -286,6 +303,8 @@ typing-extensions==4.12.2 # flupy # sqlalchemy # uvicorn +tzdata==2024.2 + # via pandas tzlocal==5.2 # via apscheduler urllib3==2.2.2 diff --git a/scripts/seed_db_test_data.py b/scripts/seed_db_test_data.py new file mode 100644 index 00000000..58cc3b82 --- /dev/null +++ b/scripts/seed_db_test_data.py @@ -0,0 +1,138 @@ +import sys + +import click + +sys.path.insert(1, ".") + +from app import app # noqa: E402 +from db.models.application.applications import Status # noqa: E402 +from fsd_test_utils.test_config.useful_config import UsefulConfig # noqa: E402 +from tests_apply.seed_data.seed_db import seed_in_progress_application # noqa: E402 +from tests_apply.seed_data.seed_db import ( # noqa: E402 + seed_not_started_application, + seed_completed_application, + seed_submitted_application, +) # noqa: E402 + + +FUND_CONFIG = { + "COF": { + "id": UsefulConfig.COF_FUND_ID, + "short_code": "COF", + "rounds": { + "R3W1": { + "short_code": "R3W1", + "id": UsefulConfig.COF_ROUND_3_W1_ID, + "project_name_form": "project-information-cof-r3-w1", + }, + "R3W2": { + "short_code": "R3W2", + "id": UsefulConfig.COF_ROUND_3_W2_ID, + "project_name_form": "project-information-cof-r3-w2", + }, + }, + }, + "NSTF": { + "id": UsefulConfig.NSTF_FUND_ID, + "short_code": "NSTF", + "rounds": { + "R2": { + "short_code": "R2", + "id": UsefulConfig.NSTF_ROUND_2_ID, + "project_name_form": "name-your-application-ns", + } + }, + }, + "HSRA": { + "id": UsefulConfig.HSRA_FUND_ID, + "short_code": "HSRA", + "rounds": { + "R1": { + "short_code": "R1", + "id": UsefulConfig.HSRA_ROUND_1_ID, + "project_name_form": "name-your-application-hsra", + } + }, + }, +} + + +@click.command() +@click.option( + "--fund_short_code", + default="COF", + type=click.Choice(["COF", "NSTF", "HSRA"]), + help="Fund to seed applications for", + prompt=True, +) +@click.option( + "--round_short_code", + default="R3W2", + type=click.Choice(["R3W2", "R3W1", "R2", "R1"]), + help="Round to seed applications for", + prompt=True, +) +@click.option( + "--account_id", + default="7d00296f-6dd6-47fe-a084-48d94fecf3fa", + help="Account ID to seed applications for", + prompt=True, +) +@click.option( + "--status", + default="IN_PROGRESS", + type=click.Choice(["NOT_STARTED", "IN_PROGRESS", "COMPLETED", "SUBMITTED"]), + help="Target status for seeded applications", + prompt=True, +) +@click.option("--count", default=1, help="Number of applications to create", prompt=True) +def seed_applications(fund_short_code, round_short_code, account_id, status, count): + language = "en" + + fund_config = FUND_CONFIG[fund_short_code] + round_config = fund_config["rounds"][round_short_code] + match status: + case Status.NOT_STARTED.name: + for i in range(count): + app = seed_not_started_application( + fund_config=fund_config, + round_config=round_config, + account_id=account_id, + language=language, + ) + print(f"{app.id} - {app.reference} - {app.status.name}") + case Status.IN_PROGRESS.name: + for i in range(count): + app = seed_in_progress_application( + fund_config=fund_config, + round_config=round_config, + account_id=account_id, + language=language, + ) + print(f"{app.id} - {app.reference} - {app.status.name}") + case Status.COMPLETED.name: + for i in range(count): + app = seed_completed_application( + fund_config=fund_config, + round_config=round_config, + account_id=account_id, + language=language, + ) + print(f"{app.id} - {app.reference} - {app.status.name}") + case Status.SUBMITTED.name: + for i in range(count): + app = seed_submitted_application( + fund_config=fund_config, + round_config=round_config, + account_id=account_id, + language=language, + ) + print(f"{app.id} - {app.reference} - {app.status.name}") + case _: + print(f"Status {status} is not supported") + exit(-1) + + +if __name__ == "__main__": + with app.app.app_context(): + seed_applications() diff --git a/scripts/send_application_on_closure.py b/scripts/send_application_on_closure.py new file mode 100644 index 00000000..cf9cdbb9 --- /dev/null +++ b/scripts/send_application_on_closure.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +import argparse +import sys +from datetime import datetime + +from distutils.util import strtobool + +sys.path.insert(1, ".") + +import services.apply # noqa: E402 +from app import app # noqa: E402 +from config import Config # noqa: E402 +from db.queries.apply import search_applications # noqa: E402 +from db.queries.apply import get_forms_by_app_id # noqa: E402 +from db.queries.apply.application import create_qa_base64file # noqa: E402 +from services.apply.models.notification import Notification # noqa: E402 +from services.apply.data import get_fund # noqa: E402 +from flask import current_app # noqa: E402 + + +def send_incomplete_applications_after_deadline( + fund_id, + round_id, + single_application=False, + application_id=None, + send_email=False, +): + """Retrieves a list of unsubmitted applications and associated form and user + data for each. Then, it uses the notification service to email the account ID + for each application. + + Note: + - To enable email notifications, set `send_email` to True. + - To process a single application, set `single_application` to True and provide the `application_id`. + + Args: + - fund_id (str): The ID of the fund. + - round_id (str): The ID of the funding round. + - single_application (bool, optional): Set to True if processing an individual application. + - send_email (bool): Set to True or False to determine whether to send an email. + - application_id (str, required if `single_application` is True): The application_id to process. + + """ + + fund_rounds = get_fund_round(fund_id, round_id) + deadline = datetime.strptime(fund_rounds.get("deadline"), "%Y-%m-%dT%H:%M:%S") + if datetime.now() > deadline: + fund_data = get_fund(fund_id) + search_params = { + "status_only": ["NOT_STARTED", "IN_PROGRESS", "COMPLETED"], + "fund_id": fund_id, + "round_id": round_id, + "application_id": application_id if single_application else None, + } + matching_applications = search_applications(**search_params) + + applications_to_send = [] + for application in matching_applications: + application = {**application, "fund_name": fund_data.name} + try: + application["forms"] = get_forms_by_app_id(application.get("id")) + application["fund_id"] = fund_id + application["round_name"] = fund_rounds.get("title") + try: + account_id = services.apply.get_account(account_id=application.get("account_id")) + application["account_email"] = account_id.email + application["account_name"] = account_id.full_name + applications_to_send.append({"application": application}) + except Exception: + handle_error( + f"Unable to retrieve account id ({application.get('account_id')}) for " + + f"application id {application.get('id')}", + send_email, + ) + except Exception: + handle_error( + "Unable to retrieve forms for " + f"application id {application.get('id')}", + send_email, + ) + + current_app.logger.info( + f"Found {len(matching_applications)} applications with matching" + f" statuses. Retrieved all data for {len(applications_to_send)} of" + " them." + ) + if send_email: + total_applications = len(applications_to_send) + current_app.logger.info( + "Send email set to true, will now send" + f" {total_applications} {'emails' if total_applications > 1 else 'email'}." + ) + if total_applications > 0: + for count, application in enumerate(applications_to_send, start=1): + email = {"email": application.get("account_email") for application in application.values()} + current_app.logger.info( + f"Sending application {count} of {total_applications} to {email.get('email')}" + ) + application["contact_help_email"] = fund_rounds.get("contact_email") + message_id = Notification.send( + template_type=Config.NOTIFY_TEMPLATE_INCOMPLETE_APPLICATION, # noqa + to_email=email.get("email"), + full_name=application["application"]["account_name"], + content={ + "application": create_qa_base64file(application["application"], True), + "contact_help_email": application["contact_help_email"], + }, + ) + current_app.logger.info(f"Message added to the queue msg_id: [{message_id}]") + current_app.logger.info(f"Sent {count} {'emails' if count > 1 else 'email'}") + return count + else: + current_app.logger.warning("There are no applications to be sent.") + return 0 + else: + count = len(applications_to_send) + current_app.logger.warning( + f"Send email set to false, will not send {count} {'emails' if count > 1 else 'email'}." + ) + return len(applications_to_send) + else: + current_app.logger.warning("Current round is active") + return -1 + + +def handle_error(msg, throw_on_error): + current_app.logger.error(msg) + if throw_on_error: + raise LookupError(msg) + + +def get_fund_round(fund_id, round_id): + return services.apply.get_data( + Config.FUND_STORE_API_HOST + Config.FUND_ROUND_ENDPOINT.format(fund_id=fund_id, round_id=round_id) + ) + + +def init_argparse() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser() + parser.add_argument("--fund_id", help="Provide fund id of a fund", required=True) + parser.add_argument("--round_id", help="Provide round id of a fund", required=True) + parser.add_argument( + "--single_application", + help="Whether to send just single application: True or False", + required=True, + ) + + parser.add_argument( + "--application_id", + help="Provide application id if single_application is True", + required=False, + ) + parser.add_argument( + "--send_email", + help="Whether to actually send email: True or False", + required=True, + ) + return parser + + +def main() -> None: + parser = init_argparse() + args = parser.parse_args() + single_application = ( + strtobool(args.single_application) + if args.single_application is not None and not isinstance(args.single_application, bool) + else args.single_application + ) + + if single_application and args.application_id is None: + error_message = "The application_id argument is required if single_application is True" + current_app.logger.error(error_message) + raise ValueError(error_message) + + send_incomplete_applications_after_deadline( + fund_id=args.fund_id, + round_id=args.round_id, + single_application=single_application, + application_id=args.application_id, + send_email=strtobool(args.send_email), + ) + + +if __name__ == "__main__": + with app.app.app_context(): + main() diff --git a/scripts/send_application_reminder.py b/scripts/send_application_reminder.py new file mode 100644 index 00000000..8d19a559 --- /dev/null +++ b/scripts/send_application_reminder.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +import sys + +sys.path.insert(1, ".") + +from services.apply.exceptions import NotificationError # noqa: E402 +import services.apply # noqa: E402 +from config import Config # noqa: E402 +from services.apply.models.notification import Notification # noqa: E402 +from flask import current_app # noqa: E402 +from db.queries.apply import search_applications # noqa: E402 + +from datetime import datetime # noqa: E402 +import requests # noqa: E402 + +import pytz # noqa: E402 + + +def application_deadline_reminder(flask_app): + with flask_app.app_context(): + uk_timezone = pytz.timezone("Europe/London") + current_datetime = datetime.now(uk_timezone).replace(tzinfo=None) + funds = services.apply.get_data(Config.FUND_STORE_API_HOST + Config.FUNDS_ENDPOINT) + + for fund in funds: + fund_id = fund.get("id") + round_info = services.apply.get_data( + Config.FUND_STORE_API_HOST + Config.FUND_ROUNDS_ENDPOINT.format(fund_id=fund_id) + ) + + for round in round_info: + round_deadline_str = round.get("deadline") + reminder_date_str = round.get("reminder_date") + + if not reminder_date_str: + current_app.logger.info(f"No reminder is set for the round {round.get('title')}") + continue + + application_reminder_sent = round.get("application_reminder_sent") + + # Convert the string dates to datetime objects + round_deadline = datetime.strptime(round_deadline_str, "%Y-%m-%dT%H:%M:%S") + reminder_date = datetime.strptime(reminder_date_str, "%Y-%m-%dT%H:%M:%S") + + if not application_reminder_sent and reminder_date < current_datetime < round_deadline: + round_id = round.get("id") + round_name = round.get("title") + contact_email = round.get("contact_email") + fund_info = services.apply.get_data( + Config.FUND_STORE_API_HOST + Config.FUND_ENDPOINT.format(fund_id=fund_id) + ) + fund_name = fund_info.get("name") + + status = { + "status_only": ["IN_PROGRESS", "NOT_STARTED", "COMPLETED"], + "fund_id": fund_id, + "round_id": round_id, + } + + not_submitted_applications = search_applications(**status) + + all_applications = [] + for application in not_submitted_applications: + application["round_name"] = round_name + application["fund_name"] = fund_name + application["contact_help_email"] = contact_email + account = services.apply.get_account(account_id=application.get("account_id")) + application["account_email"] = account.email + application["deadline_date"] = round_deadline_str + all_applications.append({"application": application}) + + # Only one email per account_email + unique_email_account = {} + for application in all_applications: + unique_email_account[application["application"]["account_email"]] = application + unique_application_email_addresses = list(unique_email_account.values()) + + if len(unique_application_email_addresses) > 0: + for count, application in enumerate(unique_application_email_addresses, start=1): + email = {"email": application["application"]["account_email"]} + + current_app.logger.info(f"Sending reminder {count} of {len(unique_email_account)}") + + try: + message_id = Notification.send( + template_type=Config.NOTIFY_TEMPLATE_APPLICATION_DEADLINE_REMINDER, # noqa: E501 + to_email=email.get("email"), + content=application, + ) + current_app.logger.info(f"Message added to the queue msg_id: [{message_id}]") + if len(unique_application_email_addresses) == count: + try: + application_reminder_endpoint = ( + Config.FUND_STORE_API_HOST + + Config.FUND_ROUND_APPLICATION_REMINDER_STATUS.format(round_id=round_id) + ) + response = requests.put(application_reminder_endpoint) + if response.status_code == 200: + current_app.logger.info( + "The application reminder has been" + " sent successfully for round_id" + f" {round_id}" + ) + except Exception as e: + current_app.logger.info( + "There was an issue updating the" + " application_reminder_sent column in the" + f" Round store for {round_id}. Errro {e}" + ) + + except NotificationError as e: + current_app.logger.error(e.message) + + else: + current_app.logger.info("Currently, there are no non-submitted applications") + else: + continue + + +if __name__ == "__main__": + application_deadline_reminder() diff --git a/services/apply/data.py b/services/apply/data.py index 1fe91c1c..cc22404d 100644 --- a/services/apply/data.py +++ b/services/apply/data.py @@ -61,7 +61,7 @@ def get_local_data(endpoint: str, params: Optional[dict] = None): params = {k: v for k, v in params.items() if v is not None} query_string = urlencode(params) endpoint = endpoint + "?" + query_string - api_data_json = os.path.join(Config.FLASK_ROOT, "tests", "api_data", "get_endpoint_data.json") + api_data_json = os.path.join(Config.FLASK_ROOT, "tests_apply", "api_data", "get_endpoint_data.json") with open(api_data_json) as json_file: api_data = json.load(json_file) if endpoint in api_data: diff --git a/tests_apply/README.md b/tests_apply/README.md new file mode 100644 index 00000000..625c5604 --- /dev/null +++ b/tests_apply/README.md @@ -0,0 +1,62 @@ +# How to run tests + +1. Ensure that the environment variable DATABASE_URL is set to your db's connection string. + - The command `invoke bootstrap-test-db --database-host=your-db-url` will create a db called "fsd_app_store_test". +2. Ensure you have installed the requirements-dev.txt +3. Run `pytest` + +# Test Data +## Basic Usage - Individual application records +Test data is created on a per-test basis to prevent test pollution. To create test data for a test, request the `seed_application_records` fixture in your test. That fixture then provides access to the inserted records and will clean up after itself at the end of the test session. + +More details on the fixtures in utils: https://github.com/communitiesuk/funding-service-design-utils/blob/dcc64b0b253a1056ce99e8fe7ea8530406355c96/README.md#fixtures + +Basic example: + + @pytest.mark.apps_to_insert( + [ + { + "account_id": "user_a", + "fund_id": "123", + "language": "en", + "round_id": "456", + } + ] + ) + def test_stuff(seed_application_records): + app_id = seed_application_records[0].id + # do some testing + +## Unique Fund and Round IDs - same for all applications +If you need all your test data to use the same fund and round ids, but be different from all other tests, use `unique_fund_round` in your test. This generates a random ID for fund and round and uses this when creating test applications. + + pytest.mark.apps_to_insert([test_application_data[0]]) + @pytest.mark.unique_fund_round(True) + def test_some_reports( + seed_application_records, unique_fund_round + ): + result = get_by_fund_round( + fund_id=unique_fund_round[0], round_id=unique_fund_round[1] + ) + +## Control how many funds/rounds/applications are created +If you need to seed the DB with a number of funds and rounds, each with a number of applications, request `seed_data_multiple_funds_rounds` in your test. This will generate a random fund ID and random round ID for each one requested, and insert the required number of applications. The following example will create 1 fund with 2 rounds, each containing 2 applications + + @pytest.mark.fund_round_config( + { + "funds": [ + {"rounds": [ + {"applications": [{**app_data}, {**app_data}]}, + {"applications": [{**app_data}, {**app_data}]} + ]}, + ] + } + ) + def test_multi_funds( + client, seed_data_multiple_funds_rounds + ): + result = get_all_apps_for_round( + fund_id=seed_data_multiple_funds_rounds[0].fund_id, + round_id=seed_data_multiple_funds_rounds[0].round_ids[0].round_id + ) + assert 2 == len(result) diff --git a/tests_apply/__init__.py b/tests_apply/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests_apply/api_data/get_endpoint_data.json b/tests_apply/api_data/get_endpoint_data.json new file mode 100644 index 00000000..835259bc --- /dev/null +++ b/tests_apply/api_data/get_endpoint_data.json @@ -0,0 +1,397 @@ +{ + "account_store/accounts?email_address=a%40example.com": { + "account_id": "usera", + "email_address": "a@example.com" + }, + "account_store/accounts?email_address=b%40example.com": { + "account_id": "userb", + "email_address": "b@example.com" + }, + "account_store/accounts?account_id=test-user": { + "account_id": "test-user", + "email_address": "test@example.com" + }, + "account_store/accounts?account_id=usera": { + "account_id": "usera", + "email_address": "a@example.com" + }, + "account_store/accounts?account_id=userb": { + "account_id": "userb", + "email_address": "b@example.com" + }, + "account_store/accounts?account_id=userc": { + "account_id": "userc", + "email_address": "c@example.com" + }, + "fund_store/funds": [ + { + "name": "COF", + "short_name": "COF", + "id": "47aef2f5-3fcb-4d45-acb5-f0152b5f03c4", + "description": "An example fund for testing the funding service" + }, + { + "name": "Fund B", + "short_name": "FUB", + "id": "fund-b", + "description": "An example fund for testing the funding service" + }, + { + "name": "Funding Service Design", + "short_name": "FSD", + "id": "funding-service-design", + "description": "An example fund for testing the funding service" + }, + { + "name": "Community Ownership Fund", + "short_name": "CON", + "id": "community-ownership-fund", + "description": "An example Community Ownership Fund for testing the funding service" + } + ], + "fund_store/funds/47aef2f5-3fcb-4d45-acb5-f0152b5f03c4": { + "name": "COF", + "short_name": "COF", + "id": "47aef2f5-3fcb-4d45-acb5-f0152b5f03c4", + "description": "An example fund for testing the funding service" + }, + "fund_store/funds/fund-b": { + "name": "Fund B", + "short_name": "FUB", + "id": "fund-b", + "description": "An example fund for testing the funding service" + }, + "fund_store/funds/funding-service-design": { + "name": "Funding Service Design", + "short_name": "FSD", + "id": "funding-service-design", + "description": "An example fund for testing the funding service" + }, + "fund_store/funds/community-ownership-fund": { + "name": "COF", + "short_name": "CON", + "id": "community-ownership-fund", + "description": "An example Community Ownership Fund for testing the funding service" + }, + "fund_store/funds/47aef2f5-3fcb-4d45-acb5-f0152b5f03c4/rounds": [ + { + "fund_id": "47aef2f5-3fcb-4d45-acb5-f0152b5f03c4", + "short_name": "SUM", + "title": "Summer", + "id": "c603d114-5364-4474-a0c4-c41cbf4d3bbd", + "opens": "2022-02-01T00:00:01", + "deadline": "2022-06-01T00:00:00", + "assessment_deadline": "2022-09-30T00:00:00", + "assessment_criteria_weighting": [ + { + "id": "e2fd30d2-9207-421c-b8b3-c961bcee138b", + "name": "Strategic case", + "value": 0.30 + }, + { + "id": "e557773a-74c9-43ee-a52c-88ccae279d08", + "name": "Management case", + "value": 0.30 + }, + { + "id": "9e282cdb-6c42-4430-9563-dc4995b59bdd", + "name": "Potential to delivery community benefits", + "value": 0.30 + }, + { + "id": "6020db6c-df67-4932-a2f3-2e9dd1934164", + "name": "Added value to the community", + "value": 0.10 + } + ] + } + ], + "fund_store/funds/fund-b/rounds": [ + { + "fund_id": "funding-a", + "short_name": "SUM", + "title": "Summer", + "id": "c603d114-5364-4474-a0c4-c41cbf4d3bbd", + "opens": "2022-02-01T00:00:01", + "deadline": "2022-06-01T00:00:00", + "assessment_deadline": "2022-09-30T00:00:00", + "assessment_criteria_weighting": [ + { + "id": "e2fd30d2-9207-421c-b8b3-c961bcee138b", + "name": "Strategic case", + "value": 0.30 + }, + { + "id": "e557773a-74c9-43ee-a52c-88ccae279d08", + "name": "Management case", + "value": 0.30 + }, + { + "id": "9e282cdb-6c42-4430-9563-dc4995b59bdd", + "name": "Potential to delivery community benefits", + "value": 0.30 + }, + { + "id": "6020db6c-df67-4932-a2f3-2e9dd1934164", + "name": "Added value to the community", + "value": 0.10 + } + ] + } + ], + "fund_store/funds/funding-service-design/rounds": [ + { + "fund_id": "funding-service-design", + "title": "Spring", + "short_name": "SPR", + "id": "spring", + "opens": "2022-02-01T00:00:01", + "deadline": "2022-06-01T00:00:00", + "assessment_deadline": "2022-09-30T00:00:00", + "assessment_criteria_weighting": [ + { + "id": "e2fd30d2-9207-421c-b8b3-c961bcee138b", + "name": "Strategic case", + "value": 0.30 + }, + { + "id": "e557773a-74c9-43ee-a52c-88ccae279d08", + "name": "Management case", + "value": 0.30 + }, + { + "id": "9e282cdb-6c42-4430-9563-dc4995b59bdd", + "name": "Potential to delivery community benefits", + "value": 0.30 + }, + { + "id": "6020db6c-df67-4932-a2f3-2e9dd1934164", + "name": "Added value to the community", + "value": 0.10 + } + ] + }, + { + "fund_id": "funding-service-design", + "title": "Summer", + "short_name": "SUM", + "id": "c603d114-5364-4474-a0c4-c41cbf4d3bbd", + "opens": "2022-06-01T00:00:01", + "deadline": "2022-08-31T00:00:00", + "assessment_deadline": "2022-12-30T00:00:00", + "assessment_criteria_weighting": [ + { + "id": "e2fd30d2-9207-421c-b8b3-c961bcee138b", + "name": "Strategic case", + "value": 0.30 + }, + { + "id": "e557773a-74c9-43ee-a52c-88ccae279d08", + "name": "Management case", + "value": 0.30 + }, + { + "id": "9e282cdb-6c42-4430-9563-dc4995b59bdd", + "name": "Potential to delivery community benefits", + "value": 0.30 + }, + { + "id": "6020db6c-df67-4932-a2f3-2e9dd1934164", + "name": "Added value to the community", + "value": 0.10 + } + ] + }, + { + "fund_id": "funding-service-design", + "title": "Autumn", + "short_name": "AUT", + "id": "autumn", + "opens": "2022-09-01T00:00:01", + "deadline": "2022-11-30T00:00:00", + "assessment_deadline": "2023-03-30T00:00:00", + "assessment_criteria_weighting": [ + { + "id": "e2fd30d2-9207-421c-b8b3-c961bcee138b", + "name": "Strategic case", + "value": 0.30 + }, + { + "id": "e557773a-74c9-43ee-a52c-88ccae279d08", + "name": "Management case", + "value": 0.30 + }, + { + "id": "9e282cdb-6c42-4430-9563-dc4995b59bdd", + "name": "Potential to delivery community benefits", + "value": 0.30 + }, + { + "id": "6020db6c-df67-4932-a2f3-2e9dd1934164", + "name": "Added value to the community", + "value": 0.10 + } + ] + } + ], + "fund_store/funds/47aef2f5-3fcb-4d45-acb5-f0152b5f03c4/rounds/c603d114-5364-4474-a0c4-c41cbf4d3bbd": { + "fund_id": "funding-service-design", + "title": "Summer", + "short_name": "SUM", + "id": "c603d114-5364-4474-a0c4-c41cbf4d3bbd", + "opens": "2022-02-01T00:00:01", + "deadline": "2022-06-01T00:00:00", + "assessment_deadline": "2022-09-30T00:00:00", + "title_json": {"en": "English fund name", "cy": "Welsh fund name"}, + "assessment_criteria_weighting": [ + { + "id": "e2fd30d2-9207-421c-b8b3-c961bcee138b", + "name": "Strategic case", + "value": 0.30 + }, + { + "id": "e557773a-74c9-43ee-a52c-88ccae279d08", + "name": "Management case", + "value": 0.30 + }, + { + "id": "9e282cdb-6c42-4430-9563-dc4995b59bdd", + "name": "Potential to delivery community benefits", + "value": 0.30 + }, + { + "id": "6020db6c-df67-4932-a2f3-2e9dd1934164", + "name": "Added value to the community", + "value": 0.10 + } + ], + "project_name_field_id":"KAgrBz" + }, + "fund_store/funds/fund-b/rounds/summer": { + "fund_id": "funding-service-design", + "short_name": "SUM", + "title": "Spring", + "id": "spring", + "opens": "2022-02-01T00:00:01", + "deadline": "2022-06-01T00:00:00", + "round_name": "random_round_name", + "assessment_deadline": "2022-09-30T00:00:00", + "assessment_criteria_weighting": [ + { + "id": "e2fd30d2-9207-421c-b8b3-c961bcee138b", + "name": "Strategic case", + "value": 0.30 + }, + { + "id": "e557773a-74c9-43ee-a52c-88ccae279d08", + "name": "Management case", + "value": 0.30 + }, + { + "id": "9e282cdb-6c42-4430-9563-dc4995b59bdd", + "name": "Potential to delivery community benefits", + "value": 0.30 + }, + { + "id": "6020db6c-df67-4932-a2f3-2e9dd1934164", + "name": "Added value to the community", + "value": 0.10 + } + ] + }, + "fund_store/funds/funding-service-design/rounds/spring": { + "fund_id": "funding-service-design", + "short_name": "SPR", + "title": "Spring", + "id": "spring", + "opens": "2022-02-01T00:00:01", + "deadline": "2022-06-01T00:00:00", + "round_name": "random_round_name", + "assessment_deadline": "2022-09-30T00:00:00", + "assessment_criteria_weighting": [ + { + "id": "e2fd30d2-9207-421c-b8b3-c961bcee138b", + "name": "Strategic case", + "value": 0.30 + }, + { + "id": "e557773a-74c9-43ee-a52c-88ccae279d08", + "name": "Management case", + "value": 0.30 + }, + { + "id": "9e282cdb-6c42-4430-9563-dc4995b59bdd", + "name": "Potential to delivery community benefits", + "value": 0.30 + }, + { + "id": "6020db6c-df67-4932-a2f3-2e9dd1934164", + "name": "Added value to the community", + "value": 0.10 + } + ] + }, + "fund_store/funds/funding-service-design/rounds/c603d114-5364-4474-a0c4-c41cbf4d3bbd": { + "fund_id": "funding-service-design", + "short_name": "SUM", + "title": "Summer", + "id": "c603d114-5364-4474-a0c4-c41cbf4d3bbd", + "opens": "2022-06-01T00:00:01", + "deadline": "2022-08-31T00:00:00", + "assessment_deadline": "2022-09-30T00:00:00", + "assessment_criteria_weighting": [ + { + "id": "e2fd30d2-9207-421c-b8b3-c961bcee138b", + "name": "Strategic case", + "value": 0.30 + }, + { + "id": "e557773a-74c9-43ee-a52c-88ccae279d08", + "name": "Management case", + "value": 0.30 + }, + { + "id": "9e282cdb-6c42-4430-9563-dc4995b59bdd", + "name": "Potential to delivery community benefits", + "value": 0.30 + }, + { + "id": "6020db6c-df67-4932-a2f3-2e9dd1934164", + "name": "Added value to the community", + "value": 0.10 + } + ] + }, + "fund_store/funds/funding-service-design/rounds/autumn": { + "fund_id": "funding-service-design", + "title": "Autumn", + "short_name": "AUT", + "id": "autumn", + "opens": "2022-09-01T00:00:01", + "deadline": "2022-11-30T00:00:00", + "round_name": "random_round_name", + "assessment_deadline": "2022-09-30T00:00:00", + "assessment_criteria_weighting": [ + { + "id": "e2fd30d2-9207-421c-b8b3-c961bcee138b", + "name": "Strategic case", + "value": 0.30 + }, + { + "id": "e557773a-74c9-43ee-a52c-88ccae279d08", + "name": "Management case", + "value": 0.30 + }, + { + "id": "9e282cdb-6c42-4430-9563-dc4995b59bdd", + "name": "Potential to delivery community benefits", + "value": 0.30 + }, + { + "id": "6020db6c-df67-4932-a2f3-2e9dd1934164", + "name": "Added value to the community", + "value": 0.10 + } + ] + } +} diff --git a/tests_apply/api_data/post_endpoint_data.json b/tests_apply/api_data/post_endpoint_data.json new file mode 100644 index 00000000..bc094585 --- /dev/null +++ b/tests_apply/api_data/post_endpoint_data.json @@ -0,0 +1,7 @@ +{ + "notification_service/send":{ + "_default": { + "status": "ok" + } + } +} diff --git a/tests_apply/conftest.py b/tests_apply/conftest.py new file mode 100644 index 00000000..3763291b --- /dev/null +++ b/tests_apply/conftest.py @@ -0,0 +1,358 @@ +from datetime import datetime +from datetime import timedelta +from uuid import uuid4 + +import pytest +from app import create_app +from db.models.application.applications import Applications +from db.queries.apply.application import create_application +from db.queries.apply.form import add_new_forms +from flask import Response +from services.apply.models.fund import Fund +from services.apply.models.fund import Round +from tests_apply.helpers import APPLICATION_DISPLAY_CONFIG +from tests_apply.helpers import local_api_call +from tests_apply.helpers import test_application_data +from tests_apply.helpers import test_question_data +from tests_apply.helpers import test_question_data_cy + +# Make the utils fixtures available, used in seed_application_records +pytest_plugins = ["fsd_test_utils.fixtures.db_fixtures"] + + +@pytest.fixture(scope="session") +def app(): + """Creates the test client we will be using to test the responses from our + app, this is a test fixture. + + :return: A flask test client. + + """ + app = create_app() + yield app.app + + +@pytest.fixture(scope="function") +def flask_test_client(): + with create_app().test_client() as test_client: + yield test_client + + +@pytest.fixture(scope="function") +def unique_fund_round(mock_get_fund, mock_get_round): + """Returns a tuple of 2 random uuids as fund_id and round_id. + + Requests mock_get_fund and mock_get_round so when the app looks up + fund/round data it matches with these. + + """ + return (str(uuid4()), str(uuid4())) + + +def get_args(seed_application_records, unique_fund_round, single_app): + application_ids = [str(application).split()[-1][:-1] for application in seed_application_records] + args = { + "fund_id": unique_fund_round[0], + "round_id": unique_fund_round[1], + "single_application": True, + "application_id": application_ids[0] if single_app else None, + "send_email": False, + } + return args + + +def create_app_with_blank_forms(app_to_create: dict) -> Applications: + """Creates a new application record in the database using the supplied + dictionary of application fields. + + Each inserted application has 3 blank forms attached (declarations, + project-info, org-info, or their welsh equivalents depending on + language). + + """ + app = create_application(**app_to_create) + add_new_forms( + ["datganiadau" if (app.language and app.language.name == "cy") else "declarations"], + app.id, + ) + add_new_forms( + ["gwybodaeth-am-y-prosiect" if (app.language and app.language.name == "cy") else "project-information"], + app.id, + ) + add_new_forms( + ["gwybodaeth-am-y-sefydliad" if (app.language and app.language.name == "cy") else "organisation-information"], + app.id, + ) + return app + + +@pytest.fixture(scope="function") +def seed_application_records( + request, + app, + clear_test_data, + enable_preserve_test_data, + unique_fund_round, +): + """Inserts application data on a per-test (function scoped) basis to prevent + test pollution. Provides the inserted records to tests so they can access + them. + + Each inserted application has 3 blank forms attached (declarations, + project-info, org-info, or their welsh equivalents depending on + language). + + """ + marker = request.node.get_closest_marker("apps_to_insert") + if marker is None: + apps = [test_application_data[0]] + else: + apps = marker.args[0] + unique_fr_marker = request.node.get_closest_marker("unique_fund_round") + + seeded_apps = [] + for app in apps: + if unique_fr_marker is not None: + app["fund_id"] = unique_fund_round[0] + app["round_id"] = unique_fund_round[1] + created_app = create_app_with_blank_forms(app) + seeded_apps.append(created_app) + yield seeded_apps + + +def add_org_data_for_reports(application, unique_append, client): + """Adds additional form data to the application so it is present for testing + the reports. + + Each org name is unique in the format 'Test Org Name + {unique_append}' + + """ + if application.language and application.language.name == "cy": + form_names = ["gwybodaeth-am-y-sefydliad", "gwybodaeth-am-y-prosiect"] + address = "WelshGov, CF10 3NQ" + unique_append = str(unique_append) + "cy" + question_data = test_question_data_cy + else: + address = "BBC, W1A 1AA" + form_names = ["organisation-information", "project-information"] + question_data = test_question_data + sections_put = [ + { + "questions": question_data, + "metadata": { + "application_id": str(application.id), + "form_name": form_names[0], + "is_summary_page_submit": False, + }, + }, + { + "questions": [ + { + "question": "Address", + "fields": [ + { + "key": "yEmHpp", + "title": "Address", + "type": "text", + "answer": address, + }, + ], + }, + ], + "metadata": { + "application_id": str(application.id), + "form_name": form_names[1], + "is_summary_page_submit": False, + }, + }, + ] + # Make the org names unique + sections_put[0]["questions"][1]["fields"][0]["answer"] = f"Test Org Name {unique_append}" + + for section in sections_put: + client.put( + "/apply/applications/forms", + json=section, + follow_redirects=True, + ) + + +@pytest.fixture(scope="function") +def seed_data_multiple_funds_rounds( + request, mocker, app, clear_test_data, enable_preserve_test_data, flask_test_client +): + """Alternative to seed_application_records above that allows you to specify a + set of funds/rounds and how many applications per round to allow testing of + reporting functions. Expects to find fund/round config as a marker named + 'fund_round_config' in the format: {funds: [rounds: [{applications: + [{app_data}]}]]} + + yields a data structure containing the generated IDs: + {[(fund_id: xxx, round_ids: [(round_id: yyy, + application_ids: [111, 222])])]} + + """ + marker = request.node.get_closest_marker("fund_round_config") + if marker is None: + config = {"funds": [{"rounds": [{"applications": [test_application_data[0]]}]}]} + else: + config = marker.args[0] + + from collections import namedtuple + + FundRound = namedtuple("FundRound", "fund_id round_ids") + RoundApps = namedtuple("RoundApps", "round_id application_ids") + funds_rounds = [] + for fund in config["funds"]: + fund_id = str(uuid4()) + round_ids = [] + for round in fund["rounds"]: + round_id = str(uuid4()) + i = 0 + application_ids = [] + for appl in round["applications"]: + i += 1 + appl["fund_id"] = fund_id + appl["round_id"] = round_id + created_app = create_app_with_blank_forms(appl) + add_org_data_for_reports(created_app, i, flask_test_client) + application_ids.append(created_app.id) + round_ids.append(RoundApps(round_id, application_ids)) + funds_rounds.append(FundRound(fund_id, round_ids)) + yield funds_rounds + + +def mock_get_data(endpoint, params=None): + return local_api_call(endpoint, params, "get") + + +def mock_post_data(endpoint, params=None): + return local_api_call(endpoint, params, "post") + + +def mock_get_random_choices(population, weights=None, *, cum_weights=None, k=1): + return "ABCDEF" + + +def generate_mock_fund(fund_id: str) -> Fund: + return Fund( + "Generated test fund", fund_id, "TEST", "Testing fund", True, {"en": "English title", "cy": "Welsh title"}, [] + ) + + +@pytest.fixture(scope="function", autouse=True) +def mock_get_fund(mocker): + """Generates a mock fund with the supplied fund ID. + + Used with unique_fund_round to ensure when the fund and round are + retrieved, they match what's expected + + """ + mocker.patch("apply.api.routes.application.routes.get_fund", new=generate_mock_fund) + mocker.patch("db.queries.apply.application.queries.get_fund", new=generate_mock_fund) + + +@pytest.fixture(scope="function") +def mock_get_application_display_config(mocker): + mocker.patch( + "_helpers.form.get_application_sections", + return_value=APPLICATION_DISPLAY_CONFIG, + ) + + +def generate_mock_round(fund_id: str, round_id: str) -> Round: + return Round( + title="Generated test round", + id=round_id, + fund_id=fund_id, + short_name="TEST", + opens=(datetime.now() - timedelta(days=30)).strftime("%Y-%m-%dT%H:%M:%S"), + deadline=(datetime.now() + timedelta(days=5)).strftime("%Y-%m-%dT%H:%M:%S"), + assessment_deadline=(datetime.now() + timedelta(days=10)).strftime("%Y-%m-%dT%H:%M:%S"), + project_name_field_id="TestFieldId", + contact_email="test@outlook.com", + title_json={"en": "English title", "cy": "Welsh title"}, + ) + + +@pytest.fixture(scope="function", autouse=True) +def mock_get_round(mocker): + """Generates a mock round with the supplied fund and round IDs Used with + unique_fund_round to ensure when the fund and round are retrieved, they match + what's expected.""" + mocker.patch("db.queries.apply.application.queries.get_round", new=generate_mock_round) + mocker.patch("db.queries.apply.statuses.queries.get_round", new=generate_mock_round) + mocker.patch( + "db.schemas.application.get_round_name", + return_value="Generated test round", + ) + + +@pytest.fixture(autouse=True) +def mock_get_data_fix(mocker): + # mock the function in the file it is invoked (not where it is declared) + mocker.patch( + "services.apply.get_data", + new=mock_get_data, + ) + + +@pytest.fixture() +def mock_random_choices(mocker): + # mock the function in the file it is invoked (not where it is declared) + mocker.patch("random.choices", new=mock_get_random_choices) + + +@pytest.fixture() +def mock_successful_submit_notification(mocker): + # mock the function in the file it is invoked (not where it is declared) + mocker.patch( + "apply.api.routes.application.routes.Notification.send", + lambda template, email, full_name, application: Response(200), + ) + + +@pytest.fixture(autouse=True) +def mock_post_data_fix(mocker): + # mock the function in the file it is invoked (not where it is declared) + mocker.patch( + "services.apply.post_data", + new=mock_post_data, + ) + + +@pytest.fixture(autouse=False) +def mock_get_fund_data(mocker): + mocker.patch( + "apply.api.routes.application.routes.get_fund", + return_value=Fund( + name="COF", + short_name="COF", + identifier="47aef2f5-3fcb-4d45-acb5-f0152b5f03c4", + description="An example fund for testing the funding service", + welsh_available=False, + name_json={"en": "English Fund Name", "cy": "Welsh Fund Name"}, + ), + ) + + +@pytest.fixture(autouse=False) +def mocked_get_fund(mocker): + return mocker.patch( + "scripts.send_application_on_closure.get_fund", + return_value=Fund( + name="Community Ownership Fund", + identifier="47aef2f5-3fcb-4d45-acb5-f0152b5f03c4", + short_name="COF", + description=( + "The Community Ownership Fund is a £150 million fund over 4 years to" + " support community groups across England, Wales, Scotland and Northern" + " Ireland to take ownership of assets which are at risk of being lost" + " to the community." + ), + welsh_available=True, + name_json={"en": "English Fund Name", "cy": "Welsh Fund Name"}, + rounds=None, + ), + ) diff --git a/tests_apply/get_endpoint_data.json b/tests_apply/get_endpoint_data.json new file mode 100644 index 00000000..835259bc --- /dev/null +++ b/tests_apply/get_endpoint_data.json @@ -0,0 +1,397 @@ +{ + "account_store/accounts?email_address=a%40example.com": { + "account_id": "usera", + "email_address": "a@example.com" + }, + "account_store/accounts?email_address=b%40example.com": { + "account_id": "userb", + "email_address": "b@example.com" + }, + "account_store/accounts?account_id=test-user": { + "account_id": "test-user", + "email_address": "test@example.com" + }, + "account_store/accounts?account_id=usera": { + "account_id": "usera", + "email_address": "a@example.com" + }, + "account_store/accounts?account_id=userb": { + "account_id": "userb", + "email_address": "b@example.com" + }, + "account_store/accounts?account_id=userc": { + "account_id": "userc", + "email_address": "c@example.com" + }, + "fund_store/funds": [ + { + "name": "COF", + "short_name": "COF", + "id": "47aef2f5-3fcb-4d45-acb5-f0152b5f03c4", + "description": "An example fund for testing the funding service" + }, + { + "name": "Fund B", + "short_name": "FUB", + "id": "fund-b", + "description": "An example fund for testing the funding service" + }, + { + "name": "Funding Service Design", + "short_name": "FSD", + "id": "funding-service-design", + "description": "An example fund for testing the funding service" + }, + { + "name": "Community Ownership Fund", + "short_name": "CON", + "id": "community-ownership-fund", + "description": "An example Community Ownership Fund for testing the funding service" + } + ], + "fund_store/funds/47aef2f5-3fcb-4d45-acb5-f0152b5f03c4": { + "name": "COF", + "short_name": "COF", + "id": "47aef2f5-3fcb-4d45-acb5-f0152b5f03c4", + "description": "An example fund for testing the funding service" + }, + "fund_store/funds/fund-b": { + "name": "Fund B", + "short_name": "FUB", + "id": "fund-b", + "description": "An example fund for testing the funding service" + }, + "fund_store/funds/funding-service-design": { + "name": "Funding Service Design", + "short_name": "FSD", + "id": "funding-service-design", + "description": "An example fund for testing the funding service" + }, + "fund_store/funds/community-ownership-fund": { + "name": "COF", + "short_name": "CON", + "id": "community-ownership-fund", + "description": "An example Community Ownership Fund for testing the funding service" + }, + "fund_store/funds/47aef2f5-3fcb-4d45-acb5-f0152b5f03c4/rounds": [ + { + "fund_id": "47aef2f5-3fcb-4d45-acb5-f0152b5f03c4", + "short_name": "SUM", + "title": "Summer", + "id": "c603d114-5364-4474-a0c4-c41cbf4d3bbd", + "opens": "2022-02-01T00:00:01", + "deadline": "2022-06-01T00:00:00", + "assessment_deadline": "2022-09-30T00:00:00", + "assessment_criteria_weighting": [ + { + "id": "e2fd30d2-9207-421c-b8b3-c961bcee138b", + "name": "Strategic case", + "value": 0.30 + }, + { + "id": "e557773a-74c9-43ee-a52c-88ccae279d08", + "name": "Management case", + "value": 0.30 + }, + { + "id": "9e282cdb-6c42-4430-9563-dc4995b59bdd", + "name": "Potential to delivery community benefits", + "value": 0.30 + }, + { + "id": "6020db6c-df67-4932-a2f3-2e9dd1934164", + "name": "Added value to the community", + "value": 0.10 + } + ] + } + ], + "fund_store/funds/fund-b/rounds": [ + { + "fund_id": "funding-a", + "short_name": "SUM", + "title": "Summer", + "id": "c603d114-5364-4474-a0c4-c41cbf4d3bbd", + "opens": "2022-02-01T00:00:01", + "deadline": "2022-06-01T00:00:00", + "assessment_deadline": "2022-09-30T00:00:00", + "assessment_criteria_weighting": [ + { + "id": "e2fd30d2-9207-421c-b8b3-c961bcee138b", + "name": "Strategic case", + "value": 0.30 + }, + { + "id": "e557773a-74c9-43ee-a52c-88ccae279d08", + "name": "Management case", + "value": 0.30 + }, + { + "id": "9e282cdb-6c42-4430-9563-dc4995b59bdd", + "name": "Potential to delivery community benefits", + "value": 0.30 + }, + { + "id": "6020db6c-df67-4932-a2f3-2e9dd1934164", + "name": "Added value to the community", + "value": 0.10 + } + ] + } + ], + "fund_store/funds/funding-service-design/rounds": [ + { + "fund_id": "funding-service-design", + "title": "Spring", + "short_name": "SPR", + "id": "spring", + "opens": "2022-02-01T00:00:01", + "deadline": "2022-06-01T00:00:00", + "assessment_deadline": "2022-09-30T00:00:00", + "assessment_criteria_weighting": [ + { + "id": "e2fd30d2-9207-421c-b8b3-c961bcee138b", + "name": "Strategic case", + "value": 0.30 + }, + { + "id": "e557773a-74c9-43ee-a52c-88ccae279d08", + "name": "Management case", + "value": 0.30 + }, + { + "id": "9e282cdb-6c42-4430-9563-dc4995b59bdd", + "name": "Potential to delivery community benefits", + "value": 0.30 + }, + { + "id": "6020db6c-df67-4932-a2f3-2e9dd1934164", + "name": "Added value to the community", + "value": 0.10 + } + ] + }, + { + "fund_id": "funding-service-design", + "title": "Summer", + "short_name": "SUM", + "id": "c603d114-5364-4474-a0c4-c41cbf4d3bbd", + "opens": "2022-06-01T00:00:01", + "deadline": "2022-08-31T00:00:00", + "assessment_deadline": "2022-12-30T00:00:00", + "assessment_criteria_weighting": [ + { + "id": "e2fd30d2-9207-421c-b8b3-c961bcee138b", + "name": "Strategic case", + "value": 0.30 + }, + { + "id": "e557773a-74c9-43ee-a52c-88ccae279d08", + "name": "Management case", + "value": 0.30 + }, + { + "id": "9e282cdb-6c42-4430-9563-dc4995b59bdd", + "name": "Potential to delivery community benefits", + "value": 0.30 + }, + { + "id": "6020db6c-df67-4932-a2f3-2e9dd1934164", + "name": "Added value to the community", + "value": 0.10 + } + ] + }, + { + "fund_id": "funding-service-design", + "title": "Autumn", + "short_name": "AUT", + "id": "autumn", + "opens": "2022-09-01T00:00:01", + "deadline": "2022-11-30T00:00:00", + "assessment_deadline": "2023-03-30T00:00:00", + "assessment_criteria_weighting": [ + { + "id": "e2fd30d2-9207-421c-b8b3-c961bcee138b", + "name": "Strategic case", + "value": 0.30 + }, + { + "id": "e557773a-74c9-43ee-a52c-88ccae279d08", + "name": "Management case", + "value": 0.30 + }, + { + "id": "9e282cdb-6c42-4430-9563-dc4995b59bdd", + "name": "Potential to delivery community benefits", + "value": 0.30 + }, + { + "id": "6020db6c-df67-4932-a2f3-2e9dd1934164", + "name": "Added value to the community", + "value": 0.10 + } + ] + } + ], + "fund_store/funds/47aef2f5-3fcb-4d45-acb5-f0152b5f03c4/rounds/c603d114-5364-4474-a0c4-c41cbf4d3bbd": { + "fund_id": "funding-service-design", + "title": "Summer", + "short_name": "SUM", + "id": "c603d114-5364-4474-a0c4-c41cbf4d3bbd", + "opens": "2022-02-01T00:00:01", + "deadline": "2022-06-01T00:00:00", + "assessment_deadline": "2022-09-30T00:00:00", + "title_json": {"en": "English fund name", "cy": "Welsh fund name"}, + "assessment_criteria_weighting": [ + { + "id": "e2fd30d2-9207-421c-b8b3-c961bcee138b", + "name": "Strategic case", + "value": 0.30 + }, + { + "id": "e557773a-74c9-43ee-a52c-88ccae279d08", + "name": "Management case", + "value": 0.30 + }, + { + "id": "9e282cdb-6c42-4430-9563-dc4995b59bdd", + "name": "Potential to delivery community benefits", + "value": 0.30 + }, + { + "id": "6020db6c-df67-4932-a2f3-2e9dd1934164", + "name": "Added value to the community", + "value": 0.10 + } + ], + "project_name_field_id":"KAgrBz" + }, + "fund_store/funds/fund-b/rounds/summer": { + "fund_id": "funding-service-design", + "short_name": "SUM", + "title": "Spring", + "id": "spring", + "opens": "2022-02-01T00:00:01", + "deadline": "2022-06-01T00:00:00", + "round_name": "random_round_name", + "assessment_deadline": "2022-09-30T00:00:00", + "assessment_criteria_weighting": [ + { + "id": "e2fd30d2-9207-421c-b8b3-c961bcee138b", + "name": "Strategic case", + "value": 0.30 + }, + { + "id": "e557773a-74c9-43ee-a52c-88ccae279d08", + "name": "Management case", + "value": 0.30 + }, + { + "id": "9e282cdb-6c42-4430-9563-dc4995b59bdd", + "name": "Potential to delivery community benefits", + "value": 0.30 + }, + { + "id": "6020db6c-df67-4932-a2f3-2e9dd1934164", + "name": "Added value to the community", + "value": 0.10 + } + ] + }, + "fund_store/funds/funding-service-design/rounds/spring": { + "fund_id": "funding-service-design", + "short_name": "SPR", + "title": "Spring", + "id": "spring", + "opens": "2022-02-01T00:00:01", + "deadline": "2022-06-01T00:00:00", + "round_name": "random_round_name", + "assessment_deadline": "2022-09-30T00:00:00", + "assessment_criteria_weighting": [ + { + "id": "e2fd30d2-9207-421c-b8b3-c961bcee138b", + "name": "Strategic case", + "value": 0.30 + }, + { + "id": "e557773a-74c9-43ee-a52c-88ccae279d08", + "name": "Management case", + "value": 0.30 + }, + { + "id": "9e282cdb-6c42-4430-9563-dc4995b59bdd", + "name": "Potential to delivery community benefits", + "value": 0.30 + }, + { + "id": "6020db6c-df67-4932-a2f3-2e9dd1934164", + "name": "Added value to the community", + "value": 0.10 + } + ] + }, + "fund_store/funds/funding-service-design/rounds/c603d114-5364-4474-a0c4-c41cbf4d3bbd": { + "fund_id": "funding-service-design", + "short_name": "SUM", + "title": "Summer", + "id": "c603d114-5364-4474-a0c4-c41cbf4d3bbd", + "opens": "2022-06-01T00:00:01", + "deadline": "2022-08-31T00:00:00", + "assessment_deadline": "2022-09-30T00:00:00", + "assessment_criteria_weighting": [ + { + "id": "e2fd30d2-9207-421c-b8b3-c961bcee138b", + "name": "Strategic case", + "value": 0.30 + }, + { + "id": "e557773a-74c9-43ee-a52c-88ccae279d08", + "name": "Management case", + "value": 0.30 + }, + { + "id": "9e282cdb-6c42-4430-9563-dc4995b59bdd", + "name": "Potential to delivery community benefits", + "value": 0.30 + }, + { + "id": "6020db6c-df67-4932-a2f3-2e9dd1934164", + "name": "Added value to the community", + "value": 0.10 + } + ] + }, + "fund_store/funds/funding-service-design/rounds/autumn": { + "fund_id": "funding-service-design", + "title": "Autumn", + "short_name": "AUT", + "id": "autumn", + "opens": "2022-09-01T00:00:01", + "deadline": "2022-11-30T00:00:00", + "round_name": "random_round_name", + "assessment_deadline": "2022-09-30T00:00:00", + "assessment_criteria_weighting": [ + { + "id": "e2fd30d2-9207-421c-b8b3-c961bcee138b", + "name": "Strategic case", + "value": 0.30 + }, + { + "id": "e557773a-74c9-43ee-a52c-88ccae279d08", + "name": "Management case", + "value": 0.30 + }, + { + "id": "9e282cdb-6c42-4430-9563-dc4995b59bdd", + "name": "Potential to delivery community benefits", + "value": 0.30 + }, + { + "id": "6020db6c-df67-4932-a2f3-2e9dd1934164", + "name": "Added value to the community", + "value": 0.10 + } + ] + } +} diff --git a/tests_apply/helpers.py b/tests_apply/helpers.py new file mode 100644 index 00000000..d2ee91e5 --- /dev/null +++ b/tests_apply/helpers.py @@ -0,0 +1,389 @@ +import json +import os +import re +import urllib +from datetime import datetime + +from config import Config +from db.models.application.enums import Language +from deepdiff import DeepDiff + + +def get_row_by_pk(table, primary_key): + """Retrieves a single row from the database. + + :param table: Sqlalchemy mapper object + :param primary_key: Primary key of the row to retrieve + :return: A single row from the given mapper. + + """ + + return table.query.filter_by(id=primary_key).first() + + +def local_api_call(endpoint: str, params: dict = None, method: str = "get"): + api_data_json = os.path.join( + Config.FLASK_ROOT, + "tests", + "api_data", + method.lower() + "_endpoint_data.json", + ) + fp = open(api_data_json) + api_data = json.load(fp) + fp.close() + query_params = "_" + if params: + query_params = urllib.parse.urlencode(params) + if method.lower() == "post": + if endpoint in api_data: + post_dict = api_data.get(endpoint) + if query_params in post_dict: + return post_dict.get(query_params) + else: + return post_dict.get("_default") + else: + if params: + endpoint = f"{endpoint}?{query_params}" + if endpoint in api_data: + return api_data.get(endpoint) + + +def expected_data_within_response( + test_client, + endpoint: str, + expected_data, + method="get", + data=None, + exclude_regex_paths=None, + **kwargs, +): + """Given a endpoint and expected content, check to see if response contains + expected data. + + Args: + test_client: A flask test client + endpoint (str): The request endpoint + method (str): The method of the request + data: The data to post/put if required + expected_data: The content we expect to find + exclude_regex_paths: paths to exclude from diff + + """ + if method == "put": + response = test_client.put(endpoint, data=data, follow_redirects=True) + elif method == "post": + response = test_client.post(endpoint, data=data, follow_redirects=True) + else: + response = test_client.get(endpoint, follow_redirects=True, headers={"Content-Type": "application/json"}) + response_content = json.loads(response.content) + diff = DeepDiff( + expected_data, + response_content, + exclude_regex_paths=exclude_regex_paths, + **kwargs, + ) + error_message = "Expected data does not match response: " + str(diff) + assert diff == {}, error_message + + +def put_response_return_200(test_client, endpoint): + """Given a endpoint check to see if returns a 200 success response. + + Args: + test_client: A flask test client + endpoint (str): The PUT request endpoint + + """ + response = test_client.put(endpoint, follow_redirects=True) + assert response.status_code == 200 + + +def post_data(test_client, endpoint: str, data: dict): + """Given an endpoint and data, check to see if response contains expected data + Args: + test_client: A flask test client + endpoint (str): The POST request endpoint + data (dict): The content to post to the endpoint provided + """ + return test_client.post( + endpoint, + data=json.dumps(data), + headers={"Content-Type": "application/json"}, + follow_redirects=True, + ) + + +def put_data(test_client, endpoint: str, data: dict): + """Given an endpoint and data, check to see if response contains expected + data. + + Args: + test_client: A flask test client + endpoint (str): The POST request endpoint + data (dict): The content to post to the endpoint provided + + """ + test_client.put( + endpoint, + data=json.dumps(data), + content_type="application/json", + follow_redirects=True, + ) + + +def count_fund_applications(test_client, fund_id: str, expected_application_count): + """Given a fund_id, check the number of applications for it. + + Args: + test_client: A flask test client + fund_id (str): The id of the fund to count applications + expected_application_count (int): + The expected number of applications for the fund + + """ + fund_applications_endpoint = f"/apply/applications?fund_id={fund_id}" + response = test_client.get(fund_applications_endpoint, follow_redirects=True) + response_content = json.loads(response.content) + error_message = ( + "Response from " + + fund_applications_endpoint + + " found " + + str(len(response_content)) + + " items, but expected " + + str(expected_application_count) + ) + assert len(response_content) == expected_application_count, error_message + + +test_application_data = [ + { + "account_id": "usera", + "fund_id": "47aef2f5-3fcb-4d45-acb5-f0152b5f03c4", + "round_id": "c603d114-5364-4474-a0c4-c41cbf4d3bbd", + "language": Language.en, + }, + { + "account_id": "userb", + "fund_id": "fund-b", + "round_id": "summer", + "language": None, + }, + { + "account_id": "userc", + "fund_id": "funding-service-design", + "round_id": "spring", + "language": Language.cy, + }, +] + +test_question_data = [ + { + "question": "About your organisation 1", + "fields": [ + { + "key": "application-name", + "title": "Applicant name", + "type": "text", + "answer": "Coolio", + }, + { + "key": "applicant-email", + "title": "Email", + "type": "text", + "answer": "a@example.com", + }, + { + "key": "applicant-telephone-number", + "title": "Telephone number", + "type": "text", + "answer": "Wow", + }, + { + "key": "applicant-website", + "title": "Website", + "type": "text", + "answer": "www.example.com", + }, + ], + }, + { + "question": "About your organisation 2", + "fields": [ + { + "key": "YdtlQZ", + "title": "Organisation Name", + "type": "text", + "answer": "Test Organisation Name", + }, + { + "key": "WWWWxy", + "title": "EOI Reference", + "type": "text", + "answer": "Test Reference Number", + }, + ], + }, + { + "question": "About your organisation 3", + "fields": [ + { + "key": "data", + "title": "Applicant job", + "type": "text", + "answer": "cool", + }, + ], + }, +] + +test_question_data_cy = [ + { + "question": "About your organisation 1", + "fields": [ + { + "key": "application-name", + "title": "Applicant name", + "type": "text", + "answer": "Coolio", + }, + { + "key": "applicant-email", + "title": "Email", + "type": "text", + "answer": "a@example.com", + }, + { + "key": "applicant-telephone-number", + "title": "Telephone number", + "type": "text", + "answer": "Wow", + }, + { + "key": "applicant-website", + "title": "Website", + "type": "text", + "answer": "www.example.com", + }, + ], + }, + { + "question": "About your organisation 2", + "fields": [ + { + "key": "YdtlQZ", + "title": "Organisation Name", + "type": "text", + "answer": "Test Organisation Name", + }, + { + "key": "WWWWxy", + "title": "EOI Reference", + "type": "text", + "answer": "Test Reference Number Welsh", + }, + ], + }, + { + "question": "About your organisation 3", + "fields": [ + { + "key": "data", + "title": "Applicant job", + "type": "text", + "answer": "cool", + }, + ], + }, +] + +application_expected_data = [ + { + "project_name": "project_name not set", + "date_submitted": None, + "started_at": datetime.fromisoformat("2022-05-20 14:47:12"), + "last_edited": None, + "status": None, + **application_data, + } + for application_data in test_application_data +] + + +def post_test_applications(client): + post_data(client, "/apply/applications", test_application_data[0]) + post_data(client, "/apply/applications", test_application_data[1]) + post_data(client, "/apply/applications", test_application_data[2]) + + +def key_list_to_regex( + exclude_keys: list[str] = [ + "id", + "reference", + "started_at", + "project_name", + "last_edited", + "date_submitted", + ] +): + exclude_regex_path_strings = [rf"root\[\d+\]\['{key}'\]" for key in exclude_keys] + + exclude_regex_path_strings_nested = [rf"root\[\d+\]\['{key}'\]\[\d+\]" for key in exclude_keys] + + regex_paths = exclude_regex_path_strings + exclude_regex_path_strings_nested + return [re.compile(regex_string) for regex_string in regex_paths] + + +APPLICATION_DISPLAY_CONFIG = [ + { + "children": [ + { + "children": [], + "fields": [], + "form_name": "risk", + "id": 4, + "path": "1.1.1.1", + "title": "Risk", + "title_content_id": None, + "weighting": None, + }, + { + "children": [], + "fields": [], + "form_name": "declarations", + "id": 5, + "path": "1.1.1.2", + "title": "Declarations", + "title_content_id": None, + "weighting": None, + }, + ], + "fields": [], + "form_name": None, + "id": 3, + "path": "1.1.1", + "title": "Test Section 1", + "title_content_id": None, + "weighting": None, + }, + { + "children": [ + { + "children": [], + "fields": [], + "form_name": "community-use", + "id": 7, + "path": "1.1.2.1", + "title": "Community use", + "title_content_id": None, + "weighting": None, + } + ], + "fields": [], + "form_name": None, + "id": 6, + "path": "1.1.2", + "title": "Test section 2", + "title_content_id": None, + "weighting": None, + }, +] diff --git a/tests_apply/post_endpoint_data.json b/tests_apply/post_endpoint_data.json new file mode 100644 index 00000000..bc094585 --- /dev/null +++ b/tests_apply/post_endpoint_data.json @@ -0,0 +1,7 @@ +{ + "notification_service/send":{ + "_default": { + "status": "ok" + } + } +} diff --git a/tests_apply/run/gunicorn/devtest.py b/tests_apply/run/gunicorn/devtest.py new file mode 100644 index 00000000..844e163c --- /dev/null +++ b/tests_apply/run/gunicorn/devtest.py @@ -0,0 +1,3 @@ +from fsd_utils.gunicorn.config.devtest import * # noqa + +# bind = "127.0.0.1:5000" diff --git a/tests_apply/seed_data/COF_R3W1_all_forms.json b/tests_apply/seed_data/COF_R3W1_all_forms.json new file mode 100644 index 00000000..0bc18601 --- /dev/null +++ b/tests_apply/seed_data/COF_R3W1_all_forms.json @@ -0,0 +1,941 @@ +[ + { + "status": "COMPLETED", + "name": "declarations-cof-r3-w1", + "questions": [ + { + "category": "LkvizC", + "question": "Agree to the final confirmations", + "fields": [ + { + "key": "vSQKwD", + "title": "Confirm you have considered subsidy control and state aid implications for your project, and the information you have given us is correct", + "type": "list", + "answer": false + }, + { + "key": "CQoLFp", + "title": "Confirm you have considered people with protected characteristics throughout the planning of your project", + "type": "list", + "answer": false + }, + { + "key": "jdPkiX", + "title": "Confirm you have considered sustainability and the environment throughout the planning of your project, including compliance with the government's Net Zero ambitions", + "type": "list", + "answer": true + }, + { + "key": "qWuSCy", + "title": "Confirm you have a bank account set up and associated with the organisation you are applying on behalf of", + "type": "list", + "answer": false + }, + { + "key": "tjZlml", + "title": "Confirm that the information you've provided in this application is accurate to the best of your knowledge on the date of submission", + "type": "list", + "answer": true + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "environmental-sustainability-cof-r3-w1", + "questions": [ + { + "category": "ljcxPd", + "question": "How you've considered the environment", + "fields": [ + { + "key": "dypuJs", + "title": "Tell us how you have considered the environmental sustainability of your project", + "type": "freeText", + "answer": "
Test Environmental Sustainability Form
" + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "community-benefits-cof-r3-w1", + "questions": [ + { + "category": "PTOBPV", + "question": "Benefits you'll deliver", + "fields": [ + { + "key": "pqYxJO", + "title": "What community benefits do you expect to deliver with this project?", + "type": "list", + "answer": [ + "community-pride" + ] + }, + { + "key": "lgfiGB", + "title": "Tell us about these benefits in detail, and how the asset's activities will help deliver them", + "type": "freeText", + "answer": "Test Community Benefits Form
" + }, + { + "key": "zKKouR", + "title": "Explain how you plan to deliver and sustain these benefits over time", + "type": "freeText", + "answer": "Test Community Benefits Form
" + }, + { + "key": "ZyIQGI", + "title": "Tell us how you'll make sure the whole community benefits from the asset", + "type": "freeText", + "answer": "Test Community Benefits Form
" + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "local-support-cof-r3-w1", + "questions": [ + { + "category": "apkBSm", + "question": "Your support for the project", + "fields": [ + { + "key": "tDVPnl", + "title": "Tell us about the local support for your project", + "type": "freeText", + "answer": "Tell us about the local support for your project
" + }, + { + "key": "bDWjTN", + "title": "Upload supporting evidence (optional)", + "type": "text", + "answer": null + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "community-use-cof-r3-w1", + "questions": [ + { + "category": "GMkooI", + "question": "Who uses the asset", + "fields": [ + { + "key": "zTcrYo", + "title": "Who in the community currently uses the asset, or has used it in the past?", + "type": "freeText", + "answer": "Test Community Use Form
" + }, + { + "key": "whlRYS", + "title": "Tell us how losing the asset would affect, or has already affected, people in the community", + "type": "freeText", + "answer": "Test Community Use Form
" + }, + { + "key": "NGSXHE", + "title": "Why will the asset be lost without community intervention?", + "type": "freeText", + "answer": "Test Community Use Form
" + }, + { + "key": "Ieudgn", + "title": "Explain how the community will be better served with the asset under community ownership", + "type": "freeText", + "answer": "Test Community Use Form
" + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "operational-costs-cof-r3-w1", + "questions": [ + { + "category": "oSfXFZ", + "question": "Cashflow to run the asset", + "fields": [ + { + "key": "qXNkfr", + "title": "Describe your cash flow for the running of the asset", + "type": "freeText", + "answer": "Summarise your cash flow for the running of the asset
" + }, + { + "key": "qQSVEn", + "title": "If successful, will you use your funding in the next 12 months?", + "type": "list", + "answer": false + } + ], + "status": "COMPLETED" + }, + { + "category": "oSfXFZ", + "question": "Income to run the asset", + "fields": [ + { + "key": "MSNJQD", + "title": "Sources of income", + "type": "multiInput", + "answer": [ + { + "AJEWXD": "Income Test 2", + "cHFrIp": 2300 + } + ] + } + ], + "status": "COMPLETED" + }, + { + "category": "oSfXFZ", + "question": "Running costs of the asset", + "fields": [ + { + "key": "NPgwcH", + "title": "Running costs", + "type": "multiInput", + "answer": [ + { + "IIdfRj": "Running Cost Test", + "wlGQua": 2300 + } + ] + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "skills-and-resources-cof-r3-w1", + "questions": [ + { + "category": "eLpYFr", + "question": "Your experience running similar assets", + "fields": [ + { + "key": "XXGyzn", + "title": "Describe any relevant experience you have delivering similar projects or running an asset", + "type": "freeText", + "answer": "Describe any relevant experience you have delivering similar projects or running an asset
" + } + ], + "status": "COMPLETED" + }, + { + "category": "eLpYFr", + "question": "Recruitment plans", + "fields": [ + { + "key": "Uaeyae", + "title": "Do you have plans to recruit people to help you run the asset?", + "type": "list", + "answer": false + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "project-qualifications-cof-r3-w1", + "questions": [ + { + "category": "WsFJts", + "question": "If your project meets the definition", + "fields": [ + { + "key": "ZEKMQd", + "title": "Does your project meet the definition of a subsidy?", + "type": "list", + "answer": false + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "applicant-information-cof-r3-w1", + "questions": [ + { + "category": "ZuHuGk", + "question": "Lead contact details", + "fields": [ + { + "key": "SnLGJE", + "title": "Name of lead contact", + "type": "text", + "answer": "Joe Bloggs" + }, + { + "key": "qRDTUc", + "title": "Lead contact job title", + "type": "text", + "answer": "Mr" + }, + { + "key": "NlHSBg", + "title": "Lead contact email address", + "type": "text", + "answer": "testemailfundingservice@testemailfundingservice.com" + }, + { + "key": "FhBkJQ", + "title": "Lead contact telephone number", + "type": "text", + "answer": "0000000000" + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "upload-business-plan-cof-r3-w1", + "questions": [ + { + "category": "xtwqlH", + "question": "Your business plan", + "fields": [ + { + "key": "ndpQJk", + "title": "Upload business plan", + "type": "text", + "answer": "sample.txt" + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "feasibility-cof-r3-w1", + "questions": [ + { + "category": "bBGnkL", + "question": "Feasiblity studies you've carried out", + "fields": [ + { + "key": "iSbwDM", + "title": "Tell us about the feasibility studies you have carried out for your project", + "type": "freeText", + "answer": "Tell us about the feasibility studies you have carried out for your project
" + }, + { + "key": "jFPlEJ", + "title": "Do you need to do any further feasibility work?", + "type": "list", + "answer": false + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "risk-cof-r3-w1", + "questions": [ + { + "category": "HKdODf", + "question": "Your project risk register", + "fields": [ + { + "key": "EODncR", + "title": "Risks to your project (document upload)", + "type": "text", + "answer": "sample.txt" + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "project-information-cof-r3-w1", + "questions": [ + { + "category": "qsnIGd", + "question": "Previous Community Ownership Fund applications", + "fields": [ + { + "key": "pWwCRM", + "title": "Have you applied to the Community Ownership Fund before?", + "type": "list", + "answer": false + } + ], + "status": "COMPLETED" + }, + { + "category": "qsnIGd", + "question": "Project name and summary", + "fields": [ + { + "key": "apGjFS", + "title": "Project name", + "type": "text", + "answer": "Seeded test data" + }, + { + "key": "bEWpAj", + "title": "Tell us how the asset is currently being used, or how it has been used before, and why it's important to the community", + "type": "freeText", + "answer": "Test Project Information Form
" + }, + { + "key": "uypCNM", + "title": "Give a brief summary of your project, including what you hope to achieve", + "type": "freeText", + "answer": "Test Project Information Form
" + }, + { + "key": "AgeRbd", + "title": "Tell us about the planned activities and/or services that will take place in the asset", + "type": "freeText", + "answer": "Test Project Information Form
" + } + ], + "status": "COMPLETED" + }, + { + "category": "qsnIGd", + "question": "Address of the asset", + "fields": [ + { + "key": "EfdliG", + "title": "Address of the community asset", + "type": "text", + "answer": "Test Address, null, Test Town Or City, null, QQ12 7QQ" + }, + { + "key": "fIEUcb", + "title": " In which constituency is your asset?", + "type": "text", + "answer": "Constituency" + }, + { + "key": "SWfcTo", + "title": "In which local council area is your asset?", + "type": "text", + "answer": "Local Council" + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "organisation-information-cof-r3-w1", + "questions": [ + { + "category": "JBqDtK", + "question": "Organisation names", + "fields": [ + { + "key": "WWWWxy", + "title": "Your unique tracker number", + "type": "text", + "answer": "ANON-###-###-###" + }, + { + "key": "YdtlQZ", + "title": "Organisation name", + "type": "text", + "answer": "Test Change Answers" + }, + { + "key": "iBCGxY", + "title": "Does your organisation use any other names?", + "type": "list", + "answer": false + } + ], + "status": "COMPLETED" + }, + { + "category": "JBqDtK", + "question": "Purpose and activities", + "fields": [ + { + "key": "emVGxS", + "title": "What is your organisation's main purpose?", + "type": "freeText", + "answer": "Test Org Form
" + }, + { + "key": "btTtIb", + "title": "Tell us about your organisation's main activities", + "type": "freeText", + "answer": "Test Org Form
" + }, + { + "key": "SkocDi", + "title": "Tell us about your organisation's main activities - Activity 2 ", + "type": "freeText", + "answer": null + }, + { + "key": "CNeeiC", + "title": "Tell us about your organisation's main activities - Activity 3 ", + "type": "freeText", + "answer": null + }, + { + "key": "BBlCko", + "title": "Have you delivered projects like this before?", + "type": "list", + "answer": false + } + ], + "status": "COMPLETED" + }, + { + "category": "JBqDtK", + "question": "How your organisation is classified", + "fields": [ + { + "key": "lajFtB", + "title": "Type of organisation", + "type": "list", + "answer": "CIO" + } + ], + "status": "COMPLETED" + }, + { + "category": "JBqDtK", + "question": "Charity registration details", + "fields": [ + { + "key": "aHIGbK", + "title": "Charity number ", + "type": "text", + "answer": "234388322" + } + ], + "status": "COMPLETED" + }, + { + "category": "JBqDtK", + "question": "Trading subsidiaries", + "fields": [ + { + "key": "DwfHtk", + "title": "Is your organisation a trading subsidiary of a parent company?", + "type": "list", + "answer": false + } + ], + "status": "COMPLETED" + }, + { + "category": "JBqDtK", + "question": "Organisation address", + "fields": [ + { + "key": "ZQolYb", + "title": "Organisation address", + "type": "text", + "answer": "Test Address, null, Test Town Or City, null, QQ12 7QQ" + }, + { + "key": "zsoLdf", + "title": "Is your correspondence address different to the organisation address?", + "type": "list", + "answer": false + }, + { + "key": "FhbaEy", + "title": "Website and social media", + "type": "text", + "answer": "https://twitter.com/luhc" + }, + { + "key": "FcdKlB", + "title": "Website and social media - Link or username 2", + "type": "text", + "answer": null + }, + { + "key": "BzxgDA", + "title": "Website and social media - Link or username 3", + "type": "text", + "answer": null + } + ], + "status": "COMPLETED" + }, + { + "category": "JBqDtK", + "question": "Joint applications", + "fields": [ + { + "key": "hnLurH", + "title": "Is your application a joint bid in partnership with other organisations?", + "type": "list", + "answer": false + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "community-representation-cof-r3-w1", + "questions": [ + { + "category": "KbnmOO", + "question": "How you\u2019ll run the asset", + "fields": [ + { + "key": "ReomFo", + "title": "List the members of your board", + "type": "freeText", + "answer": "Test Community Representation Form
" + }, + { + "key": "fjVmOt", + "title": "Tell us about your governance and membership structures", + "type": "freeText", + "answer": "Test Community Representation Form
" + }, + { + "key": "GETNxN", + "title": "Explain how you'll consider the views of the community in the running of the asset", + "type": "freeText", + "answer": "Test Community Representation Form
" + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "community-engagement-cof-r3-w1", + "questions": [ + { + "category": "lmdhVN", + "question": "How you've engaged with the community", + "fields": [ + { + "key": "azCutK", + "title": "Tell us how you have engaged with the community about your intention to take ownership of the asset", + "type": "freeText", + "answer": "Test Community Engagement Form
" + } + ], + "status": "COMPLETED" + }, + { + "category": "lmdhVN", + "question": "Your fundraising activities", + "fields": [ + { + "key": "jAhuWN", + "title": "Describe your fundraising activities", + "type": "freeText", + "answer": "Test Community Engagement Form
" + } + ], + "status": "COMPLETED" + }, + { + "category": "lmdhVN", + "question": "Partnerships and local plans", + "fields": [ + { + "key": "HYsezC", + "title": "Tell us about any partnerships you've formed, and how they'll help the project be successful", + "type": "freeText", + "answer": "Test Community Engagement Form
" + }, + { + "key": "GGBgBY", + "title": "Tell us how your project supports any wider local plans", + "type": "freeText", + "answer": "Test Community Engagement Form
" + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "inclusiveness-and-integration-cof-r3-w1", + "questions": [ + { + "category": "eCZBSV", + "question": "How you\u2019ll make the asset inclusive", + "fields": [ + { + "key": "mgIesb", + "title": "Tell us how the asset will be accountable to local people, and involve them in its running", + "type": "freeText", + "answer": "Test Inclusiveness and Integration Form
" + }, + { + "key": "lQEkep", + "title": "Describe anything that might prevent people from using the asset or participating in its running", + "type": "freeText", + "answer": "Test Inclusiveness and Integration Form
" + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "funding-required-cof-r3-w1", + "questions": [ + { + "category": "bgUGuD", + "question": "Total funding request from the Community Ownership Fund", + "fields": [ + { + "key": "ABROnB", + "title": "Capital funding", + "type": "text", + "answer": "2300" + }, + { + "key": "cLDRvN", + "title": "Revenue funding (optional)", + "type": "text", + "answer": "2300" + } + ], + "status": "COMPLETED" + }, + { + "category": "bgUGuD", + "question": "Capital costs for your project", + "fields": [ + { + "key": "qQLyXL", + "title": "Capital costs", + "type": "multiInput", + "answer": [ + { + "GLQlOh": "Capital Funding", + "JtwkMy": 2300 + } + ] + } + ], + "status": "COMPLETED" + }, + { + "category": "bgUGuD", + "question": "If you've secured match funding", + "fields": [ + { + "key": "DOvZvB", + "title": "Have you secured any match funding yet?", + "type": "list", + "answer": false + } + ], + "status": "COMPLETED" + }, + { + "category": "bgUGuD", + "question": "If you\u2019ve identified further match funding", + "fields": [ + { + "key": "DmgsiG", + "title": "Do you have any match funding identified but not yet secured?", + "type": "list", + "answer": false + } + ], + "status": "COMPLETED" + }, + { + "category": "bgUGuD", + "question": "Revenue funding", + "fields": [ + { + "key": "matkNH", + "title": "Are you applying for revenue funding from the Community Ownership Fund? (optional)", + "type": "list", + "answer": false + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "asset-information-cof-r3-w1", + "questions": [ + { + "category": "wxYZcT", + "question": "How the asset is used in the community", + "fields": [ + { + "key": "oXGwlA", + "title": "Asset type", + "type": "list", + "answer": "cinema" + } + ], + "status": "COMPLETED" + }, + { + "category": "wxYZcT", + "question": "The asset in community ownership", + "fields": [ + { + "key": "LaxeJN", + "title": "How do you intend to take community ownership of the asset?", + "type": "list", + "answer": "buy-the-asset" + }, + { + "key": "tTOrEp", + "title": "Upload asset valuation or lease agreement", + "type": "text", + "answer": "sample.txt" + }, + { + "key": "hdmYjg", + "title": "Do you know who currently owns your asset?", + "type": "list", + "answer": false + } + ], + "status": "COMPLETED" + }, + { + "category": "wxYZcT", + "question": "Current ownership status", + "fields": [ + { + "key": "CSsbsG", + "title": "Describe the current ownership status", + "type": "text", + "answer": "Current Owner" + } + ], + "status": "COMPLETED" + }, + { + "category": "wxYZcT", + "question": "If you've already taken ownership", + "fields": [ + { + "key": "uPvsqM", + "title": "Have you already completed the purchase or lease?", + "type": "list", + "answer": false + } + ], + "status": "COMPLETED" + }, + { + "category": "wxYZcT", + "question": "Expected terms of your ownership or lease", + "fields": [ + { + "key": "XPcbJx", + "title": "Describe the expected sale process, or the proposed terms of your lease if you are renting the asset", + "type": "freeText", + "answer": "Test Asset Information Form
" + }, + { + "key": "jGjScT", + "title": "Expected date of sale or lease", + "type": "date", + "answer": "2022-12-01" + } + ], + "status": "COMPLETED" + }, + { + "category": "wxYZcT", + "question": "Public ownership", + "fields": [ + { + "key": "VGXXyq", + "title": "Is your asset currently publicly owned?", + "type": "list", + "answer": false + } + ], + "status": "COMPLETED" + }, + { + "category": "wxYZcT", + "question": "Assets of community value", + "fields": [ + { + "key": "wjBFTf", + "title": "Is this a registered Asset of Community Value (ACV)?", + "type": "list", + "answer": false + } + ], + "status": "COMPLETED" + }, + { + "category": "wxYZcT", + "question": "Assets listed for disposal", + "fields": [ + { + "key": "HyWPwE", + "title": "Is the asset listed for disposal, or part of a Community Asset Transfer?", + "type": "list", + "answer": false + } + ], + "status": "COMPLETED" + }, + { + "category": "wxYZcT", + "question": "Risk of closure", + "fields": [ + { + "key": "KQlOaJ", + "title": "Why is the asset at risk of closure?", + "type": "list", + "answer": [ + "for-sale-or-listed-for-disposal" + ] + } + ], + "status": "COMPLETED" + } + ] + } +] diff --git a/tests_apply/seed_data/COF_R3W2_all_forms.json b/tests_apply/seed_data/COF_R3W2_all_forms.json new file mode 100644 index 00000000..5583d0cc --- /dev/null +++ b/tests_apply/seed_data/COF_R3W2_all_forms.json @@ -0,0 +1,1001 @@ +[ + { + "name": "community-use-cof-r3-w2", + "questions": [ + { + "category": "GMkooI", + "fields": [ + { + "answer": "Test Community Use Form
", + "key": "zTcrYo", + "title": "Who in the community currently uses the asset, or has used it in the past?", + "type": "freeText" + }, + { + "answer": "Test Community Use Form
", + "key": "whlRYS", + "title": "Tell us how losing the asset would affect, or has already affected, people in the community", + "type": "freeText" + }, + { + "answer": "Test Community Use Form
", + "key": "NGSXHE", + "title": "Why will the asset be lost without community intervention?", + "type": "freeText" + }, + { + "answer": "Test Community Use Form
", + "key": "Ieudgn", + "title": "Explain how the community will be better served with the asset under community ownership", + "type": "freeText" + } + ], + "question": "Who uses the asset", + "status": "COMPLETED" + } + ], + "status": "COMPLETED" + }, + { + "name": "community-engagement-cof-r3-w2", + "questions": [ + { + "category": "lmdhVN", + "fields": [ + { + "answer": "Test Community Engagement Form
", + "key": "azCutK", + "title": "Tell us how you have engaged with the community about your intention to take ownership of the asset", + "type": "freeText" + } + ], + "question": "How you've engaged with the community", + "status": "COMPLETED" + }, + { + "category": "lmdhVN", + "fields": [ + { + "answer": "Test Community Engagement Form
", + "key": "jAhuWN", + "title": "Describe your fundraising activities", + "type": "freeText" + } + ], + "question": "Your fundraising activities", + "status": "COMPLETED" + }, + { + "category": "lmdhVN", + "fields": [ + { + "answer": "Test Community Engagement Form
", + "key": "HYsezC", + "title": "Tell us about any partnerships you've formed, and how they'll help the project be successful", + "type": "freeText" + }, + { + "answer": "Test Community Engagement Form
", + "key": "GGBgBY", + "title": "Tell us how your project supports any wider local plans", + "type": "freeText" + } + ], + "question": "Partnerships and local plans", + "status": "COMPLETED" + } + ], + "status": "COMPLETED" + }, + { + "name": "asset-information-cof-r3-w2", + "questions": [ + { + "category": "wxYZcT", + "fields": [ + { + "answer": "cinema", + "key": "oXGwlA", + "title": "Asset type", + "type": "list" + } + ], + "question": "How the asset is used in the community", + "status": "COMPLETED" + }, + { + "category": "wxYZcT", + "fields": [ + { + "answer": "buy-the-asset", + "key": "LaxeJN", + "title": "How do you intend to take community ownership of the asset?", + "type": "list" + } + ], + "question": "The asset in community ownership", + "status": "COMPLETED" + }, + { + "category": "wxYZcT", + "fields": [ + { + "answer": "sample.txt", + "key": "tTOrEp", + "title": "Please upload evidence that shows the asset valuation (if you are buying the asset) or the lease agreement (if you are leasing the asset).", + "type": "text" + } + ], + "question": "Upload asset valuation or lease agreement", + "status": "COMPLETED" + }, + { + "category": "wxYZcT", + "fields": [ + { + "answer": true, + "key": "wAUFqr", + "title": "Do you know who currently owns your asset?", + "type": "list" + } + ], + "question": "Who owns the asset", + "status": "COMPLETED" + }, + { + "category": "wxYZcT", + "fields": [ + { + "answer": "Mr. Shannon Gorczany", + "key": "FOURVe", + "title": "Name of current asset owner", + "type": "text" + } + ], + "question": "Who currently owns your asset", + "status": "COMPLETED" + }, + { + "category": "wxYZcT", + "fields": [ + { + "answer": "Test Asset Information Form
", + "key": "XPcbJx", + "title": "Describe the expected sale process, or the proposed terms of your lease if you are planning to rent the asset", + "type": "freeText" + }, + { + "answer": "2022-12-01", + "key": "jGjScT", + "title": "Expected date of sale or lease", + "type": "date" + } + ], + "question": "Expected terms of your ownership or lease", + "status": "COMPLETED" + }, + { + "category": "wxYZcT", + "fields": [ + { + "answer": false, + "key": "VGXXyq", + "title": "Is your asset currently publicly owned?", + "type": "list" + } + ], + "question": "Public ownership", + "status": "COMPLETED" + }, + { + "category": "wxYZcT", + "fields": [ + { + "answer": [ + "Sale", + "Listed for disposal", + "Part of a Community Asset Transfer" + ], + "key": "qlqyUq", + "title": "Why is the asset at risk of closure?", + "type": "list" + } + ], + "question": "Risk of closure", + "status": "COMPLETED" + }, + { + "category": "wxYZcT", + "fields": [ + { + "answer": "2022-12-01", + "key": "QPIPjx", + "title": "When was the asset listed?", + "type": "date" + }, + { + "answer": "https://twitter.com/luhc", + "key": "OJWGGr", + "title": "Provide a link to the listing", + "type": "text" + } + ], + "question": "Asset listing details", + "status": "COMPLETED" + }, + { + "category": "wxYZcT", + "fields": [ + { + "answer": "Test Asset Information Form
", + "key": "WKIGQE", + "title": "Describe the current status of the Community Asset Transfer", + "type": "freeText" + } + ], + "question": "Community asset transfer", + "status": "COMPLETED" + }, + { + "category": "wxYZcT", + "fields": [ + { + "answer": false, + "key": "iqnlTk", + "title": "Is this a registered Asset of Community Value (ACV)?", + "type": "list" + } + ], + "question": "Assets of community value", + "status": "COMPLETED" + } + ], + "status": "COMPLETED" + }, + { + "name": "funding-required-cof-r3-w2", + "questions": [ + { + "category": "bgUGuD", + "fields": [ + { + "answer": "571", + "key": "ABROnB", + "title": "Capital funding request", + "type": "text" + }, + { + "answer": true, + "key": "hJkmBS", + "title": "If successful, will you use your funding in the next 12 months?", + "type": "list" + } + ], + "question": "Capital funding request", + "status": "COMPLETED" + }, + { + "category": "bgUGuD", + "fields": [ + { + "answer": [ + { + "GLQlOh": "Capital Funding", + "JtwkMy": 571 + } + ], + "key": "qQLyXL", + "title": "Capital costs", + "type": "multiInput" + } + ], + "question": "Capital costs for your project", + "status": "COMPLETED" + }, + { + "category": "bgUGuD", + "fields": [ + { + "answer": true, + "key": "DOvZvB", + "title": "Have you secured any match funding yet?", + "type": "list" + } + ], + "question": "If you've secured match funding", + "status": "COMPLETED" + }, + { + "category": "bgUGuD", + "fields": [ + { + "answer": [ + { + "JKqLWU": "Secured Match Funding", + "LVJcDC": 571 + } + ], + "key": "MopCmv", + "title": "Secured match funding", + "type": "multiInput" + } + ], + "question": "Secured match funding", + "status": "COMPLETED" + }, + { + "category": "bgUGuD", + "fields": [ + { + "answer": true, + "key": "HgpNUe", + "title": "Have you already spent the match funding you have secured?", + "type": "list" + } + ], + "question": "Have you already spent the match funding you have secured?", + "status": "COMPLETED" + }, + { + "category": "bgUGuD", + "fields": [ + { + "answer": true, + "key": "DmgsiG", + "title": "Have you identified, but not yet secured, any additional match funding?", + "type": "list" + } + ], + "question": "If you’ve identified further match funding", + "status": "COMPLETED" + }, + { + "category": "bgUGuD", + "fields": [ + { + "answer": [ + { + "THOdae": 571, + "iMJdfs": "Unsecured Match Funding" + } + ], + "key": "vEOdBS", + "title": "Unsecured match funding", + "type": "multiInput" + } + ], + "question": "Unsecured match funding", + "status": "COMPLETED" + }, + { + "category": "bgUGuD", + "fields": [ + { + "answer": true, + "key": "matkNH", + "title": "Are you applying for revenue funding from the Community Ownership Fund? (optional)", + "type": "list" + } + ], + "question": "Revenue funding", + "status": "COMPLETED" + }, + { + "category": "bgUGuD", + "fields": [ + { + "answer": [ + { + "UyaAHw": 571, + "hGsUaZ": "Revenue Costs" + } + ], + "key": "tSKhQQ", + "title": "Revenue costs (optional)", + "type": "multiInput" + } + ], + "question": "Revenue costs (optional)", + "status": "COMPLETED" + }, + { + "category": "bgUGuD", + "fields": [ + { + "answer": "Tell us how the revenue funding you've requested will help run the asset
", + "key": "XPDbsl", + "title": "Tell us how the revenue funding you've requested will help run the asset", + "type": "freeText" + } + ], + "question": "How you'll use revenue funding", + "status": "COMPLETED" + } + ], + "status": "COMPLETED" + }, + { + "name": "skills-and-resources-cof-r3-w2", + "questions": [ + { + "category": "eLpYFr", + "fields": [ + { + "answer": "Describe any relevant experience you have delivering similar projects or running an asset
", + "key": "XXGyzn", + "title": "Describe any relevant experience you have delivering similar projects or running an asset", + "type": "freeText" + } + ], + "question": "Your experience running similar assets", + "status": "COMPLETED" + }, + { + "category": "eLpYFr", + "fields": [ + { + "answer": false, + "key": "Uaeyae", + "title": "Do you have plans to recruit people to help you run the asset?", + "type": "list" + } + ], + "question": "Recruitment plans", + "status": "COMPLETED" + } + ], + "status": "COMPLETED" + }, + { + "name": "organisation-information-cof-r3-w2", + "questions": [ + { + "category": "JBqDtK", + "fields": [ + { + "answer": "ANON-###-###-###", + "key": "WWWWxy", + "title": "Your unique tracker number", + "type": "text" + }, + { + "answer": "Collier, Heaney and Bosco", + "key": "YdtlQZ", + "title": "Organisation name", + "type": "text" + }, + { + "answer": false, + "key": "iBCGxY", + "title": "Does your organisation use any other names?", + "type": "list" + } + ], + "question": "Organisation names", + "status": "COMPLETED" + }, + { + "category": "JBqDtK", + "fields": [ + { + "answer": "Test Org Form
", + "key": "emVGxS", + "title": "What is your organisation's main purpose?", + "type": "freeText" + }, + { + "answer": "Test Org Form
", + "key": "btTtIb", + "title": "Tell us about your organisation's main activities", + "type": "freeText" + }, + { + "answer": null, + "key": "SkocDi", + "title": "Tell us about your organisation's main activities - Activity 2 ", + "type": "freeText" + }, + { + "answer": null, + "key": "CNeeiC", + "title": "Tell us about your organisation's main activities - Activity 3 ", + "type": "freeText" + }, + { + "answer": false, + "key": "BBlCko", + "title": "Have you delivered projects like this before?", + "type": "list" + } + ], + "question": "Purpose and activities", + "status": "COMPLETED" + }, + { + "category": "JBqDtK", + "fields": [ + { + "answer": "CIO", + "key": "lajFtB", + "title": "Type of organisation", + "type": "list" + } + ], + "question": "How your organisation is classified", + "status": "COMPLETED" + }, + { + "category": "JBqDtK", + "fields": [ + { + "answer": "7036286351201658", + "key": "aHIGbK", + "title": "Charity number ", + "type": "text" + } + ], + "question": "Charity registration details", + "status": "COMPLETED" + }, + { + "category": "JBqDtK", + "fields": [ + { + "answer": false, + "key": "DwfHtk", + "title": "Is your organisation a trading subsidiary of a parent company?", + "type": "list" + } + ], + "question": "Trading subsidiaries", + "status": "COMPLETED" + }, + { + "category": "JBqDtK", + "fields": [ + { + "answer": "4 Laurine Fold, null, St. Nienow, null, W12 0HS", + "key": "ZQolYb", + "title": "Organisation address", + "type": "text" + }, + { + "answer": false, + "key": "zsoLdf", + "title": "Is your correspondence address different to the organisation address?", + "type": "list" + }, + { + "answer": "https://twitter.com/luhc", + "key": "FhbaEy", + "title": "Website and social media", + "type": "text" + }, + { + "answer": null, + "key": "FcdKlB", + "title": "Website and social media - Link or username 2", + "type": "text" + }, + { + "answer": null, + "key": "BzxgDA", + "title": "Website and social media - Link or username 3", + "type": "text" + } + ], + "question": "Organisation address", + "status": "COMPLETED" + }, + { + "category": "JBqDtK", + "fields": [ + { + "answer": false, + "key": "hnLurH", + "title": "Is your application a joint bid in partnership with other organisations?", + "type": "list" + } + ], + "question": "Joint applications", + "status": "COMPLETED" + } + ], + "status": "COMPLETED" + }, + { + "name": "feasibility-cof-r3-w2", + "questions": [ + { + "category": "bBGnkL", + "fields": [ + { + "answer": "Tell us about the feasibility studies you have carried out for your project
", + "key": "iSbwDM", + "title": "Tell us about the feasibility studies you have carried out for your project", + "type": "freeText" + }, + { + "answer": false, + "key": "jFPlEJ", + "title": "Do you need to do any further feasibility work?", + "type": "list" + } + ], + "question": "Feasiblity studies you've carried out", + "status": "COMPLETED" + } + ], + "status": "COMPLETED" + }, + { + "name": "project-information-cof-r3-w2", + "questions": [ + { + "category": "qsnIGd", + "fields": [ + { + "answer": false, + "key": "pWwCRM", + "title": "Have you applied to the Community Ownership Fund before?", + "type": "list" + } + ], + "question": "Previous Community Ownership Fund applications", + "status": "COMPLETED" + }, + { + "category": "qsnIGd", + "fields": [ + { + "answer": "Round 3 Window 2 E2E Wehner - Hane Project ", + "key": "apGjFS", + "title": "Project name", + "type": "text" + }, + { + "answer": "Test Project Information Form
", + "key": "bEWpAj", + "title": "Tell us how the asset is currently being used, or how it has been used before, and why it's important to the community", + "type": "freeText" + }, + { + "answer": "Test Project Information Form
", + "key": "uypCNM", + "title": "Give a brief summary of your project, including what you hope to achieve", + "type": "freeText" + }, + { + "answer": "Test Project Information Form
", + "key": "AgeRbd", + "title": "Tell us about the planned activities and/or services that will take place in the asset", + "type": "freeText" + } + ], + "question": "Project name and summary", + "status": "COMPLETED" + }, + { + "category": "qsnIGd", + "fields": [ + { + "answer": "5 Haley-Powlowski End, null, Schmidt Common, null, QQ12 7QQ", + "key": "EfdliG", + "title": "Address of the community asset", + "type": "text" + }, + { + "answer": "Constituency", + "key": "fIEUcb", + "title": " In which constituency is your asset?", + "type": "text" + }, + { + "answer": "Highlands and Islands", + "key": "SWfcTo", + "title": "In which local council area is your asset?", + "type": "text" + } + ], + "question": "Address of the asset", + "status": "COMPLETED" + } + ], + "status": "COMPLETED" + }, + { + "name": "declarations-cof-r3-w2", + "questions": [ + { + "category": "LkvizC", + "fields": [ + { + "answer": false, + "key": "vSQKwD", + "title": "Confirm you have considered subsidy control and state aid implications for your project, and the information you have given us is correct", + "type": "list" + }, + { + "answer": false, + "key": "CQoLFp", + "title": "Confirm you have considered people with protected characteristics throughout the planning of your project", + "type": "list" + }, + { + "answer": true, + "key": "jdPkiX", + "title": "Confirm you have considered sustainability and the environment throughout the planning of your project, including compliance with the government's Net Zero ambitions", + "type": "list" + }, + { + "answer": false, + "key": "qWuSCy", + "title": "Confirm you have a bank account set up and associated with the organisation you are applying on behalf of", + "type": "list" + }, + { + "answer": true, + "key": "tjZlml", + "title": "Confirm that the information you've provided in this application is accurate to the best of your knowledge on the date of submission", + "type": "list" + } + ], + "question": "Agree to the final confirmations", + "status": "COMPLETED" + } + ], + "status": "COMPLETED" + }, + { + "name": "applicant-information-cof-r3-w2", + "questions": [ + { + "category": "ZuHuGk", + "fields": [ + { + "answer": "Damion", + "key": "SnLGJE", + "title": "Name of lead contact", + "type": "text" + }, + { + "answer": "National Accounts Strategist", + "key": "qRDTUc", + "title": "Lead contact job title", + "type": "text" + }, + { + "answer": "Loraine89@yahoo.com", + "key": "NlHSBg", + "title": "Lead contact email address", + "type": "text" + }, + { + "answer": "+44 75506381249", + "key": "FhBkJQ", + "title": "Lead contact telephone number", + "type": "text" + } + ], + "question": "Lead contact details", + "status": "COMPLETED" + } + ], + "status": "COMPLETED" + }, + { + "name": "upload-business-plan-cof-r3-w2", + "questions": [ + { + "category": "xtwqlH", + "fields": [ + { + "answer": "sample.txt", + "key": "ndpQJk", + "title": "Upload business plan", + "type": "text" + } + ], + "question": "Your business plan", + "status": "COMPLETED" + } + ], + "status": "COMPLETED" + }, + { + "name": "local-support-cof-r3-w2", + "questions": [ + { + "category": "apkBSm", + "fields": [ + { + "answer": "Tell us about the local support for your project
", + "key": "tDVPnl", + "title": "Tell us about the local support for your project", + "type": "freeText" + }, + { + "answer": null, + "key": "bDWjTN", + "title": "Upload supporting evidence (optional)", + "type": "text" + } + ], + "question": "Your support for the project", + "status": "COMPLETED" + } + ], + "status": "COMPLETED" + }, + { + "name": "community-representation-cof-r3-w2", + "questions": [ + { + "category": "KbnmOO", + "fields": [ + { + "answer": "Test Community Representation Form
", + "key": "ReomFo", + "title": "List the members of your board", + "type": "freeText" + }, + { + "answer": "Test Community Representation Form
", + "key": "fjVmOt", + "title": "Tell us about your governance and membership structures", + "type": "freeText" + }, + { + "answer": "Test Community Representation Form
", + "key": "GETNxN", + "title": "Explain how you'll consider the views of the community in the running of the asset", + "type": "freeText" + } + ], + "question": "How you’ll run the asset", + "status": "COMPLETED" + } + ], + "status": "COMPLETED" + }, + { + "name": "operational-costs-cof-r3-w2", + "questions": [ + { + "category": "oSfXFZ", + "fields": [ + { + "answer": "Summarise your income and operational costs for the running of the asset
", + "key": "qXNkfr", + "title": "Summarise your income and operational costs for the running of the asset", + "type": "freeText" + } + ], + "question": "Forecasted income and operational costs to run the asset", + "status": "COMPLETED" + } + ], + "status": "COMPLETED" + }, + { + "name": "inclusiveness-and-integration-cof-r3-w2", + "questions": [ + { + "category": "eCZBSV", + "fields": [ + { + "answer": "Test Inclusiveness and Integration Form
", + "key": "mgIesb", + "title": "Tell us how the asset will be accountable to local people, and involve them in its running", + "type": "freeText" + }, + { + "answer": "Test Inclusiveness and Integration Form
", + "key": "lQEkep", + "title": "Describe anything that might prevent people from using the asset or participating in its running", + "type": "freeText" + } + ], + "question": "How you’ll make the asset inclusive", + "status": "COMPLETED" + } + ], + "status": "COMPLETED" + }, + { + "name": "environmental-sustainability-cof-r3-w2", + "questions": [ + { + "category": "ljcxPd", + "fields": [ + { + "answer": "Test Environmental Sustainability Form
", + "key": "dypuJs", + "title": "Tell us how you have considered the environmental sustainability of your project", + "type": "freeText" + } + ], + "question": "How you've considered the environment", + "status": "COMPLETED" + } + ], + "status": "COMPLETED" + }, + { + "name": "community-benefits-cof-r3-w2", + "questions": [ + { + "category": "PTOBPV", + "fields": [ + { + "answer": [ + "community-pride" + ], + "key": "pqYxJO", + "title": "What community benefits do you expect to deliver with this project?", + "type": "list" + }, + { + "answer": "Test Community Benefits Form
", + "key": "lgfiGB", + "title": "Tell us about these benefits in detail, and how the asset's activities will help deliver them", + "type": "freeText" + }, + { + "answer": "Test Community Benefits Form
", + "key": "zKKouR", + "title": "Explain how you plan to deliver and sustain these benefits over time", + "type": "freeText" + }, + { + "answer": "Test Community Benefits Form
", + "key": "ZyIQGI", + "title": "Tell us how you'll make sure the whole community benefits from the asset", + "type": "freeText" + } + ], + "question": "Benefits you'll deliver", + "status": "COMPLETED" + } + ], + "status": "COMPLETED" + }, + { + "name": "project-qualifications-cof-r3-w2", + "questions": [ + { + "category": "WsFJts", + "fields": [ + { + "answer": false, + "key": "ZEKMQd", + "title": "Does your project meet the definition of a subsidy?", + "type": "list" + } + ], + "question": "If your project meets the definition", + "status": "COMPLETED" + } + ], + "status": "COMPLETED" + }, + { + "name": "risk-cof-r3-w2", + "questions": [ + { + "category": "HKdODf", + "fields": [ + { + "answer": "sample.txt", + "key": "EODncR", + "title": "Risks to your project (document upload)", + "type": "text" + } + ], + "question": "Your project risk register", + "status": "COMPLETED" + } + ], + "status": "COMPLETED" + } + ] diff --git a/tests_apply/seed_data/HSRA_R1_all_forms.json b/tests_apply/seed_data/HSRA_R1_all_forms.json new file mode 100644 index 00000000..62ea2c83 --- /dev/null +++ b/tests_apply/seed_data/HSRA_R1_all_forms.json @@ -0,0 +1,701 @@ +[ + { + "status": "COMPLETED", + "name": "name-your-application-hsra", + "questions": [ + { + "category": "CiYZae", + "question": "What would you like to name your application?", + "fields": [ + { + "key": "qbBtUh", + "title": "What would you like to name your application?", + "type": "text", + "answer": "Dummy Application" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": null, + "question": "MarkAsComplete", + "fields": [ + { + "key": "markAsComplete", + "title": "Do you want to mark this section as complete?", + "type": "boolean", + "answer": true + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "declaration-hsra", + "questions": [ + { + "category": "wycNzR", + "question": "Do you confirm all the information provided is correct?", + "fields": [ + { + "key": "QUaOGq", + "title": "By submitting this application, you confirm that the information you have provided is correct.", + "type": "list", + "answer": [ + "confirm" + ] + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": null, + "question": "MarkAsComplete", + "fields": [ + { + "key": "markAsComplete", + "title": "Do you want to mark this section as complete?", + "type": "boolean", + "answer": true + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "organisation-information-hsra", + "questions": [ + { + "category": "eaktoV", + "question": "Which local authority are you applying from?", + "fields": [ + { + "key": "WLddBt", + "title": "Which local authority are you applying from?", + "type": "text", + "answer": "Dummy Authority" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": "eaktoV", + "question": "Who is your section 151 officer?", + "fields": [ + { + "key": "okHmBB", + "title": "Full name", + "type": "text", + "answer": "Dummy Dum" + }, + { + "key": "bQOXTi", + "title": "Email address", + "type": "text", + "answer": "dummy.dum@email.com" + }, + { + "key": "phaosT", + "title": "Telephone number", + "type": "text", + "answer": "0700123456" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": null, + "question": "MarkAsComplete", + "fields": [ + { + "key": "markAsComplete", + "title": "Do you want to mark this section as complete?", + "type": "boolean", + "answer": true + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "applicant-information-hsra", + "questions": [ + { + "category": "bnfUAs", + "question": "Who should we contact about this application?", + "fields": [ + { + "key": "OkKkMd", + "title": "Full name", + "type": "text", + "answer": "Dummy Contact Person" + }, + { + "key": "Lwkcam", + "title": "Job title", + "type": "text", + "answer": "Application Contact" + }, + { + "key": "XfiUqN", + "title": "Email address", + "type": "text", + "answer": "dummy.dum@email.com" + }, + { + "key": "DlZjvr", + "title": "Telephone number", + "type": "text", + "answer": "0700123456" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": null, + "question": "MarkAsComplete", + "fields": [ + { + "key": "markAsComplete", + "title": "Do you want to mark this section as complete?", + "type": "boolean", + "answer": true + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "designated-area-details-hsra", + "questions": [ + { + "category": "YFgsrH", + "question": "Which designated high street or town centre is the vacant property in?", + "fields": [ + { + "key": "frDgtU", + "title": "Which designated high street or town centre is the vacant property in?", + "type": "text", + "answer": "Dummy Designated high street" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": "YFgsrH", + "question": "Where have you published the designation details?", + "fields": [ + { + "key": "fmWgiF", + "title": "Where have you published the designation details?", + "type": "text", + "answer": "www.council.gov.uk/dummy-designation" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": "YFgsrH", + "question": "Number of commercial properties", + "fields": [ + { + "key": "boXxzj", + "title": "How many commercial properties are in the designated area?", + "type": "text", + "answer": "9" + }, + { + "key": "eBpXPM", + "title": "How many of these are vacant?", + "type": "text", + "answer": "2" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": null, + "question": "MarkAsComplete", + "fields": [ + { + "key": "markAsComplete", + "title": "Do you want to mark this section as complete?", + "type": "boolean", + "answer": true + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "joint-applicant-hsra", + "questions": [ + { + "category": "vpxTQD", + "question": "Are you making a joint application with another local authority?", + "fields": [ + { + "key": "luWnQp", + "title": "Are you making a joint application with another local authority?", + "type": "list", + "answer": true + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": "vpxTQD", + "question": "Which local authority are you applying with?", + "fields": [ + { + "key": "cVDqxW", + "title": "Which local authority are you applying with?", + "type": "text", + "answer": "Dummy Local Authority" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": "vpxTQD", + "question": "Who from that authority should we contact about this application?", + "fields": [ + { + "key": "CyfqVo", + "title": "Full name", + "type": "text", + "answer": "Dummy Local Authority Contact" + }, + { + "key": "EvfEzH", + "title": "Email address", + "type": "text", + "answer": "dummy.localauth.contact@email.com" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": null, + "question": "MarkAsComplete", + "fields": [ + { + "key": "markAsComplete", + "title": "Do you want to mark this section as complete?", + "type": "boolean", + "answer": true + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "milestones-hsra", + "questions": [ + { + "category": "wtecPW", + "question": "When do you expect the auction to take place?", + "fields": [ + { + "key": "yvpmIv", + "title": "When do you expect the auction to take place?", + "type": "date", + "answer": "2024-10-20" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": "wtecPW", + "question": "When do you expect to submit your claim?", + "fields": [ + { + "key": "gzJqwe", + "title": "When do you expect to submit your claim?", + "type": "date", + "answer": "2024-10-05" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": "wtecPW", + "question": "When do you expect the tenant to sign the tenancy agreement?", + "fields": [ + { + "key": "ihfalZ", + "title": "When do you expect the tenant to sign the tenancy agreement?", + "type": "date", + "answer": "2024-10-15" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": "wtecPW", + "question": "When do you expect to finish the refurbishment works?", + "fields": [ + { + "key": "fIkkRN", + "title": "When do you expect to finish the refurbishment works?", + "type": "date", + "answer": "2025-02-05" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": "wtecPW", + "question": "When do you expect the tenant to move in?", + "fields": [ + { + "key": "VoAANy", + "title": "When do you expect the tenant to move in?", + "type": "date", + "answer": "2025-02-10" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": "wtecPW", + "question": "When do you expect to submit your post-payment verification (PPV)?", + "fields": [ + { + "key": "KFjxBs", + "title": "When do you expect to submit your post-payment verification (PPV)?", + "type": "date", + "answer": "2025-02-20" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": null, + "question": "MarkAsComplete", + "fields": [ + { + "key": "markAsComplete", + "title": "Do you want to mark this section as complete?", + "type": "boolean", + "answer": true + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "vacant-property-details-hsra", + "questions": [ + { + "category": "ISBazm", + "question": "What is the vacant property's address?", + "fields": [ + { + "key": "dwLpZU", + "title": "What is the vacant property's address?", + "type": "text", + "answer": "108, Horseferry Road Westminster, London, Greater London county, SW1P 2EF" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": "ISBazm", + "question": "What is the total commercial floorspace of the property, in meters squared?", + "fields": [ + { + "key": "rFpLZQ", + "title": "What is the total commercial floorspace of the property, in meters squared?", + "type": "text", + "answer": "1000" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": "ISBazm", + "question": "Term of vacancy", + "fields": [ + { + "key": "NnOqGc", + "title": "How many days has the property been vacant?", + "type": "text", + "answer": "190" + }, + { + "key": "qYtKIg", + "title": "How have you verified this?", + "type": "freeText", + "answer": "Third party evaluation.
" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": "ISBazm", + "question": "Upload the initial notice you served the landlord", + "fields": [ + { + "key": "ndpQJk", + "title": "Upload the initial notice you served the landlord", + "type": "text", + "answer": "Screenshot 2024-05-02 at 11.10.08.png" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": "ISBazm", + "question": "Before you served notice, what contact did you make with the landlord about the property\u2019s vacant status?", + "fields": [ + { + "key": "vAvGTE", + "title": "Before you served notice, what contact did you make with the landlord about the property\u2019s vacant status?", + "type": "freeText", + "answer": "Attempt 1: 23 March 2023
\\r\\nAttempt 2: 10 May 2023
" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": null, + "question": "MarkAsComplete", + "fields": [ + { + "key": "markAsComplete", + "title": "Do you want to mark this section as complete?", + "type": "boolean", + "answer": true + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "other-costs-hsra", + "questions": [ + { + "category": "qavZyX", + "question": "What is the total of any other expected costs, in pounds?", + "fields": [ + { + "key": "uJIluf", + "title": "What is the total of any other expected costs, in pounds?", + "type": "text", + "answer": "10000" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": "qavZyX", + "question": "Upload quotes showing other costs", + "fields": [ + { + "key": "kRiNuO", + "title": "Upload quotes showing other costs", + "type": "text", + "answer": "Screenshot 2024-05-07 at 16.06.03.png" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": null, + "question": "MarkAsComplete", + "fields": [ + { + "key": "markAsComplete", + "title": "Do you want to mark this section as complete?", + "type": "boolean", + "answer": true + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "total-expected-cost-hsra", + "questions": [ + { + "category": "XDldxG", + "question": "What is the total expected cost of delivering the HSRA, in pounds?", + "fields": [ + { + "key": "lfXuaP", + "title": "What is the total expected cost of delivering the HSRA, in pounds?", + "type": "text", + "answer": "850000" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": "XDldxG", + "question": "Costs are higher than the guided price", + "fields": [ + { + "key": "OBXEXZ", + "title": "Why are your costs higher than the guided price?", + "type": "freeText", + "answer": "Because it is a big project with more expenses.
" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": "XDldxG", + "question": "Have you secured any match funding?", + "fields": [ + { + "key": "KSQYyb", + "title": "Have you secured any match funding?", + "type": "list", + "answer": true + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": "XDldxG", + "question": "Match funding details", + "fields": [ + { + "key": "QveKZm", + "title": "How much match funding have you secured, in pounds?", + "type": "text", + "answer": "10000" + }, + { + "key": "pyCINJ", + "title": "Who is providing this?", + "type": "text", + "answer": "Dummy Funding Organisation" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": null, + "question": "MarkAsComplete", + "fields": [ + { + "key": "markAsComplete", + "title": "Do you want to mark this section as complete?", + "type": "boolean", + "answer": true + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "refurbishment-costs-hsra", + "questions": [ + { + "category": "qvSwnW", + "question": "What is the total expected cost of refurbishment, in pounds?", + "fields": [ + { + "key": "pfEHzn", + "title": "What is the total expected cost of refurbishment, in pounds?", + "type": "text", + "answer": "50000" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": "qvSwnW", + "question": "Upload the independent survey of works", + "fields": [ + { + "key": "SMwXcK", + "title": "Upload the independent survey of works", + "type": "text", + "answer": "Screenshot 2024-05-02 at 11.22.06.png" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": "qvSwnW", + "question": "Upload quotes showing refurbishment costs and if applicable project management costs for properties exceeding 200 sqm.", + "fields": [ + { + "key": "xUgKLI", + "title": "Upload quotes showing refurbishment costs and if applicable project management costs for properties exceeding 200 sqm.", + "type": "text", + "answer": "Screenshot 2024-05-02 at 16.53.59.png" + } + ], + "index": 0, + "status": "COMPLETED" + }, + { + "category": null, + "question": "MarkAsComplete", + "fields": [ + { + "key": "markAsComplete", + "title": "Do you want to mark this section as complete?", + "type": "boolean", + "answer": true + } + ], + "status": "COMPLETED" + } + ] + } +] diff --git a/tests_apply/seed_data/NSTF_R2_all_forms.json b/tests_apply/seed_data/NSTF_R2_all_forms.json new file mode 100644 index 00000000..239a03f3 --- /dev/null +++ b/tests_apply/seed_data/NSTF_R2_all_forms.json @@ -0,0 +1,1314 @@ +[ + { + "status": "COMPLETED", + "name": "current-services-ns", + "questions": [ + { + "category": "xigWzk", + "question": "Have you provided a night shelter or emergency accommodation on or after 1 April 2019?", + "fields": [ + { + "key": "QOlbCV", + "title": "Have you provided a night shelter or emergency accommodation on or after 1 April 2019?", + "type": "list", + "answer": true + } + ], + "status": "COMPLETED" + }, + { + "category": "xigWzk", + "question": "Night shelter or emergency accommodation", + "fields": [ + { + "key": "affVbH", + "title": "Is the night shelter or emergency accommodation communal?", + "type": "list", + "answer": true + }, + { + "key": "ttEOXi", + "title": "Is the night shelter or emergency accommodation single room accommodation?", + "type": "list", + "answer": true + }, + { + "key": "dSdeYa", + "title": "How many bed spaces did the night shelter or emergency accommodation provide from 1 April 2022 to 31 March 2023?", + "type": "text", + "answer": "323" + }, + { + "key": "gXvnZA", + "title": "Is the night shelter or emergency accommodation used for SWEP (severe weather emergency protocol)?", + "type": "list", + "answer": false + }, + { + "key": "vStFMu", + "title": "Do you accept referrals from the local authority or other agencies for available bed spaces?", + "type": "list", + "answer": false + } + ], + "status": "COMPLETED" + }, + { + "category": "xigWzk", + "question": "How many nights did the night shelter or emergency accommodation open from 1 April 2022 to 31 March 2023?", + "fields": [ + { + "key": "YyMRdP", + "title": "How many nights did the night shelter or emergency accommodation open from 1 April 2022 to 31 March 2023?", + "type": "text", + "answer": "7" + } + ], + "status": "COMPLETED" + }, + { + "category": "xigWzk", + "question": "Do you currently provide day provision?", + "fields": [ + { + "key": "STfdvD", + "title": "Do you currently provide day provision?", + "type": "list", + "answer": true + } + ], + "status": "COMPLETED" + }, + { + "category": "xigWzk", + "question": "When did you start providing day provision?", + "fields": [ + { + "key": "bCXNtj", + "title": "When did you start providing day provision?", + "type": "monthYear", + "answer": "2023-06" + } + ], + "status": "COMPLETED" + }, + { + "category": "xigWzk", + "question": "Which day provision services are you currently providing?", + "fields": [ + { + "key": "ULPcAU", + "title": "Which day provision services are you currently providing?", + "type": "list", + "answer": [ + "other" + ] + } + ], + "status": "COMPLETED" + }, + { + "category": "xigWzk", + "question": "Which other day provision do you currently provide?", + "fields": [ + { + "key": "zwQHCl", + "title": "Which other day provision do you currently provide?", + "type": "freeText", + "answer": "Test Current Services NS Form
" + } + ], + "status": "COMPLETED" + }, + { + "category": "xigWzk", + "question": "Do you currently provide any other services?", + "fields": [ + { + "key": "GBfYfn", + "title": "Do you currently provide any other services?", + "type": "list", + "answer": true + } + ], + "status": "COMPLETED" + }, + { + "category": "xigWzk", + "question": "What other services are you currently providing?", + "fields": [ + { + "key": "wViAiU", + "title": "What other services are you currently providing?", + "type": "freeText", + "answer": "Test Current Services NS Form
" + } + ], + "status": "COMPLETED" + }, + { + "category": "xigWzk", + "question": "Do you currently provide specialist support?", + "fields": [ + { + "key": "umAyqH", + "title": "Do you currently provide specialist support?", + "type": "list", + "answer": true + } + ], + "status": "COMPLETED" + }, + { + "category": "xigWzk", + "question": "Who do you currently provide targeted specialist support to?", + "fields": [ + { + "key": "JCUQcR", + "title": "Who do you currently provide targeted specialist support to?", + "type": "list", + "answer": [ + "women" + ] + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "objectives-and-activities-ns", + "questions": [ + { + "category": "VCtwUB", + "question": "Give a brief summary of your project, including what you hope to achieve", + "fields": [ + { + "key": "pWFwci", + "title": "Give a brief summary of your project, including what you hope to achieve", + "type": "freeText", + "answer": "Test Objectives and Activities NS Form
" + } + ], + "status": "COMPLETED" + }, + { + "category": "VCtwUB", + "question": "Objectives and activities", + "fields": [ + { + "key": "kRxOHF", + "title": "Proposal milestones", + "type": "multiInput", + "answer": [ + { + "FbWEBY": "Test Objectives and Activities NS Form", + "RXrpzV": "Test Objectives and Activities NS Form" + } + ] + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "staff-and-volunteers-ns", + "questions": [ + { + "category": "ibsaRz", + "question": "Employed staff and volunteers at your organisation", + "fields": [ + { + "key": "bcJWbJ", + "title": "How many staff members are currently employed in your organisation?", + "type": "text", + "answer": "44" + }, + { + "key": "pwPYdF", + "title": "How many volunteers are actively involved in your organisation?", + "type": "text", + "answer": "39" + }, + { + "key": "VXKVmM", + "title": "What percentage of your employed staff work part-time?", + "type": "text", + "answer": "10%" + }, + { + "key": "wOUNbF", + "title": "For part-time employees, what is their average full-time equivalency (FTE)?", + "type": "text", + "answer": "0.6" + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "name-your-application-ns", + "questions": [ + { + "category": "pCEnjo", + "question": "Name your application", + "fields": [ + { + "key": "YVsPtE", + "title": "Application name", + "type": "text", + "answer": "NS E2E Automated Application" + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "project-milestones-ns", + "questions": [ + { + "category": "KdUbsr", + "question": "Proposal milestones", + "fields": [ + { + "key": "sXlkAm", + "title": "Milestones", + "type": "multiInput", + "answer": [ + { + "fFIuPP": "Test Project Milestones NS Form", + "PrulfI": { + "PrulfI__month": 6, + "PrulfI__year": 2023 + } + } + ] + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "working-in-partnership-ns", + "questions": [ + { + "category": "TsNdVD", + "question": "Describe your important local partners and how they will support your proposal", + "fields": [ + { + "key": "qMRHPz", + "title": "Describe your important local partners and how they will support your proposal", + "type": "freeText", + "answer": "Test Working In Partnership NS Form
" + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "applicant-information-ns", + "questions": [ + { + "category": "ZbxIUV", + "question": "Lead contact details", + "fields": [ + { + "key": "fUMWcd", + "title": "Name of lead contact", + "type": "text", + "answer": "Test Applicant Information Form" + }, + { + "key": "lZVkeg", + "title": "Lead contact job title", + "type": "text", + "answer": "Test Applicant Information Form" + }, + { + "key": "CDEwxp", + "title": "Lead contact email address", + "type": "text", + "answer": "test@test.com" + }, + { + "key": "DvBqCJ", + "title": "Lead contact telephone number", + "type": "text", + "answer": "0000000000" + }, + { + "key": "ayzqnK", + "title": "Is the lead contact the same person as the authorised signatory?", + "type": "list", + "answer": true + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "risk-and-deliverability-ns", + "questions": [ + { + "category": "JOYYbl", + "question": "Risks to the proposal", + "fields": [ + { + "key": "xDpgOK", + "title": "Risk", + "type": "multiInput", + "answer": [ + { + "dmKRCF": "Test Risk And Deliverability NS Form", + "GVoNOE": "high", + "SRHsAx": "Test Risk And Deliverability NS Form" + } + ] + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "joint-applications-ns", + "questions": [ + { + "category": "uFLqwl", + "question": "Is your application a joint bid in partnership with other organisations?", + "fields": [ + { + "key": "jsUbAI", + "title": "Is your application a joint bid in partnership with other organisations?", + "type": "list", + "answer": true + } + ], + "status": "COMPLETED" + }, + { + "category": "uFLqwl", + "question": "Partner organisation details", + "fields": [ + { + "key": "oxMLrb", + "title": "Your partner organisations", + "type": "multiInput", + "answer": [ + { + "EFlBMr": "Test Joint Applications NS Form", + "JFEJVf": "Test Joint Applications NS Form" + } + ] + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "proposal-sustainability-ns", + "questions": [ + { + "category": "LZWArS", + "question": "How will this funding support the longer-term sustainability of your proposal beyond the funding period?", + "fields": [ + { + "key": "PEMJEy", + "title": "How will this funding support the longer-term sustainability of your proposal beyond the funding period?", + "type": "freeText", + "answer": "Test Proposal Sustainability NS Form
" + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "organisation-information-ns", + "questions": [ + { + "category": "VoIhVi", + "question": "Organisation details", + "fields": [ + { + "key": "opFJRm", + "title": "Organisation name", + "type": "text", + "answer": "Test organisation name" + }, + { + "key": "mhYQzL", + "title": "Organisation address", + "type": "text", + "answer": "Test Organisation Information NS Form, null, Test Organisation Information NS Form, null, TE1 2NR" + }, + { + "key": "AVShTf", + "title": "Which region of England do you work in?", + "type": "text", + "answer": "Test Organisation Information NS Form" + } + ], + "status": "COMPLETED" + }, + { + "category": "VoIhVi", + "question": "What is your organisation's main purpose?", + "fields": [ + { + "key": "BwbIlM", + "title": "What is your organisation's main purpose?", + "type": "freeText", + "answer": "Test Organisation Information NS Form Formatting
\r\nTest Local Need And Support NS Form
" + } + ], + "status": "COMPLETED" + }, + { + "category": "vzkeMY", + "question": "Local authority support", + "fields": [ + { + "key": "nURkuc", + "title": "What is the name of the local authority where your proposal will be based?", + "type": "text", + "answer": "Test Local Need And Support NS Form" + }, + { + "key": "lFTgWk", + "title": "Do you have the local authority's support and endorsement for your proposal?", + "type": "list", + "answer": false + }, + { + "key": "mIGfuL", + "title": "Upload letter of endorsement from your local authority (optional)", + "type": "text", + "answer": null + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "proposed-services-ns", + "questions": [ + { + "category": "gIqXgO", + "question": "Tell us how your proposal will transform your existing services", + "fields": [ + { + "key": "lOliDH", + "title": "Tell us how your proposal will transform your existing services", + "type": "freeText", + "answer": "Test Proposed Services NS Form
" + } + ], + "status": "COMPLETED" + }, + { + "category": "gIqXgO", + "question": "Do you plan to use this funding to make any changes to the existing night shelter or emergency accommodation?", + "fields": [ + { + "key": "dWxxdq", + "title": "Do you plan to use this funding to make any changes to the existing night shelter or emergency accommodation?", + "type": "list", + "answer": true + } + ], + "status": "COMPLETED" + }, + { + "category": "gIqXgO", + "question": "How many single rooms will you provide with the funding?", + "fields": [ + { + "key": "UEndmh", + "title": "How many single rooms will you provide with the funding?", + "type": "text", + "answer": "123" + } + ], + "status": "COMPLETED" + }, + { + "category": "gIqXgO", + "question": "Do you plan to use this funding to make any changes to the existing day provision?", + "fields": [ + { + "key": "jzzBDS", + "title": "Do you plan to use this funding to make any changes to the existing day provision?", + "type": "list", + "answer": true + } + ], + "status": "COMPLETED" + }, + { + "category": "gIqXgO", + "question": "Which additional day provision will your proposal provide?", + "fields": [ + { + "key": "bGCkPI", + "title": "Which additional day provision will your proposal provide?", + "type": "list", + "answer": [ + "other" + ] + } + ], + "status": "COMPLETED" + }, + { + "category": "gIqXgO", + "question": "Which other additional day provision will your proposal provide?", + "fields": [ + { + "key": "brLcqY", + "title": "Which other additional day provision will your proposal provide?", + "type": "freeText", + "answer": "Test Proposed Services NS Form
" + } + ], + "status": "COMPLETED" + }, + { + "category": "gIqXgO", + "question": "Will you provide any other additional services?", + "fields": [ + { + "key": "bCQWFN", + "title": "Will you provide any other additional services?", + "type": "list", + "answer": true + } + ], + "status": "COMPLETED" + }, + { + "category": "gIqXgO", + "question": "Which other services will you use the funding to provide?", + "fields": [ + { + "key": "kPvpzG", + "title": "Which other services will you use the funding to provide?", + "type": "freeText", + "answer": "Test Proposed Services NS Form
" + } + ], + "status": "COMPLETED" + }, + { + "category": "gIqXgO", + "question": "Will your proposal provide additional specialist support?", + "fields": [ + { + "key": "xYNpHc", + "title": "Will your proposal provide additional specialist support?", + "type": "list", + "answer": true + } + ], + "status": "COMPLETED" + }, + { + "category": "gIqXgO", + "question": "Who will your proposal provide targeted specialist support to?", + "fields": [ + { + "key": "RKPpEV", + "title": "Who will your proposal provide targeted specialist support to?", + "type": "list", + "answer": [ + "disabled-people", + "other" + ] + } + ], + "status": "COMPLETED" + }, + { + "category": "gIqXgO", + "question": "Who will your proposal provide specialist support to?", + "fields": [ + { + "key": "HTGgzg", + "title": "Who will your proposal provide specialist support to?", + "type": "freeText", + "answer": "Test Proposed Services NS Form
" + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "outputs-and-outcomes-ns", + "questions": [ + { + "category": "VeHFTz", + "question": "Who your proposal will support", + "fields": [ + { + "key": "cYEiGS", + "title": "How many people will the proposal support with the funding in 2023 to 2024?", + "type": "text", + "answer": "5" + }, + { + "key": "ZZisap", + "title": "How many people will the proposal support with the funding in 2024 to 2025?", + "type": "text", + "answer": "10" + }, + { + "key": "ZJCVjE", + "title": "How many people with restricted eligibility will the proposal support in 2023 to 2024?", + "type": "text", + "answer": "15" + }, + { + "key": "dboegN", + "title": "How many people with restricted eligibility will the proposal support in 2024 to 2025?", + "type": "text", + "answer": "20" + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "funding-required-ns", + "questions": [ + { + "category": "LABTna", + "question": "What funding are you applying for?", + "fields": [ + { + "key": "NxVqXd", + "title": "What funding are you applying for?", + "type": "list", + "answer": "both-revenue-and-capital" + } + ], + "status": "COMPLETED" + }, + { + "category": "LABTna", + "question": "How much funding are you applying for?", + "fields": [ + { + "key": "GRWtfV", + "title": "Both revenue and capital", + "type": "text", + "answer": "33" + }, + { + "key": "zvPzXN", + "title": "Revenue for 1 April 2024 to 31 March 2025", + "type": "text", + "answer": "55" + }, + { + "key": "QUCvFy", + "title": "Capital for 1 April 2023 to 31 March 2024", + "type": "text", + "answer": "93" + }, + { + "key": "pppiYl", + "title": "Capital for 1 April 2024 to 31 March 2025", + "type": "text", + "answer": "39" + } + ], + "status": "COMPLETED" + }, + { + "category": "LABTna", + "question": "Revenue funding", + "fields": [ + { + "key": "mCbbyN", + "title": "Revenue costs", + "type": "multiInput", + "answer": [ + { + "dpDFgB": "Test Funding Required NS Form", + "iZdZrr": 40, + "leIxEX": "1 April 2023 to 31 March 2024", + "TrTaZQ": "Test Funding Required NS Form" + } + ] + } + ], + "status": "COMPLETED" + }, + { + "category": "LABTna", + "question": "Capital funding", + "fields": [ + { + "key": "XsAoTv", + "title": "Capital costs", + "type": "multiInput", + "answer": [ + { + "cpFthG": "Test Funding Required NS Form", + "JtBjFp": 50, + "mmwzGc": "1 April 2024 to 31 March 2025", + "pMffVz": null + } + ] + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "declarations-ns", + "questions": [ + { + "category": "gkTvmJ", + "question": "Agree to the final confirmations", + "fields": [ + { + "key": "kOTdzu", + "title": "Confirm you have a bank account set up and associated with the organisation you are applying on behalf of", + "type": "list", + "answer": false + }, + { + "key": "NBcyAe", + "title": "Confirm that the information you've provided in this application is accurate to the best of your knowledge on the date of submission", + "type": "list", + "answer": true + }, + { + "key": "OKtDsH", + "title": "Confirm you have a safeguarding, data protection and health and safety policy", + "type": "list", + "answer": false + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "building-works-ns", + "questions": [ + { + "category": "EDNaie", + "question": "Will you use the funding to conduct building works?", + "fields": [ + { + "key": "lifPop", + "title": "Will you use the funding to conduct building works?", + "type": "list", + "answer": true + } + ], + "status": "COMPLETED" + }, + { + "category": "EDNaie", + "question": "Number of new single room units (optional)", + "fields": [ + { + "key": "fmcTtE", + "title": "Number of new single room units (optional)", + "type": "text", + "answer": "344" + } + ], + "status": "COMPLETED" + }, + { + "category": "EDNaie", + "question": "Do you need planning approval for your proposal?", + "fields": [ + { + "key": "xGnWEW", + "title": "Do you need planning approval for your proposal?", + "type": "list", + "answer": "yes" + } + ], + "status": "COMPLETED" + }, + { + "category": "EDNaie", + "question": "Have you made an application for planning permission?", + "fields": [ + { + "key": "KhISvR", + "title": "Have you made an application for planning permission?", + "type": "list", + "answer": false + } + ], + "status": "COMPLETED" + }, + { + "category": "EDNaie", + "question": "Have you received any pre-application planning advice?", + "fields": [ + { + "key": "YFPgTB", + "title": "Have you received any pre-application planning advice?", + "type": "list", + "answer": true + } + ], + "status": "COMPLETED" + }, + { + "category": "EDNaie", + "question": "Give a brief summary of the advice you received", + "fields": [ + { + "key": "mADkNz", + "title": "Give a brief summary of the advice you received", + "type": "freeText", + "answer": "Test Building Works NS Form
" + } + ], + "status": "COMPLETED" + }, + { + "category": "EDNaie", + "question": "Will you procure a construction contract for the building works?", + "fields": [ + { + "key": "AYDIPX", + "title": "Will you procure a construction contract for the building works?", + "type": "list", + "answer": true + } + ], + "status": "COMPLETED" + }, + { + "category": "EDNaie", + "question": "Will you use any professional advisors for the proposal?", + "fields": [ + { + "key": "nyusrL", + "title": "Will you use any professional advisors for the proposal?", + "type": "list", + "answer": true + } + ], + "status": "COMPLETED" + }, + { + "category": "EDNaie", + "question": "Details of professional advisors", + "fields": [ + { + "key": "SQEpBt", + "title": "Details of professional advisors", + "type": "multiInput", + "answer": [ + { + "cbYcqS": "Test Building Works NS Form", + "muRwiL": "Test Building Works NS Form" + } + ] + } + ], + "status": "COMPLETED" + }, + { + "category": "EDNaie", + "question": "Will you be hiring contractors to complete the building works?", + "fields": [ + { + "key": "rYAgMN", + "title": "Will you be hiring contractors to complete the building works?", + "type": "list", + "answer": false + } + ], + "status": "COMPLETED" + }, + { + "category": "EDNaie", + "question": "Do you have a cost estimate for the building works?", + "fields": [ + { + "key": "wUFaUF", + "title": "Do you have a cost estimate for the building works?", + "type": "list", + "answer": true + } + ], + "status": "COMPLETED" + }, + { + "category": "EDNaie", + "question": "Upload cost estimate", + "fields": [ + { + "key": "NOMwBb", + "title": "Upload cost estimate", + "type": "text", + "answer": "sample.txt" + } + ], + "status": "COMPLETED" + }, + { + "category": "EDNaie", + "question": "Do you have a condition survey?", + "fields": [ + { + "key": "ZmYhgR", + "title": "Do you have a condition survey?", + "type": "list", + "answer": true + } + ], + "status": "COMPLETED" + }, + { + "category": "EDNaie", + "question": "Upload a condition survey", + "fields": [ + { + "key": "aTxAPP", + "title": "Upload a condition survey", + "type": "text", + "answer": "sample.txt" + } + ], + "status": "COMPLETED" + }, + { + "category": "EDNaie", + "question": "Do you have a buildings and contents insurance certificate?", + "fields": [ + { + "key": "SdwIUb", + "title": "Do you have a buildings and contents insurance certificate?", + "type": "list", + "answer": true + } + ], + "status": "COMPLETED" + }, + { + "category": "EDNaie", + "question": "Upload buildings and contents insurance certificate", + "fields": [ + { + "key": "NlDVCg", + "title": "Upload buildings and contents insurance certificate", + "type": "text", + "answer": "sample.txt" + } + ], + "status": "COMPLETED" + }, + { + "category": "EDNaie", + "question": "Do you own the premises?", + "fields": [ + { + "key": "RXIYZY", + "title": "Do you own the premises?", + "type": "list", + "answer": false + } + ], + "status": "COMPLETED" + }, + { + "category": "EDNaie", + "question": "Upload the land owner's consent", + "fields": [ + { + "key": "rMvZDG", + "title": "Upload the land owner's consent", + "type": "text", + "answer": "sample.txt" + } + ], + "status": "COMPLETED" + }, + { + "category": "EDNaie", + "question": "How long is left on the lease agreement?", + "fields": [ + { + "key": "muhnok", + "title": "How long is left on the lease agreement?", + "type": "list", + "answer": "less-than-4-years" + } + ], + "status": "COMPLETED" + }, + { + "category": "EDNaie", + "question": "Upload correspondence from your landlord to show that lease renewal discussions have started", + "fields": [ + { + "key": "GLOpmu", + "title": "Upload correspondence from your landlord to show that lease renewal discussions have started", + "type": "text", + "answer": "sample.txt" + } + ], + "status": "COMPLETED" + }, + { + "category": "EDNaie", + "question": "Upload heads of terms outlining new lease agreement", + "fields": [ + { + "key": "kQJIbS", + "title": "Upload heads of terms outlining new lease agreement", + "type": "text", + "answer": "sample.txt" + } + ], + "status": "COMPLETED" + }, + { + "category": "EDNaie", + "question": "Is the building listed?", + "fields": [ + { + "key": "skBfqS", + "title": "Is the building listed?", + "type": "list", + "answer": true + } + ], + "status": "COMPLETED" + }, + { + "category": "EDNaie", + "question": "Upload listed building consent", + "fields": [ + { + "key": "abdbzq", + "title": "Upload listed building consent", + "type": "text", + "answer": "sample.txt" + } + ], + "status": "COMPLETED" + } + ] + }, + { + "status": "COMPLETED", + "name": "match-funding-ns", + "questions": [ + { + "category": "qXObyw", + "question": "Will you use match funding for this proposal?", + "fields": [ + { + "key": "nxpXlE", + "title": "Will you use match funding for this proposal?", + "type": "list", + "answer": true + } + ], + "status": "COMPLETED" + }, + { + "category": "qXObyw", + "question": "Match funding", + "fields": [ + { + "key": "uuyBff", + "title": "Match funding", + "type": "multiInput", + "answer": [ + { + "AfAKxk": "Test Match Funding NS Form", + "CrcLtW": 1234, + "ndySbC": "1 April 2023 to 31 March 2024", + "pATWyM": "Capital", + "sIFBGc": true + } + ] + } + ], + "status": "COMPLETED" + } + ] + } +] diff --git a/tests_apply/seed_data/__init__.py b/tests_apply/seed_data/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests_apply/seed_data/application_data.py b/tests_apply/seed_data/application_data.py new file mode 100644 index 00000000..70727bd8 --- /dev/null +++ b/tests_apply/seed_data/application_data.py @@ -0,0 +1,54 @@ +from fsd_utils import NotifyConstants + +expected_application_json = { + NotifyConstants.FIELD_TYPE: NotifyConstants.TEMPLATE_TYPE_APPLICATION, + NotifyConstants.FIELD_TO: "test_application@example.com", + NotifyConstants.FIELD_CONTENT: { + NotifyConstants.MAGIC_LINK_CONTACT_HELP_EMAIL_FIELD: "COF@communities.gov.uk", + NotifyConstants.APPLICATION_FIELD: { + "id": "123456789", + "reference": "1564564564-56-4-54-4654", + "fund_id": "47aef2f5-3fcb-4d45-acb5-f0152b5f03c4", + "round_name": "summer", + "date_submitted": "2022-05-14T09:25:44.124542", + "fund_name": "Community Ownership Fund", + "language": "en", + NotifyConstants.APPLICATION_FORMS_FIELD: [ + { + NotifyConstants.APPLICATION_NAME_FIELD: "about-your-org", + NotifyConstants.APPLICATION_QUESTIONS_FIELD: [ + { + "question": "Application information", + "fields": [ + { + "key": "application-name", + "title": "Applicant name", + "type": "text", + "answer": "Jack-Simon", + }, + { + "key": "upload-file", + "title": "Upload file", + "type": "file", + "answer": "012ba4c7-e4971/test-one_two.three/programmer.jpeg", # noqa + }, + { + "key": "boolean-question-1", + "title": "Boolean Question 1 ", + "type": "list", + "answer": False, + }, + { + "key": "boolean-question-2", + "title": "Boolean Question 2", + "type": "list", + "answer": True, + }, + ], + } + ], + } + ], + }, + }, +} diff --git a/tests_apply/seed_data/seed_db.py b/tests_apply/seed_data/seed_db.py new file mode 100644 index 00000000..f08175c7 --- /dev/null +++ b/tests_apply/seed_data/seed_db.py @@ -0,0 +1,58 @@ +import json + +from _helpers import get_blank_forms +from db.models.application import Applications +from db.queries.apply import add_new_forms +from db.queries.apply.application import create_application +from db.queries.apply.application import submit_application +from db.queries.apply.updating.queries import update_form + + +def seed_not_started_application(fund_config, round_config, account_id, language): + return _seed_application(fund_config["id"], round_config["id"], account_id, language) + + +def seed_in_progress_application(fund_config, round_config, account_id, language): + app = _seed_application(fund_config["id"], round_config["id"], account_id, language) + with open( + f"tests/seed_data/{fund_config['short_code']}_{round_config['short_code']}_all_forms.json", + ) as f: + ALL_FORMS = json.load(f) + form = [form for form in ALL_FORMS if form["name"] == round_config["project_name_form"]][0] + update_form( + app.id, + round_config["project_name_form"], + form["questions"], + True, + ) + return app + + +def seed_completed_application(fund_config, round_config, account_id, language): + app = _seed_application(fund_config["id"], round_config["id"], account_id, language) + with open( + f"tests/seed_data/{fund_config['short_code']}_{round_config['short_code']}_all_forms.json", + ) as f: + ALL_FORMS = json.load(f) + for form in ALL_FORMS: + update_form( + app.id, + form["name"], + form["questions"], + True, + ) + return app + + +def seed_submitted_application(fund_config, round_config, account_id, language): + app = seed_completed_application(fund_config, round_config, account_id, language) + submit_application(str(app.id)) + return app + + +def _seed_application(fund_id, round_id, account_id, language) -> Applications: + app: Applications = create_application(account_id, fund_id, round_id, language) + empty_forms = get_blank_forms(fund_id, round_id, language) + add_new_forms(forms=empty_forms, application_id=app.id) + print(f"Created app with reference {app.reference}") + return app diff --git a/tests_apply/test_all_feedbacks.py b/tests_apply/test_all_feedbacks.py new file mode 100644 index 00000000..bba60959 --- /dev/null +++ b/tests_apply/test_all_feedbacks.py @@ -0,0 +1,140 @@ +import pytest +from config.key_report_mappings.cof_r3w2_key_report_mapping import ( + COF_R3W2_KEY_REPORT_MAPPING, +) +from db.models import Applications +from db.models import EndOfApplicationSurveyFeedback +from db.models import Feedback +from db.models import Forms +from db.queries.apply.feedback import retrieve_all_feedbacks_and_surveys + +app_sections = [ + {"id": 62, "title": "1. About your organisation"}, + {"id": 65, "title": "2. About your project"}, +] + +applications = [ + Applications( + id="app_1", + forms=[ + Forms( + name="applicant-information-cof-r3-w2", + json=[ + { + "questions": "Lead contact details", + "fields": [{"key": "NlHSBg", "answer": "test@test.com"}], + } + ], + ), + Forms( + name="organisation-information-cof-r3-w2", + json=[ + { + "questions": "organisation information", + "fields": [ + {"key": "WWWWxy", "answer": "Ref1234"}, + {"key": "YdtlQZ", "answer": "OrgName"}, + {"key": "lajFtB", "answer": "Non-Profit"}, + ], + } + ], + ), + ], + feedbacks=[ + Feedback( + section_id="62", + feedback_json={ + "comment": "test_comment", + "rating": "neither easy or difficult", + }, + ), + Feedback( + section_id="65", + feedback_json={ + "comment": "test_comment", + "rating": "neither easy or difficult", + }, + ), + ], + end_of_application_survey=[ + EndOfApplicationSurveyFeedback( + id=1, + application_id="app_1", + fund_id="test_fund", + round_id="test_round", + page_number=1, + data={ + "overall_application_experience": "neither easy or difficult", + "hours_spent": 45, + }, + ) + ], + ) +] + + +@pytest.mark.parametrize( + "app_sections,applications,report_mapping", + [ + ([app_sections, applications, COF_R3W2_KEY_REPORT_MAPPING.mapping]), + ], +) +def test_retrieve_all_feedbacks_and_surveys(mocker, app_sections, applications, report_mapping): + mocker.patch( + "db.queries.apply.feedback.queries.get_application_sections", + return_value=app_sections, + ) + mocker.patch( + "db.queries.apply.feedback.queries.get_applications", + return_value=applications, + ) + mocker.patch( + "db.queries.apply.feedback.queries.get_report_mapping_for_round", + return_value=report_mapping, + ) + + result = retrieve_all_feedbacks_and_surveys("test_fund", "test_round", "SUBMITTED") + assert "sections_feedback" in result + assert "end_of_application_survey_data" in result + + # check contents + assert result["sections_feedback"][0]["section"] == app_sections[0]["title"] + assert result["sections_feedback"][0]["comment"] == applications[0].feedbacks[0].feedback_json["comment"] + assert result["sections_feedback"][0]["rating"] == applications[0].feedbacks[0].feedback_json["rating"] + + +@pytest.mark.parametrize( + "app_sections,applications", + [ + ( + [ + app_sections, + applications, + ] + ), + ], +) +def test_api_get_all_feedbacks_and_survey_report( + mocker, + flask_test_client, + app_sections, + applications, +): + mocker.patch( + "db.queries.apply.feedback.queries.get_application_sections", + return_value=app_sections, + ) + mocker.patch( + "db.queries.apply.feedback.queries.get_applications", + return_value=applications, + ) + response = flask_test_client.get( + "/apply/applications/get_all_feedbacks_and_survey_report?" + "fund_id=test_fund&round_id=test_round&status_only=SUBMITTED", + headers={"Content-Type": "application/vnd.ms-excel"}, + follow_redirects=True, + ) + + assert response.status_code == 200 + assert "application/vnd.ms-excel" == response.headers["Content-Type"] + assert isinstance(response.content, bytes) diff --git a/tests_apply/test_application_status.py b/tests_apply/test_application_status.py new file mode 100644 index 00000000..50c3d819 --- /dev/null +++ b/tests_apply/test_application_status.py @@ -0,0 +1,590 @@ +from unittest.mock import MagicMock + +import pytest +from db.queries.apply.statuses.queries import _determine_question_page_status_from_answers +from db.queries.apply.statuses.queries import _is_all_sections_feedback_complete +from db.queries.apply.statuses.queries import _is_feedback_survey_complete +from db.queries.apply.statuses.queries import _is_field_answered +from db.queries.apply.statuses.queries import _is_research_survey_complete +from db.queries.apply.statuses.queries import update_application_status +from db.queries.apply.statuses.queries import update_form_status +from db.queries.apply.statuses.queries import update_question_page_statuses +from services.apply.models.round import FeedbackSurveyConfig + + +@pytest.mark.parametrize( + "answer_found_list,exp_status", + [ + ([True], "COMPLETED"), + ([True, True], "COMPLETED"), + ([False, False], "NOT_STARTED"), + ([False], "NOT_STARTED"), + ([True, False], "IN_PROGRESS"), + ([True, False, True], "IN_PROGRESS"), + ([], "NOT_STARTED"), + (None, "NOT_STARTED"), + ], +) +def test_determine_question_status_from_answers(answer_found_list, exp_status): + assert _determine_question_page_status_from_answers(answer_found_list) == exp_status + + +@pytest.mark.parametrize( + "field_json,exp_result", + [ + ({"answer": "abc"}, True), + ({"answer": 123}, True), + ({"answer": None}, True), + ({"answer": ["abc", 123]}, True), + ({"answer": ""}, False), + ({"answer": []}, False), + ], +) +def test_is_field_answered(field_json, exp_result): + assert _is_field_answered(field_json) == exp_result + + +def test_update_question_statuses_with_mocks(mocker): + mock_question_status = mocker.patch( + "db.queries.apply.statuses.queries._determine_question_page_status_from_answers", + return_value="NOT_STARTED", + ) + mock_answer_status = mocker.patch("db.queries.apply.statuses.queries._determine_answer_status_for_fields") + + test_json = [{"fields": [], "status": None}, {"fields": [], "status": None}] + + update_question_page_statuses(test_json) + + assert test_json[0]["status"] == "NOT_STARTED" + assert test_json[1]["status"] == "NOT_STARTED" + mock_question_status.call_count == 2 + mock_answer_status.call_count == 2 + + +@pytest.mark.parametrize( + "form_json,exp_status", + [ + ([{"fields": [{"answer": "hello"}], "status": None}], "COMPLETED"), + ( + [ + {"fields": [{"answer": "hello"}, {"answer": ""}], "status": None}, + {"fields": [{"answer": "hello"}, {"answer": ""}], "status": None}, + ], + "IN_PROGRESS", + ), + ([{"fields": [{"answer": ""}], "status": None}], "NOT_STARTED"), + ], +) +def test_update_question_statuses(form_json, exp_status): + update_question_page_statuses(form_json) + for form in form_json: + assert form["status"] == exp_status + + +@pytest.mark.parametrize( + "form_json,form_has_completed,is_summary_submit,round_mark_as_complete_enabled," + " mark_as_complete, exp_status,exp_has_completed", + [ + ( # Previously marked as complete, want to mark as not complete + [ + {"status": "COMPLETED", "question": "abc"}, + {"status": "COMPLETED", "question": "abc"}, + ], + True, + True, + True, + False, + "IN_PROGRESS", + False, + ), + ( # Marking as complete for the first time + [ + {"status": "COMPLETED", "question": "abc"}, + {"status": "COMPLETED", "question": "abc"}, + ], + False, + True, + True, + True, + "COMPLETED", + True, + ), + ( # Not on summary page, not marking as complete + [ + {"status": "COMPLETED", "question": "abc"}, + {"status": "COMPLETED", "question": "abc"}, + ], + False, + True, + True, + False, + "IN_PROGRESS", + False, + ), + ( + [{"status": "NOT_STARTED", "question": "abc"}], + False, + False, + False, + None, + "NOT_STARTED", + False, + ), + ( + [ + {"status": "IN_PROGRESS", "question": "abc"}, + {"status": "COMPLETED", "question": "abc"}, + ], + False, + False, + False, + None, + "IN_PROGRESS", + False, + ), + ( + [ + {"status": "NOT_STARTED", "question": "abc"}, + {"status": "COMPLETED", "question": "abc"}, + ], + False, + False, + False, + None, + "IN_PROGRESS", + False, + ), + ( + [ + {"status": "COMPLETED", "question": "abc"}, + {"status": "COMPLETED", "question": "abc"}, + ], + False, + False, + False, + None, + "IN_PROGRESS", + False, + ), + ( + [ + {"status": "COMPLETED", "question": "abc"}, + {"status": "COMPLETED", "question": "abc"}, + ], + False, + True, + False, + None, + "COMPLETED", + True, + ), + ( + [{"status": "NOT_STARTED", "question": "abc"}], + True, + False, + False, + None, + "NOT_STARTED", + True, + ), + ( + [{"status": "COMPLETED", "question": "abc"}], + True, + False, + False, + None, + "COMPLETED", + True, + ), + ], +) +def test_update_form_status( + form_json, + form_has_completed, + is_summary_submit, + round_mark_as_complete_enabled, + mark_as_complete, + exp_status, + exp_has_completed, +): + form_to_update = MagicMock() + form_to_update.json = form_json + form_to_update.has_completed = form_has_completed + + # If a round doesn't use mark_as_complete, the question is not in the json + if mark_as_complete is not None: + form_to_update.json.append( + { + "status": "COMPLETED", + "question": "MarkAsComplete", + "fields": [{"answer": mark_as_complete}], + }, + ) + update_form_status(form_to_update, round_mark_as_complete_enabled, is_summary_submit) + assert form_to_update.status == exp_status + assert form_to_update.has_completed == exp_has_completed + + +@pytest.mark.parametrize( + "app_sections,feedback_for_sections,exp_result", + [ + ( + [ + [{"requires_feedback": True, "id": 0}], + [None], + False, + ] + ), + ( + [ + [ + {"requires_feedback": True, "id": 0}, + {"requires_feedback": True, "id": 1}, + ], + [None, True], + False, + ] + ), + ( + [ + [ + {"requires_feedback": True, "id": 0}, + {"requires_feedback": True, "id": 1}, + ], + [True, True], + True, + ] + ), + ( + [ + [ + {"requires_feedback": True, "id": 0}, + {"requires_feedback": True, "id": 1}, + ], + [True, True], + True, + ] + ), + ], +) +def test_is_all_sections_feedback_complete(mocker, app_sections, feedback_for_sections, exp_result): + mocker.patch( + "db.queries.apply.statuses.queries.get_application_sections", + return_value=app_sections, + ) + mocker.patch( + "db.queries.apply.statuses.queries.get_feedback", + new=lambda application_id, section_id: feedback_for_sections[int(section_id)], + ) + result = _is_all_sections_feedback_complete("123", "123", "123", "en") + assert result == exp_result + + +@pytest.mark.parametrize( + "end_survey_data,exp_result", + [ + ( + [ + [True, None, None, None], + False, + ] + ), + ( + [ + [True, True, True, True], + True, + ] + ), + ], +) +def test_is_feedback_survey_complete(mocker, end_survey_data, exp_result): + mocker.patch( + "db.queries.apply.statuses.queries.retrieve_end_of_application_survey_data", + new=lambda application_id, page_number: end_survey_data[int(page_number) - 1], + ) + result = _is_feedback_survey_complete("123") + assert result == exp_result + + +@pytest.mark.parametrize( + "research_survey_data,exp_result", + [ + ( + MagicMock(data={"research_opt_in": "disagree"}), + True, + ), + ( + MagicMock( + data={"research_opt_in": "agree", "contact_name": "John Doe", "contact_email": "john@example.com"} + ), + True, + ), + ( + MagicMock(data={"research_opt_in": "agree", "contact_name": None, "contact_email": "john@example.com"}), + False, + ), + ( + MagicMock(data={"research_opt_in": "agree", "contact_name": "John Doe", "contact_email": None}), + False, + ), + ( + MagicMock(data={"research_opt_in": "agree", "contact_name": None, "contact_email": None}), + False, + ), + ], +) +def test_is_research_survey_complete(mocker, research_survey_data, exp_result): + mocker.patch( + "db.queries.apply.statuses.queries.retrieve_research_survey_data", + new=lambda application_id: research_survey_data, + ) + result = _is_research_survey_complete("123") + assert result == exp_result + + +@pytest.mark.parametrize( + "form_statuses,feedback_complete,survey_complete,research_complete,feedback_survey_config,exp_status", + [ + ( + ["NOT_STARTED"], + False, + False, + False, + FeedbackSurveyConfig( + has_feedback_survey=False, + is_feedback_survey_optional=False, + has_section_feedback=False, + is_section_feedback_optional=False, + has_research_survey=False, + is_research_survey_optional=False, + ), + "NOT_STARTED", + ), + ( + ["NOT_STARTED"], + False, + False, + False, + FeedbackSurveyConfig( + has_feedback_survey=True, + is_feedback_survey_optional=False, + has_section_feedback=True, + is_section_feedback_optional=False, + has_research_survey=True, + is_research_survey_optional=False, + ), + "NOT_STARTED", + ), + ( + ["NOT_STARTED"], + True, + True, + True, + FeedbackSurveyConfig( + has_feedback_survey=True, + is_feedback_survey_optional=False, + has_section_feedback=True, + is_section_feedback_optional=False, + has_research_survey=True, + is_research_survey_optional=False, + ), + "NOT_STARTED", + ), + ( + ["NOT_STARTED", "COMPLETED"], + False, + False, + False, + FeedbackSurveyConfig( + has_feedback_survey=False, + is_feedback_survey_optional=False, + has_section_feedback=False, + is_section_feedback_optional=False, + has_research_survey=False, + is_research_survey_optional=False, + ), + "IN_PROGRESS", + ), + ( + ["NOT_STARTED", "COMPLETED"], + False, + False, + False, + FeedbackSurveyConfig( + has_feedback_survey=True, + is_feedback_survey_optional=False, + has_section_feedback=True, + is_section_feedback_optional=False, + has_research_survey=True, + is_research_survey_optional=False, + ), + "IN_PROGRESS", + ), + ( + ["NOT_STARTED", "COMPLETED"], + True, + True, + True, + FeedbackSurveyConfig( + has_feedback_survey=True, + is_feedback_survey_optional=False, + has_section_feedback=True, + is_section_feedback_optional=False, + has_research_survey=True, + is_research_survey_optional=False, + ), + "IN_PROGRESS", + ), + ( + ["COMPLETED", "COMPLETED"], + True, + True, + True, + FeedbackSurveyConfig( + has_feedback_survey=True, + is_feedback_survey_optional=False, + has_section_feedback=True, + is_section_feedback_optional=False, + has_research_survey=True, + is_research_survey_optional=False, + ), + "COMPLETED", + ), + ( + ["COMPLETED", "COMPLETED"], + False, + False, + False, + FeedbackSurveyConfig( + has_feedback_survey=True, + is_feedback_survey_optional=False, + has_section_feedback=True, + is_section_feedback_optional=False, + has_research_survey=True, + is_research_survey_optional=False, + ), + "IN_PROGRESS", + ), + ( + ["COMPLETED", "COMPLETED"], + False, + False, + False, + FeedbackSurveyConfig( + has_feedback_survey=False, + is_feedback_survey_optional=False, + has_section_feedback=False, + is_section_feedback_optional=False, + has_research_survey=False, + is_research_survey_optional=False, + ), + "COMPLETED", + ), + ( + ["SUBMITTED"], + True, + True, + True, + FeedbackSurveyConfig( + has_feedback_survey=True, + is_feedback_survey_optional=False, + has_section_feedback=True, + is_section_feedback_optional=False, + has_research_survey=True, + is_research_survey_optional=False, + ), + "SUBMITTED", + ), + ( + ["SUBMITTED"], + False, + False, + False, + FeedbackSurveyConfig( + has_feedback_survey=True, + is_feedback_survey_optional=False, + has_section_feedback=True, + is_section_feedback_optional=False, + has_research_survey=True, + is_research_survey_optional=False, + ), + "SUBMITTED", + ), + ( + ["COMPLETED"], + True, + True, + True, + FeedbackSurveyConfig( + has_feedback_survey=True, + is_feedback_survey_optional=False, + has_section_feedback=True, + is_section_feedback_optional=False, + has_research_survey=True, + is_research_survey_optional=False, + ), + "COMPLETED", + ), + ( + ["COMPLETED"], + True, + True, + False, + FeedbackSurveyConfig( + has_feedback_survey=True, + is_feedback_survey_optional=False, + has_section_feedback=True, + is_section_feedback_optional=False, + has_research_survey=True, + is_research_survey_optional=False, + ), + "IN_PROGRESS", + ), + ( + ["COMPLETED"], + True, + True, + False, + FeedbackSurveyConfig( + has_feedback_survey=True, + is_feedback_survey_optional=False, + has_section_feedback=True, + is_section_feedback_optional=False, + has_research_survey=True, + is_research_survey_optional=True, + ), + "COMPLETED", + ), + ], +) +def test_update_application_status( + mocker, + form_statuses, + feedback_complete, + survey_complete, + research_complete, + feedback_survey_config, + exp_status, +): + mock_fb = mocker.patch( + "db.queries.apply.statuses.queries._is_all_sections_feedback_complete", + return_value=feedback_complete, + ) + mocker.patch( + "db.queries.apply.statuses.queries._is_feedback_survey_complete", + return_value=survey_complete, + ) + mocker.patch( + "db.queries.apply.statuses.queries._is_research_survey_complete", + return_value=research_complete, + ) + app_with_forms = MagicMock() + app_with_forms.forms = [] + for status in form_statuses: + form = MagicMock() + form.status.name = status + app_with_forms.forms.append(form) + update_application_status(app_with_forms, feedback_survey_config) + assert app_with_forms.status == exp_status + if feedback_survey_config.has_section_feedback: + mock_fb.assert_called_once() diff --git a/tests_apply/test_aws.py b/tests_apply/test_aws.py new file mode 100644 index 00000000..2b2bbe03 --- /dev/null +++ b/tests_apply/test_aws.py @@ -0,0 +1,42 @@ +import pytest +from services.apply.aws import list_files_by_prefix + + +# You can use this for testing the function if doing tdd. +@pytest.mark.skip() +def test_list_files_tdd(): + bucket_name = "paas-s3-broker-prod-lon-443b9fc2-55ff-4c2f-9ac3-d3ebfb18ef5a" # this is form-uploads-dev + prefix = "my-application-id/" # this was just a mock application id I used for testing. + + files = list_files_by_prefix(bucket_name, prefix) + assert len(files) != 0 + + +def test_list_files_by_prefix_multiple_files(mocker): + """GIVEN an S3 objects response with multiple files WHEN calling + list_files_by_prefix with a prefix THEN it should return a list of FileData + instances with the correct key parts.""" + prefix = "application_id/" + objects_response = { + "Contents": [ + {"Key": f"{prefix}form/path/component_id/filename1"}, + {"Key": f"{prefix}form/path/component_id/filename2"}, + {"Key": f"{prefix}wrong_path_somehow/filename2"}, # ignored, not enough key parts (this won't happen) + ] + } + mocker.patch( + "services.apply.aws._S3_CLIENT.list_objects_v2", + return_value=objects_response, + ) + + result = list_files_by_prefix(prefix) + assert len(result) == 2 + + file1, file2 = result + assert file1.application_id == file2.application_id == "application_id" + assert file1.component_id == file2.component_id == "component_id" + assert file1.form == file2.form == "form" + assert file1.path == file2.path == "path" + + assert file1.filename == "filename1" + assert file2.filename == "filename2" diff --git a/tests_apply/test_forms.py b/tests_apply/test_forms.py new file mode 100644 index 00000000..b68ac8ce --- /dev/null +++ b/tests_apply/test_forms.py @@ -0,0 +1,39 @@ +from _helpers.form import get_forms_from_sections + + +section_config = [ + { + "form_name": None, + "title": "section 1", + "children": [ + { + "children": [], + "form_name": "form-a", + }, + { + "children": [], + "form_name": "form-b", + }, + ], + }, + { + "form_name": None, + "title": "section 2", + "children": [ + { + "children": [], + "form_name": "form-c", + }, + { + "children": [], + "form_name": "form-d", + }, + ], + }, +] + + +def test_get_forms_from_sections(): + form_result = get_forms_from_sections(section_config) + assert len(form_result) == 4 + assert "form-d" in form_result diff --git a/tests_apply/test_notification.py b/tests_apply/test_notification.py new file mode 100644 index 00000000..79d36bb3 --- /dev/null +++ b/tests_apply/test_notification.py @@ -0,0 +1,75 @@ +import unittest +from unittest import mock +from unittest.mock import MagicMock + +import boto3 +import pytest +from config import Config +from fsd_utils import NotifyConstants +from fsd_utils.services.aws_extended_client import SQSExtendedClient +from moto import mock_aws +from services.apply.exceptions import NotificationError +from services.apply.models.notification import Notification + + +class NotificationTest(unittest.TestCase): + @mock_aws + @pytest.mark.usefixtures("live_server") + def test_notification_send_success(self): + with mock.patch("services.apply.models.notification.Notification._get_sqs_client") as mock_get_sqs_client: + template_type = Config.NOTIFY_TEMPLATE_SUBMIT_APPLICATION + to_email = "test@example.com" + full_name = "John" + contents = { + NotifyConstants.APPLICATION_FIELD: "Funding name", + NotifyConstants.MAGIC_LINK_CONTACT_HELP_EMAIL_FIELD: "test_gmail.com", + } + sqs_extended_client = SQSExtendedClient( + aws_access_key_id=Config.AWS_ACCESS_KEY_ID, + aws_secret_access_key=Config.AWS_SECRET_ACCESS_KEY, + region_name=Config.AWS_REGION, + large_payload_support=Config.AWS_MSG_BUCKET_NAME, + always_through_s3=True, + delete_payload_from_s3=True, + logger=MagicMock(), + ) + s3_connection = boto3.client( + "s3", region_name="us-east-1", aws_access_key_id="test_accesstoken", aws_secret_access_key="secret_key" + ) + sqs_connection = boto3.client( + "sqs", region_name="us-east-1", aws_access_key_id="test_accesstoken", aws_secret_access_key="secret_key" + ) + s3_connection.create_bucket(Bucket=Config.AWS_MSG_BUCKET_NAME) + queue_response = sqs_connection.create_queue(QueueName="notif-queue.fifo", Attributes={"FifoQueue": "true"}) + sqs_extended_client.sqs_client = sqs_connection + sqs_extended_client.s3_client = s3_connection + Config.AWS_SQS_NOTIF_APP_PRIMARY_QUEUE_URL = queue_response["QueueUrl"] + mock_get_sqs_client.return_value = sqs_extended_client + result = Notification.send(template_type, to_email, full_name, contents) + assert result is not None + + @mock_aws + @pytest.mark.usefixtures("live_server") + def test_notification_send_failure(self): + with mock.patch("services.apply.models.notification.Notification._get_sqs_client") as mock_get_sqs_client: + template_type = Config.NOTIFY_TEMPLATE_SUBMIT_APPLICATION + to_email = "test@example.com" + full_name = "John" + contents = { + NotifyConstants.APPLICATION_FIELD: "Funding name", + NotifyConstants.MAGIC_LINK_CONTACT_HELP_EMAIL_FIELD: "test_gmail.com", + } + sqs_extended_client = MagicMock() + mock_get_sqs_client.return_value = sqs_extended_client + sqs_extended_client.submit_single_message.side_effect = Exception("SQS Error") + with pytest.raises(NotificationError, match="Sorry, the notification could not be sent"): + Notification.send(template_type, to_email, full_name, contents) + + def test_notification_error_custom_message(self): + custom_message = "Custom error message" + error = NotificationError(custom_message) + assert str(error) == custom_message + + def test_notification_error_default_message(self): + error = NotificationError() + assert str(error) == "Sorry, there was a problem posting to the notification service" diff --git a/tests_apply/test_queries.py b/tests_apply/test_queries.py new file mode 100644 index 00000000..e8b7bea3 --- /dev/null +++ b/tests_apply/test_queries.py @@ -0,0 +1,511 @@ +import base64 +from unittest.mock import ANY +from uuid import uuid4 + +import pytest +from config.key_report_mappings.cof_eoi_key_report_mapping import COF_EOI_KEY_REPORT_MAPPING +from config.key_report_mappings.cof_key_report_mapping import COF_KEY_REPORT_MAPPING +from config.key_report_mappings.cof_r2_key_report_mapping import ( + COF_R2_KEY_REPORT_MAPPING, +) +from config.key_report_mappings.cof_r3w2_key_report_mapping import ( + COF_R3W2_KEY_REPORT_MAPPING, +) +from config.key_report_mappings.mappings import ROUND_ID_TO_KEY_REPORT_MAPPING +from config.key_report_mappings.model import extract_postcode +from config.key_report_mappings.model import KeyReportMapping +from db.models import Applications +from db.models import Forms +from db.queries.apply.application import create_application +from db.queries.apply.application import create_qa_base64file +from db.queries.apply.application import process_files +from db.queries.apply.reporting.queries import export_application_statuses_to_csv +from db.queries.apply.reporting.queries import map_application_key_fields +from services.apply.aws import FileData +from services.apply.models.fund import Fund +from tests_apply.seed_data.application_data import expected_application_json + + +@pytest.mark.parametrize( + "requested_language,fund_supports_welsh,exp_language", + [ + ("en", True, "en"), + ("en", False, "en"), + ("cy", True, "cy"), + ("cy", False, "en"), + ], +) +def test_create_application_language_choice(mocker, fund_supports_welsh, requested_language, exp_language): + mock_fund = Fund( + "Generated test fund no welsh", + str(uuid4()), + "TEST", + "Testing fund", + fund_supports_welsh, + {"en": "English Fund Name", "cy": "Welsh Fund Name"}, + [], + ) + mocker.patch("db.queries.apply.application.queries.get_fund", return_value=mock_fund) + mock_create_app_try = mocker.patch( + "db.queries.apply.application.queries._create_application_try", + return_value="new application", + ) + + create_application(account_id="test", fund_id="", round_id="", language=requested_language) + mock_create_app_try.assert_called_once_with( + account_id="test", + fund_id=ANY, + round_id=ANY, + key=ANY, + language=exp_language, + reference=ANY, + attempt=0, + ) + + +def test_application_map_contents_and_base64_convertor(mocker, app): + """ + GIVEN: our service running with app_context fixture. + WHEN: two separate methods on different classes chained together with given + expected incoming JSON. + THEN: we check if expected output is returned. + """ + with app.app_context(): + expected_json = expected_application_json + mock_fund = Fund( + "Community Ownership Fund", + str(uuid4()), + "TEST", + "Testing fund", + False, + {"en": "English Fund Name"}, + [], + ) + mocker.patch("db.queries.apply.application.queries.get_fund", return_value=mock_fund) + expected_json = create_qa_base64file(expected_json["content"]["application"], True) + + assert "Jack-Simon" in base64.b64decode(expected_json["questions_file"]).decode() + assert "Yes" in base64.b64decode(expected_json["questions_file"]).decode() + assert "No" in base64.b64decode(expected_json["questions_file"]).decode() + + +@pytest.mark.parametrize( + "application, all_application_files, expected", + [ + pytest.param( + Applications(forms=[Forms(json=[{"fields": [{"key": "not_a_file_component", "answer": None}]}])]), + [FileData("app1", "form1", "path1", "component1", "file1.docx")], + Applications(forms=[Forms(json=[{"fields": [{"key": "not_a_file_component", "answer": None}]}])]), + id="Irrelevant components are ignored", + ), + pytest.param( + Applications( + forms=[ + Forms(json=[{"fields": [{"key": "component1", "answer": None}]}]), + Forms(json=[{"fields": [{"key": "component2", "answer": None}]}]), + ] + ), + [ + FileData("app1", "form1", "path1", "component1", "file1.docx"), + FileData("app1", "form1", "path1", "component2", "file2.docx"), + ], + Applications( + forms=[ + Forms(json=[{"fields": [{"key": "component1", "answer": "file1.docx"}]}]), + Forms(json=[{"fields": [{"key": "component2", "answer": "file2.docx"}]}]), + ] + ), + id="Multiple forms all work as expected", + ), + pytest.param( + Applications(forms=[Forms(json=[{"fields": [{"key": "component1", "answer": None}]}])]), + [FileData("app1", "form1", "path1", "component1", "file1.docx")], + Applications(forms=[Forms(json=[{"fields": [{"key": "component1", "answer": "file1.docx"}]}])]), + id="Single file available for a component", + ), + pytest.param( + Applications(forms=[Forms(json=[{"fields": [{"key": "component1", "answer": None}]}])]), + [ + FileData("app1", "form1", "path1", "component1", "file1.docx"), + FileData("app1", "form1", "path2", "component1", "file2.pdf"), + FileData("app1", "form1", "path3", "component1", "file3.txt"), + ], + Applications( + forms=[ + Forms( + json=[ + { + "fields": [ + { + "key": "component1", + "answer": "file1.docx, file2.pdf, file3.txt", + } + ] + } + ] + ) + ] + ), + id="Multiple files available for a component", + ), + pytest.param( + Applications( + forms=[ + Forms( + json=[ + { + "fields": [ + {"key": "component1", "answer": None}, + {"key": "component2", "answer": None}, + ] + } + ] + ) + ] + ), + [ + FileData("app1", "form1", "path1", "component1", "file1.docx"), + FileData("app1", "form1", "path2", "component1", "file2.pdf"), + FileData("app1", "form1", "path3", "component2", "file3.txt"), + ], + Applications( + forms=[ + Forms( + json=[ + { + "fields": [ + { + "key": "component1", + "answer": "file1.docx, file2.pdf", + }, + {"key": "component2", "answer": "file3.txt"}, + ] + } + ] + ) + ] + ), + id="Files available for multiple components", + ), + ], +) +def test_process_files(application, all_application_files, expected): + """GIVEN an application object and a list of all files belonging to that + application WHEN the process_files function is invoked with these parameters + THEN the application object is expected to be updated with the relevant file + information.""" + result = process_files(application, all_application_files) + for form, expected_form in zip(result.forms, expected.forms): + assert form.json == pytest.approx(expected_form.json) + + +@pytest.mark.parametrize( + "data,lines_exp", + [ + ( + [ + { + "fund_id": "111", + "rounds": [ + { + "round_id": "r1r1r1", + "application_statuses": { + "NOT_STARTED": 1, + "IN_PROGRESS": 2, + "COMPLETED": 3, + "SUBMITTED": 4, + }, + } + ], + } + ], + ["111,r1r1r1,1,2,3,4"], + ), + ( + [ + { + "fund_id": "111", + "rounds": [ + { + "round_id": "r1r1r1", + "application_statuses": { + "NOT_STARTED": 1, + "IN_PROGRESS": 2, + "COMPLETED": 3, + "SUBMITTED": 4, + }, + }, + { + "round_id": "r2", + "application_statuses": { + "NOT_STARTED": 2, + "IN_PROGRESS": 3, + "COMPLETED": 4, + "SUBMITTED": 5, + }, + }, + ], + } + ], + ["111,r1r1r1,1,2,3,4", "111,r2,2,3,4,5"], + ), + ( + [ + { + "fund_id": "f1", + "rounds": [ + { + "round_id": "r1", + "application_statuses": { + "NOT_STARTED": 1, + "IN_PROGRESS": 2, + "COMPLETED": 3, + "SUBMITTED": 4, + }, + }, + { + "round_id": "r2", + "application_statuses": { + "NOT_STARTED": 0, + "IN_PROGRESS": 0, + "COMPLETED": 0, + "SUBMITTED": 4, + }, + }, + ], + }, + { + "fund_id": "f2", + "rounds": [ + { + "round_id": "r1", + "application_statuses": { + "NOT_STARTED": 2, + "IN_PROGRESS": 2, + "COMPLETED": 1, + "SUBMITTED": 6, + }, + }, + ], + }, + ], + ["f1,r1,1,2,3,4", "f1,r2,0,0,0,4", "f2,r1,2,2,1,6"], + ), + ], +) +def test_application_status_csv(data, lines_exp): + result = export_application_statuses_to_csv(data) + assert result + lines = result.readlines() + assert lines[0].decode().strip() == "fund_id,round_id,NOT_STARTED,IN_PROGRESS,COMPLETED,SUBMITTED" + idx = 1 + for line in lines_exp: + assert lines[idx].decode().strip() == line + idx += 1 + + +@pytest.mark.parametrize( + "input_str, expected_output", + [ + # Valid postcodes + ("SW1A 1AA", "SW1A 1AA"), + ("BD23 1DN", "BD23 1DN"), + ("W1A 0AX", "W1A 0AX"), + ("GIR 0AA", "GIR 0AA"), # special case for GIR 0AA + # Invalid postcodes + ("123456", None), + ("ABCDEFG", None), + ("XYZ 123", None), + # Mixed strings + ("My postcode is SW1A 1AA in London.", "SW1A 1AA"), + ("The code is BD23 1DN for that location.", "BD23 1DN"), + ("No postcode here.", None), + ], +) +def test_extract_postcode(input_str, expected_output): + assert extract_postcode(input_str) == expected_output + + +@pytest.mark.parametrize( + "key_report_mapping, application, expected_output", + [ + ( + COF_R2_KEY_REPORT_MAPPING, + { + "language": "en", + "forms": [ + { + "name": "organisation-information", + "questions": [ + { + "fields": [ + {"key": "WWWWxy", "answer": "Ref1234"}, + {"key": "YdtlQZ", "answer": "OrgName"}, + {"key": "lajFtB", "answer": "Non-Profit"}, + ] + } + ], + }, + { + "name": "asset-information", + "questions": [ + { + "fields": [ + {"key": "yaQoxU", "answer": "Building"}, + ] + } + ], + }, + { + "name": "project-information", + "questions": [ + { + "fields": [ + {"key": "yEmHpp", "answer": "GIR 0AA"}, + ] + } + ], + }, + { + "name": "funding-required", + "questions": [ + { + "fields": [ + {"key": "JzWvhj", "answer": 50000}, + {"key": "jLIgoi", "answer": 10000}, + ] + } + ], + }, + { + "name": "organisation-information-ns", + "questions": [ + { + "fields": [ + {"key": "opFJRm", "answer": "OrgName NSTF"}, + ] + } + ], + }, + ], + }, + { + "eoi_reference": "Ref1234", + "organisation_name": "OrgName", + "organisation_type": "Non-Profit", + "asset_type": "Building", + "geography": "GIR 0AA", + "capital": 50000, + "revenue": 10000, + "organisation_name_nstf": "OrgName NSTF", + }, + ), + ( + COF_R3W2_KEY_REPORT_MAPPING, + { + "language": "en", + "forms": [ + { + "name": "applicant-information-cof-r3-w2", + "questions": [ + { + "fields": [ + {"key": "NlHSBg", "answer": "test@test.com"}, + ] + } + ], + }, + { + "name": "organisation-information-cof-r3-w2", + "questions": [ + { + "fields": [ + {"key": "WWWWxy", "answer": "Ref1234"}, + {"key": "YdtlQZ", "answer": "OrgName"}, + {"key": "lajFtB", "answer": "Non-Profit"}, + ] + } + ], + }, + { + "name": "asset-information-cof-r3-w2", + "questions": [ + { + "fields": [ + {"key": "oXGwlA", "answer": "Building"}, + {"key": "aJGyCR", "answer": "Other"}, + ] + } + ], + }, + { + "name": "project-information-cof-r3-w2", + "questions": [ + { + "fields": [ + {"key": "EfdliG", "answer": "GIR 0AA"}, + {"key": "apGjFS", "answer": "A name"}, + ] + } + ], + }, + { + "name": "funding-required-cof-r3-w2", + "questions": [ + { + "fields": [ + {"key": "ABROnB", "answer": 50000}, + { + "key": "tSKhQQ", + "answer": [ + {"UyaAHw": 5000}, + {"UyaAHw": 5000}, + ], + }, + ] + } + ], + }, + ], + "asset_type_other": "Other", + "reference": "ref123", + "id": "id123", + }, + { + "eoi_reference": "Ref1234", + "applicant_email": "test@test.com", + "organisation_name": "OrgName", + "organisation_type": "Non-Profit", + "asset_type": "Building", + "asset_type_other": "Other", + "geography": "GIR 0AA", + "capital": 50000, + "project_name": "A name", + "ref": "ref123", + "link": "id123", + "revenue": 10000, + }, + ), + ], +) +def test_map_application_key_fields(key_report_mapping: KeyReportMapping, application, expected_output): + result = map_application_key_fields(application, key_report_mapping.mapping, key_report_mapping.round_id) + assert result == expected_output + + +@pytest.mark.parametrize( + "round_id, exp_mapping", + [ + ("c603d114-5364-4474-a0c4-c41cbf4d3bbd", COF_R2_KEY_REPORT_MAPPING.mapping), # COF R2W2 + ("5cf439bf-ef6f-431e-92c5-a1d90a4dd32f", COF_R2_KEY_REPORT_MAPPING.mapping), # COF R2W3 + ("e85ad42f-73f5-4e1b-a1eb-6bc5d7f3d762", COF_R2_KEY_REPORT_MAPPING.mapping), # COF R3W1 + ("6af19a5e-9cae-4f00-9194-cf10d2d7c8a7", COF_R3W2_KEY_REPORT_MAPPING.mapping), # COF R3W2 + ("4efc3263-aefe-4071-b5f4-0910abec12d2", COF_KEY_REPORT_MAPPING.mapping), # COF R3W3 + ("33726b63-efce-4749-b149-20351346c76e", COF_KEY_REPORT_MAPPING.mapping), # COF R4W1 + ("6a47c649-7bac-4583-baed-9c4e7a35c8b3", COF_EOI_KEY_REPORT_MAPPING.mapping), # COF EOI + ("asdf-wer-234-sdf-234", COF_R2_KEY_REPORT_MAPPING.mapping), # any ID + ], +) +def test_map_round_id_to_report_fields(round_id, exp_mapping): + result = ROUND_ID_TO_KEY_REPORT_MAPPING[round_id] + assert result == exp_mapping diff --git a/tests_apply/test_reports.py b/tests_apply/test_reports.py new file mode 100644 index 00000000..fe484fc1 --- /dev/null +++ b/tests_apply/test_reports.py @@ -0,0 +1,260 @@ +import pytest +from db.models import Applications +from db.models.application.enums import Status +from tests_apply.helpers import get_row_by_pk +from tests_apply.helpers import test_application_data + + +@pytest.mark.apps_to_insert(test_application_data) +def test_get_application_statuses_csv(flask_test_client, seed_application_records, _db): + response = flask_test_client.get( + "/apply/applications/reporting/applications_statuses_data", + follow_redirects=True, + ) + + lines = response.content.decode("utf-8").split("\r\n") + assert lines[0] == "fund_id,round_id,NOT_STARTED,IN_PROGRESS,COMPLETED,SUBMITTED" + assert f"{test_application_data[0]['fund_id']},{test_application_data[0]['round_id']},1,0,0,0" in lines + assert f"{test_application_data[1]['fund_id']},{test_application_data[1]['round_id']},1,0,0,0" in lines + assert f"{test_application_data[2]['fund_id']},{test_application_data[2]['round_id']},1,0,0,0" in lines + + app = get_row_by_pk(Applications, seed_application_records[0].id) + app.status = "IN_PROGRESS" + _db.session.add(app) + _db.session.commit() + + response = flask_test_client.get( + "/apply/applications/reporting/applications_statuses_data", + follow_redirects=True, + ) + + lines = response.content.decode("utf-8").split("\r\n") + assert lines[0] == "fund_id,round_id,NOT_STARTED,IN_PROGRESS,COMPLETED,SUBMITTED" + assert f"{test_application_data[0]['fund_id']},{test_application_data[0]['round_id']},0,1,0,0" in lines + assert f"{test_application_data[1]['fund_id']},{test_application_data[1]['round_id']},1,0,0,0" in lines + assert f"{test_application_data[2]['fund_id']},{test_application_data[2]['round_id']},1,0,0,0" in lines + + +user_lang = { + "account_id": "usera", + "language": "en", +} + +user_lang_cy = { + "account_id": "userw", + "language": "cy", +} + + +@pytest.mark.fund_round_config( + { + "funds": [ + { + "rounds": [ + { + "applications": [ + {**user_lang_cy}, + {**user_lang_cy}, + {**user_lang}, + ] + }, + { + "applications": [ + {**user_lang}, + ] + }, + {"applications": []}, + ] + }, + { + "rounds": [ + { + "applications": [ + {**user_lang_cy}, + {**user_lang_cy}, + ] + } + ] + }, + ] + } +) +@pytest.mark.parametrize( + "fund_idx, round_idx, exp_not_started, exp_in_progress, exp_submitted, exp_completed", + [ + ([0, 1], [], 0, 5, 0, 1), + ([0], [], 0, 3, 0, 1), + ([1], [], 0, 2, 0, 0), + ([0], [1, 2], 0, 1, 0, 0), + ([0], [2], 0, 0, 0, 0), + ([], [0], 0, 2, 0, 1), + ], +) +def test_get_application_statuses_json_multi_fund( + fund_idx, + round_idx, + exp_not_started, + exp_in_progress, + exp_submitted, + exp_completed, + flask_test_client, + seed_data_multiple_funds_rounds, + _db, + mock_get_round, +): + app = get_row_by_pk(Applications, seed_data_multiple_funds_rounds[0][1][0][1][0]) + app.status = "COMPLETED" + _db.session.add(app) + _db.session.commit() + fund_ids = [seed_data_multiple_funds_rounds[idx][0] for idx in fund_idx] + fund_params = ["fund_id=" + str(id) for id in fund_ids] + round_ids = [seed_data_multiple_funds_rounds[0][1][idx][0] for idx in round_idx] + round_params = ["round_id=" + str(id) for id in round_ids] + url = ( + "/apply/applications/reporting/applications_statuses_data?" + + f"format=json&{'&'.join(fund_params)}&{'&'.join(round_params)}" + ) + response = flask_test_client.get(url, follow_redirects=True) + assert response.status_code == 200 + result = response.json() + assert result + funds = result["metrics"] + for fund_id in fund_ids: + assert len([fund["fund_id"] for fund in funds if fund["fund_id"] == fund_id]) == 1 + total_ip = 0 + total_ns = 0 + total_c = 0 + total_s = 0 + for f in funds: + total_ns += sum([r["application_statuses"]["NOT_STARTED"] for r in f["rounds"]]) + total_ip += sum([r["application_statuses"]["IN_PROGRESS"] for r in f["rounds"]]) + total_c += sum([r["application_statuses"]["COMPLETED"] for r in f["rounds"]]) + total_s += sum([r["application_statuses"]["SUBMITTED"] for r in f["rounds"]]) + assert total_ns == exp_not_started + assert total_ip == exp_in_progress + assert total_c == exp_completed + assert total_s == exp_submitted + + +@pytest.mark.parametrize( + "language,expected_org_name,expected_address,ref_number", + [ + ("en", "Test Org Name 1", "W1A 1AA", "Test Reference Number"), + ("cy", "Test Org Name 2cy", "CF10 3NQ", "Test Reference Number Welsh"), + ], +) +@pytest.mark.fund_round_config( + { + "funds": [ + {"rounds": [{"applications": [{**user_lang}, {**user_lang_cy}]}]}, + ] + } +) +def test_get_applications_report_by_application_id( + flask_test_client, + language, + expected_org_name, + expected_address, + ref_number, + seed_data_multiple_funds_rounds, +): + application_id = ( + seed_data_multiple_funds_rounds[0].round_ids[0].application_ids[0] + if language == "en" + else seed_data_multiple_funds_rounds[0].round_ids[0].application_ids[1] + ) + application = get_row_by_pk( + Applications, + application_id, + ) + application.status = Status.IN_PROGRESS + url = "/apply/applications/reporting/key_application_metrics" + f"/{str(application.id)}" + response = flask_test_client.get( + url, + follow_redirects=True, + ) + assert 200 == response.status_code + lines = response.content.splitlines() + assert 2 == len(lines) + assert ( + "eoi_reference,organisation_name,organisation_type,asset_type," + + "geography,capital,revenue,organisation_name_nstf" + ) == lines[0].decode("utf-8") + fields = lines[1].decode("utf-8").split(",") + assert expected_org_name == fields[1] + assert ref_number == fields[0] + assert expected_address == fields[4] + + +@pytest.mark.fund_round_config( + { + "funds": [ + {"rounds": [{"applications": [{**user_lang}, {**user_lang_cy}]}]}, + ] + } +) +def test_get_applications_report_by_round_is_and_fund_id( + flask_test_client, + seed_data_multiple_funds_rounds, +): + url = "/apply/applications/reporting/key_application_metrics" + ( + f"?fund_id={seed_data_multiple_funds_rounds[0].fund_id}" + + "&status=IN_PROGRESS" + + "&round_id=" + + f"{seed_data_multiple_funds_rounds[0].round_ids[0].round_id}" + ) + response = flask_test_client.get( + url, + follow_redirects=True, + ) + assert 200 == response.status_code + lines = response.content.splitlines() + assert 3 == len(lines) + assert ( + "eoi_reference,organisation_name,organisation_type,asset_type," + + "geography,capital,revenue,organisation_name_nstf" + ) == lines[0].decode("utf-8") + row1 = lines[1].decode("utf-8").split(",") + assert row1[1] == "Test Org Name 1" + assert row1[0] == "Test Reference Number" + assert row1[4] == "W1A 1AA" + row2 = lines[2].decode("utf-8").split(",") + assert row2[1] == "Test Org Name 2cy" + assert row2[0] == "Test Reference Number Welsh" + assert row2[4] == "CF10 3NQ" + + +@pytest.mark.fund_round_config( + { + "funds": [ + {"rounds": [{"applications": [{**user_lang}, {**user_lang_cy}]}]}, + ] + } +) +def test_get_applications_report_query_param(flask_test_client, seed_data_multiple_funds_rounds, mock_get_round): + response = flask_test_client.get( + "/apply/applications/reporting/key_application_metrics?status=IN_PROGRESS&" + + f"fund_id={seed_data_multiple_funds_rounds[0].fund_id}&round_id=" + + f"{seed_data_multiple_funds_rounds[0].round_ids[0].round_id}", + follow_redirects=True, + ) + + raw_lines = response.content.splitlines() + assert len(raw_lines) == 3 + + lines = [line.decode("utf-8") for line in response.content.splitlines()] + assert ( + lines[0] == "eoi_reference,organisation_name,organisation_type,asset_type," + "geography,capital,revenue,organisation_name_nstf" + ) + + for line in lines[1:]: + field1, field2, _, _, field5, _, _, _ = line.split(",") + if field1 == "Test Reference Number": + assert field2.startswith("Test Org Name ") + assert field5 == "W1A 1AA" + elif field1 == "Test Reference Number Welsh": + assert field2.startswith("Test Org Name 2cy") + assert field5 == "CF10 3NQ" + else: + assert 1 == 0, "Unexpected value for first column" diff --git a/tests_apply/test_routes.py b/tests_apply/test_routes.py new file mode 100644 index 00000000..40d331da --- /dev/null +++ b/tests_apply/test_routes.py @@ -0,0 +1,805 @@ +import json +from datetime import datetime +from datetime import timedelta +from unittest import mock +from unittest.mock import ANY +from unittest.mock import MagicMock +from uuid import uuid4 + +import boto3 +import pytest +from config import Config +from db import db +from db.models import Applications +from db.models import ResearchSurvey +from db.queries.apply.application import get_all_applications +from db.schemas import ApplicationSchema +from fsd_utils.services.aws_extended_client import SQSExtendedClient +from moto import mock_aws +from services.apply.models.fund import Fund +from services.apply.models.round import Round +from tests_apply.helpers import application_expected_data +from tests_apply.helpers import count_fund_applications +from tests_apply.helpers import expected_data_within_response +from tests_apply.helpers import get_row_by_pk +from tests_apply.helpers import key_list_to_regex +from tests_apply.helpers import post_data +from tests_apply.helpers import test_application_data +from tests_apply.helpers import test_question_data + + +@pytest.mark.unique_fund_round(True) +def test_create_application_is_successful(flask_test_client, unique_fund_round, mock_get_application_display_config): + """GIVEN We have a functioning Application Store API WHEN we try to create an + application THEN applications are created with the correct parameters.""" + + application_data_a1 = { + "account_id": "usera", + "fund_id": unique_fund_round[0], + "round_id": unique_fund_round[0], + "language": "en", + } + application_data_a2 = { + "account_id": "usera", + "fund_id": unique_fund_round[0], + "round_id": unique_fund_round[0], + "language": "cy", + } + response = post_data(flask_test_client, "/apply/applications", application_data_a1) + assert response.status_code == 201 + assert response.json()["language"] == "en" + count_fund_applications(flask_test_client, unique_fund_round[0], 1) + response = post_data(flask_test_client, "/apply/applications", application_data_a2) + assert response.json()["language"] == "cy" + count_fund_applications(flask_test_client, unique_fund_round[0], 2) + + +@pytest.mark.parametrize( + "requested_language,fund_supports_welsh,exp_language", + [ + ("en", True, "en"), + ("en", False, "en"), + ("cy", True, "cy"), + ("cy", False, "en"), + ], +) +def test_create_application_language_choice( + mocker, flask_test_client, fund_supports_welsh, requested_language, exp_language +): + mock_fund = Fund( + "Generated test fund no welsh", + str(uuid4()), + "TEST", + "Testing fund", + fund_supports_welsh, + {"en": "English Fund Name", "cy": "Welsh Fund Name"}, + [], + ) + mocker.patch("apply.api.routes.application.routes.get_fund", return_value=mock_fund) + blank_forms_mock = mocker.patch( + "apply.api.routes.application.routes.get_blank_forms", + return_value=MagicMock(), + ) + test_application = Applications(**application_expected_data[2]) + create_application_mock = mocker.patch( + "apply.api.routes.application.routes.create_application", + return_value=test_application, + ) + mocker.patch("apply.api.routes.application.routes.add_new_forms") + + # Post one application and check it's created in the expected language + application_data_a1 = { + "account_id": "usera", + "fund_id": "", + "round_id": "", + "language": requested_language, + } + response = post_data(flask_test_client, "/apply/applications", application_data_a1) + assert response.status_code == 201 + + blank_forms_mock.assert_called_once_with( + fund_id=ANY, + round_id=ANY, + language=exp_language, + ) + create_application_mock.assert_called_once_with( + account_id="usera", + fund_id=ANY, + round_id=ANY, + language=exp_language, + ) + + +def test_create_application_creates_formatted_reference( + flask_test_client, clear_test_data, mock_get_application_display_config +): + """GIVEN We have a functioning Application Store API WHEN we try to create an + application THEN a correctly formatted reference is created for the + application.""" + create_application_json = { + "account_id": "usera", + "fund_id": "47aef2f5-3fcb-4d45-acb5-f0152b5f03c4", + "round_id": "c603d114-5364-4474-a0c4-c41cbf4d3bbd", + "language": "en", + } + response = flask_test_client.post( + "/apply/applications", + data=json.dumps(create_application_json), + headers={"Content-Type": "application/json"}, + follow_redirects=True, + ) + application = response.json() + assert application["reference"].startswith("TEST-TEST") + assert application["reference"][-6:].isupper() + assert application["reference"][-6:].isalpha() + + +def test_create_application_creates_unique_reference( + flask_test_client, + mock_random_choices, + clear_test_data, + mock_get_application_display_config, +): + """GIVEN We have a functioning Application Store API WHEN we try to create an + application THEN a unique application_reference is created for the + application.""" + create_application_json = { + "account_id": "usera", + "fund_id": "47aef2f5-3fcb-4d45-acb5-f0152b5f03c4", + "round_id": "c603d114-5364-4474-a0c4-c41cbf4d3bbd", + "language": "en", + } + response = flask_test_client.post( + "/apply/applications", + data=json.dumps(create_application_json), + headers={"Content-Type": "application/json"}, + follow_redirects=True, + ) + assert response.status_code == 201, f"First creation failed with status {response.status_code}: {response.content}" + application = response.json() + assert application["reference"] == "TEST-TEST-ABCDEF", f"Unexpected reference: {application['reference']}" + + # Second creation should fail + response = flask_test_client.post( + "/apply/applications", + data=json.dumps(create_application_json), + headers={"Content-Type": "application/json"}, + follow_redirects=True, + ) + + assert response.status_code == 500, f"Expected status 500, but got {response.status_code}" + error_data = response.json() + + assert "detail" in error_data, f"Expected 'detail' in error response, got: {error_data}" + assert ( + "Max (10) tries exceeded for create application" in error_data["detail"] + ), f"Unexpected error message: {error_data['detail']}" + + +@pytest.mark.apps_to_insert(test_application_data) +def test_get_all_applications(flask_test_client, app): + """GIVEN We have a functioning Application Store API WHEN a request for + applications with no set params THEN the response should return all + applications.""" + with app.app_context(): + serialiser = ApplicationSchema(exclude=["forms"]) + expected_data = [serialiser.dump(row) for row in get_all_applications()] + expected_data_within_response( + flask_test_client, + "/apply/applications", + expected_data, + exclude_regex_paths=key_list_to_regex(["round_name", "date_submitted", "last_edited"]), + ) + + +@pytest.mark.apps_to_insert([{"account_id": "unique_user", "language": "en"}]) +@pytest.mark.unique_fund_round(True) +def test_get_applications_of_account_id(flask_test_client, seed_application_records, unique_fund_round, app): + """GIVEN We have a functioning Application Store API WHEN a request for + applications of account_id THEN the response should return applications of the + account_id.""" + with app.app_context(): + expected_data_within_response( + flask_test_client, + "/apply/applications?account_id=unique_user", + [seed_application_records[0].as_dict()], + exclude_regex_paths=key_list_to_regex( + [ + "started_at", + "last_edited", + "date_submitted", + "round_name", + "forms", + ] + ), + ) + + +@pytest.mark.apps_to_insert([test_application_data[0]]) +def test_update_section_of_application(flask_test_client, seed_application_records): + """GIVEN We have a functioning Application Store API WHEN A put is made with a + completed section THEN The section json should be updated to match the PUT'ed + json and be marked as in-progress.""" + section_put = { + "questions": test_question_data, + "metadata": { + "application_id": str(seed_application_records[0].id), + "form_name": "declarations", + "is_summary_page_submit": False, + }, + } + response = flask_test_client.put( + "/apply/applications/forms", + json=section_put, + follow_redirects=True, + ) + assert 201 == response.status_code + answer_found_list = [field["answer"] not in [None, ""] for field in response.json()["questions"][0]["fields"]] + section_status = response.json()["status"] + assert all(answer_found_list) + assert section_status == "IN_PROGRESS" + + +@pytest.mark.apps_to_insert([test_application_data[0]]) +def test_update_section_of_application_with_optional_field(flask_test_client, seed_application_records): + """GIVEN We have a functioning Application Store API WHEN A put is made with a + completed section THEN The section json should be updated to match the PUT'ed + json and be marked as in-progress.""" + section_put = { + "questions": [ + { + "question": "Management case", + "fields": [ + { + "key": "application-name", + "title": "Applicant name", + "type": "text", + "answer": "Coolio", + }, + { + "key": "applicant-email", + "title": "Email", + "type": "text", + }, + ], + } + ], + "metadata": { + "application_id": str(seed_application_records[0].id), + "form_name": "declarations", + "is_summary_page_submit": False, + }, + } + response = flask_test_client.put( + "/apply/applications/forms", + json=section_put, + follow_redirects=True, + ) + assert 201 == response.status_code + section_status = response.json()["status"] + assert section_status == "IN_PROGRESS" + + +@pytest.mark.apps_to_insert([test_application_data[0]]) +def test_update_section_of_application_with_incomplete_answers(flask_test_client, seed_application_records): + """GIVEN We have a functioning Application Store API WHEN A put is made with a + completed section THEN The section json should be updated to match the PUT'ed + json and be marked as complete.""" + section_put = { + "questions": test_question_data, + "metadata": { + "application_id": str(seed_application_records[0].id), + "form_name": "declarations", + }, + } + # Update an optional field to have no answer + section_put["questions"][0]["fields"][2]["answer"] = "" + expected_data = section_put.copy() + # The whole section has been COMPLETED here so it will have a status of + # COMPLETE not IN_PROGRESS + expected_data.update({"status": "IN_PROGRESS"}) + # exclude_question_keys = ["category", "index", "id"] + response = flask_test_client.put( + "/apply/applications/forms", + json=section_put, + follow_redirects=True, + ) + assert 201 == response.status_code + section_status = response.json()["status"] + assert section_status == "IN_PROGRESS" + + +@pytest.mark.apps_to_insert([test_application_data[0]]) +def test_get_application_by_application_id(flask_test_client, seed_application_records): + """GIVEN We have a functioning Application Store API WHEN a GET + /applications/