diff --git a/.gitignore b/.gitignore index b13fa4173..725b32d7d 100644 --- a/.gitignore +++ b/.gitignore @@ -87,6 +87,7 @@ coverage.xml *.py,cover .hypothesis/ .pytest_cache/ +.ruff_cache cover/ # Translations diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..4182b8ef1 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Docker runner Pre-award Stores", + "type": "debugpy", + "request": "attach", + "connect": { + "host": "localhost", + "port": 5692 + }, + "pathMappings": [ + { + "localRoot": "${workspaceFolder:funding-service-pre-award-stores}", + "remoteRoot": "." + } + ], + "justMyCode": true + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..6fbbf1ba8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, +} diff --git a/fund_store/.devcontainer/devcontainer.json b/fund_store/.devcontainer/devcontainer.json new file mode 100644 index 000000000..7d21227fd --- /dev/null +++ b/fund_store/.devcontainer/devcontainer.json @@ -0,0 +1,19 @@ +{ + "dockerComposeFile": [ + "../docker-compose.yml" + ], + "service": "fund-store", + "workspaceFolder": "/fund-store", + "shutdownAction": "none", + "customizations": { + "vscode": { + "extensions": [ + "ms-python.debugpy", + "ms-python.vscode-pylance", + "eamodio.gitlens", + "ms-python.flake8", + "ms-python.black-formatter" + ] + } + } +} diff --git a/fund_store/Dockerfile b/fund_store/Dockerfile new file mode 100644 index 000000000..176ca4b21 --- /dev/null +++ b/fund_store/Dockerfile @@ -0,0 +1,23 @@ +FROM python:3.10-bullseye + +WORKDIR /app + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +# Install the project's dependencies using the lockfile and settings +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --frozen --no-install-project + +# Then, add the rest of the project source code and install it +# Installing separately from its dependencies allows optimal layer caching +COPY . . +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --frozen + +# Place executables in the environment at the front of the path +ENV PATH="/app/.venv/bin:$PATH" +EXPOSE 8080 + +CMD ["gunicorn", "--worker-class", "uvicorn.workers.UvicornWorker", "wsgi:app", "-b", "0.0.0.0:8080"] diff --git a/fund_store/README.md b/fund_store/README.md new file mode 100644 index 000000000..13bc9a004 --- /dev/null +++ b/fund_store/README.md @@ -0,0 +1,109 @@ +# funding-service-design-fund-store + +[![made-with-python](https://img.shields.io/badge/Made%20with-Python-1f425f.svg)](https://www.python.org/) +[![Code style : black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) + +This is the fund store for funding service design Access Funding. This service provides an API and associated model implementation for fund and round configuration data. + +[Developer setup guide](https://github.com/communitiesuk/funding-service-design-workflows/blob/main/readmes/python-repos-setup.md) + +This service depends on: +- A postgres database +- No other microservices + +# Data +## Local DB Setup +General instructions for local db development are available here: [Local database development](https://github.com/communitiesuk/funding-service-design-workflows/blob/main/readmes/python-repos-db-development.md) + +## DB Helper Scripts +This repository uses `invoke` to provide scripts for dropping and recreating the local database in [tasks.py](./tasks.py) + +### Running Locally + +To run locally, make sure `psql` client is installed on your machine(https://www.postgresql.org/download/) and set the environment variable `DATABASE_URL`, +```bash +# pragma: allowlist nextline secret +export DATABASE_URL=postgresql://postgres:password@127.0.0.1:5432/fund_store +``` + +### Running in-container +To run the tasks inside the docker container used by docker compose, first bash into the container: +```bash +docker exec -it $(docker ps -qf "name=fund-store") bash +``` +Then execute the required tasks using `inv` as below. + +Or to combine the two into one command: +```bash + docker exec -it $(docker ps -qf "name=fund-store") inv truncate-data +``` + +### Available scripts +The following commands are the same locally or in container + +### Recreate DB instance + + inv recreate-local-db + +this drops (if it exists) and recreates the DB + +### Truncate data + + inv truncate-data +) + +## Seeding Fund Data +To seed fund & round data to db for all funds and rounds, use the fund/round loaders scripts. + +If running against a local postgresql instance: +```bash + python -m scripts.load_all_fund_rounds +``` + +If running with the docker compose setup: + +```bash + docker exec -ti $(docker ps -qf "name=fund-store") python -m scripts.load_all_fund_rounds +``` + +Further details on the fund/round loader scripts, and how to load data for a specific fund or round can be found [here](https://dluhcdigital.atlassian.net/wiki/spaces/FS/pages/40337455/Adding+or+updating+fund+and+round+data) + +## Amending round dates +This script allows you to open/close rounds using their dates to test different functionality as needed. You can also use the keywords 'PAST', 'FUTURE' and 'UNCHANGED' to save typing dates. + +```bash +docker exec -ti $(docker ps -qf "name=fund-store") python -m scripts.amend_round_dates -q update-round-dates --round_id c603d114-5364-4474-a0c4-c41cbf4d3bbd --application_deadline "2023-03-30 12:00:00" + +docker exec -ti $(docker ps -qf "name=fund-store") python -m scripts.amend_round_dates -q update-round-dates -r COF_R3W3 -o "2022-10-04 12:00:00" -d "2022-12-14 11:59:00" -ad "2023-03-30 12:00:00" -as NONE + +docker exec -ti $(docker ps -qf "name=fund-store") python -m scripts.amend_round_dates -q update-round-dates -r COF_R3W3 -o PAST -d FUTURE +``` +For an interactive prompt where you can supply (or leave unchanged) all dates: +```bash +docker exec -ti $(docker ps -qf "name=fund-store") python -m scripts.amend_round_dates update-round-dates +``` +To reset the dates for a round to those in the fund loader config: +```bash +docker exec -ti $(docker ps -qf "name=fund-store") python -m scripts.amend_round_dates -q reset-round-dates -r COF_R4W1 +``` +And with an interactive prompt: +```bash +docker exec -ti $(docker ps -qf "name=fund-store") python -m scripts.amend_round_dates reset-round-dates +``` + +# Testing +[Testing in Python repos](https://github.com/communitiesuk/funding-service-design-workflows/blob/main/readmes/python-repos-db-development.md) + + +# IDE Setup +[Python IDE Setup](https://github.com/communitiesuk/funding-service-design-workflows/blob/main/readmes/python-repos-ide-setup.md) + + +# Builds and Deploys +Details on how our pipelines work and the release process is available [here](https://dluhcdigital.atlassian.net/wiki/spaces/FS/pages/73695505/How+do+we+deploy+our+code+to+prod) +## Paketo +Paketo is used to build the docker image which gets deployed to our test and production environments. Details available [here](https://github.com/communitiesuk/funding-service-design-workflows/blob/main/readmes/python-repos-paketo.md) +## Copilot +Copilot is used for infrastructure deployment. Instructions are available [here](https://github.com/communitiesuk/funding-service-design-workflows/blob/main/readmes/python-repos-copilot.md), with the following values for the fund store: +- service-name: fsd-fund-store +- image-name: funding-service-design-fund-store diff --git a/fund_store/api/routes.py b/fund_store/api/routes.py new file mode 100644 index 000000000..3fb2713cf --- /dev/null +++ b/fund_store/api/routes.py @@ -0,0 +1,403 @@ +import uuid +from datetime import datetime +from distutils.util import strtobool + +from flask import abort, current_app, jsonify, request +from fsd_utils.locale_selector.get_lang import get_lang + +from db import db +from db.models import Round +from db.models.event import EventType +from db.queries import create_event as create_event_in_db +from db.queries import ( + get_all_funds, + get_application_sections_for_round, + get_assessment_sections_for_round, + get_fund_by_id, + get_fund_by_short_name, + get_round_by_id, + get_round_by_short_name, + get_rounds_for_fund_by_id, + get_rounds_for_fund_by_short_name, +) +from db.queries import get_event as get_event_from_db +from db.queries import get_events as get_events_from_db +from db.queries import set_event_to_processed as set_event_to_processed_in_db +from db.schemas.event import EventSchema +from db.schemas.fund import FundSchema +from db.schemas.round import RoundSchema +from db.schemas.section import SECTION_SCHEMA_MAP + + +def is_valid_uuid(value): + try: + obj = uuid.UUID(value) + return str(obj) == value.lower() + except Exception: + return False + + +def is_valid_isoformat_datetime(datetime_str): + try: + datetime.fromisoformat(datetime_str) + except Exception: + return False + return True + + +def filter_fund_by_lang(fund_data, lang_key: str = "en"): + def filter_fund(data): + data["name"] = data["name_json"].get(lang_key) or data["name_json"]["en"] + data["title"] = data["title_json"].get(lang_key) or data["title_json"]["en"] + data["description"] = data["description_json"].get(lang_key) or data["description_json"]["en"] + return data + + if isinstance(fund_data, dict): + fund = filter_fund(fund_data) + elif isinstance(fund_data, list): + fund = [filter_fund(item) for item in fund_data] + else: + fund = fund_data + + return fund + + +def filter_round_by_lang(round_data, lang_key: str = "en"): + def filter_round(data): + data["title"] = data["title_json"].get(lang_key) or data["title_json"]["en"] + data["contact_us_banner"] = ( + data["contact_us_banner_json"].get(lang_key) or data["contact_us_banner_json"].get("en") + if data["contact_us_banner_json"] + else "" + ) + data["instructions"] = ( + data["instructions_json"].get(lang_key) or data["instructions_json"].get("en") + if data["instructions_json"] + else "" + ) + data["application_guidance"] = ( + data["application_guidance_json"].get(lang_key) or data["application_guidance_json"].get("en") + if data["application_guidance_json"] + else "" + ) + return data + + if isinstance(round_data, dict): + round = filter_round(round_data) + elif isinstance(round_data, list): + round = [filter_round(item) for item in round_data] + else: + round = round_data + + return round + + +def get_funds(): + language = request.args.get("language", "en").replace("?", "") + funds = get_all_funds() + + if funds: + serialiser = FundSchema() + return jsonify(filter_fund_by_lang(fund_data=serialiser.dump(funds, many=True), lang_key=language)) + current_app.logger.warning("No funds were found, please check this.") + return jsonify(funds) + + +def get_fund(fund_id): + language = request.args.get("language", "en").replace("?", "") + short_name_arg = request.args.get("use_short_name") + use_short_name = short_name_arg and strtobool(short_name_arg) + + if use_short_name: + fund = get_fund_by_short_name(fund_id) if fund_id else None + else: + fund = get_fund_by_id(fund_id) if is_valid_uuid(fund_id) else None + + if fund: + serialiser = FundSchema() + return jsonify(filter_fund_by_lang(fund_data=serialiser.dump(fund), lang_key=language)) + + abort(404) + + +def get_round_from_db(fund_id, round_id) -> Round: + short_name_arg = request.args.get("use_short_name") + use_short_name = short_name_arg and strtobool(short_name_arg) + + if use_short_name: + round = get_round_by_short_name(fund_id, round_id) + else: + round = get_round_by_id(fund_id, round_id) if is_valid_uuid(fund_id) and is_valid_uuid(round_id) else None + return round + + +def get_round(fund_id, round_id): + round = get_round_from_db(fund_id, round_id) + language = request.args.get("language", "en").replace("?", "") + if round: + serialiser = RoundSchema() + return filter_round_by_lang(round_data=serialiser.dump(round), lang_key=language) + + abort(404) + + +def get_eoi_deicision_schema_for_round(fund_id, round_id): + language = request.args.get("language", "en").replace("?", "").lower() + round = get_round_from_db(fund_id=fund_id, round_id=round_id) + if not round: + abort(404) + + if not round.eoi_decision_schema: + return {} + + return round.eoi_decision_schema.get(language) or {} + + +def get_rounds_for_fund(fund_id): + language = request.args.get("language", "en").replace("?", "") + short_name_arg = request.args.get("use_short_name") + use_short_name = short_name_arg and strtobool(short_name_arg) + + if use_short_name: + rounds = get_rounds_for_fund_by_short_name(fund_id) + else: + rounds = get_rounds_for_fund_by_id(fund_id) if is_valid_uuid(fund_id) else None + + if rounds: + serialiser = RoundSchema() + dumped = [serialiser.dump(r) for r in rounds] + return filter_round_by_lang(round_data=dumped, lang_key=language) + + abort(404) + + +def get_sections_for_round_application(fund_id, round_id): + language = request.args.get("language", "en").replace("?", "") + if is_valid_uuid(fund_id) and is_valid_uuid(round_id): + sections = get_application_sections_for_round(fund_id, round_id) + if sections: + section_schema = SECTION_SCHEMA_MAP.get(language) + serialiser = section_schema() + dumped = serialiser.dump(sections, many=True) + return dumped + abort(404) + + +def get_sections_for_round_assessment(fund_id, round_id): + language = request.args.get("language", "en").replace("?", "") + if is_valid_uuid(fund_id) and is_valid_uuid(round_id): + sections = get_assessment_sections_for_round(fund_id, round_id, get_lang()) + if sections: + section_schema = SECTION_SCHEMA_MAP.get(language) + serialiser = section_schema() + return serialiser.dump(sections, many=True) + + abort(404) + + +def create_event(): + args = request.get_json() + if "type" not in args: + abort(400, "Post body must contain event type field") + + if "activation_date" not in args or not is_valid_isoformat_datetime(args["activation_date"]): + abort(400, "Activation date must be in isoformat datetime") + + if "processed" in args and not (is_valid_isoformat_datetime(args["processed"])): + abort(400, "Processed field must be an isoformat datetime") + + if "round_id" in args and not is_valid_uuid(args["round_id"]): + abort(400, "Round ID must be a UUID") + + event = create_event_in_db( + type=args["type"], + activation_date=args["activation_date"], + round_id=args.get("round_id"), + processed=args.get("processed"), + ) + + if event: + serialiser = EventSchema() + return serialiser.dump(event), 201 + abort(500) + + +def get_events_for_round(fund_id, round_id): + if not is_valid_uuid(round_id): + abort(400, "One or more IDs is not of format UUID") + + only_unprocessed = request.args.get("only_unprocessed", False, type=lambda x: x.lower() == "true") + events = get_events_from_db(round_id=round_id, only_unprocessed=only_unprocessed) + if events: + serialiser = EventSchema() + return serialiser.dump(events, many=True) + abort(404) + + +def get_events_by_type(type): + if not any(type == event_type.value for event_type in EventType): + abort(400, "Event type not recognised") + only_unprocessed = request.args.get("only_unprocessed", False, type=lambda x: x.lower() == "true") + events = get_events_from_db(type=type, only_unprocessed=only_unprocessed) + if events: + serialiser = EventSchema() + return serialiser.dump(events, many=True) + abort(404) + + +# TODO: deprecate in favour of get_event_by_id +def get_event_for_round(fund_id, round_id, event_id): + if not is_valid_uuid(event_id) or not is_valid_uuid(round_id): + abort(400, "One or more IDs is not of format UUID") + + event = get_event_from_db(round_id=round_id, event_id=event_id) + if event: + serialiser = EventSchema() + return serialiser.dump(event) + + abort(404) + + +def get_event_by_id(event_id): + if not is_valid_uuid(event_id): + abort(400, "One or more IDs is not of format UUID") + event = get_event_from_db(event_id=event_id) + if event: + serialiser = EventSchema() + return serialiser.dump(event) + abort(404) + + +def set_round_event_to_processed(fund_id, round_id, event_id): + if not is_valid_uuid(event_id) or not is_valid_uuid(round_id): + abort(400, "One or more IDs is not of format UUID") + processed = request.args.get("processed", type=lambda x: x.lower() == "true") + event = set_event_to_processed_in_db(event_id=event_id, processed=processed) + if event: + serialiser = EventSchema() + return jsonify(serialiser.dump(event)) + abort(404) + + +def set_event_to_processed(event_id): + if not is_valid_uuid(event_id): + abort(400, "One or more IDs is not of format UUID") + processed = request.args.get("processed", type=lambda x: x.lower() == "true") + event = set_event_to_processed_in_db(event_id=event_id, processed=processed) + if event: + serialiser = EventSchema() + return jsonify(serialiser.dump(event)) + abort(404) + + +def get_available_flag_allocations(fund_id, round_id): + # TODO: Currently teams are hardcoded, move it to database implementation + from config.fund_loader_config.cof.cof_r2 import ( + COF_ROUND_2_WINDOW_2_ID, + COF_ROUND_2_WINDOW_3_ID, + ) + from config.fund_loader_config.cof.cof_r3 import ( + COF_FUND_ID, + COF_ROUND_3_WINDOW_1_ID, + COF_ROUND_3_WINDOW_2_ID, + COF_ROUND_3_WINDOW_3_ID, + ) + from config.fund_loader_config.cof.cof_r4 import COF_ROUND_4_WINDOW_1_ID + from config.fund_loader_config.cyp.cyp_r1 import CYP_FUND_ID, CYP_ROUND_1_ID + from config.fund_loader_config.digital_planning.dpi_r2 import ( + DPI_FUND_ID, + DPI_ROUND_2_ID, + ) + from config.fund_loader_config.night_shelter.ns_r2 import ( + NIGHT_SHELTER_FUND_ID, + NIGHT_SHELTER_ROUND_2_ID, + ) + + cof_teams = [ + {"key": "ASSESSOR", "value": "Assessor"}, + {"key": "COMMERCIAL_ASSESSOR", "value": "Commercial Assessor"}, + {"key": "LEAD_ASSESSOR", "value": "Lead Assessor"}, + {"key": "LEAD_COMMERCIAL_ASSESSOR", "value": "Lead Commercial Assessor"}, + {"key": "COF_POLICY", "value": "COF Policy"}, + ] + + nstf_teams = [ + {"key": "COMMERCIAL", "value": "Commercial"}, + {"key": "NSTF_TEAM", "value": "NSTF Team"}, + {"key": "HOUSING_JUSTICE", "value": "Housing Justice"}, + {"key": "HOMELESS_LINK", "value": "Homeless Link"}, + {"key": "RS_ADVISORS", "value": "RS Advisors"}, + ] + + cyp_teams = [ + {"key": "COMMERCIAL_ASSESSOR", "value": "Commercial Assessor"}, + {"key": "LEAD_ASSESSOR", "value": "Lead Assessor"}, + ] + + dpif_teams = [ + {"key": "ELIGIBILITY", "value": "Eligibility"}, + {"key": "MODERATION", "value": "Moderation"}, + {"key": "LEAD_ASSESSOR", "value": "Lead Assessor"}, + ] + if is_valid_uuid(fund_id) and is_valid_uuid(round_id): + if fund_id == COF_FUND_ID and round_id in COF_ROUND_2_WINDOW_2_ID: + return cof_teams + elif fund_id == COF_FUND_ID and round_id == COF_ROUND_2_WINDOW_3_ID: + return cof_teams + elif fund_id == COF_FUND_ID and round_id == COF_ROUND_3_WINDOW_1_ID: + return cof_teams + elif fund_id == COF_FUND_ID and round_id == COF_ROUND_3_WINDOW_2_ID: + return cof_teams + elif fund_id == COF_FUND_ID and round_id == COF_ROUND_3_WINDOW_3_ID: + return cof_teams + elif fund_id == COF_FUND_ID and round_id == COF_ROUND_4_WINDOW_1_ID: + return cof_teams + elif fund_id == NIGHT_SHELTER_FUND_ID and round_id == NIGHT_SHELTER_ROUND_2_ID: + return nstf_teams + elif fund_id == CYP_FUND_ID and round_id == CYP_ROUND_1_ID: + return cyp_teams + elif fund_id == DPI_FUND_ID and round_id == DPI_ROUND_2_ID: + return dpif_teams + abort(404) + + +def update_application_reminder_sent_status(round_id): + try: + status = request.args.get("status") + round_instance = Round.query.filter_by(id=round_id).first() if is_valid_uuid(round_id) else None + + if not round_instance: + return jsonify({"message": "Round ID not found"}), 404 + reminder_status = round_instance.application_reminder_sent + + if status.lower() == "true" and reminder_status is False: + round_instance.application_reminder_sent = True + db.session.commit() + ( + current_app.logger.info( + {f"application_reminder_sent status has been updated to True for round {round_id}"} + ), + 200, + ) + return ( + jsonify({"message": f"application_reminder_sent status has been updated to True for round {round_id}"}), + 200, + ) + else: + return ( + jsonify({"message": "application_reminder_sent Status should be True"}), + 400, + ) + + except Exception as e: + ( + current_app.logger.error( + "The application_reminder_sent status could not be updated {error}", + extra=dict(error=str(e)), + ), + 400, + ) + return ( + jsonify({"message": f"The application_reminder_sent status could not be updated for round_id {round_id}"}), + 400, + ) diff --git a/fund_store/app.py b/fund_store/app.py new file mode 100644 index 000000000..e4da1a9fb --- /dev/null +++ b/fund_store/app.py @@ -0,0 +1,56 @@ +import connexion +import psycopg2 +from connexion import FlaskApp +from flask import jsonify +from fsd_utils import init_sentry +from fsd_utils.healthchecks.checkers import DbChecker, FlaskRunningChecker +from fsd_utils.healthchecks.healthcheck import Healthcheck +from fsd_utils.logging import logging +from sqlalchemy_utils import Ltree + +from db.models import ( + Fund, # noqa + Round, # noqa + Section, # noqa +) +from openapi.utils import get_bundled_specs + + +def create_app() -> FlaskApp: + init_sentry() + connexion_app = connexion.App( + "Fund Store", + ) + + connexion_app.add_api( + get_bundled_specs("/openapi/api.yml"), + validate_responses=True, + ) + flask_app = connexion_app.app + flask_app.config.from_object("config.Config") + from db import db, migrate + + # Bind SQLAlchemy ORM to Flask app + db.init_app(flask_app) + # Bind Flask-Migrate db utilities to Flask app + migrate.init_app(flask_app, db, directory="db/migrations", render_as_batch=True) + # Enable mapping of ltree datatype for sections + psycopg2.extensions.register_adapter(Ltree, lambda ltree: psycopg2.extensions.QuotedString(str(ltree))) + + # Initialise logging + logging.init_app(flask_app) + + health = Healthcheck(flask_app) + health.add_check(FlaskRunningChecker()) + health.add_check(DbChecker(db)) + + @flask_app.errorhandler(404) + def not_found(error): + flask_app.logger.warning("requested URL was not found on the server") + return jsonify({"code": 404, "message": "Requested URL was not found on the server"}), 404 + + return connexion_app + + +app = create_app() +application = app.app diff --git a/fund_store/config/__init__.py b/fund_store/config/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/fund_store/config/fund_loader_config/FAB/__init__.py b/fund_store/config/fund_loader_config/FAB/__init__.py new file mode 100644 index 000000000..52be7adcb --- /dev/null +++ b/fund_store/config/fund_loader_config/FAB/__init__.py @@ -0,0 +1,62 @@ +import ast +import os +from pathlib import Path + +""" +Goes through all the files in config/fund_loader_config/FAB and adds each round to FAB_FUND_ROUND_CONFIGS + +Each file in that directory needs to be python format as per the FAB exports, +containing one property called LOADER_CONFIG +See test_fab_round_config.py for example + +FAB_FUND_ROUND_CONFIGS example: +{ + "COF25":{ + "id": "xxx", + "short_name": "COF25" + "rounds":{ + "R1": { + "id": "yyy", + "fund_id": "xxx", + "short_name": "R1", + "sections_config": {} + } + } + } +} +""" + +FAB_FUND_ROUND_CONFIGS = {} + +this_dir = Path("config") / "fund_loader_config" / "FAB" + +for file in os.listdir(this_dir): + if file.startswith("__") or not file.endswith(".py"): + continue + + with open(this_dir / file, "r") as json_file: + + content = json_file.read() + if content.startswith("LOADER_CONFIG = "): + content = content.split("LOADER_CONFIG = ")[1] + elif content.startswith("LOADER_CONFIG="): + content = content.split("LOADER_CONFIG=")[1] + else: + raise ValueError(f"fund config file {file.title()} does not start with 'LOADER_CONFIG='") + loader_config = ast.literal_eval(content) + if not loader_config.get("fund_config", None): + print("No fund config found in the loader config.") + raise ValueError(f"No fund_config found in {file}") + if not loader_config.get("round_config", None): + print("No round config found in the loader config.") + raise ValueError(f"No round_config found in {file}") + fund_short_name = loader_config["fund_config"]["short_name"] + round_short_name = loader_config["round_config"]["short_name"] + FAB_FUND_ROUND_CONFIGS[fund_short_name] = loader_config["fund_config"] + if not FAB_FUND_ROUND_CONFIGS[fund_short_name].get("rounds", None): + FAB_FUND_ROUND_CONFIGS[fund_short_name]["rounds"] = {} + FAB_FUND_ROUND_CONFIGS[fund_short_name]["rounds"][round_short_name] = loader_config["round_config"] + FAB_FUND_ROUND_CONFIGS[fund_short_name]["rounds"][round_short_name]["sections_config"] = loader_config[ + "sections_config" + ] + FAB_FUND_ROUND_CONFIGS[fund_short_name]["rounds"][round_short_name]["base_path"] = loader_config["base_path"] diff --git a/fund_store/config/fund_loader_config/FAB/cof_25.py b/fund_store/config/fund_loader_config/FAB/cof_25.py new file mode 100644 index 000000000..a16fd53b3 --- /dev/null +++ b/fund_store/config/fund_loader_config/FAB/cof_25.py @@ -0,0 +1,280 @@ +LOADER_CONFIG = { + "sections_config": [ + { + "section_name": { + "en": "1. About your organisation", + "cy": "1. Ynglŷn â'ch sefydliad", + }, + "tree_path": "1036.1.1", + "requires_feedback": True, + }, + { + "section_name": { + "en": "1.1 Organisation information", + "cy": "1.1 Gwybodaeth am y sefydliad", + }, + "form_name_json": { + "en": "organisation-information-cof-25", + "cy": "gwybodaeth-am-y-sefydliad-cof-25", + }, + "tree_path": "1036.1.1.1", + }, + { + "section_name": { + "en": "1.2 Applicant information", + "cy": "1.2 Gwybodaeth am yr ymgeisydd", + }, + "form_name_json": { + "en": "applicant-information-cof-25", + "cy": "gwybodaeth-am-yr-ymgeisydd-cof-25", + }, + "tree_path": "1036.1.1.2", + }, + { + "section_name": { + "en": "2. About your project", + "cy": "2. Ynglŷn â'ch prosiect", + }, + "tree_path": "1036.1.2", + "requires_feedback": True, + }, + { + "section_name": { + "en": "2.1 Project information", + "cy": "2.1 Gwybodaeth am y prosiect", + }, + "form_name_json": { + "en": "project-information-cof-25", + "cy": "gwybodaeth-am-y-prosiect-cof-25", + }, + "tree_path": "1036.1.2.1", + }, + { + "section_name": { + "en": "2.2 Asset information", + "cy": "2.2 Gwybodaeth am yr ased", + }, + "form_name_json": { + "en": "asset-information-cof-25", + "cy": "gwybodaeth-am-yr-ased-cof-25", + }, + "tree_path": "1036.1.2.2", + }, + { + "section_name": {"en": "3. Strategic case", "cy": "3. Achos strategol"}, + "tree_path": "1036.1.3", + "requires_feedback": True, + "weighting": 53, + }, + { + "section_name": {"en": "3.1 Community use/significance", "cy": "3.1 Defnydd/arwyddocâd cymunedol"}, + "form_name_json": { + "en": "community-use-cof-25", + "cy": "defnydd-cymunedol-cof-25", + }, + "tree_path": "1036.1.3.1", + }, + { + "section_name": { + "en": "3.2 Community engagement", + "cy": "3.2 Ymgysylltu â'r gymuned", + }, + "form_name_json": { + "en": "community-engagement-cof-25", + "cy": "ymgysylltiad-cymunedol-cof-25", + }, + "tree_path": "1036.1.3.2", + }, + { + "section_name": {"en": "3.3 Local support", "cy": "3.3 Cefnogaeth leol"}, + "form_name_json": { + "en": "local-support-cof-25", + "cy": "cefnogaeth-leol-cof-25", + }, + "tree_path": "1036.1.3.3", + }, + { + "section_name": { + "en": "3.4 Community benefits", + "cy": "3.4 Buddion cymunedol", + }, + "form_name_json": { + "en": "community-benefits-cof-25", + "cy": "buddion-cymunedol-cof-25", + }, + "tree_path": "1036.1.3.4", + }, + { + "section_name": { + "en": "3.5 Environmental sustainability", + "cy": "3.5 Cynaliadwyedd amgylcheddol", + }, + "form_name_json": { + "en": "environmental-sustainability-cof-25", + "cy": "cynaliadwyedd-amgylcheddol-cof-25", + }, + "tree_path": "1036.1.3.5", + }, + { + "section_name": {"en": "4. Management case", "cy": "4. Achos rheoli"}, + "tree_path": "1036.1.4", + "weighting": 47, + "requires_feedback": True, + }, + { + "section_name": { + "en": "4.1 Funding required", + "cy": "4.1 Cyllid sydd ei angen", + }, + "form_name_json": { + "en": "funding-required-cof-25", + "cy": "cyllid-sydd-ei-angen-cof-25", + }, + "tree_path": "1036.1.4.1", + }, + { + "section_name": {"en": "4.2 Feasibility ", "cy": "4.2 Dichonoldeb"}, + "form_name_json": { + "en": "feasibility-cof-25", + "cy": "dichonoldeb-cof-25", + }, + "tree_path": "1036.1.4.2", + }, + { + "section_name": {"en": "4.3 Risk", "cy": "4.3 Risg"}, + "tree_path": "1036.1.4.3", + "form_name_json": {"en": "risk-cof-25", "cy": "risg-cof-25"}, + }, + { + "section_name": {"en": "4.4 Operational costs", "cy": "4.4 Costau gweithredol"}, + "form_name_json": { + "en": "operational-costs-cof-25", + "cy": "costau-gweithredol-cof-25", + }, + "tree_path": "1036.1.4.4", + }, + { + "section_name": { + "en": "4.5 Skills and resources", + "cy": "4.5 Sgiliau ac adnoddau", + }, + "form_name_json": { + "en": "skills-and-resources-cof-25", + "cy": "sgiliau-ac-adnoddau-cof-25", + }, + "tree_path": "1036.1.4.5", + }, + { + "section_name": { + "en": "4.6 Community representation", + "cy": "4.6 Cynrychiolaeth gymunedol", + }, + "form_name_json": { + "en": "community-representation-cof-25", + "cy": "cynrychiolaeth-gymunedol-cof-25", + }, + "tree_path": "1036.1.4.6", + }, + { + "section_name": { + "en": "4.7 Inclusiveness and integration", + "cy": "4.7 Cynhwysiant ac integreiddio", + }, + "form_name_json": { + "en": "inclusiveness-and-integration-cof-25", + "cy": "cynhwysiant-ac-integreiddio-cof-25", + }, + "tree_path": "1036.1.4.7", + }, + { + "section_name": { + "en": "4.8 Upload business plan", + "cy": "4.8 Lanlwythwch y cynllun busnes", + }, + "form_name_json": { + "en": "upload-business-plan-cof-25", + "cy": "lanlwythwch-y-cynllun-busnes-cof-25", + }, + "tree_path": "1036.1.4.8", + }, + { + "section_name": {"en": "5. Check declarations", "cy": "5. Gwirio datganiadau"}, + "tree_path": "1036.1.5", + "requires_feedback": None, + }, + { + "section_name": {"en": "5.1 Declarations", "cy": "5.1 Datganiadau"}, + "form_name_json": { + "en": "declarations-cof-25", + "cy": "cadarnhadau-terfynol-cof-25", + }, + "tree_path": "1036.1.5.1", + }, + ], + "fund_config": { + "id": "604450fe-65c0-4a2e-a4ba-30ccf256056b", + "short_name": "COF25", + "welsh_available": True, + "owner_organisation_name": "None", + "owner_organisation_shortname": "None", + "owner_organisation_logo_uri": "None", + "funding_type": "COMPETITIVE", + "name_json": {"en": "Community Ownership Fund 2025", "cy": "Y Cronfa Perchnogaeth Gymunedol 2025"}, + "title_json": {"en": "funding to save an asset in your community", "cy": "gyllid i achub ased yn eich cymuned"}, + "description_json": { + "en": "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.", + "cy": "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.", + }, + }, + "round_config": { + "id": "38914fbf-9c31-41be-8547-c24d997aaba2", + "fund_id": "604450fe-65c0-4a2e-a4ba-30ccf256056b", + "short_name": "R1", + "opens": "2024-10-30T11:59:00", + "assessment_start": "2025-01-01T09:00:00", + "deadline": "2024-12-11T14:00:00", + "application_reminder_sent": False, + "reminder_date": "2024-12-01T11:59:00", + "assessment_deadline": "2025-01-31T17:00:00", + "prospectus": "https://www.gov.uk/government/publications/community-ownership-fund-prospectus/community-ownership-fund-2025-prospectus", + "privacy_notice": "https://www.gov.uk/government/organisations/ministry-of-housing-communities-local-government", + "reference_contact_page_over_email": True, + "contact_email": "COF@communities.gov.uk", + "contact_phone": "", + "contact_textphone": "", + "support_times": "9am to 5pm", + "support_days": "Monday to Friday", + "instructions_json": { + "en": "You must have received an invitation to apply. If we did not invite you, first express your interest in the fund.", + "cy": "Mae'n rhaid i chi fod wedi derbyn gwahoddiad i ymgeisio. Os na wnaethom eich gwahodd, mynegwch eich diddordeb yn y gronfa yn gyntaf.", + }, + "feedback_link": "https://www.gov.uk/government/organisations/ministry-of-housing-communities-local-government", + "project_name_field_id": "apGjFS", + "application_guidance_json": { + "en": "

What we'll ask you for

You can preview the full list of application questions.

We'll also ask you to upload a business plan to support the answers you've given us in the management case section.

", + "cy": "

Beth fyddwn ni'n gofyn i chi amdano

Gallwch gael cip ymlaen llaw o restr lawn cwestiynau'r cais.

Byddwn hefyd yn gofyn i chi lanlwytho cynllun busnes i ategu'r atebion rydych wedi'u rhoi i ni yn yr adran achos rheoli.

", + }, + "guidance_url": "https://www.gov.uk/government/organisations/ministry-of-housing-communities-local-government", + "all_uploaded_documents_section_available": False, + "application_fields_download_available": True, + "display_logo_on_pdf_exports": False, + "mark_as_complete_enabled": True, + "is_expression_of_interest": False, + "eoi_decision_schema": None, + "feedback_survey_config": { + "has_feedback_survey": True, + "has_section_feedback": True, + "has_research_survey": True, + "is_feedback_survey_optional": False, + "is_research_survey_optional": True, + "is_section_feedback_optional": False, + }, + "eligibility_config": {"has_eligibility": False}, + "title_json": {"en": "Round 1", "cy": None}, + "contact_us_banner_json": { + "en": '

Get application support

\r\n

\r\n Visit the My Community website\r\n for information and guidance on applying to Community Ownership Fund 2025.\r\n Fill out the enquiry form\r\n to request advice from My Community.\r\n

\r\n

\r\n We cannot provide direct support to applicants outside of this service.\r\n

\r\n

Get technical support

\r\n

\r\n Contact the Ministry of Housing, Communities and Local Government funding team if you need\r\n help with accessing or submitting an application form.\r\n

\r\n', + "cy": '

Cael cymorth â\'r cais

\r\n

\r\n Ewch i wefan My Community\r\n i gael gwybodaeth ac arweiniad ar wneud cais i\'r Gronfa Perchnogaeth Gymunedol 2025.\r\n Llenwch y ffurflen ymholiad\r\n i ofyn am gyngor gan My Community.\r\n

\r\n

\r\n Ni allwn ddarparu cymorth uniongyrchol i ymgeiswyr tu hwnt i\'r gwasanaeth hwn.\r\n

\r\n

Cael cymorth technegol

\r\n

\r\n Cysylltwch â thîm cyllid yr Adran Ffyniant Bro, Tai a Chymunedau os oes angen help arnoch i gael at ffurflen gais neu ei chyflwyno.\r\n

', + }, + }, + "base_path": 1036, +} diff --git a/fund_store/config/fund_loader_config/FAB/cof_25_eoi.py b/fund_store/config/fund_loader_config/FAB/cof_25_eoi.py new file mode 100644 index 000000000..93306cdb9 --- /dev/null +++ b/fund_store/config/fund_loader_config/FAB/cof_25_eoi.py @@ -0,0 +1,319 @@ +LOADER_CONFIG = { + "sections_config": [ + { + "section_name": { + "en": "1. Expression of interest", + "cy": "1. Mynegi diddordeb", + }, + "tree_path": "1037.1.1", + "requires_feedback": True, + }, + { + "section_name": { + "en": "1.1 Organisation details", + "cy": "1.1 Manylion y sefydliad", + }, + "tree_path": "1037.1.1.1", + "form_name_json": { + "en": "organisation-details-25", + "cy": "manylion-y-sefydliad-25", + }, + }, + { + "section_name": { + "en": "1.2 About your asset", + "cy": "1.2 Ynglŷn â'ch ased", + }, + "tree_path": "1037.1.1.2", + "form_name_json": { + "en": "about-your-asset-25", + "cy": "ynglyn-ach-ased-25", + }, + }, + { + "section_name": { + "en": "1.3 Your funding request", + "cy": "1.3 Eich cais am gyllid", + }, + "tree_path": "1037.1.1.3", + "form_name_json": { + "en": "your-funding-request-25", + "cy": "eich-cais-am-gyllid-25", + }, + }, + { + "section_name": { + "en": "1.4 Development support provider (not scored)", + "cy": "1.4 Darparwr cymorth datblygu (heb ei sgorio)", + }, + "tree_path": "1037.1.1.4", + "form_name_json": { + "en": "development-support-provider-25", + "cy": "darparwr-cymorth-datblygu-25", + }, + }, + { + "section_name": { + "en": "1.5 Declaration", + "cy": "1.5 Datganiad", + }, + "tree_path": "1037.1.1.5", + "form_name_json": { + "en": "declaration-25", + "cy": "datganiad-25", + }, + }, + ], + "fund_config": { + "id": "4db6072c-4657-458d-9f57-9ca59638317b", + "short_name": "COF25-EOI", + "welsh_available": True, + "owner_organisation_name": "None", + "owner_organisation_shortname": "None", + "owner_organisation_logo_uri": "None", + "funding_type": "EOI", + "name_json": {"en": "Community Ownership Fund 2025", "cy": "Y Cronfa Perchnogaeth Gymunedol 2025"}, + "title_json": { + "en": "expression of interest in applying for the Community Ownership Fund 2025", + "cy": "Gwneud cais am ddatganiad o ddiddordeb mewn gwneud cais i'r Gronfa Perchnogaeth Gymunedol 2025", + }, + "description_json": { + "en": "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.", + "cy": "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.", + }, + }, + "round_config": { + "id": "9104d809-0fb0-4144-b514-55e81cc2b6fa", + "fund_id": "4db6072c-4657-458d-9f57-9ca59638317b", + "short_name": "R1", + "opens": "2024-10-30T11:59:00", + "assessment_start": "2024-10-30T09:00:00", + "deadline": "2024-12-11T14:00:00", + "application_reminder_sent": False, + "reminder_date": "2024-12-01T11:59:00", + "assessment_deadline": "2025-01-31T17:00:00", + "prospectus": "https://www.gov.uk/government/publications/community-ownership-fund-prospectus/community-ownership-fund-2025-prospectus", + "privacy_notice": "https://www.gov.uk/government/publications/community-ownership-fund-privacy-notice/community-ownership-fund-privacy-notice", + "reference_contact_page_over_email": True, + "contact_email": "COF@communities.gov.uk", + "contact_phone": "", + "contact_textphone": "", + "support_times": "9am to 5pm", + "support_days": "Monday to Friday", + "instructions_json": { + "en": "You must complete this Expression of Interest (EOI) form if you are interested in applying for the Community Ownership Fund 2025 (COF25).", + "cy": "Mae'n rhaid i chi gwblhau'r ffurflen Datganiad o Ddiddordeb hon os oes diddordeb gennych mewn gwneud cais i'r Gronfa Perchnogaeth Gymunedol 2025.", + }, + "feedback_link": "https://www.gov.uk/government/organisations/ministry-of-housing-communities-local-government", + "project_name_field_id": "SMRWjl", + "application_guidance_json": { + "en": "

What we'll ask you for

You can preview the full list of application questions.

", + "cy": "

Beth y byddwn yn gofyn i chi amdano

Gallwch gael rhagolwg o'r rhestr lawn o gwestiynau yn y cais.

", + }, + "guidance_url": "https://www.gov.uk/government/organisations/ministry-of-housing-communities-local-government", + "all_uploaded_documents_section_available": False, + "application_fields_download_available": True, + "display_logo_on_pdf_exports": False, + "mark_as_complete_enabled": False, + "is_expression_of_interest": True, + "eoi_decision_schema": { + "en": { + "BykoQQ": [ + { + "answerValue": ["Not sure"], + "caveat": "Make progress in securing match funding: COF will contribute up to 80% of the capital costs you require, and you must raise at least 20% from other sources.You do not need to have secured all your match funding by the time you apply, but we will ask you to set out your total costs, funding already secured, and plans to raise any additional funding.You must use COF funding within 12 months, so you must be able to show that you've made good progress to secure the remaining match funding. This is so that we're confident you can draw down this funding within this timeframe.", + "result": 1, + } + ], + "NcQSbU": [{"answerValue": True, "caveat": None, "result": 2}], + "UORyaF": [ + { + "answerValue": "Not sure", + "caveat": "Get planning permission, if needed: When you apply, you must be able to show that you have secured or have made good progress in securing planning permission, if needed (and building warrants, if required). This is so that we're confident that COF funding will be used within the 12 month timeframe.", + "result": 1, + } + ], + "XuAyrs": [ + { + "answerValue": "Yes, a town, parish or community council", + "caveat": "Understand the rules on acquiring assets from town, parish or community councils: We cannot fund you to acquire a publicly owned asset if this involves transferring responsibility for delivering statutory services (services paid for by tax payers) from the public authority to your organisation. You should only apply to acquire an asset from a town, parish or community council if you do not plan to deliver statutory services.", + "result": 1, + }, + { + "answerValue": "Yes, another type of public authority", + "caveat": "Understand the rules on acquiring public sector assets: COF funding can only be used for renovation and refurbishment costs once a publicly owned asset has been transferred to you. We cannot fund capital receipts, unless the costs incurred in transferring the asset to you are nominal (very small and far below the real value).In your application, you should show that you are not asking COF to fund a capital receipt to a public authority (for example, by sharing a letter confirming the authority is willing/has already agreed a long-term lease and no capital receipt is involved).We also cannot fund you to acquire a publicly owned asset if this involves transferring responsibility for delivering statutory services (services paid for by tax payers) from the public authority to your organisation", + "result": 1, + }, + ], + "eEaDGz": [{"answerValue": False, "caveat": None, "result": 2}], + "eOWKoO": [{"answerValue": False, "caveat": None, "result": 2}], + "fBhSNc": [{"answerValue": False, "caveat": None, "result": 2}], + "fZAMFv": [{"caveat": None, "compareValue": 2000000, "operator": ">", "result": 2}], + "foQgiy": [{"answerValue": False, "caveat": None, "result": 2}], + "jICagT": [ + { + "answerValue": "Not yet started", + "caveat": "Get planning permission: When you apply, you must be able to show that you have secured or have made good progress in securing planning permission (and building warrants, if required). This is so that we're confident that COF funding will be used within the 12 month timeframe.", + "result": 1, + }, + { + "answerValue": "Early stage", + "caveat": "Get planning permission: When you apply, you must be able to show that you have secured or have made good progress in securing planning permission (and building warrants, if required). This is so that we're confident that COF funding will be used within the 12 month timeframe.", + "result": 1, + }, + ], + "kWRuac": [ + { + "answerValue": "Not yet approached any funders", + "caveat": "Make progress in securing match funding: COF will contribute up to 80% of the capital costs you require, and you must raise at least 20% from other sources. You do not need to have secured all your match funding by the time you apply, but we will ask you to set out your total costs, funding already secured, and plans to raise any additional funding. You must use COF funding within 12 months, so you must be able to show that you've made good progress to secure the remaining match funding. This is so that we're confident you can draw down this funding within this timeframe.", + "result": 1, + }, + { + "answerValue": "Approached some funders but not yet secured", + "caveat": "Make progress in securing match funding: COF will contribute up to 80% of the capital costs you require, and you must raise at least 20% from other sources. You do not need to have secured all your match funding by the time you apply, but we will ask you to set out your total costs, funding already secured, and plans to raise any additional funding. You must use COF funding within 12 months, so you must be able to show that you've made good progress to secure the remaining match funding. This is so that we're confident you can draw down this funding within this timeframe.", + "result": 1, + }, + { + "answerValue": "Approached all funders but not yet secured", + "caveat": "Make progress in securing match funding: COF will contribute up to 80% of the capital costs you require, and you must raise at least 20% from other sources. You do not need to have secured all your match funding by the time you apply, but we will ask you to set out your total costs, funding already secured, and plans to raise any additional funding. You must use COF funding within 12 months, so you must be able to show that you've made good progress to secure the remaining match funding. This is so that we're confident you can draw down this funding within this timeframe.", + "result": 1, + }, + { + "answerValue": "Secured some match funding", + "caveat": "Make progress in securing match funding: COF will contribute up to 80% of the capital costs you require, and you must raise at least 20% from other sources. You do not need to have secured all your match funding by the time you apply, but we will ask you to set out your total costs, funding already secured, and plans to raise any additional funding. You must use COF funding within 12 months, so you must be able to show that you've made good progress to secure the remaining match funding. This is so that we're confident you can draw down this funding within this timeframe.", + "result": 1, + }, + ], + "lLQmNb": [{"answerValue": False, "caveat": None, "result": 2}], + "oblxxv": [ + { + "answerValue": False, + "caveat": "Consider requesting revenue funding: We encourage all organisations to apply for revenue funding to help cover the initial running costs of your project. When you apply, you'll need to show us how you plan to use any revenue funding. See [Section 9 of the COF prospectus for more guidance](https://www.gov.uk/government/publications/community-ownership-fund-prospectus/community-ownership-fund-2025-prospectus#funding-available).", + "result": 1, + } + ], + "uYiLsv": [ + { + "answerValue": "not-yet-incorporated", + "caveat": "Incorporate your organisation: You must have incorporated your organisation by the time you submit a full application. If you remain unincorporated, your application will be ineligible.", + "result": 1, + } + ], + "yZxdeJ": [ + { + "answerValue": True, + "caveat": "Understand the rules on housing: We will not provide funding if your project's main purpose is to purchase or develop housing assets, including social housing. However, you can include housing elements in your project where these are only a small part of supporting the overall financial sustainability of the asset in community ownership.", + "result": 1, + } + ], + "zurxox": [{"answerValue": False, "caveat": None, "result": 2}], + }, + "cy": { + "uYiLsv": [ + { + "answerValue": "Ddim yn gorfforedig eto", + "result": 1, + "caveat": "Dylech gorffori eich sefydliad: Mae'n rhaid eich bod wedi corffori eich sefydliad erbyn eich bod yn cyflwyno cais llawn. Os byddwch yn anghorfforedig o hyd, ni fydd eich cais yn gymwys.", + } + ], + "NcQSbU": [{"answerValue": True, "result": 2, "caveat": None}], + "eEaDGz": [{"answerValue": False, "result": 2, "caveat": None}], + "zurxox": [{"answerValue": False, "result": 2, "caveat": None}], + "lLQmNb": [{"answerValue": False, "result": 2, "caveat": None}], + "fBhSNc": [{"answerValue": False, "result": 2, "caveat": None}], + "XuAyrs": [ + { + "answerValue": "Ydy, cyngor tref, plwyf neu gymuned", + "result": 1, + "caveat": "Dylech ddeall y rheolau yngl\\u0177n \\u00e2 chaffael asedau gan gynghorau tref, plwyf neu gymuned: Ni allwn eich ariannu i gaffael ased dan berchnogaeth gyhoeddus os yw'n golygu trosglwyddo cyfrifoldeb am ddarparu gwasanaethau statudol (gwasanaethau y mae trethdalwyr yn talu amdanynt) o'r awdurdod cyhoeddus i'ch sefydliad. Dim ond os nad ydych yn bwriadu darparu gwasanaethau statudol y dylech wneud cais i gaffael ased gan gyngor tref, plwyf neu gymuned.", + }, + { + "answerValue": "Ydy, math arall o awdurdod cyhoeddus", + "result": 1, + "caveat": "Dylech ddeall y rheolau yngl\\u0177n \\u00e2 chaffael asedau'r sector cyhoeddus: Dim ond ar \\u00f4l trosglwyddo ased sydd dan berchnogaeth gyhoeddus i chi y gellir defnyddio cyllid o'r Gronfa Perchnogaeth Gymunedol ar gyfer costau adnewyddu ac ailwampio. Ni allwn ariannu derbyniad cyfalaf, oni bai bod y costau yr aed iddynt wrth drosglwyddo'r ased i chi yn nominal (bach iawn ac yn llawer is na'r gwerth gwirioneddol). Yn eich cais, dylech ddangos nad ydych yn gofyn i'r Gronfa Perchnogaeth Gymunedol ariannu derbyniad cyfalaf i awdurdod cyhoeddus (er enghraifft drwy rannu llythyr yn cadarnhau bod yr awdurdod yn fodlon ar/eisoes wedi cytuno i les hirdymor ac nad oes derbyniad cyfalaf yn gysylltiedig). Ni allwn ychwaith eich ariannu i gaffael ased dan berchnogaeth gyhoeddus os yw'n golygu trosglwyddo cyfrifoldeb am ddarparu gwasanaethau statudol (gwasanaethau y mae trethdalwyr yn talu amdanynt) o'r awdurdod cyhoeddus i'ch sefydliad.", + }, + ], + "foQgiy": [{"answerValue": False, "result": 2, "caveat": None}], + "BykoQQ": [ + { + "answerValue": ["none"], + "result": 1, + "caveat": "Gwneud cynnydd i sicrhau arian cyfatebol: Bydd y Gronfa Perchnogaeth Gymunedol yn cyfrannu hyd at 80% o'r costau cyfalaf sydd eu hangen arnoch, ac mae'n rhaid i chi godi o leiaf 20% o ffynonellau eraill. Nid oes angen i chi fod wedi sicrhau eich holl arian cyfatebol erbyn i chi wneud cais, ond byddwn yn gofyn i chi nodi cyfanswm eich costau, y cyllid rydych eisoes wedi'i sicrhau, a chynlluniau i godi unrhyw gyllid ychwanegol. Mae'n rhaid i chi ddefnyddio cyllid o'r Gronfa Perchnogaeth Gymunedol o fewn 12 mis, felly mae'n rhaid i chi allu dangos eich bod wedi gwneud cynnydd da i sicrhau'r arian cyfatebol sy'n weddill. Diben hyn yw rhoi'r hyder i ni y gallwch ddefnyddio'r cyllid hwn o fewn yr amserlen hon.", + } + ], + "eOWKoO": [{"answerValue": False, "result": 2, "caveat": None}], + "oblxxv": [ + { + "answerValue": False, + "result": 1, + "caveat": "Ystyriwch wneud cais am gyllid refeniw: Rydym yn annog pob sefydliad i wneud cais am gyllid refeniw er mwyn helpu i dalu costau rhedeg cychwynnol eich prosiect. Pan fyddwch yn gwneud cais, bydd angen i chi ddangos i ni sut rydych yn bwriadu defnyddio unrhyw gyllid refeniw. [Gweler Adran 9 o brosbectws y Gronfa Perchnogaeth Gymunedol am ragor o ganllawiau.](https://www.gov.uk/government/publications/community-ownership-fund-prospectus/community-ownership-fund-prospectus--3#funding-available)", + } + ], + "kWRuac": [ + { + "answerValue": "Heb gysylltu ag unrhyw gyllidwyr eto", + "result": 1, + "caveat": "Gwneud cynnydd i sicrhau arian cyfatebol: Bydd y Gronfa Perchnogaeth Gymunedol yn cyfrannu hyd at 80% o'r costau cyfalaf sydd eu hangen arnoch, ac mae'n rhaid i chi godi o leiaf 20% o ffynonellau eraill. Nid oes angen i chi fod wedi sicrhau eich holl arian cyfatebol erbyn i chi wneud cais, ond byddwn yn gofyn i chi nodi cyfanswm eich costau, y cyllid rydych eisoes wedi'i sicrhau, a chynlluniau i godi unrhyw gyllid ychwanegol. Mae'n rhaid i chi ddefnyddio cyllid o'r Gronfa Perchnogaeth Gymunedol o fewn 12 mis, felly mae'n rhaid i chi allu dangos eich bod wedi gwneud cynnydd da i sicrhau'r arian cyfatebol sy'n weddill. Diben hyn yw rhoi'r hyder i ni y gallwch ddefnyddio'r cyllid hwn o fewn yr amserlen hon.", + }, + { + "answerValue": "Wedi cysylltu \\u00e2 rhai cyllidwyr ond heb sicrhau cyllid eto", + "result": 1, + "caveat": "Gwneud cynnydd i sicrhau arian cyfatebol: Bydd y Gronfa Perchnogaeth Gymunedol yn cyfrannu hyd at 80% o'r costau cyfalaf sydd eu hangen arnoch, ac mae'n rhaid i chi godi o leiaf 20% o ffynonellau eraill. Nid oes angen i chi fod wedi sicrhau eich holl arian cyfatebol erbyn i chi wneud cais, ond byddwn yn gofyn i chi nodi cyfanswm eich costau, y cyllid rydych eisoes wedi'i sicrhau, a chynlluniau i godi unrhyw gyllid ychwanegol. Mae'n rhaid i chi ddefnyddio cyllid o'r Gronfa Perchnogaeth Gymunedol o fewn 12 mis, felly mae'n rhaid i chi allu dangos eich bod wedi gwneud cynnydd da i sicrhau'r arian cyfatebol sy'n weddill. Diben hyn yw rhoi'r hyder i ni y gallwch ddefnyddio'r cyllid hwn o fewn yr amserlen hon.", + }, + { + "answerValue": "Wedi cysylltu \\u00e2'r holl gyllidwyr ond heb sicrhau cyllid eto", + "result": 1, + "caveat": "Gwneud cynnydd i sicrhau arian cyfatebol: Bydd y Gronfa Perchnogaeth Gymunedol yn cyfrannu hyd at 80% o'r costau cyfalaf sydd eu hangen arnoch, ac mae'n rhaid i chi godi o leiaf 20% o ffynonellau eraill. Nid oes angen i chi fod wedi sicrhau eich holl arian cyfatebol erbyn i chi wneud cais, ond byddwn yn gofyn i chi nodi cyfanswm eich costau, y cyllid rydych eisoes wedi'i sicrhau, a chynlluniau i godi unrhyw gyllid ychwanegol. Mae'n rhaid i chi ddefnyddio cyllid o'r Gronfa Perchnogaeth Gymunedol o fewn 12 mis, felly mae'n rhaid i chi allu dangos eich bod wedi gwneud cynnydd da i sicrhau'r arian cyfatebol sy'n weddill. Diben hyn yw rhoi'r hyder i ni y gallwch ddefnyddio'r cyllid hwn o fewn yr amserlen hon.", + }, + { + "answerValue": "Wedi sicrhau rhywfaint o arian cyfatebol", + "result": 1, + "caveat": "Gwneud cynnydd i sicrhau arian cyfatebol: Bydd y Gronfa Perchnogaeth Gymunedol yn cyfrannu hyd at 80% o'r costau cyfalaf sydd eu hangen arnoch, ac mae'n rhaid i chi godi o leiaf 20% o ffynonellau eraill. Nid oes angen i chi fod wedi sicrhau eich holl arian cyfatebol erbyn i chi wneud cais, ond byddwn yn gofyn i chi nodi cyfanswm eich costau, y cyllid rydych eisoes wedi'i sicrhau, a chynlluniau i godi unrhyw gyllid ychwanegol. Mae'n rhaid i chi ddefnyddio cyllid o'r Gronfa Perchnogaeth Gymunedol o fewn 12 mis, felly mae'n rhaid i chi allu dangos eich bod wedi gwneud cynnydd da i sicrhau'r arian cyfatebol sy'n weddill. Diben hyn yw rhoi'r hyder i ni y gallwch ddefnyddio'r cyllid hwn o fewn yr amserlen hon.", + }, + ], + "yZxdeJ": [ + { + "answerValue": True, + "result": 1, + "caveat": "Dylech ddeall y rheolau ynglyn \\u00e2 thai: Ni fyddwn yn darparu cyllid os mai prif ddiben eich prosiect yw prynu neu ddatblygu asedau tai, gan gynnwys tai cymdeithasol. Fodd bynnag, gallwch gynnwys elfennau tai yn eich prosiect os mai dim ond rhan fach o gefnogi cynaliadwyedd ariannol gyffredinol yr ased dan berchnogaeth gymunedol yw'r rhain.", + } + ], + "UORyaF": [ + { + "answerValue": "Ddim yn si\\u0175r", + "result": 1, + "caveat": "Sicrhewch ganiat\\u00e2d cynllunio, os oes angen: Pan fyddwch yn gwneud cais, rhaid i chi allu dangos eich bod wedi sicrhau caniat\\u00e2d cynllunio os oes angen (a gwarantau adeiladu, os oes angen), neu'ch bod wedi gwneud cynnydd da yn hyn o beth. Diben hyn yw rhoi'r hyder i ni y caiff cyllid o'r Gronfa Perchnogaeth Gymunedol ei ddefnyddio o fewn y cyfnod o 12 mis.", + } + ], + "jICagT": [ + { + "answerValue": "Heb ddechrau eto", + "result": 1, + "caveat": "Sicrhewch ganiat\\u00e2d cynllunio: Pan fyddwch yn gwneud cais, rhaid i chi allu dangos eich bod wedi sicrhau caniat\\u00e2d cynllunio (a gwarantau adeiladu, os oes angen), neu'ch bod wedi gwneud cynnydd da yn hyn o beth. Diben hyn yw rhoi'r hyder i ni y caiff cyllid o'r Gronfa Perchnogaeth Gymunedol ei ddefnyddio o fewn y cyfnod o 12 mis.", + }, + { + "answerValue": "Cam cynnar", + "result": 1, + "caveat": "Sicrhewch ganiat\\u00e2d cynllunio: Pan fyddwch yn gwneud cais, rhaid i chi allu dangos eich bod wedi sicrhau caniat\\u00e2d cynllunio (a gwarantau adeiladu, os oes angen), neu'ch bod wedi gwneud cynnydd da yn hyn o beth. Diben hyn yw rhoi'r hyder i ni y caiff cyllid o'r Gronfa Perchnogaeth Gymunedol ei ddefnyddio o fewn y cyfnod o 12 mis.", + }, + ], + "fZAMFv": [{"operator": ">", "compareValue": 2000000, "result": 2, "caveat": None}], + }, + }, + "feedback_survey_config": { + "has_feedback_survey": False, + "has_section_feedback": True, + "has_research_survey": True, + "is_feedback_survey_optional": False, + "is_research_survey_optional": True, + "is_section_feedback_optional": True, + }, + "eligibility_config": {"has_eligibility": False}, + "title_json": {"en": "Round 1", "cy": "Rownd 1"}, + "contact_us_banner_json": { + "en": '

Get application support

\r\n

\r\n Visit the My Community website\r\n for information and guidance on applying to Community Ownership Fund 2025.\r\n Fill out the enquiry form\r\n to request advice from My Community.\r\n

\r\n

\r\n We cannot provide direct support to applicants outside of this service.\r\n

\r\n

Get technical support

\r\n

\r\n Contact the Ministry of Housing, Communities & Local Government funding team if you need\r\n help with accessing or submitting an application form.\r\n

', + "cy": '

Cael cymorth â\'r cais

\r\n

\r\n Ewch i wefan My Community\r\n i gael gwybodaeth ac arweiniad ar wneud cais i\'r Gronfa Perchnogaeth Gymunedol 2025.\r\n Llenwch y ffurflen ymholiad\r\n i ofyn am gyngor gan My Community.\r\n

\r\n

\r\n Ni allwn ddarparu cymorth uniongyrchol i ymgeiswyr tu hwnt i\'r gwasanaeth hwn.\r\n

\r\n

Cael cymorth technegol

\r\n

\r\n Cysylltwch â thîm cyllid yr Adran Ffyniant Bro, Tai a Chymunedau os oes angen help arnoch i gael at ffurflen gais neu ei chyflwyno.\r\n

', + }, + }, + "base_path": 1037, +} diff --git a/fund_store/config/fund_loader_config/cof/cof_r2.py b/fund_store/config/fund_loader_config/cof/cof_r2.py new file mode 100644 index 000000000..4b0539be3 --- /dev/null +++ b/fund_store/config/fund_loader_config/cof/cof_r2.py @@ -0,0 +1,427 @@ +# flake8: noqa +import textwrap +from datetime import datetime +from datetime import timezone + +from config.fund_loader_config.cof.shared import COF_APPLICATION_GUIDANCE +from config.fund_loader_config.cof.shared import fund_config +from config.fund_loader_config.common_fund_config.fund_base_tree_paths import ( + COF_R2_W2_BASE_PATH, +) + +COF_FUND_ID = fund_config["id"] +COF_ROUND_2_WINDOW_2_ID = "c603d114-5364-4474-a0c4-c41cbf4d3bbd" +COF_ROUND_2_WINDOW_3_ID = "5cf439bf-ef6f-431e-92c5-a1d90a4dd32f" +APPLICATION_BASE_PATH = ".".join([str(COF_R2_W2_BASE_PATH), str(1)]) +ASSESSMENT_BASE_PATH = ".".join([str(COF_R2_W2_BASE_PATH), str(2)]) +COF_R2_OPENS_DATE = datetime(2022, 10, 4, 12, 0, 0, tzinfo=timezone.utc) # 2022-10-04 12:00:00 +COF_R2_DEADLINE_DATE = datetime(2022, 12, 14, 11, 59, 0, tzinfo=timezone.utc) # 2022-12-14 11:59:00 +COF_R2_ASSESSMENT_DEADLINE_DATE = datetime(2023, 3, 30, 12, 0, 0, tzinfo=timezone.utc) # 2023-03-30 12:00:00 + +rounds_config = [ + { + "id": COF_ROUND_2_WINDOW_2_ID, + "title_json": { + "en": "Round 2 Window 2", + "cy": "Round 2 Window 2", + }, # TODO: Provide welsh translation + "short_name": "R2W2", + "opens": COF_R2_OPENS_DATE, + "assessment_start": None, + "deadline": COF_R2_DEADLINE_DATE, + "application_reminder_sent": True, + "reminder_date": None, + "fund_id": "47aef2f5-3fcb-4d45-acb5-f0152b5f03c4", + "assessment_deadline": COF_R2_ASSESSMENT_DEADLINE_DATE, + "prospectus": "https://www.gov.uk/government/publications/community-ownership-fund-prospectus", + "privacy_notice": ( + ( + "https://www.gov.uk/government/publications/community-ownership-fund-privacy-notice/" + "community-ownership-fund-privacy-notice" + ), + ), + "reference_contact_page_over_email": True, + "contact_us_banner_json": { + "en": textwrap.dedent( + """ +

Get application support

+

+ Visit the My Community website + for information and guidance on applying to Community Ownership Fund. + Fill out the enquiry form + to request advice from My Community. +

+

+ We cannot provide direct support to applicants outside of this service. +

+

Get technical support

+

+ Contact the Department of Levelling Up, Housing and Communities funding team if you need + help with accessing or submitting an application form. +

+ """ + ), + "cy": textwrap.dedent( + """ +

Cael cymorth â'r cais

+

+ Ewch i wefan My Community + i gael gwybodaeth ac arweiniad ar wneud cais i'r Gronfa Perchnogaeth Gymunedol. + Llenwch y ffurflen ymholiad + i ofyn am gyngor gan My Community. +

+

+ Ni allwn ddarparu cymorth uniongyrchol i ymgeiswyr tu hwnt i'r gwasanaeth hwn. +

+

Cael cymorth technegol

+

+ Cysylltwch â thîm cyllid yr Adran Ffyniant Bro, Tai a Chymunedau os oes angen help arnoch i gael at ffurflen gais neu ei chyflwyno. +

+ """ + ), + }, + "contact_email": "COF@communities.gov.uk", + "contact_phone": None, + "contact_textphone": None, + "support_times": "9am to 5pm", + "support_days": "Monday to Friday", + "instructions_json": { + "en": ( + "You must have received an invitation to apply. If we did not invite you," + " first ' + " express your interest in the fund." + ), + "cy": ( + "You must have received an invitation to apply. If we did not invite you," + " first ' + " express your interest in the fund." + ), + }, + "feedback_link": ( + "https://forms.office.com/Pages/ResponsePage.aspx?id=" + "EGg0v32c3kOociSi7zmVqN48ORk8WN5LlJITE3Swt-lURUNCR0dHMjgxWFZOMTMxQzlOTVIxVkQ0Sy4u" + ), + "project_name_field_id": "KAgrBz", + "application_guidance_json": COF_APPLICATION_GUIDANCE, + "guidance_url": ( + "https://mhclg.sharepoint.com.mcas.ms/:w:/s/CommunityOwnershipFund" + "/Ecv3iM7U0AtKtyHnzRrQ9dsB0HdMPvHWqAoGn1WrWM7EMA?e=6QpdUT" + ), + "all_uploaded_documents_section_available": True, + "application_fields_download_available": False, + "display_logo_on_pdf_exports": False, + "mark_as_complete_enabled": False, + "is_expression_of_interest": False, + "feedback_survey_config": { + "has_feedback_survey": False, + "has_section_feedback": False, + "is_feedback_survey_optional": True, + "is_section_feedback_optional": True, + }, + "eligibility_config": {"has_eligibility": False}, + "eoi_decision_schema": None, + }, + { + "id": COF_ROUND_2_WINDOW_3_ID, + "title_json": { + "en": "Round 2 Window 3", + "cy": "Round 2 Window 3", + }, # TODO: Provide welsh translation + "short_name": "R2W3", + "opens": "2022-10-04 12:00:00", + "assessment_start": None, + "deadline": "2023-04-14 11:59:00", + "application_reminder_sent": True, + "reminder_date": None, + "fund_id": "47aef2f5-3fcb-4d45-acb5-f0152b5f03c4", + "assessment_deadline": "2023-05-17 12:00:00", + "prospectus": "https://www.gov.uk/government/publications/community-ownership-fund-prospectus", + "privacy_notice": ( + "https://www.gov.uk/government/publications/community-ownership-fund-privacy-notice/" + "community-ownership-fund-privacy-notice" + ), + "reference_contact_page_over_email": True, + "contact_us_banner_json": { + "en": textwrap.dedent( + """ +

Get application support

+

+ Visit the My Community website + for information and guidance on applying to Community Ownership Fund. + Fill out the enquiry form + to request advice from My Community. +

+

+ We cannot provide direct support to applicants outside of this service. +

+

Get technical support

+

+ Contact the Department of Levelling Up, Housing and Communities funding team if you need + help with accessing or submitting an application form. +

+ """ + ), + "cy": textwrap.dedent( + """ +

Cael cymorth â'r cais

+

+ Ewch i wefan My Community + i gael gwybodaeth ac arweiniad ar wneud cais i'r Gronfa Perchnogaeth Gymunedol. + Llenwch y ffurflen ymholiad + i ofyn am gyngor gan My Community. +

+

+ Ni allwn ddarparu cymorth uniongyrchol i ymgeiswyr tu hwnt i'r gwasanaeth hwn. +

+

Cael cymorth technegol

+

+ Cysylltwch â thîm cyllid yr Adran Ffyniant Bro, Tai a Chymunedau os oes angen help arnoch i gael at ffurflen gais neu ei chyflwyno. +

+ """ + ), + }, + "contact_email": "COF@communities.gov.uk", + "contact_phone": None, + "contact_textphone": None, + "support_times": "9am to 5pm", + "support_days": "Monday to Friday", + "instructions_json": { + "en": ( + "You must have received an invitation to apply. If we did not invite you," + " first ' + " express your interest in the fund." + ), + "cy": ( + "You must have received an invitation to apply. If we did not invite you," + " first ' + " express your interest in the fund." + ), + }, + "feedback_link": ( + "https://forms.office.com/Pages/ResponsePage.aspx?id=" + "EGg0v32c3kOociSi7zmVqFJBHpeOL2tNnpiwpdL2iElUREIySU9OWTU4R0RTNjhBUDE1Q1VYVFBEMi4u" + ), + "project_name_field_id": "KAgrBz", + "application_guidance_json": COF_APPLICATION_GUIDANCE, + "guidance_url": ( + "https://mhclg.sharepoint.com.mcas.ms/:w:/s/CommunityOwnershipFund" + "/Ecv3iM7U0AtKtyHnzRrQ9dsB0HdMPvHWqAoGn1WrWM7EMA?e=6QpdUT" + ), + "all_uploaded_documents_section_available": True, + "application_fields_download_available": False, + "display_logo_on_pdf_exports": False, + "mark_as_complete_enabled": False, + "is_expression_of_interest": False, + "feedback_survey_config": { + "has_feedback_survey": False, + "has_section_feedback": False, + "is_feedback_survey_optional": True, + "is_section_feedback_optional": True, + }, + "eligibility_config": {"has_eligibility": False}, + "eoi_decision_schema": None, + }, +] + +cof_r2_sections = [ + { + "section_name": { + "en": "1. About your organisation", + "cy": "1. Ynglŷn â'ch sefydliad", + }, + "tree_path": f"{APPLICATION_BASE_PATH}.1", + }, + { + "section_name": { + "en": "Organisation Information", + "cy": "Gwybodaeth am y sefydliad", + }, + "tree_path": f"{APPLICATION_BASE_PATH}.1.1", + "form_name_json": { + "en": "organisation-information", + "cy": "gwybodaeth-am-y-sefydliad", + }, + }, + { + "section_name": { + "en": "Applicant Information", + "cy": "Gwybodaeth am yr ymgeisydd", + }, + "tree_path": f"{APPLICATION_BASE_PATH}.1.2", + "form_name_json": { + "en": "applicant-information", + "cy": "gwybodaeth-am-y-ymgeisydd", + }, + }, + { + "section_name": { + "en": "2. About your project", + "cy": "2. Ynglŷn â'ch prosiect", + }, + "tree_path": f"{APPLICATION_BASE_PATH}.2", + }, + { + "section_name": {"en": "Project Information", "cy": "Gwybodaeth am y prosiect"}, + "tree_path": f"{APPLICATION_BASE_PATH}.2.1", + "form_name_json": { + "en": "project-information", + "cy": "gwybodaeth-am-y-prosiect", + }, + }, + { + "section_name": {"en": "Asset Information", "cy": "Gwybodaeth am yr ased"}, + "tree_path": f"{APPLICATION_BASE_PATH}.2.2", + "form_name_json": {"en": "asset-information", "cy": "gwybodaeth-am-yr-ased"}, + }, + { + "section_name": {"en": "3. Strategic case", "cy": "3. Achos strategol"}, + "tree_path": f"{APPLICATION_BASE_PATH}.3", + "weighting": 30, + }, + { + "section_name": {"en": "Community Use", "cy": "Defnydd Cymunedol"}, + "tree_path": f"{APPLICATION_BASE_PATH}.3.1", + "form_name_json": {"en": "community-use", "cy": "defnydd-cymunedol"}, + }, + { + "section_name": {"en": "Community Engagement", "cy": "Ymgysylltu â'r gymuned"}, + "tree_path": f"{APPLICATION_BASE_PATH}.3.2", + "form_name_json": { + "en": "community-engagement", + "cy": "ymgysylltu-a'r-gymuned", + }, + }, + { + "section_name": {"en": "Local Support", "cy": "Cefnogaeth leol"}, + "tree_path": f"{APPLICATION_BASE_PATH}.3.3", + "form_name_json": {"en": "local-support", "cy": "cefnogaeth-leol"}, + }, + { + "section_name": { + "en": "Environmental Sustainability", + "cy": "Cynaliadwyedd amgylcheddol", + }, + "tree_path": f"{APPLICATION_BASE_PATH}.3.4", + "form_name_json": { + "en": "environmental-sustainability", + "cy": "cynaliadwyedd-amgylcheddol", + }, + }, + { + "section_name": {"en": "4. Management case", "cy": "4. Achos rheoli"}, + "tree_path": f"{APPLICATION_BASE_PATH}.4", + "weighting": 30, + }, + { + "section_name": {"en": "Funding Required", "cy": "Cyllid sydd ei angen"}, + "tree_path": f"{APPLICATION_BASE_PATH}.4.1", + "form_name_json": {"en": "funding-required", "cy": "cyllid-sydd-ei-angen"}, + }, + { + "section_name": {"en": "Feasibility", "cy": "Dichonoldeb"}, + "tree_path": f"{APPLICATION_BASE_PATH}.4.2", + "form_name_json": {"en": "feasibility", "cy": "cynhwysiant-ac-integreiddio"}, + }, + { + "section_name": {"en": "Risk", "cy": "Risg"}, + "tree_path": f"{APPLICATION_BASE_PATH}.4.3", + "form_name_json": {"en": "risk", "cy": "risg"}, + }, + { + "section_name": {"en": "Project Costs", "cy": "Costau'r prosiect"}, + "tree_path": f"{APPLICATION_BASE_PATH}.4.4", + "form_name_json": {"en": "project-costs", "cy": "costau'r-prosiect"}, + }, + { + "section_name": {"en": "Skills And Resources", "cy": "Sgiliau ac Adnoddau"}, + "tree_path": f"{APPLICATION_BASE_PATH}.4.5", + "form_name_json": {"en": "skills-and-resources", "cy": "sgiliau-ac-adnoddau"}, + }, + { + "section_name": { + "en": "Community Representation", + "cy": "Cynrychiolaeth gymunedol", + }, + "tree_path": f"{APPLICATION_BASE_PATH}.4.6", + "form_name_json": { + "en": "community-representation", + "cy": "cynrychiolaeth-gymunedol", + }, + }, + { + "section_name": { + "en": "Inclusiveness And Integration", + "cy": "Cynhwysiant ac Integreiddio", + }, + "tree_path": f"{APPLICATION_BASE_PATH}.4.7", + "form_name_json": { + "en": "inclusiveness-and-integration", + "cy": "cynhwysiant-ac-integreiddio", + }, + }, + { + "section_name": { + "en": "Upload Business Plan", + "cy": "Lanlwythwch y cynllun busnes", + }, + "tree_path": f"{APPLICATION_BASE_PATH}.4.8", + "form_name_json": { + "en": "upload-business-plan", + "cy": "lanlwythwch-y-cynllun-busnes", + }, + }, + { + "section_name": { + "en": "5. Potential to deliver community benefits", + "cy": "5. Potensial i gyflawni buddion cymunedol", + }, + "tree_path": f"{APPLICATION_BASE_PATH}.5", + "weighting": 30, + }, + { + "section_name": {"en": "Community Benefits", "cy": "Buddion cymunedol"}, + "tree_path": f"{APPLICATION_BASE_PATH}.5.1", + "form_name_json": {"en": "community-benefits", "cy": "ymgysylltu-a'r-gymuned"}, + }, + { + "section_name": { + "en": "6. Added value to community", + "cy": "6. Gwerth ychwanegol i'r gymuned", + }, + "tree_path": f"{APPLICATION_BASE_PATH}.6", + "weighting": 10, + }, + { + "section_name": {"en": "Value To The Community", "cy": "Gwerth i'r Gymuned"}, + "tree_path": f"{APPLICATION_BASE_PATH}.6.1", + "form_name_json": {"en": "value-to-the-community", "cy": "gwerth-i'r-gymuned"}, + }, + { + "section_name": { + "en": "7. Subsidy control / state aid", + "cy": "7. Rheoli cymorthdaliadau a chymorth gwladwriaethol", + }, + "tree_path": f"{APPLICATION_BASE_PATH}.7", + }, + { + "section_name": {"en": "Project Qualification", "cy": "Cymhwystra'r prosiect"}, + "tree_path": f"{APPLICATION_BASE_PATH}.7.1", + "form_name_json": { + "en": "project-qualification", + "cy": "cymhwystra'r-prosiect", + }, + }, + { + "section_name": {"en": "8. Check declarations", "cy": "8. Gwirio datganiadau"}, + "tree_path": f"{APPLICATION_BASE_PATH}.8", + }, + { + "section_name": {"en": "Declarations", "cy": "Datganiadau"}, + "tree_path": f"{APPLICATION_BASE_PATH}.8.1", + "form_name_json": {"en": "declarations", "cy": "datganiadau"}, + }, +] diff --git a/fund_store/config/fund_loader_config/cof/cof_r3.py b/fund_store/config/fund_loader_config/cof/cof_r3.py new file mode 100644 index 000000000..3bd3e214c --- /dev/null +++ b/fund_store/config/fund_loader_config/cof/cof_r3.py @@ -0,0 +1,1022 @@ +# flake8: noqa +import textwrap +from datetime import datetime +from datetime import timezone + +from config.fund_loader_config.cof.shared import COF_APPLICATION_GUIDANCE +from config.fund_loader_config.cof.shared import fund_config +from config.fund_loader_config.common_fund_config.fund_base_tree_paths import ( + COF_R3_W1_BASE_PATH, +) +from config.fund_loader_config.common_fund_config.fund_base_tree_paths import ( + COF_R3_W2_BASE_PATH, +) +from config.fund_loader_config.common_fund_config.fund_base_tree_paths import ( + COF_R3_W3_BASE_PATH, +) + +COF_FUND_ID = fund_config["id"] +COF_ROUND_3_WINDOW_1_ID = "e85ad42f-73f5-4e1b-a1eb-6bc5d7f3d762" +COF_ROUND_3_WINDOW_2_ID = "6af19a5e-9cae-4f00-9194-cf10d2d7c8a7" +COF_ROUND_3_WINDOW_3_ID = "4efc3263-aefe-4071-b5f4-0910abec12d2" + +APPLICATION_BASE_PATH_COF_R3_W1 = ".".join([str(COF_R3_W1_BASE_PATH), str(1)]) +ASSESSMENT_BASE_PATH_COF_R3_W1 = ".".join([str(COF_R3_W1_BASE_PATH), str(2)]) + +APPLICATION_BASE_PATH_COF_R3_W2 = ".".join([str(COF_R3_W2_BASE_PATH), str(1)]) +ASSESSMENT_BASE_PATH_COF_R3_W2 = ".".join([str(COF_R3_W2_BASE_PATH), str(2)]) + +APPLICATION_BASE_PATH_COF_R3_W3 = ".".join([str(COF_R3_W3_BASE_PATH), str(1)]) +ASSESSMENT_BASE_PATH_COF_R3_W3 = ".".join([str(COF_R3_W3_BASE_PATH), str(2)]) + +COF_R3W1_OPENS_DATE = datetime(2023, 5, 31, 11, 0, 0, tzinfo=timezone.utc) # 2023-05-31 11:00:00 +COF_R3W1_DEADLINE_DATE = datetime(2023, 7, 12, 11, 59, 0, tzinfo=timezone.utc) # 2023-07-12 11:59:00 +COF_R3W1_ASSESSMENT_DEADLINE_DATE = datetime(2023, 8, 9, 12, 0, 0, tzinfo=timezone.utc) # 2023-08-09 12:00:00 +COF_R3W2_OPENS_DATE = datetime(2023, 8, 30, 11, 0, 0, tzinfo=timezone.utc) # 2023-08-30 11:00:00 +COF_R3W2_DEADLINE_DATE = datetime(2023, 10, 11, 11, 59, 0, tzinfo=timezone.utc) # 2023-10-11 11:59:00 +COF_R3W2_ASSESSMENT_DEADLINE_DATE = datetime(2023, 11, 20, 12, 0, 0, tzinfo=timezone.utc) # 2023-11-20 12:00:00 +COF_R3W3_OPENS_DATE = datetime(2023, 12, 6, 11, 00, 0, tzinfo=timezone.utc) # 2023-12-06 11:00:00 +COF_R3W3_SEND_REMINDER_DATE = datetime(2024, 1, 29, 11, 59, 0, tzinfo=timezone.utc) # 2024-01-29 11:59:00 +COF_R3W3_DEADLINE_DATE = datetime(2024, 1, 31, 11, 59, 0, tzinfo=timezone.utc) # 2024-01-31 11:59:00 +COF_R3W3_ASSESSMENT_DEADLINE_DATE = datetime(2024, 2, 23, 12, 0, 0, tzinfo=timezone.utc) # 2024-02-23 12:00:00 + +COF_EOI_OPENS_DATE = datetime(2024, 3, 6, 11, 00, 0, tzinfo=timezone.utc) # 2023-12-06 11:00:00 +COF_EOI_DEADLINE_DATE = datetime(2124, 3, 6, 11, 59, 0, tzinfo=timezone.utc) # 2124-03-06 11:59:00 +COF_EOI_ASSESSMENT_DEADLINE_DATE = datetime(2124, 3, 6, 12, 0, 0, tzinfo=timezone.utc) # 2124-03-06 12:00:00 +COF_EOI_SEND_REMINDER_DATE = datetime(2024, 3, 1, 11, 59, 0, tzinfo=timezone.utc) # 2024-03-1 11:59:00 + +cof_r3_sections = [ + { + "section_name": { + "en": "1. About your organisation", + "cy": "1. Ynglŷn â'ch sefydliad", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.1", + }, + { + "section_name": { + "en": "1.1 Organisation information", + "cy": "1.1 Gwybodaeth am y sefydliad", + }, + "form_name_json": { + "en": "organisation-information-cof-r3-w1", + "cy": "gwybodaeth-am-y-sefydliad-cof-r3-w1", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.1.1", + }, + { + "section_name": { + "en": "1.2 Applicant information", + "cy": "1.2 Gwybodaeth am yr ymgeisydd", + }, + "form_name_json": { + "en": "applicant-information-cof-r3-w1", + "cy": "gwybodaeth-am-yr-ymgeisydd-cof-r3-w1", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.1.2", + }, + { + "section_name": { + "en": "2. About your project", + "cy": "2. Ynglŷn â'ch prosiect", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.2", + }, + { + "section_name": { + "en": "2.1 Project information", + "cy": "2.1 Gwybodaeth am y prosiect", + }, + "form_name_json": { + "en": "project-information-cof-r3-w1", + "cy": "gwybodaeth-am-y-prosiect-cof-r3-w1", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.2.1", + }, + { + "section_name": { + "en": "2.2 Asset information", + "cy": "2.2 Gwybodaeth am yr ased", + }, + "form_name_json": { + "en": "asset-information-cof-r3-w1", + "cy": "gwybodaeth-am-yr-ased-cof-r3-w1", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.2.2", + }, + { + "section_name": {"en": "3. Strategic case", "cy": "3. Achos strategol"}, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.3", + "weighting": 53, + }, + { + "section_name": {"en": "3.1 Community use", "cy": "3.1 Defnydd cymunedol"}, + "form_name_json": { + "en": "community-use-cof-r3-w1", + "cy": "defnydd-cymunedol-cof-r3-w1", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.3.1", + }, + { + "section_name": { + "en": "3.2 Community engagement", + "cy": "3.2 Ymgysylltu â'r gymuned", + }, + "form_name_json": { + "en": "community-engagement-cof-r3-w1", + "cy": "ymgysylltiad-cymunedol-cof-r3-w1", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.3.2", + }, + { + "section_name": {"en": "3.3 Local support", "cy": "3.3 Cefnogaeth leol"}, + "form_name_json": { + "en": "local-support-cof-r3-w1", + "cy": "cefnogaeth-leol-cof-r3-w1", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.3.3", + }, + { + "section_name": { + "en": "3.4 Community benefits", + "cy": "3.4 Buddion i'r gymuned", + }, + "form_name_json": { + "en": "community-benefits-cof-r3-w1", + "cy": "buddion-cymunedol-cof-r3-w1", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.3.4", + }, + { + "section_name": { + "en": "3.5 Environmental sustainability", + "cy": "3.5 Cynaliadwyedd amgylcheddol", + }, + "form_name_json": { + "en": "environmental-sustainability-cof-r3-w1", + "cy": "cynaliadwyedd-amgylcheddol-cof-r3-w1", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.3.5", + }, + { + "section_name": {"en": "4. Management case", "cy": "4. Achos rheoli"}, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.4", + "weighting": 47, + }, + { + "section_name": { + "en": "4.1 Funding required", + "cy": "4.1 Cyllid sydd ei angen", + }, + "form_name_json": { + "en": "funding-required-cof-r3-w1", + "cy": "cyllid-sydd-ei-angen-cof-r3-w1", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.4.1", + }, + { + "section_name": {"en": "4.2 Feasibility", "cy": "4.2 Dichonoldeb"}, + "form_name_json": { + "en": "feasibility-cof-r3-w1", + "cy": "dichonoldeb-cof-r3-w1", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.4.2", + }, + { + "section_name": {"en": "4.3 Risk", "cy": "4.3 Risg"}, + "form_name_json": {"en": "risk-cof-r3-w1", "cy": "risg-cof-r3-w1"}, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.4.3", + }, + { + "section_name": {"en": "4.4 Operational costs", "cy": "4.4 Costau gweithredol"}, + "form_name_json": { + "en": "operational-costs-cof-r3-w1", + "cy": "costau-gweithredol-cof-r3-w1", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.4.4", + }, + { + "section_name": { + "en": "4.5 Skills and resources", + "cy": "4.5 Sgiliau ac Adnoddau", + }, + "form_name_json": { + "en": "skills-and-resources-cof-r3-w1", + "cy": "sgiliau-ac-adnoddau-cof-r3-w1", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.4.5", + }, + { + "section_name": { + "en": "4.6 Community representation", + "cy": "4.6 Cynrychiolaeth gymunedol", + }, + "form_name_json": { + "en": "community-representation-cof-r3-w1", + "cy": "cynrychiolaeth-gymunedol-cof-r3-w1", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.4.6", + }, + { + "section_name": { + "en": "4.7 Inclusiveness and integration", + "cy": "4.7 Cynhwysiant ac Integreiddio", + }, + "form_name_json": { + "en": "inclusiveness-and-integration-cof-r3-w1", + "cy": "cynhwysiant-ac-integreiddio-cof-r3-w1", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.4.7", + }, + { + "section_name": { + "en": "4.8 Upload business plan", + "cy": "4.8 Lanlwythwch y cynllun busnes", + }, + "form_name_json": { + "en": "upload-business-plan-cof-r3-w1", + "cy": "lanlwythwch-y-cynllun-busnes-cof-r3-w1", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.4.8", + }, + { + "section_name": { + "en": "5. Subsidy control and state aid", + "cy": "5. Rheoli cymorthdaliadau a chymorth gwladwriaethol", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.5", + }, + { + "section_name": { + "en": "5.1 Project qualification", + "cy": "5.1 Cymhwystra'r prosiect", + }, + "form_name_json": { + "en": "project-qualifications-cof-r3-w1", + "cy": "cymhwysedd-y-prosiect-cof-r3-w1", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.5.1", + }, + { + "section_name": {"en": "6. Check declarations", "cy": "6. Gwirio datganiadau"}, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.6", + }, + { + "section_name": {"en": "6.1 Declarations", "cy": "6.1 Datganiadau"}, + "form_name_json": { + "en": "declarations-cof-r3-w1", + "cy": "cadarnhadau-terfynol-cof-r3-w1", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W1}.6.1", + }, +] + +cof_r3w2_sections = [ + { + "section_name": { + "en": "1. About your organisation", + "cy": "1. Ynglŷn â'ch sefydliad", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.1", + "requires_feedback": True, + }, + { + "section_name": { + "en": "1.1 Organisation information", + "cy": "1.1 Gwybodaeth am y sefydliad", + }, + "form_name_json": { + "en": "organisation-information-cof-r3-w2", + "cy": "gwybodaeth-am-y-sefydliad-cof-r3-w2", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.1.1", + }, + { + "section_name": { + "en": "1.2 Applicant information", + "cy": "1.2 Gwybodaeth am yr ymgeisydd", + }, + "form_name_json": { + "en": "applicant-information-cof-r3-w2", + "cy": "gwybodaeth-am-yr-ymgeisydd-cof-r3-w2", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.1.2", + }, + { + "section_name": { + "en": "2. About your project", + "cy": "2. Ynglŷn â'ch prosiect", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.2", + "requires_feedback": True, + }, + { + "section_name": { + "en": "2.1 Project information", + "cy": "2.1 Gwybodaeth am y prosiect", + }, + "form_name_json": { + "en": "project-information-cof-r3-w2", + "cy": "gwybodaeth-am-y-prosiect-cof-r3-w2", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.2.1", + }, + { + "section_name": { + "en": "2.2 Asset information", + "cy": "2.2 Gwybodaeth am yr ased", + }, + "form_name_json": { + "en": "asset-information-cof-r3-w2", + "cy": "gwybodaeth-am-yr-ased-cof-r3-w2", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.2.2", + }, + { + "section_name": {"en": "3. Strategic case", "cy": "3. Achos strategol"}, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.3", + "requires_feedback": True, + "weighting": 53, + }, + { + "section_name": {"en": "3.1 Community use", "cy": "3.1 Defnydd cymunedol"}, + "form_name_json": { + "en": "community-use-cof-r3-w2", + "cy": "defnydd-cymunedol-cof-r3-w2", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.3.1", + }, + { + "section_name": { + "en": "3.2 Community engagement", + "cy": "3.2 Ymgysylltu â'r gymuned", + }, + "form_name_json": { + "en": "community-engagement-cof-r3-w2", + "cy": "ymgysylltiad-cymunedol-cof-r3-w2", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.3.2", + }, + { + "section_name": {"en": "3.3 Local support", "cy": "3.3 Cefnogaeth leol"}, + "form_name_json": { + "en": "local-support-cof-r3-w2", + "cy": "cefnogaeth-leol-cof-r3-w2", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.3.3", + }, + { + "section_name": { + "en": "3.4 Community benefits", + "cy": "3.4 Buddion i'r gymuned", + }, + "form_name_json": { + "en": "community-benefits-cof-r3-w2", + "cy": "buddion-cymunedol-cof-r3-w2", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.3.4", + }, + { + "section_name": { + "en": "3.5 Environmental sustainability", + "cy": "3.5 Cynaliadwyedd amgylcheddol", + }, + "form_name_json": { + "en": "environmental-sustainability-cof-r3-w2", + "cy": "cynaliadwyedd-amgylcheddol-cof-r3-w2", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.3.5", + }, + { + "section_name": {"en": "4. Management case", "cy": "4. Achos rheoli"}, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.4", + "weighting": 47, + "requires_feedback": True, + }, + { + "section_name": { + "en": "4.1 Funding required", + "cy": "4.1 Cyllid sydd ei angen", + }, + "form_name_json": { + "en": "funding-required-cof-r3-w2", + "cy": "cyllid-sydd-ei-angen-cof-r3-w2", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.4.1", + }, + { + "section_name": {"en": "4.2 Feasibility", "cy": "4.2 Dichonoldeb"}, + "form_name_json": { + "en": "feasibility-cof-r3-w2", + "cy": "dichonoldeb-cof-r3-w2", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.4.2", + }, + { + "section_name": {"en": "4.3 Risk", "cy": "4.3 Risg"}, + "form_name_json": {"en": "risk-cof-r3-w2", "cy": "risg-cof-r3-w2"}, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.4.3", + }, + { + "section_name": {"en": "4.4 Operational costs", "cy": "4.4 Costau gweithredol"}, + "form_name_json": { + "en": "operational-costs-cof-r3-w2", + "cy": "costau-gweithredol-cof-r3-w2", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.4.4", + }, + { + "section_name": { + "en": "4.5 Skills and resources", + "cy": "4.5 Sgiliau ac Adnoddau", + }, + "form_name_json": { + "en": "skills-and-resources-cof-r3-w2", + "cy": "sgiliau-ac-adnoddau-cof-r3-w2", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.4.5", + }, + { + "section_name": { + "en": "4.6 Community representation", + "cy": "4.6 Cynrychiolaeth gymunedol", + }, + "form_name_json": { + "en": "community-representation-cof-r3-w2", + "cy": "cynrychiolaeth-gymunedol-cof-r3-w2", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.4.6", + }, + { + "section_name": { + "en": "4.7 Inclusiveness and integration", + "cy": "4.7 Cynhwysiant ac Integreiddio", + }, + "form_name_json": { + "en": "inclusiveness-and-integration-cof-r3-w2", + "cy": "cynhwysiant-ac-integreiddio-cof-r3-w2", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.4.7", + }, + { + "section_name": { + "en": "4.8 Upload business plan", + "cy": "4.8 Lanlwythwch y cynllun busnes", + }, + "form_name_json": { + "en": "upload-business-plan-cof-r3-w2", + "cy": "lanlwythwch-y-cynllun-busnes-cof-r3-w2", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.4.8", + }, + { + "section_name": { + "en": "5. Subsidy control and state aid", + "cy": "5. Rheoli cymorthdaliadau a chymorth gwladwriaethol", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.5", + }, + { + "section_name": { + "en": "5.1 Project qualification", + "cy": "5.1 Cymhwystra'r prosiect", + }, + "form_name_json": { + "en": "project-qualifications-cof-r3-w2", + "cy": "cymhwysedd-y-prosiect-cof-r3-w2", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.5.1", + }, + { + "section_name": {"en": "6. Check declarations", "cy": "6. Gwirio datganiadau"}, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.6", + }, + { + "section_name": {"en": "6.1 Declarations", "cy": "6.1 Datganiadau"}, + "form_name_json": { + "en": "declarations-cof-r3-w2", + "cy": "cadarnhadau-terfynol-cof-r3-w2", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W2}.6.1", + }, +] + +cof_r3w3_sections = [ + { + "section_name": { + "en": "1. About your organisation", + "cy": "1. Ynglŷn â'ch sefydliad", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W3}.1", + "requires_feedback": True, + }, + { + "section_name": { + "en": "1.1 Organisation information", + "cy": "1.1 Gwybodaeth am y sefydliad", + }, + "form_name_json": { + "en": "organisation-information-cof", + "cy": "gwybodaeth-am-y-sefydliad-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W3}.1.1", + }, + { + "section_name": { + "en": "1.2 Applicant information", + "cy": "1.2 Gwybodaeth am yr ymgeisydd", + }, + "form_name_json": { + "en": "applicant-information-cof", + "cy": "gwybodaeth-am-yr-ymgeisydd-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W3}.1.2", + }, + { + "section_name": { + "en": "2. About your project", + "cy": "2. Ynglŷn â'ch prosiect", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W3}.2", + "requires_feedback": True, + }, + { + "section_name": { + "en": "2.1 Project information", + "cy": "2.1 Gwybodaeth am y prosiect", + }, + "form_name_json": { + "en": "project-information-cof", + "cy": "gwybodaeth-am-y-prosiect-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W3}.2.1", + }, + { + "section_name": { + "en": "2.2 Asset information", + "cy": "2.2 Gwybodaeth am yr ased", + }, + "form_name_json": { + "en": "asset-information-cof", + "cy": "gwybodaeth-am-yr-ased-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W3}.2.2", + }, + { + "section_name": {"en": "3. Strategic case", "cy": "3. Achos strategol"}, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W3}.3", + "requires_feedback": True, + "weighting": 53, + }, + { + "section_name": {"en": "3.1 Community use", "cy": "3.1 Defnydd cymunedol"}, + "form_name_json": { + "en": "community-use-cof", + "cy": "defnydd-cymunedol-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W3}.3.1", + }, + { + "section_name": { + "en": "3.2 Community engagement", + "cy": "3.2 Ymgysylltu â'r gymuned", + }, + "form_name_json": { + "en": "community-engagement-cof", + "cy": "ymgysylltiad-cymunedol-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W3}.3.2", + }, + { + "section_name": {"en": "3.3 Local support", "cy": "3.3 Cefnogaeth leol"}, + "form_name_json": { + "en": "local-support-cof", + "cy": "cefnogaeth-leol-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W3}.3.3", + }, + { + "section_name": { + "en": "3.4 Community benefits", + "cy": "3.4 Buddion cymunedol", + }, + "form_name_json": { + "en": "community-benefits-cof", + "cy": "buddion-cymunedol-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W3}.3.4", + }, + { + "section_name": { + "en": "3.5 Environmental sustainability", + "cy": "3.5 Cynaliadwyedd amgylcheddol", + }, + "form_name_json": { + "en": "environmental-sustainability-cof", + "cy": "cynaliadwyedd-amgylcheddol-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W3}.3.5", + }, + { + "section_name": {"en": "4. Management case", "cy": "4. Achos rheoli"}, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W3}.4", + "weighting": 47, + "requires_feedback": True, + }, + { + "section_name": { + "en": "4.1 Funding required", + "cy": "4.1 Cyllid sydd ei angen", + }, + "form_name_json": { + "en": "funding-required-cof", + "cy": "cyllid-sydd-ei-angen-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W3}.4.1", + }, + { + "section_name": {"en": "4.2 Feasibility", "cy": "4.2 Dichonoldeb"}, + "form_name_json": { + "en": "feasibility-cof", + "cy": "dichonoldeb-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W3}.4.2", + }, + { + "section_name": {"en": "4.3 Risk", "cy": "4.3 Risg"}, + "form_name_json": {"en": "risk-cof", "cy": "risg-cof"}, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W3}.4.3", + }, + { + "section_name": {"en": "4.4 Operational costs", "cy": "4.4 Costau gweithredol"}, + "form_name_json": { + "en": "operational-costs-cof", + "cy": "costau-gweithredol-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W3}.4.4", + }, + { + "section_name": { + "en": "4.5 Skills and resources", + "cy": "4.5 Sgiliau ac adnoddau", + }, + "form_name_json": { + "en": "skills-and-resources-cof", + "cy": "sgiliau-ac-adnoddau-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W3}.4.5", + }, + { + "section_name": { + "en": "4.6 Community representation", + "cy": "4.6 Cynrychiolaeth gymunedol", + }, + "form_name_json": { + "en": "community-representation-cof", + "cy": "cynrychiolaeth-gymunedol-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W3}.4.6", + }, + { + "section_name": { + "en": "4.7 Inclusiveness and integration", + "cy": "4.7 Cynhwysiant ac integreiddio", + }, + "form_name_json": { + "en": "inclusiveness-and-integration-cof", + "cy": "cynhwysiant-ac-integreiddio-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W3}.4.7", + }, + { + "section_name": { + "en": "4.8 Upload business plan", + "cy": "4.8 Lanlwythwch y cynllun busnes", + }, + "form_name_json": { + "en": "upload-business-plan-cof", + "cy": "lanlwythwch-y-cynllun-busnes-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W3}.4.8", + }, + { + "section_name": {"en": "5. Check declarations", "cy": "5. Gwirio datganiadau"}, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W3}.5", + }, + { + "section_name": {"en": "5.1 Declarations", "cy": "5.1 Datganiadau"}, + "form_name_json": { + "en": "declarations-cof", + "cy": "cadarnhadau-terfynol-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R3_W3}.5.1", + }, +] + +round_config = [ + { + "id": COF_ROUND_3_WINDOW_1_ID, + "fund_id": COF_FUND_ID, + "title_json": {"en": "Round 3 Window 1", "cy": "Rownd 3 Cyfnod Ymgeisio 1"}, + "short_name": "R3W1", + "opens": COF_R3W1_OPENS_DATE, + "assessment_start": None, + "deadline": COF_R3W1_DEADLINE_DATE, + "application_reminder_sent": True, + "reminder_date": None, + "assessment_deadline": COF_R3W1_ASSESSMENT_DEADLINE_DATE, + "prospectus": "https://www.gov.uk/government/publications/community-ownership-fund-prospectus", + "privacy_notice": ( + "https://www.gov.uk/government/publications/community-ownership-fund-" + "privacy-notice/community-ownership-fund-privacy-notice" + ), + "reference_contact_page_over_email": True, + "contact_us_banner_json": { + "en": textwrap.dedent( + """ +

Get application support

+

+ Visit the My Community website + for information and guidance on applying to Community Ownership Fund. + Fill out the enquiry form + to request advice from My Community. +

+

+ We cannot provide direct support to applicants outside of this service. +

+

Get technical support

+

+ Contact the Department of Levelling Up, Housing and Communities funding team if you need + help with accessing or submitting an application form. +

+ """ + ), + "cy": textwrap.dedent( + """ +

Cael cymorth â'r cais

+

+ Ewch i wefan My Community + i gael gwybodaeth ac arweiniad ar wneud cais i'r Gronfa Perchnogaeth Gymunedol. + Llenwch y ffurflen ymholiad + i ofyn am gyngor gan My Community. +

+

+ Ni allwn ddarparu cymorth uniongyrchol i ymgeiswyr tu hwnt i'r gwasanaeth hwn. +

+

Cael cymorth technegol

+

+ Cysylltwch â thîm cyllid yr Adran Ffyniant Bro, Tai a Chymunedau os oes angen help arnoch i gael at ffurflen gais neu ei chyflwyno. +

+ """ + ), + }, + "contact_email": "COF@communities.gov.uk", + "contact_phone": None, + "contact_textphone": None, + "support_times": "9am to 5pm", + "support_days": "Monday to Friday", + "instructions_json": { + "en": ( + "You must have received an invitation to apply. If we did not invite you," + " first ' + " express your interest in the fund." + ), + "cy": ( + "You must have received an invitation to apply. If we did not invite you," + " first ' + " express your interest in the fund." + ), + }, + "feedback_link": ( + "https://forms.office.com/Pages/ResponsePage.aspx?id=" + "EGg0v32c3kOociSi7zmVqFJBHpeOL2tNnpiwpdL2iElURUY1WkhaS0NFMlZVQUhYQ1NaN0E4RjlQMC4u" + ), + "project_name_field_id": "apGjFS", + "application_guidance_json": COF_APPLICATION_GUIDANCE, + "guidance_url": ( + "https://www.gov.uk/government/publications/community-ownership-fund-round-3-application-form" + "-assessment-criteria-guidance" + ), + "all_uploaded_documents_section_available": True, + "application_fields_download_available": True, + "display_logo_on_pdf_exports": False, + "mark_as_complete_enabled": False, + "is_expression_of_interest": False, + "feedback_survey_config": { + "has_feedback_survey": False, + "has_section_feedback": False, + "is_feedback_survey_optional": True, + "is_section_feedback_optional": True, + }, + "eligibility_config": {"has_eligibility": False}, + "eoi_decision_schema": None, + } +] + +round_config_w2 = [ + { + "id": COF_ROUND_3_WINDOW_2_ID, + "fund_id": COF_FUND_ID, + "title_json": {"en": "Round 3 Window 2", "cy": "Rownd 3 Cyfnod Ymgeisio 2"}, + "short_name": "R3W2", + "opens": COF_R3W2_OPENS_DATE, + "assessment_start": None, + "deadline": COF_R3W2_DEADLINE_DATE, + "application_reminder_sent": True, + "reminder_date": None, + "assessment_deadline": COF_R3W2_ASSESSMENT_DEADLINE_DATE, + "prospectus": "https://www.gov.uk/government/publications/community-ownership-fund-prospectus", + "privacy_notice": ( + "https://www.gov.uk/government/publications/community-ownership-fund-" + "privacy-notice/community-ownership-fund-privacy-notice" + ), + "reference_contact_page_over_email": True, + "contact_us_banner_json": { + "en": textwrap.dedent( + """ +

Get application support

+

+ Visit the My Community website + for information and guidance on applying to Community Ownership Fund. + Fill out the enquiry form + to request advice from My Community. +

+

+ We cannot provide direct support to applicants outside of this service. +

+

Get technical support

+

+ Contact the Department of Levelling Up, Housing and Communities funding team if you need + help with accessing or submitting an application form. +

+ """ + ), + "cy": textwrap.dedent( + """ +

Cael cymorth â'r cais

+

+ Ewch i wefan My Community + i gael gwybodaeth ac arweiniad ar wneud cais i'r Gronfa Perchnogaeth Gymunedol. + Llenwch y ffurflen ymholiad + i ofyn am gyngor gan My Community. +

+

+ Ni allwn ddarparu cymorth uniongyrchol i ymgeiswyr tu hwnt i'r gwasanaeth hwn. +

+

Cael cymorth technegol

+

+ Cysylltwch â thîm cyllid yr Adran Ffyniant Bro, Tai a Chymunedau os oes angen help arnoch i gael at ffurflen gais neu ei chyflwyno. +

+ """ + ), + }, + "contact_email": "COF@communities.gov.uk", + "contact_phone": None, + "contact_textphone": None, + "support_times": "9am to 5pm", + "support_days": "Monday to Friday", + "instructions_json": { + "en": ( + "You must have received an invitation to apply. If we did not invite you," + " first ' + " express your interest in the fund." + ), + "cy": ( + "You must have received an invitation to apply. If we did not invite you," + " first ' + " express your interest in the fund." + ), + }, + "feedback_link": ( + "https://forms.office.com/Pages/ResponsePage.aspx?id=" + "EGg0v32c3kOociSi7zmVqFJBHpeOL2tNnpiwpdL2iElURUY1WkhaS0NFMlZVQUhYQ1NaN0E4RjlQMC4u" + ), + "project_name_field_id": "apGjFS", + "application_guidance_json": COF_APPLICATION_GUIDANCE, + "guidance_url": ( + "https://www.gov.uk/government/publications/community-ownership-fund-round-3-application-form" + "-assessment-criteria-guidance" + ), + "all_uploaded_documents_section_available": True, + "application_fields_download_available": True, + "display_logo_on_pdf_exports": False, + "mark_as_complete_enabled": False, + "is_expression_of_interest": False, + "feedback_survey_config": { + "has_feedback_survey": True, + "has_section_feedback": True, + "is_feedback_survey_optional": False, + "is_section_feedback_optional": False, + }, + "eligibility_config": {"has_eligibility": False}, + "eoi_decision_schema": None, + }, +] + +round_config_w3 = [ + { + "id": COF_ROUND_3_WINDOW_3_ID, + "fund_id": COF_FUND_ID, + "title_json": {"en": "Round 3 Window 3", "cy": "Rownd 3 Cyfnod Ymgeisio 3"}, + "short_name": "R3W3", + "opens": COF_R3W3_OPENS_DATE, + "assessment_start": None, + "deadline": COF_R3W3_DEADLINE_DATE, + "application_reminder_sent": False, + "reminder_date": COF_R3W3_SEND_REMINDER_DATE, + "assessment_deadline": COF_R3W3_ASSESSMENT_DEADLINE_DATE, + "prospectus": "https://www.gov.uk/government/publications/community-ownership-fund-prospectus", + "privacy_notice": ( + "https://www.gov.uk/government/publications/community-ownership-fund-" + "privacy-notice/community-ownership-fund-privacy-notice" + ), + "reference_contact_page_over_email": True, + "contact_us_banner_json": { + "en": textwrap.dedent( + """ +

Get application support

+

+ Visit the My Community website + for information and guidance on applying to Community Ownership Fund. + Fill out the enquiry form + to request advice from My Community. +

+

+ We cannot provide direct support to applicants outside of this service. +

+

Get technical support

+

+ Contact the Department of Levelling Up, Housing and Communities funding team if you need + help with accessing or submitting an application form. +

+ """ + ), + "cy": textwrap.dedent( + """ +

Cael cymorth â'r cais

+

+ Ewch i wefan My Community + i gael gwybodaeth ac arweiniad ar wneud cais i'r Gronfa Perchnogaeth Gymunedol. + Llenwch y ffurflen ymholiad + i ofyn am gyngor gan My Community. +

+

+ Ni allwn ddarparu cymorth uniongyrchol i ymgeiswyr tu hwnt i'r gwasanaeth hwn. +

+

Cael cymorth technegol

+

+ Cysylltwch â thîm cyllid yr Adran Ffyniant Bro, Tai a Chymunedau os oes angen help arnoch i gael at ffurflen gais neu ei chyflwyno. +

+ """ + ), + }, + "contact_email": "COF@communities.gov.uk", + "contact_phone": None, + "contact_textphone": None, + "support_times": "9am to 5pm", + "support_days": "Monday to Friday", + "instructions_json": { + "en": ( + "You must have received an invitation to apply. If we did not invite you," + " first ' + " express your interest in the fund." + ), + "cy": ( + "You must have received an invitation to apply. If we did not invite you," + " first ' + " express your interest in the fund." + ), + }, + "feedback_link": ( + "https://forms.office.com/Pages/ResponsePage.aspx?id=" + "EGg0v32c3kOociSi7zmVqFJBHpeOL2tNnpiwpdL2iElURUY1WkhaS0NFMlZVQUhYQ1NaN0E4RjlQMC4u" + ), + "project_name_field_id": "apGjFS", + "application_guidance_json": COF_APPLICATION_GUIDANCE, + "guidance_url": ( + "https://www.gov.uk/government/publications/community-ownership-fund-round-3-application-form" + "-assessment-criteria-guidance" + ), + "all_uploaded_documents_section_available": True, + "application_fields_download_available": True, + "display_logo_on_pdf_exports": False, + "mark_as_complete_enabled": True, + "is_expression_of_interest": False, + "feedback_survey_config": { + "has_feedback_survey": True, + "has_section_feedback": True, + "is_feedback_survey_optional": False, + "is_section_feedback_optional": False, + }, + "eligibility_config": {"has_eligibility": False}, + "eoi_decision_schema": None, + } +] diff --git a/fund_store/config/fund_loader_config/cof/cof_r4.py b/fund_store/config/fund_loader_config/cof/cof_r4.py new file mode 100644 index 000000000..a9ae685cc --- /dev/null +++ b/fund_store/config/fund_loader_config/cof/cof_r4.py @@ -0,0 +1,670 @@ +# flake8: noqa +import textwrap +from datetime import datetime +from datetime import timezone + +from config.fund_loader_config.cof.shared import COF_APPLICATION_GUIDANCE +from config.fund_loader_config.cof.shared import fund_config +from config.fund_loader_config.common_fund_config.fund_base_tree_paths import ( + COF_R4_W1_BASE_PATH, +) +from config.fund_loader_config.common_fund_config.fund_base_tree_paths import ( + COF_R4_W2_BASE_PATH, +) + +COF_FUND_ID = fund_config["id"] +COF_ROUND_4_WINDOW_1_ID = "33726b63-efce-4749-b149-20351346c76e" +COF_ROUND_4_WINDOW_2_ID = "27ab26c2-e58e-4bfe-917d-64be10d16496" + +APPLICATION_BASE_PATH_COF_R4_W1 = ".".join([str(COF_R4_W1_BASE_PATH), str(1)]) +ASSESSMENT_BASE_PATH_COF_R4_W1 = ".".join([str(COF_R4_W1_BASE_PATH), str(2)]) + +APPLICATION_BASE_PATH_COF_R4_W2 = ".".join([str(COF_R4_W2_BASE_PATH), str(1)]) +ASSESSMENT_BASE_PATH_COF_R4_W2 = ".".join([str(COF_R4_W2_BASE_PATH), str(2)]) + + +COF_R4W1_OPENS_DATE = datetime(2024, 3, 25, 14, 00, 0, tzinfo=timezone.utc) # 2024-03-25 14:00:00 +COF_R4W1_SEND_REMINDER_DATE = datetime(2024, 4, 8, 11, 59, 0, tzinfo=timezone.utc) # 2024-04-08 11:59:00 +COF_R4W1_DEADLINE_DATE = datetime(2024, 4, 10, 14, 00, 0, tzinfo=timezone.utc) # 2024-04-10 14:00:00 +COF_R4W1_ASSESSMENT_DEADLINE_DATE = datetime(2024, 6, 23, 12, 0, 0, tzinfo=timezone.utc) # 2024-06-23 12:00:00 + + +COF_R4W2_OPENS_DATE = datetime(2024, 5, 22, 14, 00, 0, tzinfo=timezone.utc) # 2024-05-22 14:00:00 +COF_R4W2_SEND_REMINDER_DATE = datetime(2024, 5, 22, 11, 59, 0, tzinfo=timezone.utc) # 2024-05-22 11:59:00 +COF_R4W2_DEADLINE_DATE = datetime(2024, 6, 26, 14, 00, 0, tzinfo=timezone.utc) # 2024-06-26 14:00:00 +COF_R4W2_ASSESSMENT_START_DATE = datetime(2024, 5, 22, 14, 00, 0, tzinfo=timezone.utc) # 2024-05-22 14:00:00 +COF_R4W2_ASSESSMENT_DEADLINE_DATE = datetime(2024, 7, 31, 12, 0, 0, tzinfo=timezone.utc) # 2024-07-31 12:00:00 + +# Date far in the future as a temporary measure to stop this going live by accident +COF_R4W2_HOLD_DATE = datetime(2124, 1, 1, 11, 59, 0) # 2124-01-01 11:59:00 + + +cof_r4w1_sections = [ + { + "section_name": { + "en": "1. About your organisation", + "cy": "1. Ynglŷn â'ch sefydliad", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W1}.1", + "requires_feedback": True, + }, + { + "section_name": { + "en": "1.1 Organisation information", + "cy": "1.1 Gwybodaeth am y sefydliad", + }, + "form_name_json": { + "en": "organisation-information-cof", + "cy": "gwybodaeth-am-y-sefydliad-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W1}.1.1", + }, + { + "section_name": { + "en": "1.2 Applicant information", + "cy": "1.2 Gwybodaeth am yr ymgeisydd", + }, + "form_name_json": { + "en": "applicant-information-cof", + "cy": "gwybodaeth-am-yr-ymgeisydd-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W1}.1.2", + }, + { + "section_name": { + "en": "2. About your project", + "cy": "2. Ynglŷn â'ch prosiect", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W1}.2", + "requires_feedback": True, + }, + { + "section_name": { + "en": "2.1 Project information", + "cy": "2.1 Gwybodaeth am y prosiect", + }, + "form_name_json": { + "en": "project-information-cof", + "cy": "gwybodaeth-am-y-prosiect-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W1}.2.1", + }, + { + "section_name": { + "en": "2.2 Asset information", + "cy": "2.2 Gwybodaeth am yr ased", + }, + "form_name_json": { + "en": "asset-information-cof", + "cy": "gwybodaeth-am-yr-ased-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W1}.2.2", + }, + { + "section_name": {"en": "3. Strategic case", "cy": "3. Achos strategol"}, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W1}.3", + "requires_feedback": True, + "weighting": 53, + }, + { + "section_name": {"en": "3.1 Community use/significance", "cy": "3.1 Defnydd/arwyddocâd cymunedol"}, + "form_name_json": { + "en": "community-use-cof", + "cy": "defnydd-cymunedol-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W1}.3.1", + }, + { + "section_name": { + "en": "3.2 Community engagement", + "cy": "3.2 Ymgysylltu â'r gymuned", + }, + "form_name_json": { + "en": "community-engagement-cof", + "cy": "ymgysylltiad-cymunedol-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W1}.3.2", + }, + { + "section_name": {"en": "3.3 Local support", "cy": "3.3 Cefnogaeth leol"}, + "form_name_json": { + "en": "local-support-cof", + "cy": "cefnogaeth-leol-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W1}.3.3", + }, + { + "section_name": { + "en": "3.4 Community benefits", + "cy": "3.4 Buddion cymunedol", + }, + "form_name_json": { + "en": "community-benefits-cof", + "cy": "buddion-cymunedol-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W1}.3.4", + }, + { + "section_name": { + "en": "3.5 Environmental sustainability", + "cy": "3.5 Cynaliadwyedd amgylcheddol", + }, + "form_name_json": { + "en": "environmental-sustainability-cof", + "cy": "cynaliadwyedd-amgylcheddol-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W1}.3.5", + }, + { + "section_name": {"en": "4. Management case", "cy": "4. Achos rheoli"}, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W1}.4", + "weighting": 47, + "requires_feedback": True, + }, + { + "section_name": { + "en": "4.1 Funding required", + "cy": "4.1 Cyllid sydd ei angen", + }, + "form_name_json": { + "en": "funding-required-cof", + "cy": "cyllid-sydd-ei-angen-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W1}.4.1", + }, + { + "section_name": {"en": "4.2 Feasibility", "cy": "4.2 Dichonoldeb"}, + "form_name_json": { + "en": "feasibility-cof", + "cy": "dichonoldeb-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W1}.4.2", + }, + { + "section_name": {"en": "4.3 Risk", "cy": "4.3 Risg"}, + "form_name_json": {"en": "risk-cof", "cy": "risg-cof"}, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W1}.4.3", + }, + { + "section_name": {"en": "4.4 Operational costs", "cy": "4.4 Costau gweithredol"}, + "form_name_json": { + "en": "operational-costs-cof", + "cy": "costau-gweithredol-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W1}.4.4", + }, + { + "section_name": { + "en": "4.5 Skills and resources", + "cy": "4.5 Sgiliau ac adnoddau", + }, + "form_name_json": { + "en": "skills-and-resources-cof", + "cy": "sgiliau-ac-adnoddau-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W1}.4.5", + }, + { + "section_name": { + "en": "4.6 Community representation", + "cy": "4.6 Cynrychiolaeth gymunedol", + }, + "form_name_json": { + "en": "community-representation-cof", + "cy": "cynrychiolaeth-gymunedol-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W1}.4.6", + }, + { + "section_name": { + "en": "4.7 Inclusiveness and integration", + "cy": "4.7 Cynhwysiant ac integreiddio", + }, + "form_name_json": { + "en": "inclusiveness-and-integration-cof", + "cy": "cynhwysiant-ac-integreiddio-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W1}.4.7", + }, + { + "section_name": { + "en": "4.8 Upload business plan", + "cy": "4.8 Lanlwythwch y cynllun busnes", + }, + "form_name_json": { + "en": "upload-business-plan-cof", + "cy": "lanlwythwch-y-cynllun-busnes-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W1}.4.8", + }, + { + "section_name": {"en": "5. Check declarations", "cy": "5. Gwirio datganiadau"}, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W1}.5", + }, + { + "section_name": {"en": "5.1 Declarations", "cy": "5.1 Datganiadau"}, + "form_name_json": { + "en": "declarations-cof", + "cy": "cadarnhadau-terfynol-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W1}.5.1", + }, +] + +cof_r4w2_sections = [ + { + "section_name": { + "en": "1. About your organisation", + "cy": "1. Ynglŷn â'ch sefydliad", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W2}.1", + "requires_feedback": True, + }, + { + "section_name": { + "en": "1.1 Organisation information", + "cy": "1.1 Gwybodaeth am y sefydliad", + }, + "form_name_json": { + "en": "organisation-information-cof", + "cy": "gwybodaeth-am-y-sefydliad-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W2}.1.1", + }, + { + "section_name": { + "en": "1.2 Applicant information", + "cy": "1.2 Gwybodaeth am yr ymgeisydd", + }, + "form_name_json": { + "en": "applicant-information-cof", + "cy": "gwybodaeth-am-yr-ymgeisydd-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W2}.1.2", + }, + { + "section_name": { + "en": "2. About your project", + "cy": "2. Ynglŷn â'ch prosiect", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W2}.2", + "requires_feedback": True, + }, + { + "section_name": { + "en": "2.1 Project information", + "cy": "2.1 Gwybodaeth am y prosiect", + }, + "form_name_json": { + "en": "project-information-cof", + "cy": "gwybodaeth-am-y-prosiect-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W2}.2.1", + }, + { + "section_name": { + "en": "2.2 Asset information", + "cy": "2.2 Gwybodaeth am yr ased", + }, + "form_name_json": { + "en": "asset-information-cof", + "cy": "gwybodaeth-am-yr-ased-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W2}.2.2", + }, + { + "section_name": {"en": "3. Strategic case", "cy": "3. Achos strategol"}, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W2}.3", + "requires_feedback": True, + "weighting": 53, + }, + { + "section_name": {"en": "3.1 Community use/significance", "cy": "3.1 Defnydd/arwyddocâd cymunedol"}, + "form_name_json": { + "en": "community-use-cof", + "cy": "defnydd-cymunedol-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W2}.3.1", + }, + { + "section_name": { + "en": "3.2 Community engagement", + "cy": "3.2 Ymgysylltu â'r gymuned", + }, + "form_name_json": { + "en": "community-engagement-cof", + "cy": "ymgysylltiad-cymunedol-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W2}.3.2", + }, + { + "section_name": {"en": "3.3 Local support", "cy": "3.3 Cefnogaeth leol"}, + "form_name_json": { + "en": "local-support-cof", + "cy": "cefnogaeth-leol-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W2}.3.3", + }, + { + "section_name": { + "en": "3.4 Community benefits", + "cy": "3.4 Buddion cymunedol", + }, + "form_name_json": { + "en": "community-benefits-cof", + "cy": "buddion-cymunedol-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W2}.3.4", + }, + { + "section_name": { + "en": "3.5 Environmental sustainability", + "cy": "3.5 Cynaliadwyedd amgylcheddol", + }, + "form_name_json": { + "en": "environmental-sustainability-cof", + "cy": "cynaliadwyedd-amgylcheddol-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W2}.3.5", + }, + { + "section_name": {"en": "4. Management case", "cy": "4. Achos rheoli"}, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W2}.4", + "weighting": 47, + "requires_feedback": True, + }, + { + "section_name": { + "en": "4.1 Funding required", + "cy": "4.1 Cyllid sydd ei angen", + }, + "form_name_json": { + "en": "funding-required-cof", + "cy": "cyllid-sydd-ei-angen-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W2}.4.1", + }, + { + "section_name": {"en": "4.2 Feasibility", "cy": "4.2 Dichonoldeb"}, + "form_name_json": { + "en": "feasibility-cof", + "cy": "dichonoldeb-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W2}.4.2", + }, + { + "section_name": {"en": "4.3 Risk", "cy": "4.3 Risg"}, + "form_name_json": {"en": "risk-cof", "cy": "risg-cof"}, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W2}.4.3", + }, + { + "section_name": {"en": "4.4 Operational costs", "cy": "4.4 Costau gweithredol"}, + "form_name_json": { + "en": "operational-costs-cof", + "cy": "costau-gweithredol-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W2}.4.4", + }, + { + "section_name": { + "en": "4.5 Skills and resources", + "cy": "4.5 Sgiliau ac adnoddau", + }, + "form_name_json": { + "en": "skills-and-resources-cof", + "cy": "sgiliau-ac-adnoddau-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W2}.4.5", + }, + { + "section_name": { + "en": "4.6 Community representation", + "cy": "4.6 Cynrychiolaeth gymunedol", + }, + "form_name_json": { + "en": "community-representation-cof", + "cy": "cynrychiolaeth-gymunedol-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W2}.4.6", + }, + { + "section_name": { + "en": "4.7 Inclusiveness and integration", + "cy": "4.7 Cynhwysiant ac integreiddio", + }, + "form_name_json": { + "en": "inclusiveness-and-integration-cof", + "cy": "cynhwysiant-ac-integreiddio-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W2}.4.7", + }, + { + "section_name": { + "en": "4.8 Upload business plan", + "cy": "4.8 Lanlwythwch y cynllun busnes", + }, + "form_name_json": { + "en": "upload-business-plan-cof", + "cy": "lanlwythwch-y-cynllun-busnes-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W2}.4.8", + }, + { + "section_name": {"en": "5. Check declarations", "cy": "5. Gwirio datganiadau"}, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W2}.5", + }, + { + "section_name": {"en": "5.1 Declarations", "cy": "5.1 Datganiadau"}, + "form_name_json": { + "en": "declarations-cof", + "cy": "cadarnhadau-terfynol-cof", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_R4_W2}.5.1", + }, +] + +round_config_w1 = [ + { + "id": COF_ROUND_4_WINDOW_1_ID, + "fund_id": COF_FUND_ID, + "title_json": {"en": "Round 4 Window 1", "cy": "Rownd 4 Cyfnod Ymgeisio 1"}, + "short_name": "R4W1", + "opens": COF_R4W1_OPENS_DATE, + "assessment_start": None, + "deadline": COF_R4W1_DEADLINE_DATE, + "application_reminder_sent": False, + "reminder_date": COF_R4W1_SEND_REMINDER_DATE, + "assessment_deadline": COF_R4W1_ASSESSMENT_DEADLINE_DATE, + "prospectus": "https://www.gov.uk/government/publications/community-ownership-fund-prospectus", + "privacy_notice": ( + "https://www.gov.uk/government/publications/community-ownership-fund-" + "privacy-notice/community-ownership-fund-privacy-notice" + ), + "reference_contact_page_over_email": True, + "contact_us_banner_json": { + "en": textwrap.dedent( + """ +

Get application support

+

+ Visit the My Community website + for information and guidance on applying to Community Ownership Fund. + Fill out the enquiry form + to request advice from My Community. +

+

+ We cannot provide direct support to applicants outside of this service. +

+

Get technical support

+

+ Contact the Department of Levelling Up, Housing and Communities funding team if you need + help with accessing or submitting an application form. +

+ """ + ), + "cy": textwrap.dedent( + """ +

Cael cymorth â'r cais

+

+ Ewch i wefan My Community + i gael gwybodaeth ac arweiniad ar wneud cais i'r Gronfa Perchnogaeth Gymunedol. + Llenwch y ffurflen ymholiad + i ofyn am gyngor gan My Community. +

+

+ Ni allwn ddarparu cymorth uniongyrchol i ymgeiswyr tu hwnt i'r gwasanaeth hwn. +

+

Cael cymorth technegol

+

+ Cysylltwch â thîm cyllid yr Adran Ffyniant Bro, Tai a Chymunedau os oes angen help arnoch i gael at ffurflen gais neu ei chyflwyno. +

+ """ + ), + }, + "contact_email": "COF@communities.gov.uk", + "contact_phone": None, + "contact_textphone": None, + "support_times": "9am to 5pm", + "support_days": "Monday to Friday", + "instructions_json": { + "en": ( + "You must have received an invitation to apply. If we did not invite you," + " first ' + " express your interest in the fund." + ), + "cy": ( + "Mae'n rhaid i chi fod wedi derbyn gwahoddiad i ymgeisio. Os na wnaethom eich gwahodd, ' + " mynegwch eich diddordeb yn y gronfa yn gyntaf." + ), + }, + "feedback_link": ( + "https://forms.office.com/Pages/ResponsePage.aspx?id=" + "EGg0v32c3kOociSi7zmVqFJBHpeOL2tNnpiwpdL2iElURUY1WkhaS0NFMlZVQUhYQ1NaN0E4RjlQMC4u" + ), + "project_name_field_id": "apGjFS", + "application_guidance_json": COF_APPLICATION_GUIDANCE, + "guidance_url": ( + "https://www.gov.uk/government/publications/community-ownership-fund-round-3-application-form" + "-assessment-criteria-guidance" + ), + "all_uploaded_documents_section_available": True, + "application_fields_download_available": True, + "display_logo_on_pdf_exports": False, + "mark_as_complete_enabled": True, + "is_expression_of_interest": False, + "feedback_survey_config": { + "has_feedback_survey": True, + "has_section_feedback": True, + "is_feedback_survey_optional": False, + "is_section_feedback_optional": False, + }, + "eligibility_config": {"has_eligibility": False}, + "eoi_decision_schema": None, + } +] + +round_config_w2 = [ + { + "id": COF_ROUND_4_WINDOW_2_ID, + "fund_id": COF_FUND_ID, + "title_json": {"en": "Round 4 Window 2", "cy": "Rownd 4 Cyfnod Ymgeisio 2"}, + "short_name": "R4W2", + "opens": COF_R4W2_HOLD_DATE, + "assessment_start": COF_R4W2_HOLD_DATE, + "deadline": COF_R4W2_HOLD_DATE, + "application_reminder_sent": False, + "reminder_date": COF_R4W2_HOLD_DATE, + "assessment_deadline": COF_R4W2_HOLD_DATE, + "prospectus": "https://www.gov.uk/government/publications/community-ownership-fund-prospectus", + "privacy_notice": ( + "https://www.gov.uk/government/publications/community-ownership-fund-" + "privacy-notice/community-ownership-fund-privacy-notice" + ), + "reference_contact_page_over_email": True, + "contact_us_banner_json": { + "en": textwrap.dedent( + """ +

Get application support

+

+ Visit the My Community website + for information and guidance on applying to Community Ownership Fund. + Fill out the enquiry form + to request advice from My Community. +

+

+ We cannot provide direct support to applicants outside of this service. +

+

Get technical support

+

+ Contact the Department of Levelling Up, Housing and Communities funding team if you need + help with accessing or submitting an application form. +

+ """ + ), + "cy": textwrap.dedent( + """ +

Cael cymorth â'r cais

+

+ Ewch i wefan My Community + i gael gwybodaeth ac arweiniad ar wneud cais i'r Gronfa Perchnogaeth Gymunedol. + Llenwch y ffurflen ymholiad + i ofyn am gyngor gan My Community. +

+

+ Ni allwn ddarparu cymorth uniongyrchol i ymgeiswyr tu hwnt i'r gwasanaeth hwn. +

+

Cael cymorth technegol

+

+ Cysylltwch â thîm cyllid yr Adran Ffyniant Bro, Tai a Chymunedau os oes angen help arnoch i gael at ffurflen gais neu ei chyflwyno. +

+ """ + ), + }, + "contact_email": "COF@communities.gov.uk", + "contact_phone": None, + "contact_textphone": None, + "support_times": "9am to 5pm", + "support_days": "Monday to Friday", + "instructions_json": { + "en": ( + "You must have received an invitation to apply. If we did not invite you," + " first ' + " express your interest in the fund." + ), + "cy": ( + "Mae'n rhaid i chi fod wedi derbyn gwahoddiad i ymgeisio. Os na wnaethom eich gwahodd, ' + " mynegwch eich diddordeb yn y gronfa yn gyntaf." + ), + }, + "feedback_link": ( + "https://forms.office.com/Pages/ResponsePage.aspx?id=" + "EGg0v32c3kOociSi7zmVqFJBHpeOL2tNnpiwpdL2iElURUY1WkhaS0NFMlZVQUhYQ1NaN0E4RjlQMC4u" + ), + "project_name_field_id": "apGjFS", + "application_guidance_json": COF_APPLICATION_GUIDANCE, + "guidance_url": ( + "https://www.gov.uk/government/publications/community-ownership-fund-round-3-application-form" + "-assessment-criteria-guidance" + ), + "all_uploaded_documents_section_available": True, + "application_fields_download_available": True, + "display_logo_on_pdf_exports": False, + "mark_as_complete_enabled": True, + "is_expression_of_interest": False, + "feedback_survey_config": { + "has_feedback_survey": True, + "has_section_feedback": True, + "is_feedback_survey_optional": False, + "is_section_feedback_optional": False, + }, + "eligibility_config": {"has_eligibility": False}, + "eoi_decision_schema": None, + } +] diff --git a/fund_store/config/fund_loader_config/cof/deprecated_fund_config/assessment_section_config.py b/fund_store/config/fund_loader_config/cof/deprecated_fund_config/assessment_section_config.py new file mode 100644 index 000000000..eed933776 --- /dev/null +++ b/fund_store/config/fund_loader_config/cof/deprecated_fund_config/assessment_section_config.py @@ -0,0 +1,1305 @@ +# Extract sections at all levels as in application and alpha-numerically sort +# flake8: noqa +# Ignore line length + +scored_sections = [ + { + "id": "strategic_case", + "name": "Strategic case", + "weighting": 0.30, + "sub_criteria": [ + { + "id": "benefits", + "name": "Benefits", + "themes": [ + { + "id": "community_use", + "name": "Community use", + "answers": [ + { + "field_id": "kxgWTy", + "form_name": "community-use", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": ( + "Who in the community uses the asset, or has used" + " it in the past, and who benefits from it?" + ), + }, + { + "field_id": "wudRxx", + "form_name": "project-information", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": ( + "Tell us how the asset is currently being used, or" + " how it has been used before, and why it's" + " important to the community" + ), + }, + ], + }, + { + "id": "risk_loss_impact", + "name": "Risk and impact of loss", + "answers": [ + { + "field_id": "TlGjXb", + "form_name": "project-information", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": ( + "Explain why the asset is at risk of being lost to" + " the community, or why it has already been lost" + ), + }, + { + "field_id": "UDTxqC", + "form_name": "asset-information", + "field_type": "checkboxesField", + "presentation_type": "list", + "question": "Why is the asset at risk of closure?", + }, + { + "field_id": "GNhrIs", + "form_name": "community-use", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": ( + "Tell us how losing the asset would affect, or has" + " already affected, people in the community" + ), + }, + { + "field_id": "qsZLjZ", + "form_name": "community-use", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": "Why will the asset be lost without community intervention?", + }, + ], + }, + ], + }, + { + "id": "engagement", + "name": "Engagement", + "themes": [ + { + "id": "engaging-the-community", + "name": "Engaging the community", + "answers": [ + { + "field_id": "HJBgvw", + "form_name": "community-engagement", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": ( + "Tell us how you have engaged with the community" + " about your intention to take ownership of the" + " asset, and explain how this has shaped your" + " project plans" + ), + }, + { + "field_id": "JCACTy", + "form_name": "community-engagement", + "field_type": "yesNoField", + "presentation_type": "text", + "question": "Have you done any fundraising in the community?", + # Yes-No determines dpLyQh + }, + { + "field_id": "dpLyQh", + "form_name": "community-engagement", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": "Describe your fundraising activities", + # Determined by Yes-No JCACTy + }, + ], + }, + { + "id": "local-support", + "name": "Local support", + "answers": [ + { + "field_id": "NZKHOp", + "form_name": "community-engagement", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": "Tell us how your project supports any wider local plans", + }, + { + "field_id": "KqoaJL", + "form_name": "local-support", + "field_type": "yesNoField", + "presentation_type": "text", + "question": "Are you confident there is local support for your project?", + # Yes-No determines KqoaJL + }, + { + "field_id": "BFbzux", + "form_name": "local-support", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": "Tell us more about the local support for your project", + # Determined by Yes-No KqoaJL + }, + { + "field_id": "EEBFao", + "form_name": "local-support", + "field_type": "fileUploadField", + "presentation_type": "file", + "question": "Upload supporting evidence (optional)", + }, + ], + }, + ], + }, + { + "id": "environmental_sustainability", + "name": "Environmental Sustainability", + "themes": [ + { + "id": "environmental-considerations", + "name": "Environmental considerations", + "answers": [ + { + "field_id": "CvVZJv", + "form_name": "environmental-sustainability", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": ( + "Tell us how you have considered the environmental sustainability of your project" + ), + } + ], + } + ], + }, + ], + }, + { + "id": "management_case", + "name": "Management case", + "weighting": 0.30, + "sub_criteria": [ + { + "id": "funding_breakdown", + "name": "Funding breakdown", + "themes": [ + { + "id": "funding_requested", + "name": "Funding requested", + "answers": [ + { + # These 2 fields are capital and revenue funding respectively + "field_id": ["JzWvhj", "jLIgoi"], + "form_name": "", + "field_type": "numberField", + "presentation_type": "grouped_fields", + "question": [ + "Total funding request", + "Total funding request", + ], + }, + { + "field_id": "NdFwgy", + "form_name": "funding-required", + "field_type": "multiInputField", + "presentation_type": "heading", + "question": "Capital costs", + }, + { + "field_id": "NdFwgy", + "form_name": "funding-required", + "field_type": "textField", + "presentation_type": "description", + "question": "Describe the cost", + }, + { + "field_id": "NdFwgy", + "form_name": "funding-required", + "field_type": "numberField", + "presentation_type": "amount", + "question": "Amount", + }, + { + "field_id": "NWTKzQ", + "form_name": "funding-required", + "field_type": "yesNoField", + "presentation_type": "text", + "question": "Are you applying for revenue funding from the Community Ownership Fund?", + }, + { + "field_id": "NyudvF", + "form_name": "funding-required", + "field_type": "multiInputField", + "presentation_type": "heading", + "question": "Revenue costs", + }, + { + "field_id": "NyudvF", + "form_name": "funding-required", + "field_type": "textField", + "presentation_type": "description", + "question": "Describe the cost", + }, + { + "field_id": "NyudvF", + "form_name": "funding-required", + "field_type": "numberField", + "presentation_type": "amount", + "question": "Amount", + }, + { + "field_id": "DIZZOC", + "form_name": "funding-required", + "field_type": "yesNoField", + "presentation_type": "text", + "question": "Have you secured any match funding yet?", + }, + { + "field_id": "abkrwo", + "form_name": "funding-required", + "field_type": "multiInputField", + "presentation_type": "heading", + "question": "Secured match funding", + }, + { + "field_id": "abkrwo", + "form_name": "funding-required", + "field_type": "textField", + "presentation_type": "description", + "question": "Source of funding", + }, + { + "field_id": "abkrwo", + "form_name": "funding-required", + "field_type": "numberField", + "presentation_type": "amount", + "question": "Amount", + }, + { + "field_id": "RvbwSX", + "form_name": "funding-required", + "field_type": "yesNoField", + "presentation_type": "text", + "question": "Do you have any match funding identified but not yet secured?", + }, + { + "field_id": "AOLYnV", + "form_name": "funding-required", + "field_type": "multiInputField", + "presentation_type": "heading", + "question": "Unsecured match funding", + }, + { + "field_id": "AOLYnV", + "form_name": "funding-required", + "field_type": "textField", + "presentation_type": "description", + "question": "Source of funding", + }, + { + "field_id": "AOLYnV", + "form_name": "funding-required", + "field_type": "numberField", + "presentation_type": "amount", + "question": "Amount", + }, + { + "field_id": "fnIdkJ", + "form_name": "funding-required", + "field_type": "numberField", + "presentation_type": "text", + "question": "Asset value", + }, + { + "field_id": "oaIntA", + "form_name": "funding-required", + "field_type": "yesNoField", + "presentation_type": "text", + "question": "If successful, will you use your funding in the next 12 months? (Y/N)", + }, + ], + } + ], + }, + { + "id": "financial_and_risk_forecasts", + "name": "Financial and risk forecasts", + "themes": [ + { + "id": "feasibility", + "name": "Feasibility", + "answers": [ + { + "field_id": "ieRCkI", + "form_name": "feasibility", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": ( + "Tell us about the feasibility studies you have carried out for your project" + ), + }, + { + "field_id": "aAeszH", + "form_name": "feasibility", + "field_type": "yesNoField", + "presentation_type": "text", + "question": "Do you need to do any further feasibility work?", + }, + ], + }, + { + "id": "risk", + "name": "Risk", + "answers": [ + { + "field_id": "ozgwXq", + "form_name": "risk", + "field_type": "fileUploadField", + "presentation_type": "file", + "question": "Risks to your project (document upload)", + }, + ], + }, + { + "id": "income_and_running_costs", + "name": "Income and running costs", + "answers": [ + { + "field_id": "WDDkVB", + "form_name": "project-costs", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": "Summarise your cash flow for the running of the asset", + }, + { + "field_id": "TzOokX", + "form_name": "project-costs", + "field_type": "multiInputField", + "presentation_type": "heading", + "question": "Sources of income", + }, + { + "field_id": "TzOokX", + "form_name": "project-costs", + "field_type": "multiInputField", + "presentation_type": "description", + "question": "Income source", + }, + { + "field_id": "TzOokX", + "form_name": "project-costs", + "field_type": "multiInputField", + "presentation_type": "amount", + "question": "Amount", + }, + { + "field_id": "fbFeEx", + "form_name": "project-costs", + "field_type": "multiInputField", + "presentation_type": "heading", + "question": "Running costs", + }, + { + "field_id": "fbFeEx", + "form_name": "project-costs", + "field_type": "multiInputField", + "presentation_type": "description", + "question": "Running costs", + }, + { + "field_id": "fbFeEx", + "form_name": "project-costs", + "field_type": "multiInputField", + "presentation_type": "amount", + "question": "Amount", + }, + ], + }, + ], + }, + { + "id": "skills_and_resources", + "name": "Skills and resources", + "themes": [ + { + "id": "previous_experience", + "name": "Previous experience", + "answers": [ + { + "field_id": "BBlCko", + "form_name": "organisation-information", + "field_type": "yesNoField", + "presentation_type": "text", + "question": "Have you delivered projects like this before? (Y/N)", + }, + { + "field_id": ["wxCszQ", "QJFQgi", "DGNWqE"], + "form_name": "organisation-information", + "field_type": "multilineTextField", + "presentation_type": "grouped_fields", + "question": [ + "Describe your previous projects", + "Describe your previous projects", + ], + }, + { + "field_id": "CBIWnt", + "form_name": "skills-and-resources", + "field_type": "yesNoField", + "presentation_type": "text", + "question": "Do you have experience of managing a community asset?", + }, + { + "field_id": "QWveYc", + "form_name": "skills-and-resources", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": "Describe any experience you have with community assets similar to this", + }, + ], + }, + { + "id": "governance_and_structures", + "name": "Governance and structures", + "answers": [ + { + "field_id": "JnvsPq", + "form_name": "community-representation", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": "List the members of your board", + }, + { + "field_id": "yMCivI", + "form_name": "community-representation", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": "Tell us about your governance and membership structures", + }, + ], + }, + { + "id": "recruitment", + "name": "Recruitment", + "answers": [ + { + "field_id": "vKnMPG", + "form_name": "skills-and-resources", + "field_type": "yesNoField", + "presentation_type": "text", + "question": "Do you have plans to recruit people to help you manage the asset?", + }, + { + "field_id": "VNjRgZ", + "form_name": "skills-and-resources", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": "Tells us about the roles you'll recruit", + }, + ], + }, + ], + }, + { + "id": "representation_inclusiveness_and_integration", + "name": "Representation, inclusiveness and integration", + "themes": [ + { + "id": "representing_community_views", + "name": "Representing community views", + "answers": [ + { + "field_id": "NUZOvS", + "form_name": "community-representation", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": ( + "Explain how you'll consider the views of the community in the running of the asset" + ), + }, + ], + }, + { + "id": "accessibility_and_inclusivity", + "name": "Accessibility and inclusivity", + "answers": [ + { + "field_id": "YbfbSC", + "form_name": "inclusiveness-and-integration", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": ( + "Describe anything that might prevent people from" + " using the asset or participating in its running" + ), + }, + { + "field_id": "KuhSWw", + "form_name": "inclusiveness-and-integration", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": ( + "Tell us how you'll make your project accessible" + " and inclusive to everyone in the community" + ), + }, + { + "field_id": "bkJsiO", + "form_name": "inclusiveness-and-integration", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": ( + "Describe how the project will bring people together from all over the community" + ), + }, + ], + }, + ], + }, + ], + }, + { + "id": "potential_to_deliver_community_benefit", + "name": "Potential to deliver community benefit", + "weighting": 0.30, + "sub_criteria": [ + { + "id": "community-benefits", + "name": "How the community benefits ", + "themes": [ + { + "id": "delivering_and_sustaining_benefits", + "name": "Delivering and sustaining benefits", + "answers": [ + { + "field_id": "SrtVAs", + "form_name": "inclusiveness_and_integration", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": ( + "Describe the planned activities or services that will take place at the asset" + ), + }, + { + "field_id": "QjJtbs", + "form_name": "community_benefits", + "field_type": "checkboxesField", + "presentation_type": "list", + "question": "What community benefits do you expect to deliver with this project?", + }, + { + "field_id": "gDTsgG", + "form_name": "community_benefits", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": ( + "Tell us about these benefits in detail, and" + " explain how you'll measure the benefits it'll" + " bring for the community" + ), + }, + { + "field_id": "kYjJFy", + "form_name": "community_benefits", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": ( + "Explain how you plan to sustain, and potentially expand, these benefits over time" + ), + }, + ], + } + ], + }, + { + "id": "how-the-asset-will-be-inclusive", + "name": "How the asset will be inclusive", + "themes": [ + { + "id": "benefitting_the_whole_community", + "name": "Benefitting the whole community", + "answers": [ + { + "field_id": "UbjYqE", + "form_name": "community_benefits", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": "Tell us how you'll make sure the whole community benefits from the asset", + }, + ], + }, + ], + }, + ], + }, + { + "id": "added_value_of_the_community_asset", + "name": "Added value of the community asset", + "weighting": 0.10, + "sub_criteria": [ + { + "id": "value-to-the-community", + "name": "Value to the community", + "themes": [ + { + "id": "addressing_community_challenges", + "name": "Addressing community challenges", + "answers": [ + { + "field_id": "oOPUXI", + "form_name": "value-to-the-community", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": "Tell us about your local community as a whole", + }, + { + "field_id": "NKOmNL", + "form_name": "value-to-the-community", + "field_type": "multilineTextField", + "presentation_type": "text", + "question": ( + "Describe any specific challenges your community" + " faces, and how the asset will address these" + ), + }, + ], + } + ], + } + ], + }, +] + +unscored_sections = [ + { + "id": "unscored", + "name": "Unscored", + "sub_criteria": [ + { + "id": "org_info", + "name": "Organisation information", + "themes": [ + { + "id": "general_info", + "name": "General information", + "answers": [ + { + "field_id": "WWWWxy", + "form_name": "organisation-information", + "field_type": "textField", + "presentation_type": "text", + "question": "Your unique tracker number", + }, + { + "field_id": "YdtlQZ", + "form_name": "organisation-information", + "field_type": "textField", + "presentation_type": "text", + "question": "Organisation name", + }, + { + "field_id": "iBCGxY", + "form_name": "organisation-information", + "field_type": "yesNoField", + "presentation_type": "text", + "question": "Does your organisation use any other names", + }, + { + "field_id": ["PHFkCs", "QgNhXX", "XCcqae"], + "form_name": "organisation-information", + "field_type": "textField", + "presentation_type": "grouped_fields", + "question": [ + "Alternative names of your organisation", + "Alternative names of your organisation", + ], + }, + { + "field_id": ["lajFtB", "plmwJv"], + "form_name": "organisation-information", + "field_type": "radiosField", + "presentation_type": "grouped_fields", + "question": [ + "Type of organisation", + "Type of organisation", + ], + }, + { + "field_id": "GlPmCX", + "form_name": "organisation-information", + "field_type": "textField", + "presentation_type": "text", + "question": "Company registration number", + }, + { + "field_id": ["GvPSna", "zsbmRx"], + "form_name": "organisation-information", + "field_type": "radiosField", + "presentation_type": "grouped_fields", + "question": [ + "Which regulatory body is your company registered with?", + "Which regulatory body is your company registered with?", + ], + }, + { + "field_id": "aHIGbK", + "form_name": "organisation-information", + "field_type": "textField", + "presentation_type": "text", + "question": "Charity number", + }, + { + "field_id": "DwfHtk", + "form_name": "organisation-information", + "field_type": "yesNoField", + "presentation_type": "text", + "question": "Is your organisation a trading subsidiary of a parent company?", + }, + { + "field_id": "MPNlZx", + "form_name": "organisation-information", + "field_type": "textField", + "presentation_type": "text", + "question": "Name of parent organisation", + }, + { + "field_id": "MyiYMw", + "form_name": "organisation-information", + "field_type": "datePartsField", + "presentation_type": "text", + "question": "Date parent organisation was established", + }, + { + "field_id": "ZQolYb", + "form_name": "organisation-information", + "field_type": "UkAddressField", + "presentation_type": "address", + "question": "Organisation address", + }, + { + "field_id": "zsoLdf", + "form_name": "organisation-information", + "field_type": "yesNoField", + "presentation_type": "text", + "question": "Is your correspondence address different to the organisation address?", + }, + { + "field_id": "VhkCbM", + "form_name": "organisation-information", + "field_type": "UkAddressField", + "presentation_type": "address", + "question": "Correspondence address", + }, + { + "field_id": ["FhbaEy", "FcdKlB", "BzxgDA"], + "form_name": "organisation-information", + "field_type": "websiteField", + "presentation_type": "grouped_fields", + "question": [ + "Website and social media", + "Website and social media", + ], + }, + ], + }, + { + "id": "activities", + "name": "Activities", + "answers": [ + { + "field_id": "emVGxS", + "form_name": "organisation-information", + "field_type": "multilineTextField", + "presentation_type": "list", + "question": "What is your organisation's main purpose?", + }, + { + "field_id": ["btTtIb", "SkocDi", "CNeeiC"], + "form_name": "organisation-information", + "field_type": "multilineTextField", + "presentation_type": "grouped_fields", + "question": [ + "Tell us about your organisation's main activities", + "Tell us about your organisation's main activities", + ], + }, + ], + }, + { + "id": "partnerships", + "name": "Partnerships", + "answers": [ + { + "field_id": "hnLurH", + "form_name": "organisation-information", + "field_type": "yesNoField", + "presentation_type": "text", + "question": "Is your application a joint bid in partnership with other organisations?", + }, + { + "field_id": "APSjeB", + "form_name": "organisation-information", + "field_type": "textField", + "presentation_type": "text", + "question": "Partner organisation name", + }, + { + "field_id": "biTJjF", + "form_name": "organisation-information", + "field_type": "UkAddressField", + "presentation_type": "address", + "question": "Partner organisation address", + }, + { + "field_id": "IkmvEt", + "form_name": "organisation-information", + "field_type": "multilineTextField", + "presentation_type": "list", + "question": "Tell us about your partnership and how you plan to work together", + }, + ], + }, + ], + }, + { + "id": "applicant_info", + "name": "Applicant information", + "themes": [ + { + "id": "contact_information", + "name": "Contact information", + "answers": [ + { + "field_id": "ZBjDTn", + "form_name": "applicant-information", + "field_type": "textField", + "presentation_type": "text", + "question": "Name of lead contact", + }, + { + "field_id": "LZBrEj", + "form_name": "applicant-information", + "field_type": "emailAddressField", + "presentation_type": "text", + "question": "Lead contact email address", + }, + { + "field_id": "lRfhGB", + "form_name": "applicant-information", + "field_type": "telephoneNumberField", + "presentation_type": "text", + "question": "Lead contact telephone number", + }, + ], + }, + ], + }, + { + "id": "project_info", + "name": "Project information", + "themes": [ + { + "id": "previous_funding", + "name": "Previous funding", + "answers": [ + { + "field_id": "gScdbf", + "form_name": "project-information", + "field_type": "yesNoField", + "presentation_type": "text", + "question": "Have you been given funding through the Community Ownership Fund before?", + }, + { + "field_id": "IrIYcA", + "form_name": "project-information", + "field_type": "multilineTextField", + "presentation_type": "list", + "question": "Describe your previous project", + }, + { + "field_id": "TFdnGq", + "form_name": "project-information", + "field_type": "numberField", + "presentation_type": "text", + "question": "Amount of funding received", + }, + ], + }, + { + "id": "project_summary", + "name": "Project summary", + "answers": [ + { + "field_id": "KAgrBz", + "form_name": "project-information", + "field_type": "textField", + "presentation_type": "text", + "question": "Project name", + }, + { + "field_id": "GCjCse", + "form_name": "project-information", + "field_type": "multilineTextField", + "presentation_type": "list", + "question": "Give a brief summary of your project, including what you hope to achieve", + }, + ], + }, + ], + }, + { + "id": "asset_info", + "name": "Asset information", + "themes": [ + { + "id": "asset_ownership", + "name": "Asset ownership", + "answers": [ + { + "field_id": "VWkLlk", + "form_name": "asset-information", + "field_type": "radiosField", + "presentation_type": "text", + "question": "What do you intend to do with the asset?", + }, + { + "field_id": "IRfSZp", + "form_name": "asset-information", + "field_type": "yesNoField", + "presentation_type": "text", + "question": "Do you know who currently owns your asset?", + }, + { + "field_id": "ymlmrX", + "form_name": "asset-information", + "field_type": "textField", + "presentation_type": "text", + "question": "Name of current asset owner", + }, + { + "field_id": "FtDJfK", + "form_name": "asset-information", + "field_type": "textField", + "presentation_type": "text", + "question": "Describe the current ownership status", + }, + { + "field_id": "gkulUE", + "form_name": "asset-information", + "field_type": "yesNoField", + "presentation_type": "text", + "question": "Have you already completed the purchase or lease?", + }, + { + "field_id": "uBXptf", + "form_name": "asset-information", + "field_type": "multilineTextField", + "presentation_type": "list", + "question": ( + "Describe the sale process, e.g. an auction, or the" + " terms of your lease if you have rented the asset" + ), + }, + { + "field_id": "nvMmGE", + "form_name": "asset-information", + "field_type": "multilineTextField", + "presentation_type": "list", + "question": ( + "Describe the expected sale process, or the" + " proposed terms of your lease if you are renting" + " the asset" + ), + }, + { + "field_id": "ghzLRv", + "form_name": "asset-information", + "field_type": "datePartsField", + "presentation_type": "text", + "question": "Expected date of sale or lease", + }, + { + "field_id": "Wyesgy", + "form_name": "asset-information", + "field_type": "yesNoField", + "presentation_type": "text", + "question": "Is your asset currently publicly owned?", + }, + { + "field_id": "fHvilU", + "form_name": "asset-information", + "field_type": "textField", + "presentation_type": "text", + "question": "Name of contact", + }, + { + "field_id": "scYeIU", + "form_name": "asset-information", + "field_type": "textField", + "presentation_type": "text", + "question": "Job title of contact", + }, + { + "field_id": "ZHPwln", + "form_name": "asset-information", + "field_type": "textField", + "presentation_type": "text", + "question": "Organisation name", + }, + { + "field_id": "nkPfyn", + "form_name": "asset-information", + "field_type": "checkboxesField", + "presentation_type": "list", + "question": ( + "When you buy or lease a publicly owned asset, the" + " public authority cannot transfer statutory" + " services or duties to the community group" + ), + }, + { + "field_id": "PraPAq", + "form_name": "asset-information", + "field_type": "checkboxesField", + "presentation_type": "list", + "question": ( + "Grants from this fund cannot be used to buy the" + " freehold or premium on the lease of a publicly" + " owned asset. Money must only be used for" + " renovation and refurbishment costs" + ), + }, + ], + }, + { + "id": "asset_evidence", + "name": "Asset evidence", + "answers": [ + { + "field_id": "ArVrka", + "form_name": "asset-information", + "field_type": "fileUploadField", + "presentation_type": "file", + "question": "Supporting evidence", + } + ], + }, + { + "id": "asset_background", + "name": "Asset background", + "answers": [ + { + "field_id": ["yaQoxU", "GjzaqR"], + "form_name": "asset-information", + "field_type": "textField", + "presentation_type": "grouped_fields", + "question": ["Asset type", "Asset type"], + }, + { + "field_id": "hvzzWB", + "form_name": "asset-information", + "field_type": "yesNoField", + "presentation_type": "text", + "question": "Is this a registered Asset of Community Value?", + }, + { + "field_id": "MLwpjP", + "form_name": "asset-information", + "field_type": "yesNoField", + "presentation_type": "text", + "question": "Will you purchase the asset within the appropriate time frame?", + }, + { + "field_id": "VwxiGn", + "form_name": "asset-information", + "field_type": "yesNoField", + "presentation_type": "text", + "question": "Is the asset listed for disposal, or part of a Community Asset Transfer?", + }, + { + "field_id": "bkbGIE", + "form_name": "asset-information", + "field_type": "datePartsField", + "presentation_type": "text", + "question": "When was the asset listed?", + }, + { + "field_id": "kBCjwC", + "form_name": "asset-information", + "field_type": "websiteField", + "presentation_type": "text", + "question": "Provide a link to the listing", + }, + { + "field_id": "vKSMwi", + "form_name": "asset-information", + "field_type": "multilineTextField", + "presentation_type": "list", + "question": "Describe the current status of the Community Asset Transfer", + }, + ], + }, + { + "id": "asset_location", + "name": "Asset location", + "answers": [ + { + "field_id": "yEmHpp", + "form_name": "project-information", + "field_type": "UkAddressField", + "presentation_type": "address", + "question": "Address of the community asset", + }, + { + "field_id": "iTeLGU", + "form_name": "project-information", + "field_type": "textField", + "presentation_type": "text", + "question": "In which constituency is your asset?", + }, + { + "field_id": "MGRlEi", + "form_name": "project-information", + "field_type": "textField", + "presentation_type": "text", + "question": "In which local council area is your asset?", + }, + ], + }, + ], + }, + { + "id": "business_plan", + "name": "Business plan", + "themes": [ + { + "id": "business_plan", + "name": "Business plan", + "answers": [ + { + "field_id": "rFXeZo", + "form_name": "upload-business-plan", + "field_type": "fileUploadField", + "presentation_type": "file", + "question": "Business plan (document upload)", + }, + ], + } + ], + }, + ], + }, + { + "id": "declarations", + "name": "Declarations", + "sub_criteria": [ + { + "id": "declarations", + "name": "Declarations", + "themes": [ + { + "id": "declarations", + "name": "Declarations", + "answers": [ + { + "field_id": "LlvhYl", + "form_name": "declarations", + "field_type": "yesNoField", + "presentation_type": "text", + "question": ( + "Confirm you have considered subsidy control and" + " state aid implications for your project, and the" + " information you have given us is correct" + ), + }, + { + "field_id": "wJrJWY", + "form_name": "declarations", + "field_type": "yesNoField", + "presentation_type": "text", + "question": ( + "Confirm you have considered people with protected" + " characteristics throughout the planning of your" + " project" + ), + }, + { + "field_id": "COiwQr", + "form_name": "declarations", + "field_type": "yesNoField", + "presentation_type": "text", + "question": ( + "Confirm you have considered sustainability and the" + " environment throughout the planning of your" + " project, including compliance with the" + " government's Net Zero ambitions" + ), + }, + { + "field_id": "bRPzWU", + "form_name": "declarations", + "field_type": "yesNoField", + "presentation_type": "text", + "question": ( + "Confirm you have a bank account set up and" + " associated with the organisation you are applying" + " on behalf of" + ), + }, + ], + } + ], + }, + { + "id": "subsidy_control_and_state_aid", + "name": "Subsidy control and state aid", + "themes": [ + { + "id": "project_qualification", + "name": "Project qualification", + "answers": [ + { + "field_id": "HvxXPI", + "form_name": "project-qualification", + "field_type": "yesNoField", + "presentation_type": "text", + "question": "Does your project meet the definition of a subsidy?", + }, + { + "field_id": "RmMKzM", + "form_name": "project-qualification", + "field_type": "multilineTextField", + "presentation_type": "list", + "question": ( + "Explain how you think a grant from this fund can" + " be provided in compliance with the Subsidy" + " Control Act (2022)" + ), + }, + { + "field_id": "UPmQrD", + "form_name": "project-qualification", + "field_type": "yesNoField", + "presentation_type": "text", + "question": "Is your project based in Northern Ireland?", + }, + { + "field_id": "xPkdRX", + "form_name": "project-qualification", + "field_type": "multilineTextField", + "presentation_type": "list", + "question": "Explain how your project will comply with state aid rules", + }, + ], + } + ], + }, + ], + }, +] diff --git a/fund_store/config/fund_loader_config/cof/deprecated_fund_config/cof_form_config.py b/fund_store/config/fund_loader_config/cof/deprecated_fund_config/cof_form_config.py new file mode 100644 index 000000000..04755ebd9 --- /dev/null +++ b/fund_store/config/fund_loader_config/cof/deprecated_fund_config/cof_form_config.py @@ -0,0 +1,117 @@ +COF_R2_ORDERED_FORMS_CONFIG = ( + { + "section_title": { + "en": "About your organisation", + "cy": "Ynglŷn â'ch sefydliad", + }, + "ordered_form_names_within_section": [ + { + "en": "organisation-information", + "cy": "gwybodaeth-am-y-sefydliad", + }, + { + "en": "applicant-information", + "cy": "gwybodaeth-am-yr-ymgeisydd", + }, + ], + "section_weighting": None, + }, + { + "section_title": { + "en": "About your project", + "cy": "Ynglŷn â'ch prosiect", + }, + "ordered_form_names_within_section": [ + { + "en": "project-information", + "cy": "gwybodaeth-am-y-prosiect", + }, + {"en": "asset-information", "cy": "gwybodaeth-am-yr-ased"}, + ], + "section_weighting": None, + }, + { + "section_title": {"en": "Strategic case", "cy": "Achos strategol"}, + "ordered_form_names_within_section": [ + {"en": "community-use", "cy": "defnydd-cymunedol"}, + {"en": "community-engagement", "cy": "ymgysylltu-a'r-gymuned"}, + {"en": "local-support", "cy": "cefnogaeth-leol"}, + { + "en": "environmental-sustainability", + "cy": "cynaliadwyedd-amgylcheddol", + }, + ], + "section_weighting": 30, + }, + { + "section_title": {"en": "Management case", "cy": "Achos rheoli"}, + "ordered_form_names_within_section": [ + {"en": "funding-required", "cy": "cyllid-sydd-ei-angen"}, + {"en": "feasibility", "cy": "dichonoldeb"}, + {"en": "risk", "cy": "risg"}, + {"en": "project-costs", "cy": "costau'r-prosiect"}, + {"en": "skills-and-resources", "cy": "sgiliau-ac-adnoddau"}, + { + "en": "community-representation", + "cy": "cynrychiolaeth-gymunedol", + }, + { + "en": "inclusiveness-and-integration", + "cy": "cynhwysiant-ac-integreiddio", + }, + { + "en": "upload-business-plan", + "cy": "lanlwythwch-y-cynllun-busnes", + }, + ], + "section_weighting": 30, + }, + { + "section_title": { + "en": "Potential to deliver community benefits", + "cy": "Potensial i gyflawni buddion cymunedol", + }, + "ordered_form_names_within_section": [ + {"en": "community-benefits", "cy": "buddion-cymunedol"}, + ], + "section_weighting": 30, + }, + { + "section_title": { + "en": "Added value to community", + "cy": "Gwerth ychwanegol i'r gymuned", + }, + "ordered_form_names_within_section": [ + {"en": "value-to-the-community", "cy": "gwerth-i'r-gymuned"}, + ], + "section_weighting": 10, + }, + { + "section_title": { + "en": "Subsidy control / state aid", + "cy": "Rheoli cymorthdaliadau a chymorth gwladwriaethol", + }, + "ordered_form_names_within_section": [ + {"en": "project-qualification", "cy": "cymhwystra'r-prosiect"}, + ], + "section_weighting": None, + }, + { + "section_title": { + "en": "Check declarations", + "cy": "Gwirio datganiadau", + }, + "ordered_form_names_within_section": [ + {"en": "declarations", "cy": "datganiadau"}, + ], + "section_weighting": None, + }, +) + +# --------------- +# Fund Config +# --------------- + +COF_FUND_ID = "47aef2f5-3fcb-4d45-acb5-f0152b5f03c4" +COF_ROUND_2_ID = "c603d114-5364-4474-a0c4-c41cbf4d3bbd" +COF_ROUND_2_W3_ID = "5cf439bf-ef6f-431e-92c5-a1d90a4dd32f" diff --git a/fund_store/config/fund_loader_config/cof/deprecated_fund_config/sort_application_sections.py b/fund_store/config/fund_loader_config/cof/deprecated_fund_config/sort_application_sections.py new file mode 100755 index 000000000..293c9ff75 --- /dev/null +++ b/fund_store/config/fund_loader_config/cof/deprecated_fund_config/sort_application_sections.py @@ -0,0 +1,31 @@ +def alpha_numeric_sort_section(index, section_config, tree_base_path): + an_sorted_sections = [] + top_section_tree_level = f"{tree_base_path}.{index + 1}" + an_sorted_sections.append( + { + "section_name": str(section_config["section_title"]["en"]), + "tree_path": str(top_section_tree_level), + } + ) + + for index, form_section in enumerate(section_config["ordered_form_names_within_section"]): + current_form_section_tree_level = index + 1 + an_sorted_sections.append( + { + "section_name": str(form_section["en"]).replace("-", " ").title(), + "tree_path": str(f"{top_section_tree_level}.{current_form_section_tree_level}"), + "form_name": form_section["en"], + } + ) + return an_sorted_sections + + +def sort_sections_from_config(sections_config, tree_base_path): + all_an_sorted_sections = [] + for index, sc in enumerate(sections_config): + all_an_sorted_sections += alpha_numeric_sort_section(index, sc, tree_base_path) + return all_an_sorted_sections + + +def return_numerically_sorted_section_for_application(application_display_config, tree_base_path): + return {"sorted_sections": sort_sections_from_config(application_display_config, tree_base_path)} diff --git a/fund_store/config/fund_loader_config/cof/deprecated_fund_config/sort_assessment_sections.py b/fund_store/config/fund_loader_config/cof/deprecated_fund_config/sort_assessment_sections.py new file mode 100644 index 000000000..76dcf7497 --- /dev/null +++ b/fund_store/config/fund_loader_config/cof/deprecated_fund_config/sort_assessment_sections.py @@ -0,0 +1,104 @@ +from config.fund_loader_config.cof.deprecated_fund_config.assessment_section_config import ( + scored_sections, + unscored_sections, +) + + +def map_fields(fields, all_fields): + ordered_fields = [] + for index, field in enumerate(fields): + # check to see if there are grouped fields + # they should be shown in a single assessment component - grouped + # therefore give them the same display order + if type(field["field_id"]) is list: + for index, grouped_field in enumerate(field["field_id"]): + ordered_fields.append({"form_json_id": grouped_field, "display_order": 10 * (index + 1)}) + all_fields.append( + { + "form_json_id": grouped_field, + "type": field["field_type"], + "presentation_type": field["presentation_type"], + "title": field["question"], + } + ) + else: + ordered_fields.append({"form_json_id": field["field_id"], "display_order": 10 * (index + 1)}) + all_fields.append( + { + "form_json_id": field["field_id"], + "type": field["field_type"], + "presentation_type": field["presentation_type"], + "title": field["question"], + } + ) + + return ordered_fields + + +def alpha_numeric_sort_section( + index, + section_config, + all_an_sorted_sections, + all_fields, + nested_keys_in_config, + parent_tree_path, + depth_count, +): + # resets when we drop down to the next tree level + current_tree_path = index + 1 + + # depth count tracks the level of recursive call + if depth_count < len(nested_keys_in_config): + next_level_key = nested_keys_in_config[depth_count] + depth_count += 1 + else: + next_level_key = None + + # If there is a parent tree path then this should prepend the current tree path + tree_path = f"{parent_tree_path}.{current_tree_path}" if parent_tree_path else current_tree_path + + # Add current section + new_section = { + "section_name": section_config["id"], + "tree_path": tree_path, + "weighting": section_config["weighting"] if "weighting" in section_config else None, + } + + # Check if this section has field_ids and then add section + if "answers" in section_config: + all_an_sorted_sections += [{**new_section, "fields": map_fields(section_config["answers"], all_fields)}] + else: + all_an_sorted_sections += [{**new_section}] + + # Before continuing to the next iteration at this level, check is there are any sub_levels to iterate through + if next_level_key: + for index, sub_section_config in enumerate(section_config[next_level_key]): + alpha_numeric_sort_section( + index, + sub_section_config, + all_an_sorted_sections, + all_fields, + nested_keys_in_config, + tree_path, + depth_count, + ) + + +def sort_sections_from_config(sections_config, sub_keys, all_fields, starting_path): + all_an_sorted_sections = [] + for index, sc in enumerate(sections_config): + alpha_numeric_sort_section(index, sc, all_an_sorted_sections, all_fields, sub_keys, starting_path, 0) + return all_an_sorted_sections + + +def return_numerically_sorted_section_for_assessment(scored_sections, unscored_sections): + sub_keys = ["sub_criteria", "themes"] + all_fields = [] + return { + "sorted_scored_sections": sort_sections_from_config(scored_sections, sub_keys, all_fields, 1), + "sorted_unscored_sections": sort_sections_from_config(unscored_sections, sub_keys, all_fields, 2), + "all_fields": all_fields, + } + + +sorted_sections_and_field_ids = return_numerically_sorted_section_for_assessment(scored_sections, unscored_sections) diff --git a/fund_store/config/fund_loader_config/cof/deprecated_fund_config/what_to_do_with_output.py b/fund_store/config/fund_loader_config/cof/deprecated_fund_config/what_to_do_with_output.py new file mode 100644 index 000000000..4caa1fba3 --- /dev/null +++ b/fund_store/config/fund_loader_config/cof/deprecated_fund_config/what_to_do_with_output.py @@ -0,0 +1,148 @@ +# flake8: noqa +# The following lists are samples of the extracted data from the current section display config +# for both application and assessment +# Use the functions in the following files to get the data required for database input: +# 1. sort_application_sections.py +# 2. sort_assessment_sections.py +# ^ These functions are only necessary to transform existing config +# A sample data output of functions 1 and 2 is below +# It is envisaged that this data format below (and the steps outlined) will be the required input config for future rounds +### APPLICATION ### +# an_sorted_application_sections_SAMPLE = [ +# {'section_name': 'About your organisation', 'tree_path': '1'}, +# {'section_name': 'Organisation Information', 'tree_path': '1.1'}, +# {'section_name': 'Applicant Information', 'tree_path': '1.2'}, +# {'section_name': 'About your project', 'tree_path': '2'}, +# {'section_name': 'Project Information', 'tree_path': '2.1'}, +# {'section_name': 'Asset Information', 'tree_path': '2.2'}, +# {'section_name': 'Strategic case', 'tree_path': '3'}, +# {'section_name': 'Community Use', 'tree_path': '3.1'}, +# {'section_name': 'Community Engagement', 'tree_path': '3.2'}, +# {'section_name': 'Local Support', 'tree_path': '3.3'}, +# {'section_name': 'Environmental Sustainability', 'tree_path': '3.4'}, +# {'section_name': 'Management case', 'tree_path': '4'}, +# {'section_name': 'Funding Required', 'tree_path': '4.1'}, +# {'section_name': 'Feasibility', 'tree_path': '4.2'} +# # ...etc +# ] +# ### SQL +# # 3. Insert into SECTIONS_TABLE +# # for section in an_sorted_sections : +# # INSERT INTO SECTIONS_TABLE(path_value, section_names, service) +# # values (section["tree_path"], section["section_name"], assessmentORapplication); +# ### ASSESSMENT ### +# # The assessment script outputs three items for database insert (they are explained in the scripts further down this page) +# # { +# # "sorted_scored_sections": [...], +# # "sorted_unscored_sections": [...], +# # "all_fields": [...] +# # } +# all_an_sorted_sections_scored_SAMPLE = [ +# {'section_name': 'strategic_case', 'tree_path': '1.1', 'weighting': 0.3}, +# {'section_name': 'benefits', 'tree_path': '1.1.1', 'weighting': None}, +# {'section_name': 'community_use', 'tree_path': '1.1.1.1', 'weighting': None, 'fields': [ +# {'id': 'WWWWxy', 'display_order': 10}, +# {'id': 'YdtlQZ', 'display_order': 20}, +# {'id': 'iBCGxY', 'display_order': 30}, +# {'id': 'PHFkCs', 'display_order': 10}, +# {'id': 'QgNhXX', 'display_order': 20}, +# {'id': 'XCcqae', 'display_order': 30}, +# {'id': 'lajFtB', 'display_order': 10}, +# {'id': 'plmwJv', 'display_order': 20}, +# {'id': 'GlPmCX', 'display_order': 60}, +# {'id': 'GvPSna', 'display_order': 10}, +# {'id': 'zsbmRx', 'display_order': 20}, +# {'id': 'aHIGbK', 'display_order': 80}, +# {'id': 'DwfHtk', 'display_order': 90}, +# {'id': 'MPNlZx', 'display_order': 100}, +# ... +# ]}, +# {'section_name': 'risk_loss_impact', 'tree_path': '1.1.1.2', +# 'weighting': None, 'fields': [...]}, +# {'section_name': 'engagement', 'tree_path': '1.1.2', 'weighting': None}, +# {'section_name': 'engaging-the-community', +# 'tree_path': '1.1.2.1', 'weighting': None, 'fields': [...]}, +# {'section_name': 'local-support', 'tree_path': '1.1.2.2', +# 'weighting': None, 'fields': [...]}, +# {'section_name': 'environmental_sustainability', +# 'tree_path': '1.1.3', 'weighting': None}, +# {'section_name': 'environmental-considerations', +# 'tree_path': '1.1.3.1', 'weighting': None, 'fields': [...]}, +# {'section_name': 'management_case', 'tree_path': '1.2', 'weighting': 0.3}, +# {'section_name': 'funding_breakdown', 'tree_path': '1.2.1', 'weighting': None}, +# {'section_name': 'funding_requested', 'tree_path': '1.2.1.1', +# 'weighting': None, 'fields': [...]}, +# {'section_name': 'financial_and_risk_forecasts', +# 'tree_path': '1.2.2', 'weighting': None}, +# {'section_name': 'feasibility', 'tree_path': '1.2.2.1', +# 'weighting': None, 'fields': [...]}, +# ...] +# all_an_sorted_sections_unscored_SAMPLE = [ +# {'section_name': 'unscored', 'tree_path': '2.1', 'weighting': None}, +# {'section_name': 'org_info', 'tree_path': '2.1.1', 'weighting': None}, +# {'section_name': 'general_info', 'tree_path': '2.1.1.1', +# 'weighting': None, 'fields': [...]}, +# {'section_name': 'activities', 'tree_path': '2.1.1.2', +# 'weighting': None, 'fields': [...]}, +# {'section_name': 'partnerships', 'tree_path': '2.1.1.3', +# 'weighting': None, 'fields': [...]}, +# {'section_name': 'applicant_info', 'tree_path': '2.1.2', 'weighting': None}, +# {'section_name': 'contact_information', 'tree_path': '2.1.2.1', +# 'weighting': None, 'fields': [...]}, +# {'section_name': 'project_info', 'tree_path': '2.1.3', 'weighting': None}, +# {'section_name': 'previous_funding', 'tree_path': '2.1.3.1', +# 'weighting': None, 'fields': [...]}, +# {'section_name': 'project_summary', 'tree_path': '2.1.3.2', +# 'weighting': None, 'fields': [...]}, +# {'section_name': 'asset_info', 'tree_path': '2.1.4', 'weighting': None}, +# {'section_name': 'asset_ownership', 'tree_path': '2.1.4.1', +# 'weighting': None, 'fields': [...]}, +# {'section_name': 'asset_evidence', 'tree_path': '2.1.4.2', +# 'weighting': None, 'fields': [...]}, +# {'section_name': 'asset_background', 'tree_path': '2.1.4.3', +# 'weighting': None, 'fields': [...]}, +# ...] +# all_fields_SAMPLE = [ +# { +# 'form_json_id': 'kxgWTy', +# 'type': 'multilineTextField', +# 'presentation_type': 'text', +# 'title': 'Who in the community uses the asset, or has used it in the past, and who benefits from it?' +# }, +# { +# 'form_json_id': 'wudRxx', +# 'type': 'multilineTextField', +# 'presentation_type': 'text', +# '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" +# } +# ...] +### SQL +# INSERT ALL NEW FIELDS +# for field in all_fields: +# INSERT INTO ASSESSMENT_FIELDS(form_json_id,type,presentation_type,title) +# VALUES(field["form_json_id"],field["type"],field["presentation_type"],field["title"], ) +# reject conflicts +# Funds will be reusing forms so we only need to load new fields +# INSERT SECTIONS +# for section in all_an_sorted_sections_unscored: +# INSERT INTO SECTIONS_TABLE(path_value, section_name, weighting, assessmentORapplication) +# values (etc); +# If this is the lowest level section it will have a "fields" key. +# if "fields" in section: +# for field in fields: +# INSERT INTO SECTIONS_FIELDS(field_id, section_id, display_order) +# values (etc); +# ^ get the section_id from section INSERT above +# NOTE +# There are some instances of duplicate field_ids in the assessment mapping (a single field_id to many assessment field mapping objects), +# suspected because we need to break an application field into multiple assessment elements. +# We should aim to have a 1-1 mapping of fields from application to assessment +# We could handle this locally in the assessment frontend for that type and presentation_type, +# rather than complicate our data model. This may only be true for "add-another" fields (need further investigation). +# This needs further discussion and as it stands, in some cases we store many fields_ids in our mapping +# for one application component +# Changes to remove this behavior on add-another fields to be implemented as part of fs-2500 +# the same is found in reverse (many field_ids to one assessment field mapping object), +# many field ids map (are grouped) to one assessment componenet as a list of field_ids +# suspected because we need to consume multiple fields into one assessment field view. +# At the moment this is handled by giving these grouped ids the same display_order (i.e: all 10) diff --git a/fund_store/config/fund_loader_config/cof/eoi.py b/fund_store/config/fund_loader_config/cof/eoi.py new file mode 100644 index 000000000..10f5d1f9c --- /dev/null +++ b/fund_store/config/fund_loader_config/cof/eoi.py @@ -0,0 +1,208 @@ +# flake8: noqa +import textwrap +from datetime import datetime +from datetime import timezone + +from config.fund_loader_config.cof.eoi_r1_schema import COF_R3_EOI_SCHEMA_CY +from config.fund_loader_config.cof.eoi_r1_schema import COF_R3_EOI_SCHEMA_EN +from config.fund_loader_config.cof.shared import EOI_APPLICATION_GUIDANCE +from config.fund_loader_config.cof.shared import fund_config +from config.fund_loader_config.common_fund_config.fund_base_tree_paths import ( + COF_EOI_BASE_PATH, +) +from config.fund_loader_config.logo import DLUHC_LOGO_PNG +from db.models.fund import FundingType + +COF_FUND_ID = "54c11ec2-0b16-46bb-80d2-f210e47a8791" +COF_EOI_ROUND_ID = "6a47c649-7bac-4583-baed-9c4e7a35c8b3" + +APPLICATION_BASE_PATH_COF_EOI = ".".join([str(COF_EOI_BASE_PATH), str(1)]) +ASSESSMENT_BASE_PATH_COF_EOI = ".".join([str(COF_EOI_BASE_PATH), str(2)]) + +COF_EOI_OPENS_DATE = datetime(2024, 3, 6, 14, 00, 0, tzinfo=timezone.utc) # 2023-12-06 14:00:00 +COF_EOI_ASSESSMENT_OPENS_DATE = COF_EOI_OPENS_DATE +COF_EOI_DEADLINE_DATE = datetime(2024, 5, 25, 0, 1, 0, tzinfo=timezone.utc) # 2024-05-25 00:01:00 (1min past midnight) +COF_EOI_ASSESSMENT_DEADLINE_DATE = datetime(2124, 3, 6, 12, 0, 0, tzinfo=timezone.utc) # 2124-03-06 12:00:00 + +fund_config = { + "id": COF_FUND_ID, + "name_json": { + "en": "Community Ownership Fund - Expression of Interest", + "cy": "Y Cronfa Perchnogaeth Gymunedol - Mynegi diddordeb", + }, + "title_json": { + "en": "expression of interest in applying for the Community Ownership Fund", + "cy": "Gwneud cais am ddatganiad o ddiddordeb mewn gwneud cais i'r Gronfa Perchnogaeth Gymunedol", + }, + "short_name": "COF-EOI", + "funding_type": FundingType.EOI, + "description_json": fund_config["description_json"], + "welsh_available": True, + "owner_organisation_name": "Department for Levelling Up, Housing and Communities", + "owner_organisation_shortname": "DLUHC", + "owner_organisation_logo_uri": DLUHC_LOGO_PNG, +} + +cof_eoi_sections = [ + { + "section_name": { + "en": "1. Expression of interest", + "cy": "1. Mynegi diddordeb", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_EOI}.1", + "requires_feedback": True, + }, + { + "section_name": { + "en": "1.1 Organisation details", + "cy": "1.1 Manylion y sefydliad", + }, + "form_name_json": { + "en": "organisation-details", + "cy": "manylion-y-sefydliad", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_EOI}.1.1", + }, + { + "section_name": { + "en": "1.2 About your asset", + "cy": "1.2 Ynglŷn â'ch ased", + }, + "form_name_json": { + "en": "about-your-asset", + "cy": "ynglyn-ach-ased", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_EOI}.1.2", + }, + { + "section_name": { + "en": "1.3 Your funding request", + "cy": "1.3 Eich cais am gyllid", + }, + "form_name_json": { + "en": "your-funding-request", + "cy": "eich-cais-am-gyllid", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_EOI}.1.3", + }, + { + "section_name": { + "en": "1.4 Development support provider (not scored)", + "cy": "1.4 Darparwr cymorth datblygu (heb ei sgorio)", + }, + "form_name_json": { + "en": "development-support-provider", + "cy": "darparwr-cymorth-datblygu", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_EOI}.1.4", + }, + { + "section_name": { + "en": "1.5 Declaration", + "cy": "1.5 Datganiad", + }, + "form_name_json": { + "en": "declaration", + "cy": "datganiad", + }, + "tree_path": f"{APPLICATION_BASE_PATH_COF_EOI}.1.5", + }, +] + +round_config_eoi = [ + { + "id": COF_EOI_ROUND_ID, + "fund_id": COF_FUND_ID, + "title_json": {"en": "Expression of interest", "cy": "Mynegi diddordeb"}, + "short_name": "R1", + "opens": COF_EOI_OPENS_DATE, + "assessment_start": COF_EOI_ASSESSMENT_OPENS_DATE, + "deadline": COF_EOI_DEADLINE_DATE, + "application_reminder_sent": False, + "reminder_date": None, + "assessment_deadline": COF_EOI_ASSESSMENT_DEADLINE_DATE, + "prospectus": "https://www.gov.uk/government/publications/community-ownership-fund-prospectus", + "privacy_notice": ( + "https://www.gov.uk/government/publications/community-ownership-fund-" + "privacy-notice/community-ownership-fund-privacy-notice" + ), + "reference_contact_page_over_email": True, + "contact_us_banner_json": { + "en": textwrap.dedent( + """ +

Get application support

+

+ Visit the My Community website + for information and guidance on applying to Community Ownership Fund. + Fill out the enquiry form + to request advice from My Community. +

+

+ We cannot provide direct support to applicants outside of this service. +

+

Get technical support

+

+ Contact the Department of Levelling Up, Housing and Communities funding team if you need + help with accessing or submitting an application form. +

+ """ + ), + "cy": textwrap.dedent( + """ +

Cael cymorth â'r cais

+

+ Ewch i wefan My Community + i gael gwybodaeth ac arweiniad ar wneud cais i'r Gronfa Perchnogaeth Gymunedol. + Llenwch y ffurflen ymholiad + i ofyn am gyngor gan My Community. +

+

+ Ni allwn ddarparu cymorth uniongyrchol i ymgeiswyr tu hwnt i'r gwasanaeth hwn. +

+

Cael cymorth technegol

+

+ Cysylltwch â thîm cyllid yr Adran Ffyniant Bro, Tai a Chymunedau os oes angen help arnoch i gael at ffurflen gais neu ei chyflwyno. +

+ """ + ), + }, + "contact_email": "COF@communities.gov.uk", + "contact_phone": None, + "contact_textphone": None, + "support_times": "9am to 5pm", + "support_days": "Monday to Friday", + "instructions_json": { + "en": ( + "You must complete this Expression of Interest (EOI) form if you" + " are interested in applying for the Community Ownership Fund (COF)." + ), + "cy": ( + "Mae'n rhaid i chi gwblhau'r ffurflen Datganiad o Ddiddordeb hon os" + " oes diddordeb gennych mewn gwneud cais i'r Gronfa Perchnogaeth Gymunedol." + ), + }, + "feedback_link": ( + "https://forms.office.com/Pages/ResponsePage.aspx?id=" + "EGg0v32c3kOociSi7zmVqFJBHpeOL2tNnpiwpdL2iElURUY1WkhaS0NFMlZVQUhYQ1NaN0E4RjlQMC4u" + ), + "project_name_field_id": "SMRWjl", + "application_guidance_json": EOI_APPLICATION_GUIDANCE, + "guidance_url": ( + "https://www.gov.uk/government/publications/community-ownership-fund-round-3-application-form" + "-assessment-criteria-guidance" + ), + "all_uploaded_documents_section_available": False, + "application_fields_download_available": True, + "display_logo_on_pdf_exports": False, + "mark_as_complete_enabled": False, + "is_expression_of_interest": True, + "feedback_survey_config": { + "has_feedback_survey": False, + "has_section_feedback": True, + "is_feedback_survey_optional": False, + "is_section_feedback_optional": True, + }, + "eligibility_config": {"has_eligibility": False}, + "eoi_decision_schema": {"en": COF_R3_EOI_SCHEMA_EN, "cy": COF_R3_EOI_SCHEMA_CY}, + } +] diff --git a/fund_store/config/fund_loader_config/cof/eoi_r1_schema.py b/fund_store/config/fund_loader_config/cof/eoi_r1_schema.py new file mode 100644 index 000000000..72fc98630 --- /dev/null +++ b/fund_store/config/fund_loader_config/cof/eoi_r1_schema.py @@ -0,0 +1,419 @@ +from fsd_utils import Decision + +COF_SECURE_MATCH_FUNDING_CAVEAT_EN = ( + "Make progress in securing match funding: COF will contribute up to 80% of the" + " capital costs you require, and you must raise at least 20% from other sources." + " You do not need to have secured all your match funding by the time you apply, but" + " we will ask you to set out your total costs, funding already secured, and plans" + " to raise any additional funding. You must use COF funding within 12 months, so" + " you must be able to show that you've made good progress to secure the remaining" + " match funding. This is so that we're confident you can draw down this funding" + " within this timeframe." +) +COF_SECURE_MATCH_FUNDING_CAVEAT_CY = ( + "Gwneud cynnydd i sicrhau arian cyfatebol: Bydd y Gronfa Perchnogaeth Gymunedol yn cyfrannu hyd at 80% o'r costau " + "cyfalaf sydd eu hangen arnoch, ac mae'n rhaid i chi godi o leiaf 20% o ffynonellau eraill. Nid oes angen i chi " + "fod wedi sicrhau eich holl arian cyfatebol erbyn i chi wneud cais, ond byddwn yn gofyn i chi nodi cyfanswm eich " + "costau, y cyllid rydych eisoes wedi'i sicrhau, a chynlluniau i godi unrhyw gyllid ychwanegol. Mae'n rhaid i chi " + "ddefnyddio cyllid o'r Gronfa Perchnogaeth Gymunedol o fewn 12 mis, felly mae'n rhaid i chi allu dangos eich bod " + "wedi gwneud cynnydd da i sicrhau'r arian cyfatebol sy'n weddill. Diben hyn yw rhoi'r hyder i ni y gallwch " + "ddefnyddio'r cyllid hwn o fewn yr amserlen hon." +) + + +COF_PLANNING_PERMISSION_IF_NEEDED_CAVEAT_EN = ( + "Get planning permission, if needed: When you apply, you must be able to show that you have secured or have made" + " good progress in securing planning permission, if needed (and building warrants, if required). This is so that" + " we're confident that COF funding will be used within the 12 month timeframe." +) +COF_PLANNING_PERMISSION_IF_NEEDED_CAVEAT_CY = ( + "Sicrhewch ganiatâd cynllunio, os oes angen: Pan fyddwch yn gwneud cais, rhaid i chi allu dangos eich bod wedi " + "sicrhau caniatâd cynllunio os oes angen (a gwarantau adeiladu, os oes angen), neu'ch bod wedi gwneud cynnydd da " + "yn hyn o beth. Diben hyn yw rhoi'r hyder i ni y caiff cyllid o'r Gronfa Perchnogaeth Gymunedol ei ddefnyddio o " + "fewn y cyfnod o 12 mis." +) + + +COF_PLANNING_PERMISSION_CAVEAT_EN = ( + "Get planning permission: When you apply, you must be able to show that you have secured or have made good progress" + " in securing planning permission (and building warrants, if required). This is so that we're confident that COF" + " funding will be used within the 12 month timeframe." +) +COF_PLANNING_PERMISSION_CAVEAT_CY = ( + "Sicrhewch ganiatâd cynllunio: Pan fyddwch yn gwneud cais, rhaid i chi allu dangos eich bod wedi sicrhau caniatâd " + "cynllunio (a gwarantau adeiladu, os oes angen), neu'ch bod wedi gwneud cynnydd da yn hyn o beth. Diben hyn yw " + "rhoi'r hyder i ni y caiff cyllid o'r Gronfa Perchnogaeth Gymunedol ei ddefnyddio o fewn y cyfnod o 12 mis." +) + + +COF_R3_EOI_SCHEMA_EN = { + "uYiLsv": [ + { + "answerValue": "not-yet-incorporated", + "result": Decision.PASS_WITH_CAVEATS, + "caveat": ( + "Incorporate your organisation: You must have incorporated your" + " organisation by the time you submit a full application. If you remain" + " unincorporated, your application will be ineligible." + ), + }, + ], + "NcQSbU": [ + { + "answerValue": True, + "result": Decision.FAIL, + "caveat": None, + }, + ], + "eEaDGz": [ + { + "answerValue": False, + "result": Decision.FAIL, + "caveat": None, + }, + ], + "zurxox": [ + { + "answerValue": False, + "result": Decision.FAIL, + "caveat": None, + }, + ], + "lLQmNb": [ + { + "answerValue": False, + "result": Decision.FAIL, + "caveat": None, + }, + ], + "fBhSNc": [ + { + "answerValue": False, + "result": Decision.FAIL, + "caveat": None, + }, + ], + "XuAyrs": [ + { + "answerValue": "Yes, a town, parish or community council", + "result": Decision.PASS_WITH_CAVEATS, + "caveat": ( + "Understand the rules on acquiring assets from town, parish or" + " community councils: We cannot fund you to acquire a publicly owned" + " asset if this involves transferring responsibility for delivering" + " statutory services (services paid for by tax payers) from the public" + " authority to your organisation. You should only apply to acquire an" + " asset from a town, parish or community council if you do not plan to" + " deliver statutory services." + ), + }, + { + "answerValue": "Yes, another type of public authority", + "result": Decision.PASS_WITH_CAVEATS, + "caveat": ( + "Understand the rules on acquiring public sector assets: COF funding" + " can only be used for renovation and refurbishment costs once a" + " publicly owned asset has been transferred to you. We cannot fund" + " capital receipts, unless the costs incurred in transferring the asset" + " to you are nominal (very small and far below the real value).In your" + " application, you should show that you are not asking COF to fund a" + " capital receipt to a public authority (for example, by sharing a" + " letter confirming the authority is willing/has already agreed a" + " long-term lease and no capital receipt is involved).We also cannot" + " fund you to acquire a publicly owned asset if this involves" + " transferring responsibility for delivering statutory services" + " (services paid for by tax payers) from the public authority to your" + " organisation" + ), + }, + ], + "foQgiy": [ + { + "answerValue": False, + "result": Decision.FAIL, + "caveat": None, + }, + ], + "BykoQQ": [ + { + "answerValue": ["Not sure"], + "result": Decision.PASS_WITH_CAVEATS, + "caveat": ( + "Make progress in securing match funding: COF will contribute up to 80%" + " of the capital costs you require, and you must raise at least 20%" + " from other sources.You do not need to have secured all your match" + " funding by the time you apply, but we will ask you to set out your" + " total costs, funding already secured, and plans to raise any" + " additional funding.You must use COF funding within 12 months, so you" + " must be able to show that you've made good progress to secure the" + " remaining match funding. This is so that we're confident you can draw" + " down this funding within this timeframe." + ), + }, + ], + "eOWKoO": [ + { + "answerValue": False, + "result": Decision.FAIL, + "caveat": None, + }, + ], + "oblxxv": [ + { + "answerValue": False, + "result": Decision.PASS_WITH_CAVEATS, + "caveat": ( + "Consider requesting revenue funding: We encourage all organisations" + " to apply for revenue funding to help cover the initial running costs" + " of your project. When you apply, you'll need to show us how you" + " plan to use any revenue funding." + " See [Section 9 of the COF prospectus for more" + " guidance](https://www.gov.uk/government/publications/community-" + "ownership-fund-prospectus/community-ownership-fund-prospectus--3#funding-available)." + ), + }, + ], + "kWRuac": [ + { + "answerValue": "Not yet approached any funders", + "result": Decision.PASS_WITH_CAVEATS, + "caveat": COF_SECURE_MATCH_FUNDING_CAVEAT_EN, + }, + { + "answerValue": "Approached some funders but not yet secured", + "result": Decision.PASS_WITH_CAVEATS, + "caveat": COF_SECURE_MATCH_FUNDING_CAVEAT_EN, + }, + { + "answerValue": "Approached all funders but not yet secured", + "result": Decision.PASS_WITH_CAVEATS, + "caveat": COF_SECURE_MATCH_FUNDING_CAVEAT_EN, + }, + { + "answerValue": "Secured some match funding", + "result": Decision.PASS_WITH_CAVEATS, + "caveat": COF_SECURE_MATCH_FUNDING_CAVEAT_EN, + }, + ], + "yZxdeJ": [ + { + "answerValue": True, + "result": Decision.PASS_WITH_CAVEATS, + "caveat": ( + "Understand the rules on housing: We will not provide funding if your" + " project's main purpose is to purchase or develop housing assets," + " including social housing. However, you can include housing elements" + " in your project where these are only a small part of supporting the" + " overall financial sustainability of the asset in community ownership." + ), + } + ], + "UORyaF": [ + { + "answerValue": "Not sure", + "result": Decision.PASS_WITH_CAVEATS, + "caveat": COF_PLANNING_PERMISSION_IF_NEEDED_CAVEAT_EN, + } + ], + "jICagT": [ + { + "answerValue": "Not yet started", + "result": Decision.PASS_WITH_CAVEATS, + "caveat": COF_PLANNING_PERMISSION_CAVEAT_EN, + }, + { + "answerValue": "Early stage", + "result": Decision.PASS_WITH_CAVEATS, + "caveat": COF_PLANNING_PERMISSION_CAVEAT_EN, + }, + ], + "fZAMFv": [ + { + "operator": ">", + "compareValue": 2000000, # 2 million + "result": Decision.FAIL, + "caveat": None, + } + ], +} +COF_R3_EOI_SCHEMA_CY = { + "uYiLsv": [ + { + "answerValue": "Ddim yn gorfforedig eto", + "result": Decision.PASS_WITH_CAVEATS, + "caveat": ( + "Dylech gorffori eich sefydliad: Mae'n rhaid eich bod wedi corffori eich sefydliad erbyn eich bod yn " + "cyflwyno cais llawn. Os byddwch yn anghorfforedig o hyd, ni fydd eich cais yn gymwys." + ), + }, + ], + "NcQSbU": [ + { + "answerValue": True, + "result": Decision.FAIL, + "caveat": None, + }, + ], + "eEaDGz": [ + { + "answerValue": False, + "result": Decision.FAIL, + "caveat": None, + }, + ], + "zurxox": [ + { + "answerValue": False, + "result": Decision.FAIL, + "caveat": None, + }, + ], + "lLQmNb": [ + { + "answerValue": False, + "result": Decision.FAIL, + "caveat": None, + }, + ], + "fBhSNc": [ + { + "answerValue": False, + "result": Decision.FAIL, + "caveat": None, + }, + ], + "XuAyrs": [ + { + "answerValue": "Ydy, cyngor tref, plwyf neu gymuned", + "result": Decision.PASS_WITH_CAVEATS, + "caveat": ( + "Dylech ddeall y rheolau ynglŷn â chaffael asedau gan gynghorau tref, plwyf neu gymuned: Ni allwn " + "eich ariannu i gaffael ased dan berchnogaeth gyhoeddus os yw'n golygu trosglwyddo cyfrifoldeb am " + "ddarparu gwasanaethau statudol (gwasanaethau y mae trethdalwyr yn talu amdanynt) o'r awdurdod " + "cyhoeddus i'ch sefydliad. Dim ond os nad ydych yn bwriadu darparu gwasanaethau statudol y dylech " + "wneud cais i gaffael ased gan gyngor tref, plwyf neu gymuned." + ), + }, + { + "answerValue": "Ydy, math arall o awdurdod cyhoeddus", + "result": Decision.PASS_WITH_CAVEATS, + "caveat": ( + "Dylech ddeall y rheolau ynglŷn â chaffael asedau'r sector cyhoeddus: Dim ond ar ôl trosglwyddo ased " + "sydd dan berchnogaeth gyhoeddus i chi y gellir defnyddio cyllid o'r Gronfa Perchnogaeth Gymunedol ar " + "gyfer costau adnewyddu ac ailwampio. Ni allwn ariannu derbyniad cyfalaf, oni bai bod y costau yr aed " + "iddynt wrth drosglwyddo'r ased i chi yn nominal (bach iawn ac yn llawer is na'r gwerth gwirioneddol). " + "Yn eich cais, dylech ddangos nad ydych yn gofyn i'r Gronfa Perchnogaeth Gymunedol ariannu derbyniad " + "cyfalaf i awdurdod cyhoeddus (er enghraifft drwy rannu llythyr yn cadarnhau bod yr awdurdod yn fodlon " + "ar/eisoes wedi cytuno i les hirdymor ac nad oes derbyniad cyfalaf yn gysylltiedig). Ni allwn ychwaith " + "eich ariannu i gaffael ased dan berchnogaeth gyhoeddus os yw'n golygu trosglwyddo cyfrifoldeb am " + "ddarparu gwasanaethau statudol (gwasanaethau y mae trethdalwyr yn talu amdanynt) o'r awdurdod " + "cyhoeddus i'ch sefydliad." + ), + }, + ], + "foQgiy": [ + { + "answerValue": False, + "result": Decision.FAIL, + "caveat": None, + }, + ], + "BykoQQ": [ + { + "answerValue": ["none"], + "result": Decision.PASS_WITH_CAVEATS, + "caveat": ( + "Gwneud cynnydd i sicrhau arian cyfatebol: Bydd y Gronfa Perchnogaeth Gymunedol yn cyfrannu hyd at " + "80% o'r costau cyfalaf sydd eu hangen arnoch, ac mae'n rhaid i chi godi o leiaf 20% o ffynonellau " + "eraill. Nid oes angen i chi fod wedi sicrhau eich holl arian cyfatebol erbyn i chi wneud cais, " + "ond byddwn yn gofyn i chi nodi cyfanswm eich costau, y cyllid rydych eisoes wedi'i sicrhau, " + "a chynlluniau i godi unrhyw gyllid ychwanegol. Mae'n rhaid i chi ddefnyddio cyllid o'r Gronfa " + "Perchnogaeth Gymunedol o fewn 12 mis, felly mae'n rhaid i chi allu dangos eich bod wedi gwneud " + "cynnydd da i sicrhau'r arian cyfatebol sy'n weddill. Diben hyn yw rhoi'r hyder i ni y gallwch " + "ddefnyddio'r cyllid hwn o fewn yr amserlen hon." + ), + }, + ], + "eOWKoO": [ + { + "answerValue": False, + "result": Decision.FAIL, + "caveat": None, + }, + ], + "oblxxv": [ + { + "answerValue": False, + "result": Decision.PASS_WITH_CAVEATS, + "caveat": ( + "Ystyriwch wneud cais am gyllid refeniw: Rydym yn annog pob sefydliad i wneud cais am gyllid refeniw " + "er mwyn helpu i dalu costau rhedeg cychwynnol eich prosiect. Pan fyddwch yn gwneud cais, bydd angen " + "i chi ddangos i ni sut rydych yn bwriadu defnyddio unrhyw gyllid refeniw. [Gweler Adran 9 o " + "brosbectws y Gronfa Perchnogaeth Gymunedol am ragor o ganllawiau.](" + "https://www.gov.uk/government/publications/community-ownership-fund-prospectus/community-ownership" + "-fund-prospectus--3#funding-available)" + ), + }, + ], + "kWRuac": [ + { + "answerValue": "Heb gysylltu ag unrhyw gyllidwyr eto", + "result": Decision.PASS_WITH_CAVEATS, + "caveat": COF_SECURE_MATCH_FUNDING_CAVEAT_CY, + }, + { + "answerValue": "Wedi cysylltu â rhai cyllidwyr ond heb sicrhau cyllid eto", + "result": Decision.PASS_WITH_CAVEATS, + "caveat": COF_SECURE_MATCH_FUNDING_CAVEAT_CY, + }, + { + "answerValue": "Wedi cysylltu â'r holl gyllidwyr ond heb sicrhau cyllid eto", + "result": Decision.PASS_WITH_CAVEATS, + "caveat": COF_SECURE_MATCH_FUNDING_CAVEAT_CY, + }, + { + "answerValue": "Wedi sicrhau rhywfaint o arian cyfatebol", + "result": Decision.PASS_WITH_CAVEATS, + "caveat": COF_SECURE_MATCH_FUNDING_CAVEAT_CY, + }, + ], + "yZxdeJ": [ + { + "answerValue": True, + "result": Decision.PASS_WITH_CAVEATS, + "caveat": ( + "Dylech ddeall y rheolau ynglyn â thai: Ni fyddwn yn darparu cyllid os mai prif ddiben eich prosiect " + "yw prynu neu ddatblygu asedau tai, gan gynnwys tai cymdeithasol. Fodd bynnag, gallwch gynnwys " + "elfennau tai yn eich prosiect os mai dim ond rhan fach o gefnogi cynaliadwyedd ariannol gyffredinol " + "yr ased dan berchnogaeth gymunedol yw'r rhain." + ), + } + ], + "UORyaF": [ + { + "answerValue": "Ddim yn siŵr", + "result": Decision.PASS_WITH_CAVEATS, + "caveat": COF_PLANNING_PERMISSION_IF_NEEDED_CAVEAT_CY, + } + ], + "jICagT": [ + { + "answerValue": "Heb ddechrau eto", + "result": Decision.PASS_WITH_CAVEATS, + "caveat": COF_PLANNING_PERMISSION_CAVEAT_CY, + }, + { + "answerValue": "Cam cynnar", + "result": Decision.PASS_WITH_CAVEATS, + "caveat": COF_PLANNING_PERMISSION_CAVEAT_CY, + }, + ], + "fZAMFv": [ + { + "operator": ">", + "compareValue": 2000000, # 2 million + "result": Decision.FAIL, + "caveat": None, + } + ], +} diff --git a/fund_store/config/fund_loader_config/cof/shared.py b/fund_store/config/fund_loader_config/cof/shared.py new file mode 100644 index 000000000..1fc2ac806 --- /dev/null +++ b/fund_store/config/fund_loader_config/cof/shared.py @@ -0,0 +1,64 @@ +from config.fund_loader_config.logo import DLUHC_LOGO_PNG +from db.models.fund import FundingType + +fund_config = { + "id": "47aef2f5-3fcb-4d45-acb5-f0152b5f03c4", + "name_json": { + "en": "Community Ownership Fund", + "cy": "Y Cronfa Perchnogaeth Gymunedol", + }, + "title_json": { + "en": "funding to save an asset in your community", + "cy": "gyllid i achub ased yn eich cymuned", + }, + "short_name": "COF", + "funding_type": FundingType.COMPETITIVE, + "description_json": { + "en": ( + "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." + ), + "cy": ( # TODO: Provide welsh translation + "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, + "owner_organisation_name": "Department for Levelling Up, Housing and Communities", + "owner_organisation_shortname": "DLUHC", + "owner_organisation_logo_uri": DLUHC_LOGO_PNG, +} + +COF_APPLICATION_GUIDANCE = { + "en": ( + "

What we'll ask you for

You can preview the full list of" + " application questions.

We'll also ask you to upload" + " a business plan to support the answers you've given us in the management case" + " section.

" + ), + "cy": ( + "

Beth fyddwn ni'n gofyn i chi amdano

Gallwch gael cip ymlaen llaw o restr lawn" + " cwestiynau'r cais.

Byddwn hefyd yn gofyn i chi " + "lanlwytho cynllun busnes i ategu'r atebion rydych wedi'u rhoi i ni yn" + " yr adran achos rheoli.

" + ), +} + +EOI_APPLICATION_GUIDANCE = { + "en": ( + "

What we'll ask you for

You can preview the full list of" + " application questions.

" + ), + "cy": ( + "

Beth y byddwn yn gofyn i chi amdano

Gallwch gael rhagolwg o'r rhestr lawn o" + " gwestiynau yn y cais.

" + ), +} diff --git a/fund_store/config/fund_loader_config/common_fund_config/fund_base_tree_paths.py b/fund_store/config/fund_loader_config/common_fund_config/fund_base_tree_paths.py new file mode 100644 index 000000000..dfb07179d --- /dev/null +++ b/fund_store/config/fund_loader_config/common_fund_config/fund_base_tree_paths.py @@ -0,0 +1,17 @@ +# Should increment for each new round, anything that shares the same base path will also share +# the child tree path config. +# +COF_R2_W2_BASE_PATH = 1 +COF_R2_W3_BASE_PATH = 1 +COF_R3_W1_BASE_PATH = 2 +NSTF_R2_BASE_PATH = 3 +COF_R3_W2_BASE_PATH = 4 +CYP_R1_BASE_PATH = 5 +DPI_R2_BASE_PATH = 6 +COF_R3_W3_BASE_PATH = 7 +COF_EOI_BASE_PATH = 8 +COF_R4_W1_BASE_PATH = 9 +HSRA_BASE_PATH = 10 +COF_R4_W2_BASE_PATH = 11 + +FAB_BASE_PATH = 0 diff --git a/fund_store/config/fund_loader_config/cyp/cyp_r1.py b/fund_store/config/fund_loader_config/cyp/cyp_r1.py new file mode 100644 index 000000000..7891efed0 --- /dev/null +++ b/fund_store/config/fund_loader_config/cyp/cyp_r1.py @@ -0,0 +1,195 @@ +from datetime import datetime, timezone + +from config.fund_loader_config.common_fund_config.fund_base_tree_paths import ( + CYP_R1_BASE_PATH, +) +from config.fund_loader_config.logo import DLUHC_LOGO_PNG +from db.models.fund import FundingType + +CYP_FUND_ID = "1baa0f68-4e0a-4b02-9dfe-b5646f089e65" +CYP_ROUND_1_ID = "888aae3d-7e2c-4523-b9c1-95952b3d1644" +APPLICATION_BASE_PATH = ".".join([str(CYP_R1_BASE_PATH), str(1)]) +ASSESSMENT_BASE_PATH = ".".join([str(CYP_R1_BASE_PATH), str(2)]) +CYP_R1_OPENS_DATE = datetime(2023, 9, 27, 10, 0, 0, tzinfo=timezone.utc) # 2023-09-27 10:00:00 +CYP_R1_DEADLINE_DATE = datetime(2023, 11, 1, 11, 59, 0, tzinfo=timezone.utc) # 2023-11-1 11:59:00 +CYP_R1_ASSESSMENT_DEADLINE_DATE = datetime(2023, 12, 24, 12, 0, 0, tzinfo=timezone.utc) # 2023-12-24 12:00:00 + +CYP_PROSPECTS_LINK = "https://www.gov.uk/government/publications/the-children-and-young-peoples-resettlement-fund-prospectus/the-children-and-young-peoples-resettlement-fund-prospectus" # noqa +CYP_APPLICATION_GUIDANCE = { + "en": ( + "

Before you start

Read the fund's prospectus" + " before you apply.

You can preview the full list of application" + " questions.

" + ) +} + +r1_application_sections = [ + { + "section_name": {"en": "Before you start", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.1", + }, + { + "section_name": {"en": "1.1 Name your application", "cy": ""}, + "form_name_json": {"en": "name-your-application-cyp", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.1.1", + }, + { + "section_name": {"en": "2. About your organisation", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.2", + "requires_feedback": True, + }, + { + "section_name": {"en": "2.1 About your organisation", "cy": ""}, + "form_name_json": {"en": "about-your-organisation-cyp", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.2.1", + }, + { + "section_name": {"en": "2.2 Applicant information", "cy": ""}, + "form_name_json": {"en": "applicant-information-cyp", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.2.2", + }, + { + "section_name": {"en": "3. Your skills and experience", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.3", + "requires_feedback": True, + }, + { + "section_name": {"en": "3.1 Your skills and experience", "cy": ""}, + "form_name_json": {"en": "skills-and-experience-cyp", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.3.1", + }, + { + "section_name": {"en": "4. Your project", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.4", + "requires_feedback": True, + }, + { + "section_name": {"en": "4.1 Outputs and outcomes", "cy": ""}, + "form_name_json": {"en": "outputs-and-outcomes-cyp", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.4.1", + }, + { + "section_name": {"en": "4.2 Existing work", "cy": ""}, + "form_name_json": {"en": "existing-work-cyp", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.4.2", + }, + { + "section_name": {"en": "4.3 Project milestones", "cy": ""}, + "form_name_json": {"en": "project-milestones-cyp", "cy": " "}, + "tree_path": f"{APPLICATION_BASE_PATH}.4.3", + }, + { + "section_name": {"en": "4.4 Objectives and activities", "cy": ""}, + "form_name_json": {"en": "objectives-and-activities-cyp", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.4.4", + }, + { + "section_name": {"en": "4.5 Location of activities", "cy": ""}, + "form_name_json": {"en": "location-of-activities-cyp", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.4.5", + }, + { + "section_name": {"en": "4.6 Working with fund beneficiaries", "cy": ""}, + "form_name_json": {"en": "working-with-fund-beneficiaries-cyp", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.4.6", + }, + { + "section_name": {"en": "5. Risk and deliverability", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.5", + "requires_feedback": True, + }, + { + "section_name": {"en": "5.1 Risk and deliverability", "cy": ""}, + "form_name_json": {"en": "risk-and-deliverability-cyp", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.5.1", + }, + { + "section_name": {"en": "6. Value for money", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.6", + "requires_feedback": True, + }, + { + "section_name": {"en": "6.1 Value for money", "cy": ""}, + "form_name_json": {"en": "value-for-money-cyp", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.6.1", + }, + { + "section_name": {"en": "7. Declarations", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.7", + }, + { + "section_name": {"en": "7.1 Declarations", "cy": ""}, + "form_name_json": {"en": "declarations-cyp", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.7.1", + }, +] + +fund_config = { + "id": CYP_FUND_ID, + "name_json": { + "en": "The Children and Young People's Resettlement Fund", + "cy": "", + }, + "title_json": { + "en": ( + "funding to support children and young people on pathways to the UK from Ukraine, Hong Kong and Afghanistan" + ), + "cy": "", + }, + "short_name": "CYP", + "funding_type": FundingType.COMPETITIVE, + "description_json": {"en": "", "cy": ""}, + "welsh_available": False, + "owner_organisation_name": "Department for Levelling Up, Housing and Communities", + "owner_organisation_shortname": "DLUHC", + "owner_organisation_logo_uri": DLUHC_LOGO_PNG, +} + +round_config = [ + { + "id": CYP_ROUND_1_ID, + "fund_id": CYP_FUND_ID, + "title_json": {"en": "Round 1", "cy": "Rownd 1"}, + "short_name": "R1", + "opens": CYP_R1_OPENS_DATE, + "assessment_start": None, + "deadline": CYP_R1_DEADLINE_DATE, + "application_reminder_sent": True, + "reminder_date": None, + "assessment_deadline": CYP_R1_ASSESSMENT_DEADLINE_DATE, + "prospectus": CYP_PROSPECTS_LINK, + "privacy_notice": "https://www.gov.uk/guidance/the-children-and-young-peoples-resettlement-fund-privacy-notice", + "reference_contact_page_over_email": False, + "contact_us_banner_json": {"en": "", "cy": ""}, + "contact_email": "cyprfund@levellingup.gov.uk", + "contact_phone": None, + "contact_textphone": None, + "support_times": "9am to 5pm", + "support_days": "Monday to Friday", + "instructions_json": None, + "feedback_link": "", + "project_name_field_id": "bsUoNG", + "application_guidance_json": CYP_APPLICATION_GUIDANCE, + "guidance_url": ( + "https://www.gov.uk/government/publications/" + "the-children-and-young-peoples-resettlement-" + "fund-prospectus/the-children-and-young-peoples-" + "resettlement-fund-prospectus#scoring-criteria" + ), + "all_uploaded_documents_section_available": False, + "application_fields_download_available": False, + "display_logo_on_pdf_exports": False, + "mark_as_complete_enabled": True, + "is_expression_of_interest": False, + "feedback_survey_config": { + "has_feedback_survey": True, + "has_section_feedback": True, + "is_feedback_survey_optional": False, + "is_section_feedback_optional": False, + }, + "eligibility_config": {"has_eligibility": False}, + "eoi_decision_schema": None, + } +] diff --git a/fund_store/config/fund_loader_config/digital_planning/dpi_r2.py b/fund_store/config/fund_loader_config/digital_planning/dpi_r2.py new file mode 100644 index 000000000..a1198fb09 --- /dev/null +++ b/fund_store/config/fund_loader_config/digital_planning/dpi_r2.py @@ -0,0 +1,169 @@ +from datetime import datetime, timezone + +from config.fund_loader_config.common_fund_config.fund_base_tree_paths import ( + DPI_R2_BASE_PATH, +) +from config.fund_loader_config.logo import DLUHC_LOGO_PNG +from db.models.fund import FundingType + +DPI_FUND_ID = "f493d512-5eb4-11ee-8c99-0242ac120002" +DPI_ROUND_2_ID = "0059aad4-5eb5-11ee-8c99-0242ac120002" +APPLICATION_BASE_PATH = ".".join([str(DPI_R2_BASE_PATH), str(1)]) +ASSESSMENT_BASE_PATH = ".".join([str(DPI_R2_BASE_PATH), str(2)]) +DPI_R2_OPENS_DATE = datetime(2023, 10, 17, 9, 30, 0, tzinfo=timezone.utc) # 2023-10-17 10:00:00 +DPI_R2_DEADLINE_DATE = datetime(2023, 12, 1, 17, 0, 0, tzinfo=timezone.utc) # 2023-12-1 11:59:00 +DPI_R2_ASSESSMENT_DEADLINE_DATE = datetime(2024, 1, 31, 12, 0, 0, tzinfo=timezone.utc) # 2023-01-31 12:00:00 + +DPI_PROSPECTS_LINK = "https://www.localdigital.gov.uk/digital-planning/funding/digital-planning-programme-funding-2023" # noqa +DPI_PRIVACY_NOTICE = "https://www.gov.uk/guidance/digital-planning-improvement-fund-privacy-notice" +DPI_APPLICATION_GUIDANCE = { + "en": ( + "

Before you start

Read the fund's prospectus" + " before you apply.

You can preview the full list of application" + " questions.

" + ) +} + +r2_application_sections = [ + { + "section_name": {"en": "1. Before you start", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.1", + }, + { + "section_name": {"en": "1.1 Name your application", "cy": ""}, + "form_name_json": {"en": "name-your-application", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.1.1", + }, + { + "section_name": {"en": "2. About your organisation", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.2", + "requires_feedback": True, + }, + { + "section_name": {"en": "2.1 Organisation information", "cy": ""}, + "form_name_json": {"en": "organisation-information-dpi", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.2.1", + }, + { + "section_name": {"en": "3. Your skills and experience", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.3", + "weighting": 50, + "requires_feedback": True, + }, + { + "section_name": {"en": "3.1 Your skills and experience", "cy": ""}, + "form_name_json": {"en": "your-skills-and-experience-dpi", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.3.1", + }, + { + "section_name": {"en": "3.2 Roles and recruitment", "cy": ""}, + "form_name_json": {"en": "roles-and-recruitment-dpi", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.3.2", + }, + { + "section_name": {"en": "4. About your project", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.4", + "weighting": 50, + "requires_feedback": True, + }, + { + "section_name": {"en": "4.1 Engaging the ODP community", "cy": ""}, + "form_name_json": {"en": "engaging-the-odp-community-dpi", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.4.1", + }, + { + "section_name": {"en": "4.2 Engaging the organisation", "cy": ""}, + "form_name_json": {"en": "engaging-the-organisation-dpi", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.4.2", + }, + { + "section_name": {"en": "4.3 Dataset information", "cy": ""}, + "form_name_json": {"en": "dataset-information-dpi", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.4.3", + }, + { + "section_name": {"en": "5. Future work", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.5", + "requires_feedback": True, + }, + { + "section_name": {"en": "5.1 Future work", "cy": ""}, + "form_name_json": {"en": "future-work-dpi", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.5.1", + }, + { + "section_name": {"en": "6. Declarations", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.6", + }, + { + "section_name": {"en": "6.1 Declarations", "cy": ""}, + "form_name_json": {"en": "declarations-dpi", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.6.1", + }, +] + +fund_config = { + "id": DPI_FUND_ID, + "name_json": { + "en": "Digital Planning Improvement Fund", + "cy": "", + }, + "title_json": { + "en": "funding to begin your digital planning improvement journey", + "cy": "", + }, + "short_name": "DPIF", + "funding_type": FundingType.COMPETITIVE, + "description_json": {"en": "", "cy": ""}, + "welsh_available": False, + "owner_organisation_name": "Department for Levelling Up, Housing and Communities", + "owner_organisation_shortname": "DLUHC", + "owner_organisation_logo_uri": DLUHC_LOGO_PNG, +} + +round_config = [ + { + "id": DPI_ROUND_2_ID, + "fund_id": DPI_FUND_ID, + "title_json": {"en": "Round 2", "cy": ""}, + "short_name": "R2", + "opens": DPI_R2_OPENS_DATE, + "assessment_start": None, + "deadline": DPI_R2_DEADLINE_DATE, + "application_reminder_sent": True, + "reminder_date": None, + "assessment_deadline": DPI_R2_ASSESSMENT_DEADLINE_DATE, + "prospectus": DPI_PROSPECTS_LINK, + "privacy_notice": DPI_PRIVACY_NOTICE, + "reference_contact_page_over_email": False, + "contact_us_banner_json": {"en": "", "cy": ""}, + "contact_email": "digitalplanningteam@levellingup.gov.uk", + "contact_phone": None, + "contact_textphone": None, + "support_times": "9am to 5pm", + "support_days": "Monday to Friday", + "instructions_json": None, + "feedback_link": "", + "project_name_field_id": "JAAhRP", + "application_guidance_json": DPI_APPLICATION_GUIDANCE, + "guidance_url": ( + "https://docs.google.com/document/d/1cF5eKphoBWEUe0Zv5HBwv0R3n1svCk16kUFRJhKnIQY" + "/edit#heading=h.b0vrhm5gih2k" + ), + "all_uploaded_documents_section_available": False, + "application_fields_download_available": True, + "display_logo_on_pdf_exports": False, + "mark_as_complete_enabled": True, + "is_expression_of_interest": False, + "feedback_survey_config": { + "has_feedback_survey": True, + "has_section_feedback": True, + "is_feedback_survey_optional": True, + "is_section_feedback_optional": True, + }, + "eligibility_config": {"has_eligibility": False}, + "eoi_decision_schema": None, + } +] diff --git a/fund_store/config/fund_loader_config/hsra/hsra.py b/fund_store/config/fund_loader_config/hsra/hsra.py new file mode 100644 index 000000000..dc218292b --- /dev/null +++ b/fund_store/config/fund_loader_config/hsra/hsra.py @@ -0,0 +1,172 @@ +from datetime import datetime, timezone + +from config.fund_loader_config.common_fund_config.fund_base_tree_paths import ( + HSRA_BASE_PATH, +) +from config.fund_loader_config.hsra.shared import HSRA_APPLICATION_GUIDANCE +from config.fund_loader_config.logo import DLUHC_LOGO_PNG +from db.models.fund import FundingType + +HSRA_FUND_ID = "1e4bd8b0-b399-466d-bbd1-572171bbc7bd" +HSRA_ROUND_ID = "50062ff6-e696-474d-a560-4d9af784e6e5" + +APPLICATION_BASE_PATH_HSRA = ".".join([str(HSRA_BASE_PATH), str(1)]) +ASSESSMENT_BASE_PATH_HSRA = ".".join([str(HSRA_BASE_PATH), str(2)]) + +# TODO Dates are still being debated and are likely to change +HSRA_OPENS_DATE = datetime(2024, 6, 27, 11, 00, 0, tzinfo=timezone.utc) # 2024-06-27 11:00:00 +HSRA_START_DATE = datetime(2024, 6, 27, 11, 00, 0, tzinfo=timezone.utc) # 2024-06-27 11:00:00 +# TODO The bellow dates are likely to change when the fund is live +HSRA_SEND_REMINDER_DATE = datetime(2025, 1, 27, 11, 59, 0, tzinfo=timezone.utc) # 2025-1-27 11:59:00 +HSRA_DEADLINE_DATE = datetime(2025, 1, 29, 11, 00, 0, tzinfo=timezone.utc) # 2025-01-29 11:00:00 +HSRA_ASSESSMENT_DEADLINE_DATE = datetime(2024, 6, 23, 12, 0, 0, tzinfo=timezone.utc) # 2024-06-23 12:00:00 + +hsra_sections = [ + { + "section_name": {"en": "1. Application name", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH_HSRA}.1", + }, + { + "section_name": {"en": "Name your application", "cy": ""}, + "form_name_json": {"en": "name-your-application-hsra", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH_HSRA}.1.1", + }, + { + "section_name": {"en": "2. About your organisation", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH_HSRA}.2", + }, + { + "section_name": {"en": "Organisation information", "cy": ""}, + "form_name_json": {"en": "organisation-information-hsra", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH_HSRA}.2.1", + }, + { + "section_name": {"en": "Applicant information", "cy": ""}, + "form_name_json": {"en": "applicant-information-hsra", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH_HSRA}.2.2", + }, + { + "section_name": {"en": "Joint applicant", "cy": ""}, + "form_name_json": {"en": "joint-applicant-hsra", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH_HSRA}.2.3", + }, + { + "section_name": {"en": "3. About your project", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH_HSRA}.3", + }, + { + "section_name": {"en": "Vacant property details", "cy": ""}, + "form_name_json": {"en": "vacant-property-details-hsra", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH_HSRA}.3.1", + }, + { + "section_name": {"en": "Designated area details", "cy": ""}, + "form_name_json": {"en": "designated-area-details-hsra", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH_HSRA}.3.2", + }, + { + "section_name": {"en": "Project milestones", "cy": ""}, + "form_name_json": {"en": "milestones-hsra", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH_HSRA}.3.3", + }, + { + "section_name": {"en": "4. Project costs", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH_HSRA}.4", + }, + { + "section_name": {"en": "Total expected cost", "cy": ""}, + "form_name_json": {"en": "total-expected-cost-hsra", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH_HSRA}.4.1", + }, + { + "section_name": {"en": "Refurbishment costs", "cy": ""}, + "form_name_json": {"en": "refurbishment-costs-hsra", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH_HSRA}.4.2", + }, + { + "section_name": {"en": "Other costs", "cy": ""}, + "form_name_json": {"en": "other-costs-hsra", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH_HSRA}.4.3", + }, + { + "section_name": {"en": "5. Declaration", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH_HSRA}.5", + }, + { + "section_name": {"en": "Declaration", "cy": ""}, + "form_name_json": {"en": "declaration-hsra", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH_HSRA}.5.1", + }, +] + +fund_config = { + "id": HSRA_FUND_ID, + "name_json": { + "en": "High Street Rental Auctions Fund", + "cy": "", + }, + "title_json": { + "en": "funding to cover the cost of delivering a high street rental auction", + "cy": "", + }, + "short_name": "HSRA", + "funding_type": FundingType.COMPETITIVE, + "description_json": {"en": "", "cy": ""}, + "welsh_available": False, + "owner_organisation_name": "Department for Levelling Up, Housing and Communities", + "owner_organisation_shortname": "DLUHC", + "owner_organisation_logo_uri": DLUHC_LOGO_PNG, +} + +round_config = [ + { + "id": HSRA_ROUND_ID, + "fund_id": HSRA_FUND_ID, + "title_json": {"en": "Round 1", "cy": "Rownd 1"}, + "short_name": "R1", + "opens": HSRA_OPENS_DATE, + "assessment_start": HSRA_START_DATE, + "deadline": HSRA_DEADLINE_DATE, + "application_reminder_sent": False, + "reminder_date": HSRA_SEND_REMINDER_DATE, + "assessment_deadline": HSRA_ASSESSMENT_DEADLINE_DATE, + "prospectus": "", # TODO needs to be added + "privacy_notice": "", + "reference_contact_page_over_email": False, + "contact_us_banner_json": {"en": "", "cy": ""}, + "contact_email": "HighStreetRentalAuctions@levellingup.gov.uk", + "contact_phone": None, + "contact_textphone": None, + "support_times": "9am to 5pm", + "support_days": "Monday to Friday", + "instructions_json": { + "en": ( + "You must have received an invitation to apply. If we did not invite you," + " first ' + " express your interest in the fund." + ), # TODO Need to guidance link + "cy": (""), + }, + "feedback_link": ( + "https://forms.office.com/Pages/ResponsePage.aspx?id=" + "EGg0v32c3kOociSi7zmVqFJBHpeOL2tNnpiwpdL2iElURUY1WkhaS0NFMlZVQUhYQ1NaN0E4RjlQMC4u" + ), + "project_name_field_id": "qbBtUh", + "application_guidance_json": HSRA_APPLICATION_GUIDANCE, + "guidance_url": "", # TODO add guidance link + "all_uploaded_documents_section_available": True, + "application_fields_download_available": True, + "display_logo_on_pdf_exports": False, + "mark_as_complete_enabled": True, + "is_expression_of_interest": False, + "feedback_survey_config": { + "has_feedback_survey": False, + "has_section_feedback": False, + "is_feedback_survey_optional": False, + "is_section_feedback_optional": False, + }, + "eligibility_config": {"has_eligibility": True}, + "eoi_decision_schema": None, + } +] diff --git a/fund_store/config/fund_loader_config/hsra/shared.py b/fund_store/config/fund_loader_config/hsra/shared.py new file mode 100644 index 000000000..382d0d6c7 --- /dev/null +++ b/fund_store/config/fund_loader_config/hsra/shared.py @@ -0,0 +1,9 @@ +HSRA_APPLICATION_GUIDANCE = { + "en": ( + "

What we'll ask you for

You can preview the full list of" + " application questions.

We'll also ask you to upload" + " a business plan to support the answers you've given us in the management case" + " section.

" + ) +} diff --git a/fund_store/config/fund_loader_config/logo.py b/fund_store/config/fund_loader_config/logo.py new file mode 100644 index 000000000..7f8c10450 --- /dev/null +++ b/fund_store/config/fund_loader_config/logo.py @@ -0,0 +1 @@ +DLUHC_LOGO_PNG = "data:image/png;base64, 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JEmSJEmSVDUMQEuSJEmSJEmSJEmSJEmSJEmqGgagJUmSJEmSJEmSJEmSJEmSJFUNA9CSJEmSJEmSJEmSJEmSJEmSqoYBaEmSJEmSJEmSJEmSJEmSJElVwwC0JEmSJEmSJEmSJEmSJEmSpKphAFqSJEmSJEmSJEmSJEmSJElS1TAALUmSJEmSJEmSJEmSJEmSJKlqGICWJEmSJEmSJEmSJEmSJEmSVDUMQEuSJEmSJEmSJEmSJEmSJEmqGgagJUmSJEmSJEmSJEmSJEmSJFUNA9CSJEmSJEmSJEmSJEmSJEmSqoYBaEmSJEmSJEmSJEmSJEmSJElVwwC0JEmSJEmSJEmSJEmSJEmSpKphAFqSJEmSJEmSJEmSJEmSJElS1TAALUmSJEmSJEmSJEmSJEmSJKlqGICWJEmSJEmSJEmSJEmSJEmSVDUMQEuSJEmSJEmSJEmSJEmSJEmqGgagJUmSJEmSJEmSJEmSJEmSJFUNA9CSJEmSJEmSJEmSJEmSJEmSqoYBaEmSJEmSJEmSJEmSJEmSJElVwwC0JEmSJEmSJEmSJEmSJEmSpKphAFqSJEmSJEmSJEmSJEmSJElS1TAALUmSJEmSJEmSJEmSJEmSJKlqGICWJEmSJEmSJEmSJEmSJEmSVDUMQEuSJEmSJEmSJEmSJEmSJEmqGgagJUmSJEmSJEmSJEmSJEmSJFUNA9CSJEmSJEmSJEmSJEmSJEmSqoYBaEmSJEmSJEmSJEmSJEmSJElVwwC0JEmSJEmSJEmSJEmSJEmSpKphAFqSJEmSJEmSJEmSJEmSJElS1TAALUmSJEmSJEmSJEmSJEmSJKlqGICWJEmSJEmSJEmSJEmSJEmSVDUMQEuSJEmSJEmSJEmSJEmSJEmqGgagJUmSJEmSJEmSJEmSJEmSJFUNA9CSJEmSJEmSJEmSJEmSJEmSqoYBaEmSJEmSJEmSJEmSJEmSJElVwwC0JEmSJEmSJEmSJEmSJEmSpKphAFqSJEmSJEmSJEmSJEmSJElS1TAALUmSJEmSJEmSJEmSJEmSJKlqGICWJEmSJEmSJEmSJEmSJEmSVDUMQEuSJEmSJEmSJEmSJEmSJEmqGgagJUmSJEmSJEmSJEmSJEmSJFUNA9CSJEmSJEmSJEmSJEmSJEmSqoYBaEmSJEmSJEmSJEmSJEmSJElVwwC0JEmSJEmSJEmSJEmSJEmS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" # noqa diff --git a/fund_store/config/fund_loader_config/night_shelter/ns_r2.py b/fund_store/config/fund_loader_config/night_shelter/ns_r2.py new file mode 100644 index 000000000..e0f31cacc --- /dev/null +++ b/fund_store/config/fund_loader_config/night_shelter/ns_r2.py @@ -0,0 +1,225 @@ +from datetime import datetime, timezone + +from config.fund_loader_config.common_fund_config.fund_base_tree_paths import ( + NSTF_R2_BASE_PATH, +) +from config.fund_loader_config.logo import DLUHC_LOGO_PNG +from db.models.fund import FundingType + +NIGHT_SHELTER_FUND_ID = "13b95669-ed98-4840-8652-d6b7a19964db" +NIGHT_SHELTER_ROUND_2_ID = "fc7aa604-989e-4364-98a7-d1234271435a" +APPLICATION_BASE_PATH = ".".join([str(NSTF_R2_BASE_PATH), str(1)]) +ASSESSMENT_BASE_PATH = ".".join([str(NSTF_R2_BASE_PATH), str(2)]) +NS_R2_OPENS_DATE = datetime(2023, 6, 7, 12, 0, 0, tzinfo=timezone.utc) # 2023-06-07 12:00:00 +NS_R2_DEADLINE_DATE = datetime(2023, 7, 7, 11, 59, 0, tzinfo=timezone.utc) # 2023-07-07 11:59:00 +NS_R2_ASSESSMENT_DEADLINE_DATE = datetime(2023, 8, 9, 12, 0, 0, tzinfo=timezone.utc) # 2023-08-09 12:00:00 + +NIGHT_SHELTER_PROSPECTS_LINK = ( + "https://www.gov.uk/government/publications/night-shelter-transformation-fund-round-2-prospectus" +) +NIGHT_SHELTER_APPLICATION_GUIDANCE = { + "en": ( + "

Before you start

Read the fund's" + " prospectus before you apply.

" + ) +} + +r2_application_sections = [ + { + "section_name": {"en": "Before you start", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.1", + }, + { + "section_name": {"en": "Name your application", "cy": ""}, + "form_name_json": {"en": "name-your-application-ns", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.1.1", + }, + { + "section_name": {"en": "1. About your organisation", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.2", + }, + { + "section_name": {"en": "1.1 Organisation information", "cy": ""}, + "form_name_json": {"en": "organisation-information-ns", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.2.1", + }, + { + "section_name": {"en": "1.2 Organisation type", "cy": ""}, + "form_name_json": {"en": "organisation-type-ns", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.2.2", + }, + { + "section_name": {"en": "1.3 Applicant information", "cy": ""}, + "form_name_json": {"en": "applicant-information-ns", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.2.3", + }, + { + "section_name": {"en": "1.4 Joint applications", "cy": ""}, + "form_name_json": {"en": "joint-applications-ns", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.2.4", + }, + { + "section_name": {"en": "2. Your skills and experience", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.3", + "weighting": 15, + }, + { + "section_name": {"en": "2.1 Staff and volunteers", "cy": ""}, + "form_name_json": {"en": "staff-and-volunteers-ns", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.3.1", + }, + { + "section_name": {"en": "2.2 Current services", "cy": ""}, + "form_name_json": {"en": "current-services-ns", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.3.2", + }, + { + "section_name": {"en": "3. Your proposal", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.4", + "weighting": 40, + }, + { + "section_name": {"en": "3.1 Objectives and activities", "cy": ""}, + "form_name_json": {"en": "objectives-and-activities-ns", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.4.1", + }, + { + "section_name": {"en": "3.2 Project milestones", "cy": ""}, + "form_name_json": {"en": "project-milestones-ns", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.4.2", + }, + { + "section_name": {"en": "3.3 Local need and support", "cy": ""}, + "form_name_json": {"en": "local-need-and-support-ns", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.4.3", + }, + { + "section_name": {"en": "3.4 Proposed services", "cy": ""}, + "form_name_json": {"en": "proposed-services-ns", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.4.4", + }, + { + "section_name": {"en": "3.5 Working in partnership", "cy": ""}, + "form_name_json": {"en": "working-in-partnership-ns", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.4.5", + }, + { + "section_name": {"en": "3.6 Proposal sustainability", "cy": ""}, + "form_name_json": {"en": "proposal-sustainability-ns", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.4.6", + }, + { + "section_name": {"en": "4. Outputs and outcomes", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.5", + "weighting": 15, + }, + { + "section_name": {"en": "4.1 Outputs and outcomes", "cy": ""}, + "form_name_json": {"en": "outputs-and-outcomes-ns", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.5.1", + }, + { + "section_name": {"en": "5. Risk and deliverability", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.6", + "weighting": 15, + }, + { + "section_name": {"en": "5.1 Risk and deliverability", "cy": ""}, + "form_name_json": {"en": "risk-and-deliverability-ns", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.6.1", + }, + { + "section_name": {"en": "6. Value for money", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.7", + "weighting": 15, + }, + { + "section_name": {"en": "6.1 Funding required", "cy": ""}, + "form_name_json": {"en": "funding-required-ns", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.7.1", + }, + { + "section_name": {"en": "6.2 Building works", "cy": ""}, + "form_name_json": {"en": "building-works-ns", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.7.2", + }, + { + "section_name": {"en": "6.3 Match funding", "cy": ""}, + "form_name_json": {"en": "match-funding-ns", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.7.3", + }, + { + "section_name": {"en": "7. Declarations", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.8", + }, + { + "section_name": {"en": "7.1 Declarations", "cy": ""}, + "form_name_json": {"en": "declarations-ns", "cy": ""}, + "tree_path": f"{APPLICATION_BASE_PATH}.8.1", + }, +] + +fund_config = { + "id": NIGHT_SHELTER_FUND_ID, + "name_json": { + "en": "Night Shelter Transformation Fund", + "cy": "", + }, + "title_json": { + "en": "funding to transform your night shelter services in England", + "cy": "", + }, + "funding_type": FundingType.COMPETITIVE, + "short_name": "NSTF", + "description_json": {"en": "", "cy": ""}, + "welsh_available": False, + "owner_organisation_name": "Department for Levelling Up, Housing and Communities", + "owner_organisation_shortname": "DLUHC", + "owner_organisation_logo_uri": DLUHC_LOGO_PNG, +} + +round_config = [ + { + "id": NIGHT_SHELTER_ROUND_2_ID, + "fund_id": NIGHT_SHELTER_FUND_ID, + "title_json": {"en": "Round 2", "cy": ""}, + "short_name": "R2", + "opens": NS_R2_OPENS_DATE, + "assessment_start": None, + "deadline": NS_R2_DEADLINE_DATE, + "application_reminder_sent": True, + "reminder_date": None, + "assessment_deadline": NS_R2_ASSESSMENT_DEADLINE_DATE, + "prospectus": NIGHT_SHELTER_PROSPECTS_LINK, + "privacy_notice": "https://www.gov.uk/guidance/night-shelter-transformation-fund-2022-2025-privacy-notice", + "reference_contact_page_over_email": False, + "contact_us_banner_json": {"en": "", "cy": ""}, + "contact_email": "transformationfund@levellingup.gov.uk", + "contact_phone": None, + "contact_textphone": None, + "support_times": "9am to 5pm", + "support_days": "Monday to Friday", + "instructions_json": None, + "feedback_link": "https://forms.office.com/e/n6J9KPebUy", + "project_name_field_id": "YVsPtE", + "application_guidance_json": NIGHT_SHELTER_APPLICATION_GUIDANCE, + "guidance_url": ( + "https://mhclg.sharepoint.com.mcas.ms/:w:/s/HomelessnessandRoughSleeping/EZn" + "-Dq3eBvFDtdBqhyEZxUUBj_BP53F9TVyI0imX3NdcPw?e=PtmLwH" + ), + "all_uploaded_documents_section_available": False, + "application_fields_download_available": False, + "display_logo_on_pdf_exports": False, + "mark_as_complete_enabled": False, + "is_expression_of_interest": False, + "feedback_survey_config": { + "has_feedback_survey": False, + "has_section_feedback": False, + "is_feedback_survey_optional": True, + "is_section_feedback_optional": True, + }, + "eligibility_config": {"has_eligibility": False}, + "eoi_decision_schema": None, + } +] diff --git a/fund_store/copilot/.workspace b/fund_store/copilot/.workspace new file mode 100644 index 000000000..92b20589f --- /dev/null +++ b/fund_store/copilot/.workspace @@ -0,0 +1 @@ +application: pre-award diff --git a/fund_store/copilot/environments/addons/assessment-import-queue.yml b/fund_store/copilot/environments/addons/assessment-import-queue.yml new file mode 100644 index 000000000..b45fabd43 --- /dev/null +++ b/fund_store/copilot/environments/addons/assessment-import-queue.yml @@ -0,0 +1,50 @@ +Parameters: + App: + Type: String + Description: Your application's name. + Env: + Type: String + Description: The environment name your service, job, or workflow is being deployed to. + FifoQueueName: + Type: String + Description: Fifo Queue Name + Default: assessment-import-queue + +Resources: + AssessmentImportQueue: + Type: AWS::SQS::Queue + Properties: + QueueName: !Sub ${FifoQueueName}-${Env}.fifo + FifoQueue: true + RedrivePolicy: + deadLetterTargetArn: !GetAtt DeadLetterQueue.Arn + maxReceiveCount: 3 + DeadLetterQueue: + Type: AWS::SQS::Queue + Properties: + FifoQueue: true + QueueName: !Sub ${FifoQueueName}-${Env}-deadletter.fifo + + + +Outputs: + AssessmentImportQueueURL: + Description: Queue URL for Fifo queue + Value: !Ref AssessmentImportQueue + Export: + Name: !Sub ${App}-${Env}-AssessmentImportQueueURL + AssessmentImportQueueArn: + Description: Queue Arn for FIFO queue + Value: !GetAtt AssessmentImportQueue.Arn + Export: + Name: !Sub ${App}-${Env}-AssessmentImportQueueArn + DeadLetterQueueURL: + Description: "URL of dead-letter queue" + Value: !Ref DeadLetterQueue + Export: + Name: !Sub ${App}-${Env}-DeadLetterQueueURL + DeadLetterQueueARN: + Description: "ARN of dead-letter queue" + Value: !GetAtt DeadLetterQueue.Arn + Export: + Name: !Sub ${App}-${Env}-DeadLetterQueueARN diff --git a/fund_store/copilot/environments/addons/form-uploads.yml b/fund_store/copilot/environments/addons/form-uploads.yml new file mode 100644 index 000000000..bd25feedd --- /dev/null +++ b/fund_store/copilot/environments/addons/form-uploads.yml @@ -0,0 +1,61 @@ +Parameters: + App: + Type: String + Description: Your application's name. + Env: + Type: String + Description: The environment name your service, job, or workflow is being deployed to. + +Resources: + FormUploadsBucket: + Metadata: + 'aws:copilot:description': 'An Amazon S3 bucket, form-uploads, for storing and retrieving objects' + Type: AWS::S3::Bucket + Properties: + AccessControl: Private + BucketName: !Sub fsd-form-uploads-${Env} + BucketEncryption: + ServerSideEncryptionConfiguration: + - ServerSideEncryptionByDefault: + SSEAlgorithm: AES256 + PublicAccessBlockConfiguration: + BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls: true + RestrictPublicBuckets: true + OwnershipControls: + Rules: + - ObjectOwnership: BucketOwnerEnforced + + FormUploadsBucketPolicy: + Metadata: + 'aws:copilot:description': 'A bucket policy to deny unencrypted access to the bucket and its contents' + Type: AWS::S3::BucketPolicy + DeletionPolicy: Retain + Properties: + PolicyDocument: + Version: '2012-10-17' + Statement: + - Sid: ForceHTTPS + Effect: Deny + Principal: '*' + Action: 's3:*' + Resource: + - !Sub ${ FormUploadsBucket.Arn}/* + - !Sub ${ FormUploadsBucket.Arn} + Condition: + Bool: + "aws:SecureTransport": false + Bucket: !Ref FormUploadsBucket + +Outputs: + FormUploadsName: + Description: "The name of a user-defined bucket." + Value: !Ref FormUploadsBucket + Export: + Name: !Sub ${App}-${Env}-FormUploadsBucket + FormUploadsBucketARN: + Description: "The ARN of the form-uploads bucket." + Value: !GetAtt FormUploadsBucket.Arn + Export: + Name: !Sub ${App}-${Env}-FormUploadsBucketARN diff --git a/fund_store/copilot/environments/addons/funding-service-magic-links.yml b/fund_store/copilot/environments/addons/funding-service-magic-links.yml new file mode 100644 index 000000000..01954162b --- /dev/null +++ b/fund_store/copilot/environments/addons/funding-service-magic-links.yml @@ -0,0 +1,94 @@ +Parameters: + App: + Type: String + Description: Your application's name. + Env: + Type: String + Description: The environment name your service, job, or workflow is being deployed to. + +Resources: + # Subnet group to control where the Redis gets placed + RedisSubnetGroup: + Type: AWS::ElastiCache::SubnetGroup + Properties: + Description: Group of subnets to place Redis into + SubnetIds: !Split [ ',', { 'Fn::ImportValue': !Sub '${App}-${Env}-PrivateSubnets' } ] + + # Security group to add the Redis cluster to the VPC, + # and to allow the Fargate containers to talk to Redis on port 6379 + RedisSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: "Redis Security Group" + VpcId: + Fn::ImportValue: + !Sub '${App}-${Env}-VpcId' + + # Enable ingress from other ECS services created within the environment. + RedisIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + Description: Ingress from Fargate containers + GroupId: !Ref 'RedisSecurityGroup' + IpProtocol: tcp + FromPort: 6379 + ToPort: 6379 + SourceSecurityGroupId: + Fn::ImportValue: + !Sub '${App}-${Env}-EnvironmentSecurityGroup' + + # Secret Storage of access credentials + RedisSecret: + Metadata: + 'aws:copilot:description': 'A Secrets Manager secret to store your DB credentials' + Type: AWS::SecretsManager::Secret + Properties: + Description: !Sub 'Redis main user secret for ${AWS::StackName}' + GenerateSecretString: + SecretStringTemplate: '{"username": "redis"}' + GenerateStringKey: "password" + ExcludePunctuation: true + IncludeSpace: false + PasswordLength: 16 + + # Creation of the cluster itself + RedisReplicationGroup: + Type: AWS::ElastiCache::ReplicationGroup + Properties: + ReplicationGroupId: !Sub 'funding-service-magic-links-${Env}' + ReplicationGroupDescription: !Sub '${Env} Funding Service Magic Links' + AutomaticFailoverEnabled: true + AtRestEncryptionEnabled: true + TransitEncryptionEnabled: true + AutoMinorVersionUpgrade: true + MultiAZEnabled: true + CacheNodeType: cache.m5.large + CacheSubnetGroupName: !Ref 'RedisSubnetGroup' + SecurityGroupIds: + - !GetAtt 'RedisSecurityGroup.GroupId' + Engine: redis + NumCacheClusters: 2 + + # Redis endpoint stored in SSM so that other services can retrieve the endpoint. + RedisEndpointAddressParam: + Type: AWS::SSM::Parameter + Properties: + Name: !Sub '/${App}/${Env}/redis' # Other services can retrieve the endpoint from this path. + Type: String + Value: !GetAtt 'RedisReplicationGroup.PrimaryEndPoint.Address' + +Outputs: + RedisEndpoint: + Description: The endpoint of the redis cluster + Value: !GetAtt 'RedisReplicationGroup.PrimaryEndPoint.Address' + Export: + Name: !Sub ${App}-${Env}-RedisEndpoint + RedisInstanceURI: + Description: "The URI of the redis cluster." + Value: + !Sub + - "rediss://${HOSTNAME}:${PORT}" + - HOSTNAME: !GetAtt 'RedisReplicationGroup.PrimaryEndPoint.Address' + PORT: !GetAtt 'RedisReplicationGroup.PrimaryEndPoint.Port' + Export: + Name: !Sub ${App}-${Env}-RedisInstanceURI diff --git a/fund_store/copilot/environments/dev/manifest.yml b/fund_store/copilot/environments/dev/manifest.yml new file mode 100644 index 000000000..e9147bc99 --- /dev/null +++ b/fund_store/copilot/environments/dev/manifest.yml @@ -0,0 +1,41 @@ +# The manifest for the "dev" environment. +# Read the full specification for the "Environment" type at: +# https://aws.github.io/copilot-cli/docs/manifest/environment/ + +# Your environment name will be used in naming your resources like VPC, cluster, etc. +name: dev +type: Environment + +# Import your own VPC and subnets or configure how they should be created. +# Run this in uat/production only - in the test environments, these should be ad-hoc per deployment +network: + vpc: + id: 'vpc-0850970940cee0412' + subnets: + public: + - id: 'subnet-0f7aa03feb2923658' + - id: 'subnet-0a8dfef78a0873187' + private: + - id: 'subnet-03caaa338a263f66f' + - id: 'subnet-0f4bdb0fe7e467743' + +# Configure the load balancers in your environment, once created. +# http: +# public: +# private: + +# Configure observability for your environment resources. +observability: + container_insights: false + +cdn: true + +http: + public: + security_groups: + ingress: + restrict_to: + cdn: true + private: + ingress: + vpc: true # Enable incoming traffic within the VPC to the internal load balancer. diff --git a/fund_store/copilot/environments/test/manifest.yml b/fund_store/copilot/environments/test/manifest.yml new file mode 100644 index 000000000..c7d611627 --- /dev/null +++ b/fund_store/copilot/environments/test/manifest.yml @@ -0,0 +1,41 @@ +# The manifest for the "test" environment. +# Read the full specification for the "Environment" type at: +# https://aws.github.io/copilot-cli/docs/manifest/environment/ + +# Your environment name will be used in naming your resources like VPC, cluster, etc. +name: test +type: Environment + +# Import your own VPC and subnets or configure how they should be created. +# Run this in uat/production only - in the test environments, these should be ad-hoc per deployment +network: + vpc: + id: 'vpc-0ca7bdd50d5dba428' + subnets: + public: + - id: 'subnet-0f1f40929bdabbcdd' + - id: 'subnet-0e686586655747458' + private: + - id: 'subnet-07f5736fe61f32266' + - id: 'subnet-054d3a0257e2c809d' + +# Configure the load balancers in your environment, once created. +# http: +# public: +# private: + +# Configure observability for your environment resources. +observability: + container_insights: false + +cdn: true + +http: + public: + security_groups: + ingress: + restrict_to: + cdn: true + private: + ingress: + vpc: true # Enable incoming traffic within the VPC to the internal load balancer. diff --git a/fund_store/copilot/fsd-fund-store/addons/fsd-fund-store-cluster.yml b/fund_store/copilot/fsd-fund-store/addons/fsd-fund-store-cluster.yml new file mode 100644 index 000000000..a3da805c2 --- /dev/null +++ b/fund_store/copilot/fsd-fund-store/addons/fsd-fund-store-cluster.yml @@ -0,0 +1,158 @@ +Parameters: + App: + Type: String + Description: Your application's name. + Env: + Type: String + Description: The environment name your service, job, or workflow is being deployed to. + Name: + Type: String + Description: The name of the service, job, or workflow being deployed. + # Customize your Aurora Serverless cluster by setting the default value of the following parameters. + fsdfundstoreclusterDBName: + Type: String + Description: The name of the initial database to be created in the Aurora Serverless v2 cluster. + Default: fsd_fund_store + # Cannot have special characters + # Naming constraints: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Limits.html#RDS_Limits.Constraints +Mappings: + fsdfundstoreclusterEnvScalingConfigurationMap: + All: + "DBMinCapacity": 0.5 # AllowedValues: from 0.5 through 128 + "DBMaxCapacity": 8 # AllowedValues: from 0.5 through 128 + BastionMap: + dev: + "SecurityGroup": "sg-0b6c7aabb95bf14a9" + test: + "SecurityGroup": "sg-0cf75a004dbade7b8" + uat: + "SecurityGroup": "sg-04017abfef2079894" + prod: + "SecurityGroup": "sg-08cecea8f9b8a4ec9" + +Resources: + fsdfundstoreclusterDBSubnetGroup: + Type: 'AWS::RDS::DBSubnetGroup' + Properties: + DBSubnetGroupDescription: Group of Copilot private subnets for Aurora Serverless v2 cluster. + SubnetIds: + !Split [',', { 'Fn::ImportValue': !Sub '${App}-${Env}-PrivateSubnets' }] + fsdfundstoreclusterSecurityGroup: + Metadata: + 'aws:copilot:description': 'A security group for your workload to access the Aurora Serverless v2 cluster fsdfundstorecluster' + Type: 'AWS::EC2::SecurityGroup' + Properties: + GroupDescription: !Sub 'The Security Group for ${Name} to access Aurora Serverless v2 cluster fsdfundstorecluster.' + VpcId: + Fn::ImportValue: + !Sub '${App}-${Env}-VpcId' + Tags: + - Key: Name + Value: !Sub 'copilot-${App}-${Env}-${Name}-Aurora' + fsdfundstoreclusterDBClusterSecurityGroup: + Metadata: + 'aws:copilot:description': 'A security group for your Aurora Serverless v2 cluster fsdfundstorecluster' + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: The Security Group for the Aurora Serverless v2 cluster. + SecurityGroupIngress: + - ToPort: 5432 + FromPort: 5432 + IpProtocol: tcp + Description: !Sub 'From the Aurora Security Group of the workload ${Name}.' + SourceSecurityGroupId: !Ref fsdfundstoreclusterSecurityGroup + - ToPort: 5432 + FromPort: 5432 + IpProtocol: tcp + Description: !Sub 'From the Bastion Security Group.' + SourceSecurityGroupId: !FindInMap [BastionMap, !Ref Env, 'SecurityGroup'] + VpcId: + Fn::ImportValue: + !Sub '${App}-${Env}-VpcId' + Tags: + - Key: Name + Value: !Sub 'copilot-${App}-${Env}-${Name}-Aurora' + fsdfundstoreclusterAuroraSecret: + Metadata: + 'aws:copilot:description': 'A Secrets Manager secret to store your DB credentials' + Type: AWS::SecretsManager::Secret + Properties: + Description: !Sub Aurora main user secret for ${AWS::StackName} + GenerateSecretString: + SecretStringTemplate: '{"username": "postgres"}' + GenerateStringKey: "password" + ExcludePunctuation: true + IncludeSpace: false + PasswordLength: 16 + fsdfundstoreclusterDBClusterParameterGroup: + Metadata: + 'aws:copilot:description': 'A DB parameter group for engine configuration values' + Type: 'AWS::RDS::DBClusterParameterGroup' + Properties: + Description: !Ref 'AWS::StackName' + Family: 'aurora-postgresql14' + Parameters: + client_encoding: 'UTF8' + fsdfundstoreclusterDBCluster: + Metadata: + 'aws:copilot:description': 'The fsdfundstorecluster Aurora Serverless v2 database cluster' + Type: 'AWS::RDS::DBCluster' + Properties: + MasterUsername: + !Join [ "", [ '{{resolve:secretsmanager:', !Ref fsdfundstoreclusterAuroraSecret, ":SecretString:username}}" ]] # pragma: allowlist secret + MasterUserPassword: + !Join [ "", [ '{{resolve:secretsmanager:', !Ref fsdfundstoreclusterAuroraSecret, ":SecretString:password}}" ]] # pragma: allowlist secret + DatabaseName: !Ref fsdfundstoreclusterDBName + Engine: 'aurora-postgresql' + EngineVersion: '14.4' + DBClusterParameterGroupName: !Ref fsdfundstoreclusterDBClusterParameterGroup + DBSubnetGroupName: !Ref fsdfundstoreclusterDBSubnetGroup + Port: 5432 + StorageEncrypted: true + BackupRetentionPeriod: 8 + VpcSecurityGroupIds: + - !Ref fsdfundstoreclusterDBClusterSecurityGroup + ServerlessV2ScalingConfiguration: + # Replace "All" below with "!Ref Env" to set different autoscaling limits per environment. + MinCapacity: !FindInMap [fsdfundstoreclusterEnvScalingConfigurationMap, All, DBMinCapacity] + MaxCapacity: !FindInMap [fsdfundstoreclusterEnvScalingConfigurationMap, All, DBMaxCapacity] + fsdfundstoreclusterDBWriterInstance: + Metadata: + 'aws:copilot:description': 'The fsdfundstorecluster Aurora Serverless v2 writer instance' + Type: 'AWS::RDS::DBInstance' + Properties: + DBClusterIdentifier: !Ref fsdfundstoreclusterDBCluster + DBInstanceClass: db.serverless + Engine: 'aurora-postgresql' + PromotionTier: 1 + AvailabilityZone: !Select + - 0 + - !GetAZs + Ref: AWS::Region + + fsdfundstoreclusterSecretAuroraClusterAttachment: + Type: AWS::SecretsManager::SecretTargetAttachment + Properties: + SecretId: !Ref fsdfundstoreclusterAuroraSecret + TargetId: !Ref fsdfundstoreclusterDBCluster + TargetType: AWS::RDS::DBCluster +Outputs: + DatabaseUrl: + Description: "The URL of this database." + Value: + !Sub + - "postgres://${USERNAME}:${PASSWORD}@${HOSTNAME}:${PORT}/${DBNAME}" + - USERNAME: !Join [ "", [ '{{resolve:secretsmanager:', !Ref fsdfundstoreclusterAuroraSecret, ":SecretString:username}}" ]] # pragma: allowlist secret + PASSWORD: !Join [ "", [ '{{resolve:secretsmanager:', !Ref fsdfundstoreclusterAuroraSecret, ":SecretString:password}}" ]] # pragma: allowlist secret + HOSTNAME: !Join [ "", [ '{{resolve:secretsmanager:', !Ref fsdfundstoreclusterAuroraSecret, ":SecretString:host}}" ]] # pragma: allowlist secret + PORT: !Join [ "", [ '{{resolve:secretsmanager:', !Ref fsdfundstoreclusterAuroraSecret, ":SecretString:port}}" ]] # pragma: allowlist secret + DBNAME: !Join [ "", [ '{{resolve:secretsmanager:', !Ref fsdfundstoreclusterAuroraSecret, ":SecretString:dbname}}" ]] # pragma: allowlist secret + + fsdfundstoreclusterSecret: # injected as FSDFUNDSTORECLUSTER_SECRET environment variable by Copilot. + Description: "The JSON secret that holds the database username and password. Fields are 'host', 'port', 'dbname', 'username', 'password', 'dbClusterIdentifier' and 'engine'" + Value: !Ref fsdfundstoreclusterAuroraSecret + fsdfundstoreclusterSecurityGroup: + Description: "The security group to attach to the workload." + Value: !Ref fsdfundstoreclusterSecurityGroup + Export: + Name: fsdfundstoreclusterSecurityGroup diff --git a/fund_store/copilot/fsd-fund-store/manifest.yml b/fund_store/copilot/fsd-fund-store/manifest.yml new file mode 100644 index 000000000..e5b95a981 --- /dev/null +++ b/fund_store/copilot/fsd-fund-store/manifest.yml @@ -0,0 +1,88 @@ +# The manifest for the "data-frontend" service. +# Read the full specification for the "Load Balanced Web Service" type at: +# https://aws.github.io/copilot-cli/docs/manifest/lb-web-service/ + +# Your service name will be used in naming your resources like log groups, ECS services, etc. +name: fsd-fund-store +type: Backend Service + +# Distribute traffic to your service. +http: + # Requests to this path will be forwarded to your service. + # To match all requests you can use the "/" path. + path: '/' + # You can specify a custom health check path. The default is "/". + healthcheck: '/healthcheck' + +# Configuration for your containers and service. +image: + # Docker build arguments. For additional overrides: https://aws.github.io/copilot-cli/docs/manifest/lb-web-service/#image-location + location: ghcr.io/communitiesuk/funding-service-design-fund-store:latest + # Port exposed through your container to route traffic to it. + port: 8080 + +# Valid values: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-cpu-memory-error.html +# Number of CPU units for the task. +cpu: 512 +# Amount of memory in MiB used by the task. +memory: 1024 + +# See https://aws.github.io/copilot-cli/docs/manifest/lb-web-service/#platform +platform: linux/x86_64 +# Number of tasks that should be running in your service. +count: 1 +# Enable running commands in your container. +exec: true + +network: + connect: true # Enable Service Connect for intra-environment traffic between services. + +# storage: + # readonly_fs: true # Limit to read-only access to mounted root filesystems. + +# Optional fields for more advanced use-cases. +# +# Pass environment variables as key value pairs. +variables: + SENTRY_DSN: "https://5ea1346b1c1b4dd8af0c95435ca77945@o1432034.ingest.sentry.io/4503903486148608" + FLASK_ENV: ${COPILOT_ENVIRONMENT_NAME} + PORT: 8080 + +secrets: + SECRET_KEY: /copilot/${COPILOT_APPLICATION_NAME}/${COPILOT_ENVIRONMENT_NAME}/secrets/SECRET_KEY + +# You can override any of the values defined above by environment. +environments: + dev: + count: + spot: 1 + test: + deployment: + rolling: 'recreate' + count: + spot: 2 + uat: + count: + range: 2-4 + cooldown: + in: 60s + out: 30s + cpu_percentage: + value: 70 + memory_percentage: + value: 80 + requests: 30 + response_time: 2s + prod: + count: + range: 2-4 + cooldown: + in: 60s + out: 30s + cpu_percentage: + value: 70 + memory_percentage: + value: 80 + requests: 30 + variables: + FLASK_ENV: production diff --git a/fund_store/db/__init__.py b/fund_store/db/__init__.py new file mode 100644 index 000000000..3f48a9c84 --- /dev/null +++ b/fund_store/db/__init__.py @@ -0,0 +1,17 @@ +from flask_migrate import Migrate +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import MetaData + +convention = { + "ix": "ix_%(column_0_label)s", + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s", +} + +metadata = MetaData(naming_convention=convention) + +db = SQLAlchemy(metadata=metadata) + +migrate = Migrate() diff --git a/fund_store/db/models/__init__.py b/fund_store/db/models/__init__.py new file mode 100644 index 000000000..6787934d5 --- /dev/null +++ b/fund_store/db/models/__init__.py @@ -0,0 +1,21 @@ +from .event import Event # noqa +from .form_name import FormName # noqa +from .fund import Fund # noqa +from .round import Round # noqa +from .section import AssessmentField # noqa +from .section import Section # noqa +from .section import SectionField # noqa + +# from .translations import Translation # noqa + +# from .section import section_field_table # noqa + +__all__ = [ + "Round", + "Fund", + "Section", + "AssessmentField", + "SectionField", + "FormName", + "Event", +] diff --git a/fund_store/db/models/event.py b/fund_store/db/models/event.py new file mode 100644 index 000000000..b583320c7 --- /dev/null +++ b/fund_store/db/models/event.py @@ -0,0 +1,36 @@ +import uuid +from enum import Enum + +from flask_sqlalchemy.model import DefaultMeta +from sqlalchemy import Column, DateTime, ForeignKey +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.types import Enum as SQLAEnum + +from db import db + +BaseModel: DefaultMeta = db.Model + + +class EventType(Enum): + APPLICATION_DEADLINE_REMINDER = "APPLICATION_DEADLINE_REMINDER" + SEND_INCOMPLETE_APPLICATIONS = "SEND_INCOMPLETE_APPLICATIONS" + ACCOUNT_IMPORT = "ACCOUNT_IMPORT" + + +class Event(BaseModel): + id = Column( + "id", + UUID(as_uuid=True), + default=uuid.uuid4, + primary_key=True, + nullable=False, + ) + round_id = Column( + "round_id", + UUID(as_uuid=True), + ForeignKey("round.id"), + nullable=True, + ) + type = Column("type", SQLAEnum(EventType, name="event_type"), nullable=False, unique=False) + activation_date = Column("activation_date", DateTime(), nullable=False) + processed = Column("processed", DateTime(), nullable=True) diff --git a/fund_store/db/models/form_name.py b/fund_store/db/models/form_name.py new file mode 100644 index 000000000..e3036e05f --- /dev/null +++ b/fund_store/db/models/form_name.py @@ -0,0 +1,22 @@ +from flask_sqlalchemy.model import DefaultMeta +from sqlalchemy import JSON, Column, ForeignKey, Integer + +from db import db + +BaseModel: DefaultMeta = db.Model + + +class FormName(BaseModel): + section_id = Column( + "section_id", + Integer, + ForeignKey("section.id"), + nullable=False, + primary_key=True, + ) + form_name_json = Column( + "form_name_json", + JSON(none_as_null=True), + nullable=False, + unique=False, + ) diff --git a/fund_store/db/models/fund.py b/fund_store/db/models/fund.py new file mode 100644 index 000000000..09aece5a8 --- /dev/null +++ b/fund_store/db/models/fund.py @@ -0,0 +1,42 @@ +import uuid +from enum import Enum +from typing import List + +from flask_sqlalchemy.model import DefaultMeta +from sqlalchemy import JSON, Column +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Mapped, relationship +from sqlalchemy.types import Boolean +from sqlalchemy.types import Enum as SQLAEnum + +from db import db +from db.models.round import Round + +BaseModel: DefaultMeta = db.Model + + +class FundingType(Enum): + COMPETITIVE = "COMPETITIVE" + UNCOMPETED = "UNCOMPETED" + EOI = "EOI" + + +class Fund(BaseModel): + id = Column( + "id", + UUID(as_uuid=True), + default=uuid.uuid4, + primary_key=True, + nullable=False, + ) + name_json = Column("name_json", JSON(none_as_null=True), nullable=False, unique=False) + title_json = Column("title_json", JSON(none_as_null=True), nullable=False, unique=False) + short_name = Column("short_name", db.String(), nullable=False, unique=True) + description_json = Column("description_json", JSON(none_as_null=True), nullable=False, unique=False) + rounds: Mapped[List["Round"]] = relationship("Round") + welsh_available = Column("welsh_available", Boolean, default=False, nullable=False) + owner_organisation_name = Column("owner_organisation_name", db.String(), nullable=False, unique=False) + owner_organisation_shortname = Column("owner_organisation_shortname", db.String(), nullable=False, unique=False) + owner_organisation_logo_uri = Column("owner_organisation_logo_uri", db.Text(), nullable=True, unique=False) + funding_type = Column("funding_type", SQLAEnum(FundingType, name="fundingtype"), nullable=False, unique=False) + ggis_scheme_reference_number = Column("ggis_scheme_reference_number", db.String(255), nullable=True, unique=False) diff --git a/fund_store/db/models/round.py b/fund_store/db/models/round.py new file mode 100644 index 000000000..c8bcf7009 --- /dev/null +++ b/fund_store/db/models/round.py @@ -0,0 +1,95 @@ +import uuid + +from flask_sqlalchemy.model import DefaultMeta +from sqlalchemy import JSON, Column, DateTime, ForeignKey, UniqueConstraint +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.types import Boolean + +from db import db + +BaseModel: DefaultMeta = db.Model + + +class Round(BaseModel): + __table_args__ = (UniqueConstraint("fund_id", "short_name"),) + id = Column( + "id", + UUID(as_uuid=True), + default=uuid.uuid4, + primary_key=True, + nullable=False, + ) + # fund_id: Mapped[UUID] = mapped_column(ForeignKey("fund.id")) + fund_id = Column( + "fund_id", + UUID(as_uuid=True), + ForeignKey("fund.id"), + nullable=False, + ) + title_json = Column("title_json", JSON(none_as_null=True), nullable=False, unique=False) + short_name = Column("short_name", db.String(), nullable=False, unique=False) + opens = Column("opens", DateTime()) + deadline = Column("deadline", DateTime()) + assessment_start = Column("assessment_start", DateTime()) + application_reminder_sent = Column( + "application_reminder_sent", + db.Boolean, + default=False, + nullable=False, + ) + reminder_date = Column("reminder_date", DateTime()) + assessment_deadline = Column("assessment_deadline", DateTime()) + prospectus = Column("prospectus", db.String(), nullable=False, unique=False) + privacy_notice = Column("privacy_notice", db.String(), nullable=False, unique=False) + contact_us_banner_json = Column("contact_us_banner_json", JSON(none_as_null=True), nullable=True, unique=False) + reference_contact_page_over_email = Column( + "reference_contact_page_over_email", + db.Boolean, + default=False, + nullable=False, + ) + contact_email = Column("contact_email", db.String(), nullable=True, unique=False) + contact_phone = Column("contact_phone", db.String(), nullable=True, unique=False) + contact_textphone = Column("contact_textphone", db.String(), nullable=True, unique=False) + support_times = Column("support_times", db.String(), nullable=False, unique=False) + support_days = Column("support_days", db.String(), nullable=False, unique=False) + instructions_json = Column("instructions_json", JSON(none_as_null=True), nullable=True, unique=False) + feedback_link = Column("feedback_link", db.String(), unique=False) + project_name_field_id = Column("project_name_field_id", db.String(), unique=False, nullable=False) + application_guidance_json = Column( + "application_guidance_json", JSON(none_as_null=True), nullable=True, unique=False + ) + guidance_url = Column("guidance_url", db.String(), nullable=True, unique=False) + all_uploaded_documents_section_available = Column( + "all_uploaded_documents_section_available", + Boolean, + default=False, + nullable=False, + ) + application_fields_download_available = Column( + "application_fields_download_available", + db.Boolean, + default=False, + nullable=False, + ) + display_logo_on_pdf_exports = Column( + "display_logo_on_pdf_exports", + db.Boolean, + default=False, + nullable=False, + ) + mark_as_complete_enabled = Column( + "mark_as_complete_enabled", + db.Boolean, + default=False, + nullable=False, + ) + is_expression_of_interest = Column( + "is_expression_of_interest", + db.Boolean, + default=False, + nullable=False, + ) + feedback_survey_config = Column("feedback_survey_config", JSON(none_as_null=True), nullable=True, unique=False) + eligibility_config = Column("eligibility_config", JSON(none_as_null=True), nullable=True, unique=False) + eoi_decision_schema = Column("eoi_decision_schema ", JSON(none_as_null=True), nullable=True, unique=False) diff --git a/fund_store/db/models/section.py b/fund_store/db/models/section.py new file mode 100644 index 000000000..324ceb2c5 --- /dev/null +++ b/fund_store/db/models/section.py @@ -0,0 +1,103 @@ +from flask_sqlalchemy.model import DefaultMeta +from sqlalchemy import JSON, Column, ForeignKey, Index, Integer, func +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import foreign, relationship, remote +from sqlalchemy_utils import LtreeType + +from db import db + +BaseModel: DefaultMeta = db.Model + + +# section_field_table = Table( +# "section_field", +# metadata, +# Column("section_id", ForeignKey("section.id"), primary_key=True), +# Column("field_id", ForeignKey("assessment_field.id"), primary_key=True), +# Column("display_order", Integer, nullable=False, unique=False), +# ) + + +class SectionField(BaseModel): + __tablename__ = "section_field" + section_id = Column(ForeignKey("section.id"), primary_key=True) + field_id = Column(ForeignKey("assessment_field.id"), primary_key=True) + display_order = Column("display_order", Integer, nullable=False, unique=False) + field = relationship("AssessmentField") + + +class AssessmentField(BaseModel): + id = Column(db.String, primary_key=True, nullable=False, unique=True) + field_type = Column( + "field_type", + db.String(), + nullable=False, + unique=False, + ) + display_type = Column("display_type", db.String(), nullable=False, unique=False) + title = Column("title", db.String(), nullable=False, unique=False) + # sections = relationship( + # "Section", secondary=section_field_table, back_populates="fields" + # ) + + +class Section(BaseModel): + id = Column( + Integer, + autoincrement=True, + primary_key=True, + nullable=False, + ) + title_json = Column( + "title_json", + JSON(none_as_null=True), + nullable=False, + unique=False, + ) + requires_feedback = Column( + "requires_feedback", + db.Boolean, + default=False, + nullable=False, + ) + + # title_content_id = mapped_column(Integer, nullable=True) + # title_translations = relationship("Translation", primaryjoin= + # "Section.title_content_id == Translation.content_id", viewonly=True) + # title_translations = relationship("Translation", + # primaryjoin="and_(foreign( + # Section.title_content_id) == Translation.content_id, + # Translation.language.like('%'))", + # viewonly=True) + round_id = Column( + UUID(as_uuid=True), + ForeignKey("round.id"), + nullable=False, + ) + weighting = Column( + Integer, + nullable=True, + ) + path = Column(LtreeType, nullable=False) + __table_args__ = (Index("ix_sections_path", path, postgresql_using="gist"),) + fields = relationship("SectionField", order_by=SectionField.display_order, viewonly=True) + # fields = relationship( + # "AssessmentField", + # secondary=section_field_table, + # back_populates="sections", + # ) + parent = relationship( + "Section", + primaryjoin=remote(path) == foreign(func.subpath(path, 0, -1)), + backref="children", + viewonly=True, + order_by="Section.path", + ) + + form_name = relationship("FormName") + + def __str__(self): + return self.title_json.get("en") + + def __repr__(self): + return "Section({})".format(self.title_json) diff --git a/fund_store/db/models/translations.py b/fund_store/db/models/translations.py new file mode 100644 index 000000000..a8f20f98a --- /dev/null +++ b/fund_store/db/models/translations.py @@ -0,0 +1,17 @@ +from flask_sqlalchemy.model import DefaultMeta +from sqlalchemy import Column, Integer + +from db import db + +BaseModel: DefaultMeta = db.Model + + +class Translation(BaseModel): + content_id = Column( + "content_id", + Integer, + primary_key=True, + nullable=False, + ) + language = Column("language", db.String(), nullable=False, unique=False, primary_key=True) + text = Column("text", db.String(), nullable=False, unique=False) diff --git a/fund_store/db/queries.py b/fund_store/db/queries.py new file mode 100644 index 000000000..694dbc552 --- /dev/null +++ b/fund_store/db/queries.py @@ -0,0 +1,593 @@ +import uuid +from datetime import datetime +from typing import List + +from sqlalchemy import bindparam, exc, func, insert, select, text, update +from sqlalchemy.dialects.postgresql import insert as postgres_insert +from sqlalchemy.sql import expression +from sqlalchemy_utils import Ltree +from sqlalchemy_utils.types.ltree import LQUERY + +from db import db +from db.models.event import Event +from db.models.form_name import FormName +from db.models.fund import Fund +from db.models.round import Round +from db.models.section import AssessmentField, Section, SectionField + + +def get_all_funds() -> List[Fund]: + funds = db.session.scalars(select(Fund)).all() + return funds + + +def get_fund_by_id( + fund_id: str, +) -> Fund: + fund = db.session.scalars(select(Fund).filter(Fund.id == fund_id)).one() + return fund + + +def get_fund_by_short_name( + fund_short_name: str, +) -> Fund: + fund = db.session.scalar(select(Fund).filter(func.lower(Fund.short_name) == func.lower(fund_short_name))) + return fund + + +def get_round_by_id( + fund_id: str, + round_id: str, +) -> Round: + round = db.session.scalars(select(Round).filter(Round.id == round_id).filter(Round.fund_id == fund_id)).one() + return round + + +def get_rounds_for_fund_by_id( + fund_id: str, +) -> List[Round]: + rounds = db.session.scalars(select(Round).filter(Round.fund_id == fund_id)).all() + return rounds + + +def get_round_by_short_name( + fund_short_name: str, + round_short_name: str, +) -> Round: + round = db.session.scalar( + select(Round) + .filter(func.lower(Round.short_name) == func.lower(round_short_name)) + .join(Fund) + .filter(func.lower(Fund.short_name) == func.lower(fund_short_name)) + ) + return round + + +def get_rounds_for_fund_by_short_name( + fund_short_name, +): + rounds = db.session.scalars( + select(Round).join(Fund).filter(func.lower(Fund.short_name) == func.lower(fund_short_name)) + ).all() + return rounds + + +def get_sections_for_round(round_id) -> List[Section]: + return db.session.scalars(select(Section).filter(Section.round_id == round_id).order_by(Section.path)).all() + + +def get_application_sections_for_round( + fund_id, + round_id, +) -> List[Section]: + application_level = db.session.scalar( + select(Section) + .filter(Section.round_id == round_id) + .filter(text("section.title_json->>'en' = 'Application'")) + .join(Round) + .filter(Round.fund_id == fund_id) + ) + if not application_level: + return None + + query = f"{application_level.path}.*{{1}}" + lquery = expression.cast(query, LQUERY) + application_sections = db.session.scalars( + select(Section).filter(Section.path.lquery(lquery)).order_by(Section.path) + ).all() + + return application_sections + + +def get_assessment_sections_for_round( + fund_id, + round_id, + language, +) -> List[Section]: + assessment_level = db.session.scalar( + select(Section) + .filter(Section.round_id == round_id) + .filter(text("section.title_json->>'en' = 'Assessment'")) + .join(Round) + .filter(Round.fund_id == fund_id) + ) + if not assessment_level: + return None + + query = f"{assessment_level.path}.*{'{1}'}" + lquery = expression.cast(query, LQUERY) + # select(Section).join(Translation, onclause=(Section.title_content_id == + # Translation.content_id) and (Translation.language == 'en'), + # isouter=True) + # .filter(Section.id == 12) + assessment_sections = db.session.scalars( + select(Section) + # .join + # ( + # Translation, + # onclause=and_((Translation.language == language), + # (Section.title_content_id == Translation.content_id)), + # isouter=True + # ) + # select (Section).filter(Section.path.lquery(lquery)) + # select(Section).join(Translation, onclause=f"section.title_content_id + # = translation.content_id and translation.language='{language}'", + # isouter=True).filter(Section.path.lquery(lquery)) + .filter(Section.path.lquery(lquery)) + .order_by(Section.path) + ).all() + + return assessment_sections + + +def create_event( + type: str, + activation_date: datetime, + round_id: str = None, + processed: datetime = None, +) -> Event: + event = Event(type=type, activation_date=activation_date, round_id=round_id, processed=processed) + try: + db.session.add(event) + db.session.commit() + db.session.refresh(event) + except exc.IntegrityError: + db.session.rollback() + return None + + return event + + +def get_events( + round_id: str = None, + type: str = None, + only_unprocessed: bool = False, +) -> List[Event]: + query = select(Event) + if round_id: + query = query.filter(Event.round_id == round_id) + + if type: + query = query.filter(Event.type == type) + + if only_unprocessed: + query = query.filter(Event.processed != None) # noqa + + events = db.session.scalars(query).all() + return events + + +def get_event(round_id: str, event_id: str) -> Event: + query = select(Event).filter(Event.id == event_id, Event.round_id == round_id) + event = db.session.scalar(query) + return event + + +def set_event_to_processed(event_id: str, processed: bool) -> Event: + event = Event.query.filter_by(id=event_id).first() + if not event: + return None + event.processed = datetime.now() if processed else None + db.session.commit() + return event + + +def upsert_fields(fields: list): + stmt = ( + ( + postgres_insert(AssessmentField).values( + id=bindparam("id"), + title=bindparam("title"), + field_type=bindparam("field_type"), + display_type=bindparam("display_type"), + ) + ) + .on_conflict_do_nothing(index_elements=[AssessmentField.id]) + .returning(AssessmentField.id) + ) + + update_params = [ + { + "id": item["form_json_id"], + "title": item["title"], + "field_type": item["type"], + "display_type": item["presentation_type"], + } + for item in fields + ] + + result = db.session.execute(stmt, update_params) + inserted_field_ids = [row.id for row in result] + return inserted_field_ids + + +def insert_sections(sections): + for section in sections: + db.session.add(section) + db.session.commit() + + +def insert_fund_data(fund_config, commit: bool = True): + stmt = ( + ( + postgres_insert(Fund).values( + id=bindparam("id"), + name_json=bindparam("name_json"), + title_json=bindparam("title_json"), + short_name=bindparam("short_name"), + description_json=bindparam("description_json"), + welsh_available=bindparam("welsh_available"), + owner_organisation_name=bindparam("owner_organisation_name"), + owner_organisation_shortname=bindparam("owner_organisation_shortname"), + owner_organisation_logo_uri=bindparam("owner_organisation_logo_uri"), + funding_type=bindparam("funding_type"), + ) + ) + .on_conflict_do_update( + index_elements=[Fund.id], + set_={ + "name_json": bindparam("name_json"), + "title_json": bindparam("title_json"), + "short_name": bindparam("short_name"), + "description_json": bindparam("description_json"), + "welsh_available": bindparam("welsh_available"), + "owner_organisation_name": bindparam("owner_organisation_name"), + "owner_organisation_shortname": bindparam("owner_organisation_shortname"), + "owner_organisation_logo_uri": bindparam("owner_organisation_logo_uri"), + "funding_type": bindparam("funding_type"), + }, + ) + .returning(Fund.id) + ) + + update_params = { + "id": fund_config["id"], + "name_json": fund_config["name_json"], + "title_json": fund_config["title_json"], + "short_name": fund_config["short_name"], + "description_json": fund_config["description_json"], + "welsh_available": fund_config["welsh_available"], + "owner_organisation_name": fund_config["owner_organisation_name"], + "owner_organisation_shortname": fund_config["owner_organisation_shortname"], + "owner_organisation_logo_uri": fund_config["owner_organisation_logo_uri"], + "funding_type": fund_config["funding_type"], + } + + result = db.session.execute(stmt, update_params) + inserted_fund_ids = [row.id for row in result] + + print(f"Prepared fund for insert: '{inserted_fund_ids}'.") + if commit: + db.session.commit() + print("DB changes committed") + return inserted_fund_ids + + +def upsert_round_data(round_configs, commit: bool = True): + # Create dictionary to store updated records + updated_rounds = {} + + for round_config in round_configs: + # Check if record exist + round_record = Round.query.filter_by(id=round_config["id"]).first() + + if round_record is not None: + # Update existing round record + round_record.title_json = round_config["title_json"] + round_record.short_name = round_config["short_name"] + round_record.opens = round_config["opens"] + round_record.assessment_start = round_config["assessment_start"] + round_record.deadline = round_config["deadline"] + round_record.application_reminder_sent = round_config["application_reminder_sent"] + round_record.reminder_date = round_config["reminder_date"] + round_record.fund_id = round_config["fund_id"] + round_record.assessment_deadline = round_config["assessment_deadline"] + round_record.prospectus = round_config["prospectus"] + round_record.privacy_notice = round_config["privacy_notice"] + round_record.reference_contact_page_over_email = round_config["reference_contact_page_over_email"] + round_record.contact_us_banner_json = round_config["contact_us_banner_json"] + round_record.contact_email = round_config["contact_email"] + round_record.contact_phone = round_config["contact_phone"] + round_record.contact_textphone = round_config["contact_textphone"] + round_record.support_times = round_config["support_times"] + round_record.support_days = round_config["support_days"] + round_record.instructions_json = round_config["instructions_json"] + round_record.project_name_field_id = round_config["project_name_field_id"] + round_record.feedback_link = round_config["feedback_link"] + round_record.application_guidance_json = round_config["application_guidance_json"] + round_record.guidance_url = round_config["guidance_url"] + round_record.all_uploaded_documents_section_available = round_config[ + "all_uploaded_documents_section_available" + ] + round_record.application_fields_download_available = round_config["application_fields_download_available"] + round_record.display_logo_on_pdf_exports = round_config["display_logo_on_pdf_exports"] + round_record.feedback_survey_config = round_config["feedback_survey_config"] + round_record.mark_as_complete_enabled = round_config["mark_as_complete_enabled"] + round_record.is_expression_of_interest = round_config["is_expression_of_interest"] + round_record.eligibility_config = round_config["eligibility_config"] + round_record.eoi_decision_schema = round_config["eoi_decision_schema"] + + updated_rounds[round_config["id"]] = round_record + + else: + # Insert new round record + new_round = Round( + id=round_config["id"], + title_json=round_config["title_json"], + short_name=round_config["short_name"], + opens=round_config["opens"], + assessment_start=round_config["assessment_start"], + deadline=round_config["deadline"], + application_reminder_sent=round_config["application_reminder_sent"], + reminder_date=round_config["reminder_date"], + fund_id=round_config["fund_id"], + assessment_deadline=round_config["assessment_deadline"], + prospectus=round_config["prospectus"], + privacy_notice=round_config["privacy_notice"], + reference_contact_page_over_email=round_config["reference_contact_page_over_email"], + contact_us_banner_json=round_config["contact_us_banner_json"], + contact_email=round_config["contact_email"], + contact_phone=round_config["contact_phone"], + contact_textphone=round_config["contact_textphone"], + support_times=round_config["support_times"], + support_days=round_config["support_days"], + instructions_json=round_config["instructions_json"], + project_name_field_id=round_config["project_name_field_id"], + feedback_link=round_config["feedback_link"], + application_guidance_json=round_config["application_guidance_json"], + guidance_url=round_config["guidance_url"], + all_uploaded_documents_section_available=round_config["all_uploaded_documents_section_available"], + application_fields_download_available=round_config["application_fields_download_available"], + display_logo_on_pdf_exports=round_config["display_logo_on_pdf_exports"], + feedback_survey_config=round_config["feedback_survey_config"], + mark_as_complete_enabled=round_config["mark_as_complete_enabled"], + is_expression_of_interest=round_config["is_expression_of_interest"], + eligibility_config=round_config["eligibility_config"], + eoi_decision_schema=round_config["eoi_decision_schema"], + ) + db.session.add(new_round) + + updated_rounds[round_config["id"]] = new_round + + print(f"Prepared rounds for insert: '{updated_rounds}'.") + if commit: + db.session.commit() + print("DB changes committed") + return updated_rounds + + +def insert_base_sections(APPLICATION_BASE_PATH, ASSESSMENT_BASE_PATH, round_id): + """ + Insert base sections for a fund round. + + :param APPLICATION_BASE_PATH: The base path for the application sections. + :param ASSESSMENT_BASE_PATH: The base path for the assessment sections. + :param round_id: The id of the round to insert the sections for. + :return: A dictionary of the inserted sections. + """ + tree_base_sections = [ + { + "section_name": {"en": "Application", "cy": "Application"}, + "tree_path": APPLICATION_BASE_PATH, + "weighting": None, + }, + { + "section_name": {"en": "Assessment", "cy": "Assessment"}, + "tree_path": ASSESSMENT_BASE_PATH, + "weighting": None, + }, + ] + + updated_sections = {} + for section in tree_base_sections: + section_record = Section.query.filter( + Section.path == Ltree(section["tree_path"]), + Section.round_id == uuid.UUID(round_id), + ).first() + + if section_record is not None: + # Update existing section record + section_record.round_id = round_id + section_record.title_json = section["section_name"] + section_record.weighting = section.get("weighting", None) + section_record.requires_feedback = section.get("requires_feedback") or False + + updated_sections[section["tree_path"]] = section_record + else: + # Insert new section record + new_section = Section( + round_id=round_id, + title_json=section["section_name"], + weighting=section.get("weighting", None), + path=Ltree(section["tree_path"]), + requires_feedback=section.get("requires_feedback") or False, + ) + db.session.add(new_section) + + updated_sections[section["tree_path"]] = new_section + + print(f"Prepared sections for insert: '{updated_sections}'.") + return updated_sections + + +def insert_or_update_application_sections(round_id, sorted_application_sections: dict): + print(f"Preparing insert for sections config: '{sorted_application_sections}'.") + updated_sections = {} + for section in sorted_application_sections: + section_record = Section.query.filter( + Section.path == Ltree(section["tree_path"]), + Section.round_id == uuid.UUID(round_id), + ).first() + + if section_record is not None: + # Update existing section record + section_id = section_record.id + section_record.round_id = round_id + section_record.title_json = section["section_name"] + section_record.weighting = (section.get("weighting", None),) + section_record.requires_feedback = section.get("requires_feedback") or False + + updated_sections[section_record.id] = section_record + print(f"Prepared section UPDATE '{section_record}'.") + else: + # Insert new section record + new_section = Section( + round_id=round_id, + title_json=section["section_name"], + weighting=section.get("weighting", None), + path=Ltree(section["tree_path"]), + requires_feedback=section.get("requires_feedback") or False, + ) + db.session.add(new_section) + db.session.commit() + section_id = new_section.id + + updated_sections[new_section.id] = new_section + print(f"Prepared section INSERT '{new_section}'.") + + if section.get("form_name_json"): + form_record = FormName.query.filter_by(section_id=section_id).first() + if form_record is not None: + form_record.form_name_json = section["form_name_json"] + print(f"Updated form name information to {section['form_name_json']}.") + else: + new_form_record = FormName(form_name_json=section["form_name_json"], section_id=section_id) + db.session.add(new_form_record) + print(f"Inserted form name information: '{new_form_record}'.") + db.session.commit() + print("Section UPDATES and INSERTS Prepared for insert.") + + return updated_sections + + +def update_application_section_names(round_id, sorted_application_sections: List[dict], language_code=None): + # TODO : Update this function to work with json objects in sorted_application_sections + for section in sorted_application_sections: + section_path = section["tree_path"] + if language_code is None: + split_section_name_list = section["section_name"].lower().split() + else: + split_section_name_list = section["section_name"][language_code].lower().split() + try: + float(split_section_name_list[0]) + split_section_name_list[1] = split_section_name_list[1].capitalize() + except ValueError: + split_section_name_list[0] = split_section_name_list[0].capitalize() + new_section_name = " ".join(split_section_name_list) + + # Update the section name + stmt = "" + if language_code is None: + stmt = ( + update(Section) + .where(Section.round_id == round_id) + .where(Section.path == Ltree(section_path)) + .values(title_json=new_section_name) + ) + else: + section["section_name"][language_code] = new_section_name + stmt = ( + update(Section) + .where(Section.round_id == round_id) + .where(Section.path == Ltree(section_path)) + .values(title_json=section["section_name"]) + ) + db.session.execute(stmt) + + db.session.commit() + + +def __add__section_fields(field_section_links): + stmt = ( + ( + postgres_insert(SectionField).values( + field_id=bindparam("field_id"), + section_id=bindparam("section_id"), + display_order=bindparam("display_order"), + ) + ) + .on_conflict_do_nothing(constraint="pk_section_field") + .returning( + SectionField.field_id, + SectionField.section_id, + SectionField.display_order, + ) + ) + + field_section_params = [ + { + "field_id": section_link["field_id"], + "section_id": section_link["section_id"], + "display_order": section_link["display_order"], + } + for section_link in field_section_links + ] + + inserted_field_section_result = db.session.execute(stmt, field_section_params).fetchall() + return inserted_field_section_result + + +def insert_assessment_sections(round_id, assessment_config: list): + sorted_assessment_sections = ( + assessment_config["sorted_scored_sections"] + assessment_config["sorted_unscored_sections"] + ) + + stmt = ( + insert(Section).values( + round_id=bindparam("round_id"), + title=bindparam("title"), + weighting=bindparam("weighting"), + path=bindparam("path"), + ) + ).returning(Section.id) + + inserted_section_ids = [] + field_section_links = [] + for section in sorted_assessment_sections: + section_params = { + "round_id": round_id, + "title": section["section_name"], + "weighting": None, + "path": Ltree(section["tree_path"]), + } + inserted_assessment_section_result = db.session.execute(stmt, section_params).fetchall() + inserted_section_id = inserted_assessment_section_result[0][0] + inserted_section_ids.append(inserted_section_id) + if "fields" in section: + for field in section["fields"]: + field_section_links.append( + { + "field_id": field["form_json_id"], + "section_id": inserted_section_id, + "display_order": field["display_order"], + } + ) + + # flush so we can see the rows in the db before committing + # db.session.commit() + inserted_section_field_links = __add__section_fields(field_section_links) + db.session.commit() + return { + "inserted_sections": inserted_section_ids, + "inserted_section_field_links": inserted_section_field_links, + } diff --git a/fund_store/db/schemas/event.py b/fund_store/db/schemas/event.py new file mode 100644 index 000000000..3bd25a2eb --- /dev/null +++ b/fund_store/db/schemas/event.py @@ -0,0 +1,12 @@ +from marshmallow import fields +from marshmallow_sqlalchemy import SQLAlchemyAutoSchema, auto_field + +from db.models.event import Event, EventType + + +class EventSchema(SQLAlchemyAutoSchema): + class Meta: + model = Event + + round_id = auto_field() + type = fields.Enum(EventType) diff --git a/fund_store/db/schemas/fund.py b/fund_store/db/schemas/fund.py new file mode 100644 index 000000000..75f75a87a --- /dev/null +++ b/fund_store/db/schemas/fund.py @@ -0,0 +1,11 @@ +from marshmallow import fields +from marshmallow_sqlalchemy import SQLAlchemyAutoSchema + +from db.models.fund import Fund, FundingType + + +class FundSchema(SQLAlchemyAutoSchema): + class Meta: + model = Fund + + funding_type = fields.Enum(FundingType) diff --git a/fund_store/db/schemas/round.py b/fund_store/db/schemas/round.py new file mode 100644 index 000000000..0e4181c36 --- /dev/null +++ b/fund_store/db/schemas/round.py @@ -0,0 +1,11 @@ +from marshmallow_sqlalchemy import SQLAlchemyAutoSchema, auto_field + +from db.models.round import Round + + +class RoundSchema(SQLAlchemyAutoSchema): + class Meta: + model = Round + exclude = ["eoi_decision_schema"] + + fund_id = auto_field() diff --git a/fund_store/db/schemas/section.py b/fund_store/db/schemas/section.py new file mode 100644 index 000000000..be5c8959e --- /dev/null +++ b/fund_store/db/schemas/section.py @@ -0,0 +1,97 @@ +import contextlib +from operator import itemgetter + +from marshmallow import post_dump +from marshmallow.fields import Method, String +from marshmallow_sqlalchemy import SQLAlchemyAutoSchema, auto_field +from marshmallow_sqlalchemy.fields import Nested + +from db.models.section import Section, SectionField + + +class SectionFieldSchema(SQLAlchemyAutoSchema): + class Meta: + model = SectionField + + field_id = auto_field() + display_order = auto_field() + + def get_display_type(self, obj): + return obj.field.display_type + + def get_field_type(self, obj): + return obj.field.field_type + + display_type = Method("get_display_type") + field_type = Method("get_field_type") + + +class SectionSchema(SQLAlchemyAutoSchema): + class Meta: + model = Section + + def get_form_name(self, obj): + raise NotImplementedError + + def get_title( + self, + obj, + ): + raise NotImplementedError + + def get_weighting(self, obj): + return obj.weighting if obj.weighting else None + + path = String() + fields = Nested("SectionFieldSchema", many=True, allow_none=True) + weighting = Method("get_weighting") + + +class LocalizedSectionSchema(SectionSchema): + def get_form_name(self, obj, lang_code): + with contextlib.suppress(ValueError): + (form_name_container,) = obj.form_name + return form_name_container.form_name_json[lang_code] + + def get_title(self, obj, lang_code): + return obj.title_json.get(lang_code) + + def sort_children(self, data): + if data.get("children"): + sorted_children = sorted(data["children"], key=itemgetter("path")) + data["children"] = sorted_children + return data + + @post_dump + def sort_children_post_dump(self, data, **kwargs): + return self.sort_children(data) + + +class EnglishSectionSchema(LocalizedSectionSchema): + def get_form_name(self, obj): + return super().get_form_name(obj, "en") + + def get_title(self, obj): + return super().get_title(obj, "en") + + children = Nested("EnglishSectionSchema", many=True, allow_none=True) + form_name = Method("get_form_name") + title = Method("get_title") + + +class WelshSectionSchema(LocalizedSectionSchema): + def get_form_name(self, obj): + return super().get_form_name(obj, "cy") + + def get_title(self, obj): + return super().get_title(obj, "cy") + + children = Nested("WelshSectionSchema", many=True, allow_none=True) + form_name = Method("get_form_name") + title = Method("get_title") + + +SECTION_SCHEMA_MAP = { + "en": EnglishSectionSchema, + "cy": WelshSectionSchema, +} diff --git a/fund_store/docker-compose.yml b/fund_store/docker-compose.yml new file mode 100644 index 000000000..2d6b6aac0 --- /dev/null +++ b/fund_store/docker-compose.yml @@ -0,0 +1,19 @@ +services: + + fund-store: + build: + context: . + volumes: + - .:/fund-store:cached + command: sleep infinity + environment: + - DATABASE_URL=postgresql://postgres:password@fund-store-db:5432/fund_store + - DATABASE_URL_UNIT_TEST=postgresql://postgres:password@fund-store-db:5432/fund_store_unit_test + - FLASK_ENV=development + + + + fund-store-db: + image: postgres + environment: + - POSTGRES_PASSWORD=password diff --git a/fund_store/openapi/api.yml b/fund_store/openapi/api.yml new file mode 100644 index 000000000..acd0c8e66 --- /dev/null +++ b/fund_store/openapi/api.yml @@ -0,0 +1,442 @@ +openapi: "3.0.0" + +info: + title: Funding Service Design - Fund store. + description: Fund store API for DLUHC Funding Service Design + version: "0.2.0" + +paths: + /funds: + get: + tags: + - Funds + summary: Returns list of all funds + description: Returns list of all funds + operationId: api.routes.get_funds + responses: + 200: + description: "List all funds." + content: + application/json: + schema: + type: array + items: + $ref : 'components.yml#/components/schemas/Fund' + 404: + description: "No funds exist" + content: + texts/plain: + schema: + $ref: 'components.yml#/components/schemas/Error' + /funds/{fund_id}: + get: + operationId: api.routes.get_fund + tags: + - Funds + parameters: + - $ref: "components.yml#/components/parameters/fund_id" + - name: language + in: query + schema: + type: string + required: false + - name: use_short_name + in: query + schema: + type: boolean + required: false + responses: + 200: + description: "If the fund exists then the data is returned." + content: + application/json: + schema: + $ref: 'components.yml#/components/schemas/Fund' + 404: + description: "Fund not found" + content: + texts/plain: + schema: + $ref: 'components.yml#/components/schemas/Error' + /funds/{fund_id}/rounds/{round_id}: + get: + tags: + - Rounds + summary: Returns the data on a specified round for a specific fund. + description: Given a fund ID and a round ID we return the relavant round data. + operationId: api.routes.get_round + parameters: + - $ref: "components.yml#/components/parameters/fund_id" + - $ref: "components.yml#/components/parameters/round_id" + - name: language + in: query + schema: + type: string + - name: use_short_name + in: query + schema: + type: boolean + required: false + responses: + 200: + description: "If the round exists then the data is returned." + content: + application/json: + schema: + $ref: 'components.yml#/components/schemas/Round' + 404: + description: "Round not found from given fund id and round id." + content: + application/json: + schema: + $ref: 'components.yml#/components/schemas/Error' + /funds/{fund_id}/rounds/{round_id}/eoi_decision_schema: + get: + tags: + - Rounds + summary: Returns the EOI decision schema for the specified round + description: Given a fund ID and a round ID we return the relavant round data. + operationId: api.routes.get_eoi_deicision_schema_for_round + parameters: + - $ref: "components.yml#/components/parameters/fund_id" + - $ref: "components.yml#/components/parameters/round_id" + - name: language + in: query + schema: + type: string + - name: use_short_name + in: query + schema: + type: boolean + required: false + responses: + 200: + description: "If the round exists then the data is returned." + content: + application/json: + schema: + $ref: 'components.yml#/components/schemas/EoiDecisionSchema' + 404: + description: "Round not found from given fund id and round id." + content: + application/json: + schema: + $ref: 'components.yml#/components/schemas/Error' + /funds/{fund_id}/rounds/{round_id}/events: + get: + tags: + - Rounds + summary: Returns the events set for the specified round + description: Given a fund ID and a round ID we return the associated events. + operationId: api.routes.get_events_for_round + parameters: + - $ref: "components.yml#/components/parameters/fund_id" + - $ref: "components.yml#/components/parameters/round_id" + - name: only_unprocessed + in: query + schema: + type: boolean + required: false + responses: + 200: + description: "If the round exists then the data is returned." + content: + application/json: + schema: + $ref: 'components.yml#/components/schemas/Events' + 404: + description: "Events not found from given fund id and round id." + content: + application/json: + schema: + $ref: 'components.yml#/components/schemas/Error' + /funds/{fund_id}/rounds/{round_id}/event/{event_id}: + get: + tags: + - Rounds + summary: Returns the event with the given fund, round and event IDs. + description: Returns the event with the given fund, round and event IDs. + operationId: api.routes.get_event_for_round + parameters: + - $ref: "components.yml#/components/parameters/fund_id" + - $ref: "components.yml#/components/parameters/round_id" + - name: event_id + in: path + schema: + type: string + required: true + responses: + 200: + description: "If the event exists then the data is returned." + content: + application/json: + schema: + $ref: 'components.yml#/components/schemas/Event' + 404: + description: "Event not found from given fund, round or event id." + content: + application/json: + schema: + $ref: 'components.yml#/components/schemas/Error' + put: + tags: + - Rounds + summary: Updates the event + description: Updates the event to be marked as processed + operationId: api.routes.set_round_event_to_processed + parameters: + - $ref: "components.yml#/components/parameters/fund_id" + - $ref: "components.yml#/components/parameters/round_id" + - name: event_id + in: path + schema: + type: string + required: true + - name: processed + in: query + schema: + type: boolean + required: true + responses: + 200: + description: Updates the event to be marked as processed + content: + application/json: + schema: + $ref: 'components.yml#/components/schemas/Event' + 400: + description: "Invalid fund, round or event ID supplied" + content: + texts/plain: + schema: + $ref: 'components.yml#/components/schemas/Error' + /funds/{round_id}/application_reminder_status: + put: + tags: + - Rounds + summary: Updates the application reminder sent status to True + description: Updates the application reminder sent status to True + operationId: api.routes.update_application_reminder_sent_status + parameters: + - $ref: "components.yml#/components/parameters/round_id" + - name: status + in: query + schema: + type: boolean + enum: + - true + required: true + responses: + 200: + description: Updated the application reminder sent status to True + + 400: + description: "Invalid round ID is supplied" + content: + texts/plain: + schema: + $ref: 'components.yml#/components/schemas/Error' + + /funds/{fund_id}/rounds/{round_id}/available_flag_allocations: + get: + tags: + - Static values + summary: Returns list of all available allocations for flags for this round + description: Returns list of all flag allocations + operationId: api.routes.get_available_flag_allocations + parameters: + - $ref: "components.yml#/components/parameters/fund_id" + - $ref: "components.yml#/components/parameters/round_id" + responses: + 200: + description: "List all available allocations for flags in this round." + content: + application/json: + schema: + type: array + 404: + description: "No flag allocations exist" + content: + texts/plain: + schema: + $ref: 'components.yml#/components/schemas/Error' + /funds/{fund_id}/rounds: + get: + tags: + - Rounds + summary: Given a fund ID we return all rounds for that fund. + description: Given a fund ID we return all rounds for that fund. + operationId: api.routes.get_rounds_for_fund + parameters: + - in: path + name: fund_id + schema: + type: string + required: true + - name: use_short_name + in: query + schema: + type: boolean + required: false + responses: + 200: + description: A list of rounds matching the given fund ID. + # content: + # application/json: + # schema: + # type: array + # items: + # $ref : 'components.yml#/components/schemas/Round' + 404: + description: "Rounds page not found for given fund id." + content: + application/json: + schema: + $ref: "components.yml#/components/schemas/Error" + /funds/{fund_id}/rounds/{round_id}/sections/application: + get: + tags: + - Sections + summary: Returns the application sections for the given round + description: Given a fund ID and a round ID we return the display sections for Application + operationId: api.routes.get_sections_for_round_application + parameters: + - $ref: "components.yml#/components/parameters/fund_id" + - $ref: "components.yml#/components/parameters/round_id" + - name: language + in: query + schema: + type: string + responses: + 200: + description: "If the round exists then the sections are returned." + 404: + description: "If an invalid fund/round ID is supplied" + /funds/{fund_id}/rounds/{round_id}/sections/assessment: + get: + tags: + - Sections + summary: Returns the assessment sections for the given round + description: Given a fund ID and a round ID we return the display sections for assessment + operationId: api.routes.get_sections_for_round_assessment + parameters: + - $ref: "components.yml#/components/parameters/fund_id" + - $ref: "components.yml#/components/parameters/round_id" + - name: language + in: query + schema: + type: string + responses: + 200: + description: "If the round exists then the sections are returned." + 404: + description: "If an invalid fund/round ID is supplied" + /events/{type}: + get: + summary: Returns all the event that are of a given type. + description: Returns all the event that are of a given type. + operationId: api.routes.get_events_by_type + parameters: + - name: type + in: path + schema: + type: string + required: true + responses: + 200: + description: "If there are events of the type then the data is returned." + content: + application/json: + schema: + $ref: 'components.yml#/components/schemas/Events' + 404: + description: "Event not found from given fund, round or event id." + content: + application/json: + schema: + $ref: 'components.yml#/components/schemas/Error' + /event: + post: + summary: Creates an event with the supplied parameters + description: Returns the event that has been created + operationId: api.routes.create_event + responses: + 201: + description: SUCCESS - Event created + content: + application/json: + schema: + $ref: 'components.yml#/components/schemas/Event' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + round_id: + type: string + processed: + type: string + type: + type: string + activation_date: + type: string + format: date + /event/{event_id}: + get: + summary: Returns the event with the given event ID. + description: Returns the event with the given event ID. + operationId: api.routes.get_event_by_id + parameters: + - name: event_id + in: path + schema: + type: string + required: true + responses: + 200: + description: "If the event exists then the data is returned." + content: + application/json: + schema: + $ref: 'components.yml#/components/schemas/Event' + 404: + description: "Event not found from given event id." + content: + application/json: + schema: + $ref: 'components.yml#/components/schemas/Error' + put: + summary: Updates the event + description: Updates the event to be marked as processed + operationId: api.routes.set_event_to_processed + parameters: + - name: event_id + in: path + schema: + type: string + required: true + - name: processed + in: query + schema: + type: boolean + required: true + responses: + 200: + description: Updates the event to be marked as processed + content: + application/json: + schema: + $ref: 'components.yml#/components/schemas/Event' + 400: + description: "Invalid fund, round or event ID supplied" + content: + texts/plain: + schema: + $ref: 'components.yml#/components/schemas/Error' +tags: + - name: Funds + description: "See all funds or find a fund" + - name: Rounds + description: "See all rounds or find a round" diff --git a/fund_store/openapi/components.yml b/fund_store/openapi/components.yml new file mode 100644 index 000000000..1332c1e0f --- /dev/null +++ b/fund_store/openapi/components.yml @@ -0,0 +1,144 @@ +components: + schemas: + Fund: + type: object + properties: + name: + type: string + description: The name of the fund. + example: "J's Sensible T-Shirt Fund" + description: + type: string + description: The description of the fund. + example: "Help J buy more t-shirt." + id: + type: string + description: The fund ID, uniquely identifies the fund. + example: "sdknbfs98yf48sfd" + required: + - id + - name + - description + AssessmentCriteriaWeighting: + type: array + properties: + strategy: + type: number + format: float + minimum: 0.0 + maximum: 1.0 + description: The score weighting for the strategy criteria + example: 0.4 + deliverability: + type: number + format: float + minimum: 0.0 + maximum: 1.0 + description: The score weighting for the deliverability criteria + example: 0.3 + value_for_money: + type: number + format: float + minimum: 0.0 + maximum: 1.0 + description: The score weighting for the value for money criteria + example: 0.3 + required: + - strategy + - deliverability + - value_for_money + Round: + type: object + properties: + id: + type: string + description: id of rounds. + example: "12345-qwert-....." + title: + type: string + description: title or name of the round. + example: "Spring" + fund_id: + type: string + description: The fund ID, uniquely identifies the fund. + example: "funding-service-design" + opens: + type: string + description: The date of when the round will be open to recieve applications. + example: "2022-12-25 00:00:00" + deadline: + type: string + description: The date of when the round will stop accepting applications. + example: "2022-12-25 00:00:00" + assessment_deadline: + type: string + description: The date when the assessments will be completed + example: "2022-12-25 00:00:00" + required: + - id + - fund_id + - title + - opens + - deadline + - assessment_deadline + Event: + type: object + properties: + id: + type: string + description: The id of the event. + example: "12345-qwert-....." + round_id: + type: string + nullable: true + description: The round ID, uniquely identifies the round. + example: "12345-qwert-....." + type: + type: string + description: the type of event. + example: "APPLICATION_DEADLINE_REMINDER" + activation_date: + type: string + description: The date after which the event is ready for processing + example: "2022-12-25 00:00:00" + processed: + type: string + format: string + nullable: true + description: Date of when the event has been processed, null otherwise + example: null + required: + - id + - round_id + - type + - activation_date + - processed + Events: + type: array + items: + $ref: '#/components/schemas/Event' + EoiDecisionSchema: + type: object + Error: + type: object + properties: + code: + type: integer + message: + type: string + required: + - code + - message + parameters: + fund_id: + in: path + name: fund_id + schema: + type: string + required: true + round_id: + in: path + name: round_id + schema: + type: string + required: true diff --git a/fund_store/pyproject.toml b/fund_store/pyproject.toml new file mode 100644 index 000000000..89e35d722 --- /dev/null +++ b/fund_store/pyproject.toml @@ -0,0 +1,61 @@ +[project] +name = "funding-service-design-fund-store" +version = "0.1.1" +description = "The funding service design fund store for the DLUHC." +authors = ["Version One", "HM Government, Department of Levelling Up, Housing and Communities"] +license = "MIT License" + +requires-python = ">=3.10, <3.11" +dependencies = [ + "airium>=0.2.6", + "bs4>=0.0.2", + "connexion[flask,swagger-ui,uvicorn]>=3.1.0", + "flask-json==0.4.0", + "flask-migrate==4.0.7", + "flask-sqlalchemy==3.1.1", + "flask==3.0.3", + "funding-service-design-utils>=5.0.8,<6.0.0", + "marshmallow-sqlalchemy==1.0.0", + "openapi-spec-validator>=0.7.1", + "prance>=23.6.21.0", + "psycopg2-binary==2.9.9", + "pytest-html>=3.2.0", + "pytest-mock==3.14.0", + "sqlalchemy-json==0.7.0", + "sqlalchemy-utils==0.41.2", + "sqlalchemy[mypy]>=2.0.30", + "swagger-ui-bundle==1.1.0", + "uvicorn==0.30.1", +] + +[tool.black] +line-length = 120 + +[tool.flake8] +max-line-length = 120 +ignore = ['E203', 'W503'] +count = true +exclude = ['config/fund_loader_config/FAB/*'] + +[tool.isort] +profile = "black" +force_single_line = "true" + +[tool.uv] + +[dependency-groups] +dev = [ + "asserts==0.11.1", + "black>=24.4.2", + "colored>=2.2.4", + "debugpy>=1.8.1", + "deepdiff>=7.0.1", + "flake8-pyproject>=1.2.3", + "invoke>=2.2.0", + "json2html==1.3.0", + "pre-commit~=4.0.0", + "pytest>=8.2.2", + "pytest-env>=1.1.3", + "pytest-flask>=1.3.0", + "pytest-mock==3.14.0", +] diff --git a/fund_store/scripts/__init__.py b/fund_store/scripts/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/fund_store/scripts/all_questions/README.md b/fund_store/scripts/all_questions/README.md new file mode 100644 index 000000000..3502a33ff --- /dev/null +++ b/fund_store/scripts/all_questions/README.md @@ -0,0 +1,29 @@ +# All Questions Generation +The scripts in this folder are used to generate the 'all questions' page for a fund. They take the section information from the fund-store database, combine it with the question flow from the form jsons, to generate a single HTML page containing all questions and all possible branches for a particular round. + +Instructions on running the script and what to do with the outputs are in confluence: https://dluhcdigital.atlassian.net/wiki/spaces/FS/pages/45580513/Generating+the+All+Questions+page+and+Assessment+Field+Display+info + + +## Generating test data +This is for testing [build_hierarchy_levels_for_page](./metadata_utils.py), or anything that takes in the full form json. You can use the full form json, or if you want to use a subset of the form json to make debugging easier, these instructions allow you to extract a small portion of the form for easier testing. + +These steps are to help with debugging by separating out the steps or just extracting a subset of the form. + +In order to make debugging easier, there are methods to extract a subset of data from a form, to test a particular combination of questions and conditions. You can also save the 'metadata' used for the hierarchy information to a file to again make debugging easier. + +### To generate meta data +1. Un-skip the test `test_generate_metadata` in [test_generate_all_questions.py](../../tests/test_generate_all_questions.py) +1. Update the `path_to_form` to the form you want metadata for +1. Execute this single test + +### To generate test data +If you are testing against metadata for the whole form, you don't need this bit, just the meta data. This allows you to generate metadata for a subset of a form, basically setting the start and end pages to somewhere in the middle of the form. +1. Generate metadata, as per above instructions +1. Look at the dict objects defined in [generate_test_data.py](./generate_test_data.py) - these are how you define the subset of the form you want to test. Update the start and end paths for the subset you want, and include every path you want in between the in the `pages` list. +1. Unskip `test_generate_test_data` in [test_generate_all_questions.py](../../tests/test_generate_all_questions.py) +1. Update the out path and input path (uses the metadata_path from above by default) +1. Update the `files_to_generate` path to use the dict you defined above. +1. Execute this single test + +### Testing build_hierarchy_levels_for_page +You can now write a test using `build_hierarchy_levels_for_page` with either a whole form of metadata, or just a subset. Use the output paths above as input to your test. diff --git a/fund_store/scripts/all_questions/__init__.py b/fund_store/scripts/all_questions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/fund_store/scripts/all_questions/generate_test_data.py b/fund_store/scripts/all_questions/generate_test_data.py new file mode 100644 index 000000000..0b2ada21d --- /dev/null +++ b/fund_store/scripts/all_questions/generate_test_data.py @@ -0,0 +1,56 @@ +import json +import os + +START_TO_MAIN_ACTIVITIES = { + "file_name": "start_to_main_activites.json", + "start_page": "/intro-about-your-organisation", + "end_page": "/tell-us-about-your-organisations-main-activities", + "pages": [ + "/intro-about-your-organisation", + "/alternative-organisation-name", + "/organisation-details", + "/tell-us-about-your-organisations-main-activities", + ], +} + +HOW_IS_ORG_CLASSIFIED = { + "file_name": "how_is_org_classified.json", + "start_page": "/how-is-your-organisation-classified", + "end_page": "/organisation-address", + "pages": [ + "/how-is-your-organisation-classified", + "/how-is-your-organisation-classified-other", + "/charity-number", + "/company-registration-number", + "/organisation-address", + ], +} +JOINT_BID = { + "file_name": "joint_bid_out_and_back.json", + "start_page": "/joint-bid", + "end_page": "/website-and-social-media", + "pages": [ + "/partner-organisation-details", + "/work-with-partner-organisations", + "/agreement-exists", + "/website-and-social-media", + "/joint-bid", + ], +} + + +def generate_test_data(target_test_files: [], in_path: str, out_folder: str): + with open(in_path, "r") as f: + all_data = json.load(f) + + for file in target_test_files: + cutdown_data = {"start_page": file["start_page"], "all_pages": []} + for p in file["pages"]: + cutdown_data["all_pages"].append(next(page for page in all_data["all_pages"] if page["path"] == p)) + + last_page = next(p for p in cutdown_data["all_pages"] if p["path"] == file["end_page"]) + last_page["next_paths"] = [] + last_page["all_possible_after"] = [] + + with open(os.path.join(out_folder, file["file_name"]), "w") as f_out: + json.dump(cutdown_data, f_out) diff --git a/fund_store/scripts/all_questions/metadata_utils.py b/fund_store/scripts/all_questions/metadata_utils.py new file mode 100644 index 000000000..9ea383b8f --- /dev/null +++ b/fund_store/scripts/all_questions/metadata_utils.py @@ -0,0 +1,692 @@ +import copy +import fnmatch +import json +import os +from typing import Tuple + +from bs4 import BeautifulSoup, NavigableString + +from db.models.section import Section +from scripts.all_questions.read_forms import ( + build_section_header, + determine_display_value_for_condition, + determine_if_just_html_page, + increment_lowest_in_hierarchy, + remove_lowest_in_hierarchy, + strip_leading_numbers, +) + +FIELD_TYPES_WITH_MAX_WORDS = ["freetextfield", "multilinetextfield"] + + +def get_all_child_nexts(page: dict, child_nexts: list, all_pages: dict): + """Recursively builds a list of everything that could come next from this page, + and then everything that could come next from those next pages, and so on. + + + Args: + page (dict): _description_ + child_nexts (list): _description_ + all_pages (dict): _description_ + """ + # TODO write tests + child_nexts.update([n for n in page["next_paths"]]) + for next_page_path in page["next_paths"]: + next_page = next(p for p in all_pages if p["path"] == next_page_path) + get_all_child_nexts(next_page, child_nexts, all_pages) + + +def get_all_possible_previous(page_path: str, results: list, all_pages: dict): + """Recursively finds all pages that could have come before this one, in any branch of questions + + Args: + page_path (str): _description_ + results (list): _description_ + all_pages (dict): _description_ + """ + + # TODO write tests + direct_prev = [prev["path"] for prev in all_pages if page_path in prev["next_paths"]] + results.update(direct_prev) + for prev in direct_prev: + get_all_possible_previous(prev, results, all_pages) + + +def generate_metadata(full_form_data: dict) -> dict: + """Generates metadata for a form. Basically a dict containing the following: + ``` + { + "start_page": "/intro-about-your-organisation", + "all_pages": [ + { + "path": "/organisation-details", + "next_paths": [ + Everything that could come directly after this page + ], + "all_direct_previous": [ + Everything that could come directly (immediately) before this page + ], + "direct_next_of_direct_previous": [ + Everything that could come immediately after the pages that lead directly to this one + (ie this pages siblings) + ], + "all_possible_next_of_siblings": [ + Everything that could come any point after any of this pages siblings + ], + "all_possible_previous": [ + Everthing that could come at any point before this page + ], + "all_possible_previous_direct_next": [ + Everything that could come anywhere before the pages that come directly after this one + ], + "all_possible_after": [ + Everything that could come anywhere after this page + ] + }, + ] + ``` + + Args: + full_form_data (dict): Data from the form json file + + Returns: + dict: The metadata, as described above + """ + + cutdown = {"start_page": full_form_data["startPage"], "all_pages": []} + for page in full_form_data["pages"]: + cp = {"path": page["path"], "next_paths": [p["path"] for p in page["next"]]} + cutdown["all_pages"].append(cp) + + metadata = copy.deepcopy(cutdown) + for p in metadata["all_pages"]: + # everything that could come immediately before this page + p["all_direct_previous"] = [prev["path"] for prev in cutdown["all_pages"] if p["path"] in prev["next_paths"]] + + # all the immediate next paths of the direct previous (aka siblings) + direct_next_of_direct_previous = set() + for direct_prev in p["all_direct_previous"]: + prev_page = next(prev for prev in cutdown["all_pages"] if prev["path"] == direct_prev) + direct_next_of_direct_previous.update(prev_page["next_paths"]) + p["direct_next_of_direct_previous"] = list(direct_next_of_direct_previous) + + # get all the descendents (possible next anywhere after) of the siblings + all_possible_next_of_siblings = set() + for sibling in p["direct_next_of_direct_previous"]: + sibling_page = next(page for page in cutdown["all_pages"] if page["path"] == sibling) + get_all_child_nexts(sibling_page, all_possible_next_of_siblings, cutdown["all_pages"]) + p["all_possible_next_of_siblings"] = list(all_possible_next_of_siblings) + + # everything that could come anywhere before this page + all_possible_previous = set() + get_all_possible_previous(p["path"], all_possible_previous, cutdown["all_pages"]) + p["all_possible_previous"] = list(all_possible_previous) + + # get everything that is directly after all the possible previous to this page + all_possible_previous_direct_next = set() + for prev in p["all_possible_previous"]: + prev_page = next(page for page in cutdown["all_pages"] if page["path"] == prev) + all_possible_previous_direct_next.update(prev_page["next_paths"]) + p["all_possible_previous_direct_next"] = list(all_possible_previous_direct_next) + + # everything that could come after this page + all_possible_after = set() + get_all_child_nexts(page=p, child_nexts=all_possible_after, all_pages=cutdown["all_pages"]) + p["all_possible_after"] = list(all_possible_after) + + return metadata + + +def build_hierarchy_levels_for_page(page: dict, results: dict, idx: int, all_pages: dict, start_page: bool = False): + """Recursively builds up a dict containing the path of each page, and it's level in the hierarchy of the page + Format of results: + ``` + { + "/path-to-page-1": 1, + "/path-to-sub-page": 2, + "/path-to-page-2": 1, + "/path-to-another-sub-page": 2, + } + ``` + + Args: + page (dict): Page object from metadata + results (dict): The dict that will store the hierarchy results + idx (int): The hierarchy level of this page at this point in the tree + all_pages (dict): All the pages in the form + start_page (bool, optional): Whether or not this is the first page in the form. Defaults to False. + """ + current_level_in_results = results.get(page["path"], 9999) + # We want the lowest level the page appears at, so only update if we are at it's lowest point + if idx < current_level_in_results: + results[page["path"]] = idx + + # loop through every page that comes after this page + for next_path in [n for n in page["next_paths"]]: + # default is same level + next_idx = idx + next_page = next(p for p in all_pages if p["path"] == next_path) + + # if we have more than one possible next page, go to next level + if len(page["next_paths"]) > 1 or start_page is True: + next_idx = idx + 1 + + # if this next path is also a next path of the immediate previous, go back a level + elif next_path in page["direct_next_of_direct_previous"]: + next_idx = idx - 1 + + elif next_path in page["all_possible_previous_direct_next"]: + next_idx = idx - 1 + + # if this page and all it's siblings eventually go back to this same next page, go back a level + elif len(page["direct_next_of_direct_previous"]) <= 1: + pass + elif len(next_page["all_direct_previous"]) == 1: + pass + else: + # Determine whether this next page is the return point for this page and all it's siblings + is_in_descendents_of_all_siblings = True + for sibling in page["direct_next_of_direct_previous"]: + # don't look at this page + if sibling == page["path"]: + continue + sibling_page = next(p for p in all_pages if p["path"] == sibling) + if next_path not in sibling_page["all_possible_after"]: + is_in_descendents_of_all_siblings = False + + if is_in_descendents_of_all_siblings: + next_idx = idx - 1 + + build_hierarchy_levels_for_page(next_page, results, next_idx, all_pages) + + +def strip_string_and_append_if_not_empty(string_to_check: str, list_to_append: list): + """Uses `str.strip()` to remove leading/trailing whitespace from `string_to_check`. + If the resulting string is not empty, appends this to `list_to_append` + + Args: + string_to_check (str): String to strip and append + list_to_append (list): List to append to + """ + stripped = string_to_check.strip() + if stripped: + list_to_append.append(stripped) + + +def extract_from_html(soup, results: list): + """ + Takes in a BeautifulSoup element, recursively iterates through it's children to generate text items + for rendering in the all questions page. + + Any non-empty strings are stripped of leading/trailing spaces etc and appended to `results`. + Any
    elements have their child
  • elements put into a separate list and that list is appended to `results` + + Args: + soup (_type_): HTML to extract from + results (list): results to append to + """ + for element in soup.children: + # If it's just a string, append that text + if isinstance(element, NavigableString): + strip_string_and_append_if_not_empty(element.text, results) + continue + + # If it's a list, append the list items as another list + if element.name == "ul": + bullets = [] + for li in element.children: + strip_string_and_append_if_not_empty(li.text, bullets) + results.append(bullets) + continue + + extract_from_html(element, results) + + +def update_wording_for_multi_input_fields(text: list) -> list: + text_to_filter = [item for item in text if not isinstance(item, list)] + result = fnmatch.filter(text_to_filter, "You can add more * on the next step*") + if len(result) > 0: + text.remove(*result) + return text + + +def determine_title_and_text_for_component( + component: dict, + include_html_components: bool = True, + form_lists: list = None, + is_child: bool = False, +) -> Tuple[str, list]: + """Determines the title and text to display for an individual component. + + Args: + component (dict): The component to get the text for + include_html_components (bool, optional): Whether to include html-only components. Defaults to True. + form_lists (list, optional): All lists in this form - used to determine display values for list items. + Defaults to []. + is_child (bool, optionsl): Whether this is a child field in a multi-input field. Defaults to False. + + + Returns: + Tuple[str, list]: First item is the title, second is the text to display + """ + if form_lists is None: + form_lists = [] + title: str = component["title"] if "title" in component else None + text = [] + # skip details, eg about-your-org-cyp GNpQfE + if component["type"].casefold() == "details": + return None, [] + + # For MultiInputFields, don't add a title and treat each child as a separate component + if component["type"].casefold() == "multiinputfield": + title = f"You can add multiple {component['title'].lower()}" + for child in component["children"]: + child_title, child_text = determine_title_and_text_for_component( + child, include_html_components, form_lists, is_child=True + ) + if child["type"].casefold() in FIELD_TYPES_WITH_MAX_WORDS: + first_column_title = component["options"]["columnTitles"][0].casefold() + text.append(f"{child_title} (Max {child['options']['maxWords']} words per {first_column_title})") + else: + text.append(child_title) + text.extend(child_text) + + # Skip pages that are just html, eg about-your-org-cyp uLwBuz + elif ( + include_html_components + and ("type" in component) + and (component["type"].casefold() == "html" or component["type"].casefold() == "para") + ) or ("hint" in component): + # If there is hint or content text, extract it from the html in the hint field + soup = BeautifulSoup( + component["hint"] if "hint" in component else component["content"], + "html.parser", + ) + text = [] + extract_from_html(soup, text) + update_wording_for_multi_input_fields(text) + + if component["type"].casefold() in FIELD_TYPES_WITH_MAX_WORDS and not is_child: + text.append(f"(Max {component['options']['maxWords']} words)") + + if "list" in component: + # include available options for lists + list_id = component["list"] + list_items = next(list["items"] for list in form_lists if list["name"] == list_id) + list_display = [item["text"] for item in list_items] + text.append(list_display) + return title, text + + +def build_components_from_page( + full_page_json: dict, + include_html_components: bool = True, + form_lists: list = None, + form_conditions: list = None, + index_of_printed_headers: dict = None, + lang: str = "en", +) -> list: + """Builds a list of the components to display from this page, including their title and text, and + directional text on which page to go to next if the form branches from here + + Args: + full_page_json (dict): This page from the form_jsons data + include_html_components (bool, optional): Whether or not to include components that are just HTML. + Defaults to True. + form_lists (list, optional): The lists that appear in this form. Defaults to []. + form_conditions (list, optional): The conditions that appear in this form. Defaults to []. + index_of_printed_headers (dict, optional): The set of pages and their numbers for display, used in + directing people to another section when branching. Defaults to {}. + lang (str): Language for display. Defaults to 'en'. + + Returns: + list: List of components to display, each component being a dict: + ``` + { + "title": str, + "text": str, + "hide_title": bool + } + ``` + """ + # Find out which components in this page determine, through conditions, where we go next + if form_lists is None: + form_lists = [] + if form_conditions is None: + form_conditions = [] + if index_of_printed_headers is None: + index_of_printed_headers = {} + components_with_conditions = [] + for condition in form_conditions: + components_with_conditions.extend([value["field"]["name"] for value in condition["value"]["conditions"]]) + + components = [] + for c in full_page_json["components"]: + title, text = determine_title_and_text_for_component( + c, include_html_components=include_html_components, form_lists=form_lists + ) + if not title and not text: + continue + + # If there are multiple options for the next page, include text about where to go next + if c["name"] in components_with_conditions: + for next_config in full_page_json["next"]: + if "condition" in next_config and next_config["path"] != "/summary": + condition_name = next_config["condition"] + condition_config = next(fc for fc in form_conditions if fc["name"] == condition_name) + destination = index_of_printed_headers[next_config["path"]]["heading_number"] + condition_value = next( + cc for cc in condition_config["value"]["conditions"] if cc["field"]["name"] == c["name"] + )["value"]["value"] + condition_text = determine_display_value_for_condition( + condition_value, + list_name=c["list"] if "list" in c else None, + form_lists=form_lists, + lang=lang, + ) + text.append( + f"If '{condition_text}', go to {destination}" + if lang == "en" + else (f"Os '{condition_text}', ewch i {destination}") + ) + + component = { + "title": title, + "text": text, + "hide_title": c["options"]["hideTitle"] if "hideTitle" in c["options"] else False, + } + components.append(component) + return components + + +def generate_print_headings_for_page( + page: dict, + form_metadata: dict, + this_idx: str, + form_json_page: dict, + page_index: dict, + parent_hierarchy_level: str, + pages_to_do: list, + place_in_siblings_list: int, + index_of_printed_headers: dict, + is_form_heading: bool = False, + lang: str = "en", +): + """Generates the heading text and hierarchical number for this page and it's children + + Args: + page (dict): This page from the metadata + form_metadata (dict): Full metadata for this form + this_idx (str): The heading number for the page that came before this one + form_json_page (dict): Full json for this page from the form json + page_index (dict): Hierarchy levels by page path + parent_hierarchy_level (str): Hierarchy level for the page that came before this one + pages_to_do (list): List of pages left to add to the results + place_in_siblings_list (int): Where this page is in a list of siblings, so multiple sub-pages + are numbered correctly + index_of_printed_headers (dict): Results are stored here as a dict: + ``` + index_of_printed_headers[page_path] = { + "heading_number": Number for this page eg. 1.2.3 - str, + "is_form_heading": bool, + "title": title text to display - str, + } + ``` + is_form_heading (bool, optional): Whether or not this is the heading for the entire form. Defaults to False. + """ + page_path = page["path"] + # If we've already done this page, don't do it again + if page_path not in pages_to_do: + return + + title = strip_leading_numbers(form_json_page["title"]) + + level_in_hrch = page_index[page_path] + hierarchy_difference = level_in_hrch - parent_hierarchy_level + + # If we are going up a level in the hierarchy, and this isn't the last branch + # that goes there, don't do it yet + all_siblings = set(prev for prev in page["direct_next_of_direct_previous"] if prev != page_path) + if pages_to_do.intersection(all_siblings) and hierarchy_difference < 0: + return + + if pages_to_do.intersection(page["all_direct_previous"]) and hierarchy_difference < 0: + return + + # Work out the heading number for this page + base_heading_number = this_idx + if not is_form_heading: + if hierarchy_difference < 0: + # go back a level + base_heading_number = remove_lowest_in_hierarchy(base_heading_number) + elif hierarchy_difference > 0: + # increase level + base_heading_number = f"{this_idx}.{place_in_siblings_list}" + + new_heading_number = increment_lowest_in_hierarchy(base_heading_number) + + index_of_printed_headers[page_path] = { + "heading_number": new_heading_number, + "is_form_heading": is_form_heading, + "title": title, + } + # Make sure we don't do this page again + pages_to_do.remove(page_path) + + # Go through and do the same for all the pages after this one + sibling_tracker = 0 + for next_page_path in page["next_paths"]: + next_page = next(p for p in form_metadata["all_pages"] if p["path"] == next_page_path) + next_form_json_page = next(p for p in form_metadata["full_json"]["pages"] if p["path"] == next_page_path) + generate_print_headings_for_page( + page=next_page, + form_metadata=form_metadata, + this_idx=new_heading_number, + form_json_page=next_form_json_page, + page_index=page_index, + parent_hierarchy_level=level_in_hrch, + pages_to_do=pages_to_do, + is_form_heading=False, + place_in_siblings_list=sibling_tracker, + index_of_printed_headers=index_of_printed_headers, + lang=lang, + ) + sibling_tracker += 1 + + +def generate_print_data_for_form(section_idx: int, form_metadata: dict, form_idx: int, lang: str = "en"): + """Uses `generate_print_headings_for_page()` and `build_components_from_page()` + to gather everything that needs to be printed for this form + + + Args: + section_idx (int): Index of this section (above form level) + form_metadata (dict): Metadata for this form + form_idx (int): Index of this form within the section + + Returns: + dict : Keys are page paths, values are what to print: + ``` + "/intro-to-form": { + "heading_number": Number for this page eg. 1.2.3 - str, + "is_form_heading": bool, + "title": title text to display - str, + "components": [] (generated by `build_components_from_page`) + } + ``` + """ + # Create a list of all the required pages for printing + pages_to_do = set(p["path"] for p in form_metadata["all_pages"] if p["path"] != "/summary") + start_page_path = form_metadata["start_page"] + index = form_metadata["index"] + start_page_metadata = next(p for p in form_metadata["all_pages"] if p["path"] == start_page_path) + start_page_json = next(p for p in form_metadata["full_json"]["pages"] if p["path"] == start_page_path) + + current_hierarchy_level = 0 + index_of_printed_headers = {} + # Generate the headings for the start page - this function is then recursive so goes down the + # entire tree in the form + generate_print_headings_for_page( + start_page_metadata, + form_metadata, + this_idx=f"{section_idx}.{form_idx}", + form_json_page=start_page_json, + page_index=index, + parent_hierarchy_level=current_hierarchy_level, + pages_to_do=pages_to_do, + is_form_heading=True, + place_in_siblings_list=0, + index_of_printed_headers=index_of_printed_headers, # This is updated with the results + lang=lang, + ) + + # For each page, generate the list of components to print + for page_path in index_of_printed_headers.keys(): + full_json_page = next(p for p in form_metadata["full_json"]["pages"] if p["path"] == page_path) + component_display = build_components_from_page( + full_page_json=full_json_page, + include_html_components=(not determine_if_just_html_page(full_json_page["components"])), + form_lists=form_metadata["full_json"]["lists"], + form_conditions=form_metadata["full_json"]["conditions"], + index_of_printed_headers=index_of_printed_headers, + lang=lang, + ) + index_of_printed_headers[page_path]["components"] = component_display + return index_of_printed_headers + + +form_json_to_assessment_display_types = { + "numberfield": "integer", + "textfield": "text", + "yesnofield": "text", + "freetextfield": "free_text", + "checkboxesfield": "list", + "multiinputfield": "table", + "clientsidefileuploadfield": "s3bucketPath", + "radiosfield": "text", + "emailaddressfield": "text", + "telephonenumberfield": "text", + "ukaddressfield": "address", +} + + +def generate_assessment_display_info_for_fields(form_json: dict, form_name: str) -> list: + """Generates a list of the fields and their display types for use in assessment config + + Args: + form_json (dict): Form json for this form + form_name (str): Name of the form + + Returns: + list: List of dictionaries, keys are form names, values are lists of the fields in that form + """ + # TODO write tests + results = [] + for page in form_json["pages"]: + for component in page["components"]: + question = component.get("title", None) + if component["type"].lower() == "multiinputfield": + question = [page["title"]] + child_fields = {} + for field in component["children"]: + child_fields[field["name"]] = { + "column_title": field["title"], + "type": field["type"], + } + question.append(child_fields) + + results.append( + { + "field_id": component["name"], + "form_name": form_name, + "field_type": component["type"], + "presentation_type": form_json_to_assessment_display_types.get(component["type"].lower(), None), + "question": question, + } + ) + return results + + +def generate_print_data_for_sections( + sections: list[Section], + path_to_form_jsons: str, + lang: str, + include_assessment_field_details: bool = False, +) -> dict: + """Creates a dictionary for this section containing the data to print for every form in each section + + Args: + sections (list[Section]): List of sections to generate print data + path_to_form_jsons (str): Absolute path to the form jsons directory + (eg. /dev/form-builder/fsd_config/form-jsons/cof_r2w3/en/) + lang (str): Language string: `en` or `cy` + include_assessment_field_details (bool): Whether to include field details for display in assessment + + Returns: + dict: Containing everything to print for each form + ``` + anchor-id-for-section: = { + "title_text": str Text to display for section title, + "form_print_data": {} All the data for each form in this section, as generated + by `generate_print_data_for_form`, + "assessment_display_info": {} Field details for display in assessment + + } + ``` + """ + section_map = {} + assessment_display_info = {} + + section_idx = 1 + for section in sections: + anchor, text = build_section_header(section, lang=lang) + form_print_data = {} + form_idx = 0 + for child_form in section.children: + form_name = child_form.form_name[0].form_name_json[lang] + path_to_form = os.path.join(path_to_form_jsons, f"{form_name}.json") + # Some forms live in the generic folder rather than fund-round specific + if not os.path.exists(path_to_form): + path_to_form = os.path.join(path_to_form_jsons, "..", "generic", f"{form_name}.json") + with open(path_to_form, "r") as f: + form_data = json.load(f) + form_metadata = generate_metadata(form_data) + form_index = {} + + first_page = next(p for p in form_metadata["all_pages"] if p["path"] == form_metadata["start_page"]) + + # Work out what hierarchy level each page is on + build_hierarchy_levels_for_page( + page=first_page, + results=form_index, + idx=1, + all_pages=form_metadata["all_pages"], + start_page=True, + ) + form_metadata["index"] = form_index + form_metadata["full_json"] = form_data + + # Grab the print data for this form and add it to the results + form_print_data.update( + generate_print_data_for_form( + section_idx=section_idx, + form_metadata=form_metadata, + form_idx=form_idx, + lang=lang, + ) + ) + + if include_assessment_field_details: + assessment_field_details = generate_assessment_display_info_for_fields( + form_json=form_data, form_name=form_name + ) + assessment_display_info[form_name] = assessment_field_details + + form_idx += 1 + section_map[anchor] = { + "title_text": text, + "form_print_data": form_print_data, + "assessment_display_info": assessment_display_info, + } + section_idx += 1 + return section_map diff --git a/fund_store/scripts/all_questions/read_forms.py b/fund_store/scripts/all_questions/read_forms.py new file mode 100644 index 000000000..cabcd5b90 --- /dev/null +++ b/fund_store/scripts/all_questions/read_forms.py @@ -0,0 +1,145 @@ +import os + +from db.models.section import Section + + +def determine_display_value_for_condition( + condition_value: str, + list_name: str = None, + form_lists: list[dict] = None, + lang: str = "en", +) -> str: + """Determines the display value for the given condition string - either translating true/false into + yes/no or by finding the display value from the given list. + Uses lang to determine english or welsh + + Args: + condition_value (str): Value to translate + list_name (str, optional): Name of the list for this field. Defaults to None. + form_lists (list[dict], optional): List of lists from the form_json to find value. Defaults to []. + lang (str, optional): Language to use. Defaults to 'en'. + + Returns: + str: The display value + """ + if form_lists is None: + form_lists = [] + if condition_value.casefold() == "true": + return "Yes" if lang == "en" else "Ydy" + elif condition_value.casefold() == "false": + return "No" if lang == "en" else "Nac ydy" + else: + if list_name: + list_values = next(lizt["items"] for lizt in form_lists if lizt["name"] == list_name) + condition_text = next(item["text"] for item in list_values if item["value"] == condition_value) + return condition_text + return condition_value + + +def find_forms_dir(path_to_form_jsons, fund_short_name, round_short_name, lang): + """Finds the round-specific form_jsons directory based on fund/round name and language + + Args: + path_to_form_jsons (str): Base path to form_jsons + fund_short_name (str): Fund short name, eg. COF + round_short_name (str): Round short name, eg. R2W3 + lang (str): Language, 'en' or 'cy' + + Returns: + str: Path to form_jsons for this language + """ + round_folder_path = os.path.join( + path_to_form_jsons, + f"{fund_short_name.casefold()}_{round_short_name.casefold()}", + ) + if not os.path.isdir(round_folder_path): + print(f"ERROR Could not find form_jsons at {round_folder_path}") + raise FileNotFoundError(f"Could not find {round_folder_path}") + + path_with_lang = os.path.join(round_folder_path, lang) + if not os.path.isdir(path_with_lang): + return round_folder_path + else: + return path_with_lang + + +def determine_if_just_html_page(components: list) -> bool: + """Determines whether this page contains only html (display) components + + Args: + components (list): Components present on this page + + Returns: + bool: Whether or not they are all html display components + """ + return all([(c["type"].casefold() == "para" or c["type"].casefold() == "html") for c in components]) + + +def remove_lowest_in_hierarchy(number_str: str) -> str: + """Takes in a string numerical hierarchy eg. 2.3.4 and removes the lowest member, in this case 4 + + Args: + number_str (str): Hierarchy to remove the lowest part of, eg. 4.5.6 + + Returns: + str: Resulting hierarchy string, eg. 4.5 + """ + last_dot_idx = number_str.rfind(".") + return number_str[:last_dot_idx] + + +def increment_lowest_in_hierarchy(number_str: str) -> str: + """Takes in a string numerical hierarchy, eg. 2.3.4 and increments the lowest number. + + Args: + number_str (str): Hierarchy to increment, eg. 1.2.3 + + Returns: + str: Incremented hierarchy, eg. 1.2.4 + """ + result = "" + split_by_dots = number_str.split(".") + if not split_by_dots[-1]: + split_by_dots.pop() + to_inc = int(split_by_dots[-1]) + split_by_dots.pop() + to_inc += 1 + if split_by_dots: + result = (".").join(split_by_dots) + result += "." + result += f"{to_inc}" + return result + + +def strip_leading_numbers(text: str) -> str: + """Removes leading numbers and . from a string + + Args: + text (str): String to remove leading numbers from, eg. `2.2. A Title` + + Returns: + str: Stripped string, eg. `A Title` + """ + result = text + for char in text: + if char == " ": + break + if char.isdigit() or char == ".": + result = result[1:] # strip this character + return result.strip() + + +def build_section_header(section: Section, lang: str = "en"): + """Formats the title text for this section, and creates an html-safe anchor id for that section + + Args: + section (Section): Section to create title for + lang (str, optional): Language for this title. Defaults to "en". + + Returns: + str, str: Anchor ID, followed by the title text + """ + title = section.title_json[lang] + title = strip_leading_numbers(title) + anchor = title.casefold().replace(" ", "-") + return anchor, title diff --git a/fund_store/scripts/amend_round_dates.py b/fund_store/scripts/amend_round_dates.py new file mode 100755 index 000000000..5f7f0daa5 --- /dev/null +++ b/fund_store/scripts/amend_round_dates.py @@ -0,0 +1,353 @@ +#!/usr/bin/env python3 +from datetime import datetime, timedelta + +import click + +from config.fund_loader_config.cof.cof_r2 import rounds_config as cof_r2_configs +from config.fund_loader_config.cof.cof_r3 import round_config as cof_r3w1_config +from config.fund_loader_config.cof.cof_r3 import round_config_w2 as cof_r3w2_config +from config.fund_loader_config.cof.cof_r3 import round_config_w3 as cof_r3w3_config +from config.fund_loader_config.cof.cof_r4 import round_config_w1 as cof_r4w1_config +from config.fund_loader_config.cof.cof_r4 import round_config_w2 as cof_r4w2_config +from config.fund_loader_config.cof.eoi import round_config_eoi as cof_eoi_configs +from config.fund_loader_config.cyp.cyp_r1 import round_config as cyp_config +from config.fund_loader_config.digital_planning.dpi_r2 import ( + round_config as dpif_config, +) +from config.fund_loader_config.hsra.hsra import round_config as hsra_config +from config.fund_loader_config.night_shelter.ns_r2 import round_config as nstf_config +from db import db +from db.models import Round + +ROUND_IDS = { + "COF_R2W2": "c603d114-5364-4474-a0c4-c41cbf4d3bbd", + "COF_R2W3": "5cf439bf-ef6f-431e-92c5-a1d90a4dd32f", + "COF_R3W1": "e85ad42f-73f5-4e1b-a1eb-6bc5d7f3d762", + "COF_R3W2": "6af19a5e-9cae-4f00-9194-cf10d2d7c8a7", + "COF_R3W3": "4efc3263-aefe-4071-b5f4-0910abec12d2", + "COF_R4W1": "33726b63-efce-4749-b149-20351346c76e", + "COF_R4W2": "27ab26c2-e58e-4bfe-917d-64be10d16496", + "COF_EOI": "6a47c649-7bac-4583-baed-9c4e7a35c8b3", + "NSTF_R2": "fc7aa604-989e-4364-98a7-d1234271435a", + "CYP_R1": "888aae3d-7e2c-4523-b9c1-95952b3d1644", + "DPIF_R2": "0059aad4-5eb5-11ee-8c99-0242ac120002", + "HSRA_R1": "50062ff6-e696-474d-a560-4d9af784e6e5", +} + +ALL_ROUNDS_CONFIG = { + ROUND_IDS["COF_R2W2"]: cof_r2_configs[0], + ROUND_IDS["COF_R2W3"]: cof_r2_configs[1], + ROUND_IDS["COF_R3W1"]: cof_r3w1_config[0], + ROUND_IDS["COF_R3W2"]: cof_r3w2_config[0], + ROUND_IDS["COF_R3W3"]: cof_r3w3_config[0], + ROUND_IDS["COF_R4W1"]: cof_r4w1_config[0], + ROUND_IDS["COF_R4W2"]: cof_r4w2_config[0], + ROUND_IDS["COF_EOI"]: cof_eoi_configs[0], + ROUND_IDS["NSTF_R2"]: nstf_config[0], + ROUND_IDS["CYP_R1"]: cyp_config[0], + ROUND_IDS["DPIF_R2"]: dpif_config[0], + ROUND_IDS["HSRA_R1"]: hsra_config[0], +} +NONE = "none" +UNCHANGED = "unchanged" +PAST = "past" +FUTURE = "future" + +DEFAULTS = {"round_short_name": None} + + +def update_round_dates_in_db(round_id, application_opens, application_deadline, assessment_start, assessment_deadline): # noqa: C901 + round_to_update = Round.query.get(round_id) + if not round_to_update: + raise ValueError(f"Round with ID {round_id} not found in database. No updates made") + date_in_past = (datetime.now() + timedelta(days=-5)).strftime("%Y-%m-%d %H:%M:%S") + date_in_future = (datetime.now() + timedelta(days=5)).strftime("%Y-%m-%d %H:%M:%S") + commit = False + if application_opens and not str(application_opens).casefold() == UNCHANGED: + commit = True + + if isinstance(application_opens, datetime): + round_to_update.opens = application_opens + + elif application_opens.casefold() == PAST: + round_to_update.opens = date_in_past + elif application_opens.casefold() == FUTURE: + round_to_update.opens = date_in_future + else: + round_to_update.opens = datetime.strptime(application_opens, "%Y-%m-%d %H:%M:%S") + + if application_deadline and not str(application_deadline).casefold() == UNCHANGED: + commit = True + + if isinstance(application_deadline, datetime): + round_to_update.deadline = application_deadline + elif application_deadline.casefold() == PAST: + round_to_update.deadline = date_in_past + elif application_deadline.casefold() == FUTURE: + round_to_update.deadline = date_in_future + else: + round_to_update.deadline = datetime.strptime(application_deadline, "%Y-%m-%d %H:%M:%S") + + if assessment_start and not str(assessment_start).casefold() == UNCHANGED: + commit = True + + if isinstance(assessment_start, datetime): + round_to_update.assessment_start = assessment_start + elif assessment_start.casefold() == NONE: + round_to_update.assessment_start = None + elif assessment_start.casefold() == PAST: + round_to_update.assessment_start = date_in_past + elif assessment_start.casefold() == FUTURE: + round_to_update.assessment_start = date_in_future + else: + round_to_update.assessment_start = datetime.strptime(assessment_start, "%Y-%m-%d %H:%M:%S") + + if assessment_deadline and not str(assessment_deadline).casefold() == UNCHANGED: + commit = True + + if isinstance(assessment_deadline, datetime): + round_to_update.assessment_deadline = assessment_deadline + elif assessment_deadline.casefold() == PAST: + round_to_update.assessment_deadline = date_in_past + elif assessment_deadline.casefold() == FUTURE: + round_to_update.assessment_deadline = date_in_future + else: + round_to_update.deadline = datetime.strptime(assessment_deadline, "%Y-%m-%d %H:%M:%S") + + if commit: + db.session.commit() + print(f"Sucessfully updated the round dates for {round_to_update.short_name} [{round_id}].") + else: + print("No changes supplied") + return + + +class DynamicPromptOption(click.Option): + def prompt_for_value(self, ctx): + q = ctx.obj.get("q") + if q: + return DEFAULTS.get(self.name, UNCHANGED) + return super().prompt_for_value(ctx) + + +@click.group() +@click.option("-q", help="Disable all prompts", flag_value=True, default=False) +@click.pass_context +def cli(ctx, q): + # Ensure that ctx.obj exists and is a dict + ctx.ensure_object(dict) + # Apply group-wide feature switches + ctx.obj["q"] = q + + +@cli.command +@click.option( + "-r", + "--round_short_name", + type=click.Choice(ROUND_IDS.keys()), + default="COF_R4W1", + prompt=True, + cls=DynamicPromptOption, + help="Short name for round, will be mapped to round ID. Not needed if round_id supplied.", +) +@click.option( + "-rid", + "--round_id", + prompt=False, + default=None, + help="UUID for round. Not needed if a valid round_short_name supplied", +) +@click.option( + "-o", + "--application_opens", + default=UNCHANGED, + prompt=True, + cls=DynamicPromptOption, +) +@click.option( + "-d", + "--application_deadline", + default=UNCHANGED, + prompt=True, + cls=DynamicPromptOption, +) +@click.option( + "-as", + "--assessment_start", + default=UNCHANGED, + prompt=True, + cls=DynamicPromptOption, +) +@click.option( + "-ad", + "--assessment_deadline", + default=UNCHANGED, + prompt=True, + cls=DynamicPromptOption, +) +def update_round_dates( + round_short_name=None, + round_id=None, + application_opens=None, + application_deadline=None, + assessment_start=None, + assessment_deadline=None, +): + """Updates round dates for the supplied round ID. For any property, the following values are possible: + - UNCHANGED: Leave existing value in place + - PAST: Set to a date 5 days in the past + - FUTURE: Set to a date 5 days in the future + - Specific date in the format YYYY-mm-dd HH:MM:SS + + For assessment_start, the following value is also available: + - NONE: Set the assessment_start to null""" + + # If round ID not supplied, look it up in configs above + if not round_id: + round_id = ROUND_IDS.get(round_short_name, None) + + update_round_dates_in_db(round_id, application_opens, application_deadline, assessment_start, assessment_deadline) + + +@cli.command +@click.option( + "-r", + "--round_short_name", + type=click.Choice(ROUND_IDS.keys()), + default="COF_R4W1", + prompt=True, + cls=DynamicPromptOption, +) +@click.option("-rid", "--round_id", prompt=False, default=None) +def reset_round_dates(round_id, round_short_name): + """Resets the dates for the supplied round to the dates in the fund loader config""" + if not round_id: + round_id = ROUND_IDS[round_short_name] + reset_config = ALL_ROUNDS_CONFIG[round_id] + + update_round_dates_in_db( + round_id, + reset_config["opens"], + reset_config["deadline"], + reset_config["assessment_start"], + reset_config["assessment_deadline"], + ) + + print( + f"Sucessfully reset the round dates for {round_short_name if round_short_name else ''} [{round_id}] to the" + " dates in the fund loader config" + ) + + +@cli.command +@click.option( + "-f", + "--fund_short_name", + prompt=True, + cls=DynamicPromptOption, +) +@click.option( + "-r", + "--round_short_name", + prompt=True, + cls=DynamicPromptOption, +) +@click.option("-rid", "--round_id", prompt=False, default=None) +def reset_round_dates_fab(round_id, fund_short_name, round_short_name): + """Resets the dates for the supplied round to the dates in the fund loader config""" + if not round_id: + from config.fund_loader_config.FAB import FAB_FUND_ROUND_CONFIGS + + round_id = FAB_FUND_ROUND_CONFIGS[fund_short_name]["rounds"][round_short_name]["id"] + + if not round_id: + raise ValueError(f"Round ID does not exist for {round_short_name}") + + reset_config = FAB_FUND_ROUND_CONFIGS[fund_short_name]["rounds"][round_short_name] + + update_round_dates_in_db( + round_id, + datetime.strptime(reset_config["opens"], "%Y-%m-%dT%H:%M:%S"), + datetime.strptime(reset_config["deadline"], "%Y-%m-%dT%H:%M:%S"), + datetime.strptime(reset_config["assessment_start"], "%Y-%m-%dT%H:%M:%S"), + datetime.strptime(reset_config["assessment_deadline"], "%Y-%m-%dT%H:%M:%S"), + ) + + print( + f"Sucessfully reset the round dates for {round_short_name if round_short_name else ''} [{round_id}] to the" + " dates in the fund loader config" + ) + + +@cli.command +@click.option( + "-f", + "--fund_short_name", + prompt=True, + cls=DynamicPromptOption, +) +@click.option( + "-r", + "--round_short_name", + prompt=True, + cls=DynamicPromptOption, +) +@click.option( + "-rid", + "--round_id", + prompt=False, + default=None, + help="UUID for round. Not needed if a valid round_short_name supplied", +) +@click.option( + "-o", + "--application_opens", + default=UNCHANGED, + prompt=True, + cls=DynamicPromptOption, +) +@click.option( + "-d", + "--application_deadline", + default=UNCHANGED, + prompt=True, + cls=DynamicPromptOption, +) +@click.option( + "-as", + "--assessment_start", + default=UNCHANGED, + prompt=True, + cls=DynamicPromptOption, +) +@click.option( + "-ad", + "--assessment_deadline", + default=UNCHANGED, + prompt=True, + cls=DynamicPromptOption, +) +def update_round_dates_fab( + fund_short_name=None, + round_short_name=None, + round_id=None, + application_opens=None, + application_deadline=None, + assessment_start=None, + assessment_deadline=None, +): + from config.fund_loader_config.FAB import FAB_FUND_ROUND_CONFIGS + + round_id = FAB_FUND_ROUND_CONFIGS[fund_short_name]["rounds"][round_short_name]["id"] + + if not round_id: + raise ValueError(f"Round ID does not exist for {round_short_name}") + + update_round_dates_in_db(round_id, application_opens, application_deadline, assessment_start, assessment_deadline) + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + cli() diff --git a/fund_store/scripts/data_updates/FS-2433-application_guidance.py b/fund_store/scripts/data_updates/FS-2433-application_guidance.py new file mode 100644 index 000000000..66e8e26a4 --- /dev/null +++ b/fund_store/scripts/data_updates/FS-2433-application_guidance.py @@ -0,0 +1,39 @@ +from flask import current_app +from sqlalchemy import update + +import config.fund_loader_config.cof.cof_r2 as cof_r2 +import config.fund_loader_config.cof.cof_r3 as cof_r3 +from db import db +from db.models.round import Round + + +def update_rounds_with_application_guidance(rounds): + for round in rounds: + current_app.logger.info( + "\tRound: {round_short_name} ({round_id})", + extra=dict(round_short_name=round["short_name"], round_id=str(round["id"])), + ) + if round.get("application_guidance"): + current_app.logger.info("\t\tUpdating application_guidance") + stmt = ( + update(Round).where(Round.id == round["id"]).values(application_guidance=round["application_guidance"]) + ) + + db.session.execute(stmt) + else: + current_app.logger.info("\t\tNo application_guidance defined") + db.session.commit() + + +def main() -> None: + current_app.logger.info("Updating application_guidance field for COF") + update_rounds_with_application_guidance(cof_r2.rounds_config) + update_rounds_with_application_guidance(cof_r3.round_config) + current_app.logger.info("Updates complete") + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/data_updates/FS-2900-ns-section-update.py b/fund_store/scripts/data_updates/FS-2900-ns-section-update.py new file mode 100644 index 000000000..ae80e88f1 --- /dev/null +++ b/fund_store/scripts/data_updates/FS-2900-ns-section-update.py @@ -0,0 +1,59 @@ +from flask import current_app +from sqlalchemy import update + +import config.fund_loader_config.night_shelter.ns_r2 as nstf_config +from db import db +from db.models.section import Section + + +def update_section_titles(section_config): + if len(section_config) > 0: + for section in section_config: + current_app.logger.info( + "\t\tUpdating section title from {section_old_title} to {section_new_title}.", + extra=dict( + section_old_title=section["old_title"], + section_new_title=section["new_title"], + ), + ) + stmt = ( + update(Section) + .where(Section.title == section["old_title"]) + .where(Section.round_id == section["round_id"]) + .values(title=section["new_title"]) + ) + + db.session.execute(stmt) + else: + current_app.logger.info("\t\tNo section config provided") + db.session.commit() + + +def main() -> None: + section_config = [ + { + "old_title": "Name you application", + "new_title": "Name your application", + "round_id": nstf_config.NIGHT_SHELTER_ROUND_2_ID, + }, + { + "old_title": "7. Check declarations", + "new_title": "7. Declarations", + "round_id": nstf_config.NIGHT_SHELTER_ROUND_2_ID, + }, + { + "old_title": "1.1 Organisation Information", + "new_title": "1.1 Organisation information", + "round_id": nstf_config.NIGHT_SHELTER_ROUND_2_ID, + }, + ] + current_app.logger.info("Updating sections for NSTF") + update_section_titles(section_config) + current_app.logger.info("Update complete") + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/data_updates/FS-2965_ns_guidance.py b/fund_store/scripts/data_updates/FS-2965_ns_guidance.py new file mode 100644 index 000000000..18e4d373c --- /dev/null +++ b/fund_store/scripts/data_updates/FS-2965_ns_guidance.py @@ -0,0 +1,41 @@ +from flask import current_app +from sqlalchemy import update + +import config.fund_loader_config.night_shelter.ns_r2 as ns_r2 +from db import db +from db.models.round import Round + + +def update_rounds_with_links(rounds): + for round in rounds: + current_app.logger.warning( + "\tRound: {round_short_name} ({round_id})", + extra=dict(round_short_name=round["short_name"], round_id=str(round["id"])), + ) + if round.get("application_guidance"): + current_app.logger.warning("\t\tUpdating application_guidance") + stmt = ( + update(Round) + .where(Round.id == round["id"]) + .values( + application_guidance=round["application_guidance"], + ) + ) + + db.session.execute(stmt) + else: + current_app.logger.warning("\t\tNo application_guidance defined") + db.session.commit() + + +def main() -> None: + current_app.logger.warning("Updating application_guidance for NSTF R2") + update_rounds_with_links(ns_r2.round_config) + current_app.logger.warning("Updates complete") + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/data_updates/FS-2982-date-logic.py b/fund_store/scripts/data_updates/FS-2982-date-logic.py new file mode 100644 index 000000000..cd7b1a380 --- /dev/null +++ b/fund_store/scripts/data_updates/FS-2982-date-logic.py @@ -0,0 +1,38 @@ +from flask import current_app +from sqlalchemy import update + +import config.fund_loader_config.cof.cof_r2 as cof_r2 +import config.fund_loader_config.cof.cof_r3 as cof_r3 +import config.fund_loader_config.night_shelter.ns_r2 as ns_r2 +from db import db +from db.models.round import Round + + +def update_date_format(round_config): + current_app.logger.warning("\t\tUpdating date format/ logic for rounds") + stmt = ( + update(Round) + .where(Round.id == round_config[0].get("id")) + .values( + opens=round_config[0].get("opens"), + deadline=round_config[0].get("deadline"), + assessment_deadline=round_config[0].get("assessment_deadline"), + ) + ) + db.session.execute(stmt) + db.session.commit() + + +def main() -> None: + current_app.logger.warning("Updating date format for NSTF R2, COF R3, COF R2") + update_date_format(ns_r2.round_config) + update_date_format(cof_r3.round_config) + update_date_format(cof_r2.rounds_config) + current_app.logger.warning("Updates complete") + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/data_updates/FS-3471-application_guidance.py b/fund_store/scripts/data_updates/FS-3471-application_guidance.py new file mode 100644 index 000000000..0911187ce --- /dev/null +++ b/fund_store/scripts/data_updates/FS-3471-application_guidance.py @@ -0,0 +1,37 @@ +from flask import current_app +from sqlalchemy import update + +import config.fund_loader_config.cyp.cyp_r1 as cyp_r1 +from db import db +from db.models.round import Round + + +def update_rounds_with_application_guidance(rounds): + for round in rounds: + current_app.logger.info( + "\tRound: {round_short_name} ({round_id})", + extra=dict(round_short_name=round["short_name"], round_id=str(round["id"])), + ) + if round.get("application_guidance"): + current_app.logger.info("\t\tUpdating application_guidance") + stmt = ( + update(Round).where(Round.id == round["id"]).values(application_guidance=round["application_guidance"]) + ) + + db.session.execute(stmt) + else: + current_app.logger.info("\t\tNo application_guidance defined") + db.session.commit() + + +def main() -> None: + current_app.logger.info("Updating application_guidance field for CYP") + update_rounds_with_application_guidance(cyp_r1.round_config) + current_app.logger.info("Updates complete") + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/data_updates/FS-3749_dpif_guidance_url.py b/fund_store/scripts/data_updates/FS-3749_dpif_guidance_url.py new file mode 100644 index 000000000..b288f0bed --- /dev/null +++ b/fund_store/scripts/data_updates/FS-3749_dpif_guidance_url.py @@ -0,0 +1,41 @@ +from flask import current_app +from sqlalchemy import update + +import config.fund_loader_config.digital_planning.dpi_r2 as dpi_r2 +from db import db +from db.models.round import Round + + +def update_rounds_with_links(rounds): + for round in rounds: + current_app.logger.warning( + "\tRound: {round_short_name} ({round_id})", + extra=dict(round_short_name=round["short_name"], round_id=str(round["id"])), + ) + if round.get("guidance_url"): + current_app.logger.warning("\t\tUpdating guidance_url") + stmt = ( + update(Round) + .where(Round.id == round["id"]) + .values( + guidance_url=round["guidance_url"], + ) + ) + + db.session.execute(stmt) + else: + current_app.logger.warning("\t\tNo guidance_url defined") + db.session.commit() + + +def main() -> None: + current_app.logger.warning("Updating guidance_url for DPIF R2") + update_rounds_with_links(dpi_r2.round_config) + current_app.logger.warning("Updates complete") + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/data_updates/FS-3808_dpif_application_fields_download_available.py b/fund_store/scripts/data_updates/FS-3808_dpif_application_fields_download_available.py new file mode 100644 index 000000000..33c272d0f --- /dev/null +++ b/fund_store/scripts/data_updates/FS-3808_dpif_application_fields_download_available.py @@ -0,0 +1,41 @@ +from flask import current_app +from sqlalchemy import update + +import config.fund_loader_config.digital_planning.dpi_r2 as dpi_r2 +from db import db +from db.models.round import Round + + +def update_application_fields_download_available(rounds): + for round in rounds: + current_app.logger.warning( + "\tRound: {round_short_name} ({round_id})", + extra=dict(round_short_name=round["short_name"], round_id=str(round["id"])), + ) + if round.get("application_fields_download_available"): + current_app.logger.warning("\t\tUpdating application_fields_download_available") + stmt = ( + update(Round) + .where(Round.id == round["id"]) + .values( + application_fields_download_available=round["application_fields_download_available"], + ) + ) + + db.session.execute(stmt) + else: + current_app.logger.warning("\t\tNo application_fields_download_available defined") + db.session.commit() + + +def main() -> None: + current_app.logger.warning("Updating application_fields_download_available for DPIF R2") + update_application_fields_download_available(dpi_r2.round_config) + current_app.logger.warning("Updates complete") + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/data_updates/FS-3859_patch_cof_r3w3_section_names.py b/fund_store/scripts/data_updates/FS-3859_patch_cof_r3w3_section_names.py new file mode 100644 index 000000000..c989b8b72 --- /dev/null +++ b/fund_store/scripts/data_updates/FS-3859_patch_cof_r3w3_section_names.py @@ -0,0 +1,16 @@ +# flake8: noqa +from config.fund_loader_config.cof.cof_r3 import COF_ROUND_3_WINDOW_3_ID +from config.fund_loader_config.cof.cof_r3 import cof_r3w3_sections +from db.queries import update_application_section_names + + +def main() -> None: + print("Updating section names to sentance case.") + update_application_section_names(COF_ROUND_3_WINDOW_3_ID, cof_r3w3_sections, "cy") + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/data_updates/FS-3866_fix_incorrect_instructions_url.py b/fund_store/scripts/data_updates/FS-3866_fix_incorrect_instructions_url.py new file mode 100644 index 000000000..c633a5361 --- /dev/null +++ b/fund_store/scripts/data_updates/FS-3866_fix_incorrect_instructions_url.py @@ -0,0 +1,41 @@ +from flask import current_app +from sqlalchemy import update + +import config.fund_loader_config.cof.cof_r3 as cof_r3 +from db import db +from db.models.round import Round + + +def update_rounds_with_links(rounds): + for round in rounds: + current_app.logger.warning( + "\tRound: {round_short_name} ({round_id})", + extra=dict(round_short_name=round["short_name"], round_id=round["id"]), + ) + if round.get("instructions"): + current_app.logger.warning("\t\tUpdating instructions") + stmt = ( + update(Round) + .where(Round.id == round["id"]) + .values( + instructions=round["instructions"], + ) + ) + + db.session.execute(stmt) + else: + current_app.logger.warning("\t\tNo instructions defined") + db.session.commit() + + +def main() -> None: + current_app.logger.warning("Updating instructions for COF R3W3") + update_rounds_with_links(cof_r3.round_config_w3) + current_app.logger.warning("Updates complete") + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/data_updates/FS-4264_update_eoi_welsh_instructions.py b/fund_store/scripts/data_updates/FS-4264_update_eoi_welsh_instructions.py new file mode 100644 index 000000000..5578acda1 --- /dev/null +++ b/fund_store/scripts/data_updates/FS-4264_update_eoi_welsh_instructions.py @@ -0,0 +1,41 @@ +from flask import current_app +from sqlalchemy import update + +import config.fund_loader_config.cof.eoi as eoi +from db import db +from db.models.round import Round + + +def update_rounds_with_links(round_config): + current_app.logger.warning( + "\tRound: {round_short_name} ({round_id})", + extra=dict( + round_short_name=round_config[0]["short_name"], + round_id=str(round_config[0]["id"]), + ), + ) + current_app.logger.warning("\t\tUpdating instructions & application_guidance") + stmt = ( + update(Round) + .where(Round.id == round_config[0]["id"]) + .values( + instructions_json=round_config[0]["instructions_json"], + application_guidance_json=round_config[0]["application_guidance_json"], + ) + ) + + db.session.execute(stmt) + db.session.commit() + + +def main() -> None: + current_app.logger.warning("Updating instructions & application_guidance for EOI") + update_rounds_with_links(eoi.round_config_eoi) + current_app.logger.warning("Updates complete") + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/data_updates/FS2910_ns_links.py b/fund_store/scripts/data_updates/FS2910_ns_links.py new file mode 100644 index 000000000..7dacb0cd0 --- /dev/null +++ b/fund_store/scripts/data_updates/FS2910_ns_links.py @@ -0,0 +1,42 @@ +from flask import current_app +from sqlalchemy import update + +import config.fund_loader_config.night_shelter.ns_r2 as ns_r2 +from db import db +from db.models.round import Round + + +def update_rounds_with_links(rounds): + for round in rounds: + current_app.logger.warning( + "\tRound: {round_short_name} ({round_id})", + extra=dict(round_short_name=round["short_name"], round_id=str(round["id"])), + ) + if round.get("prospectus") and round.get("privacy_notice"): + current_app.logger.warning("\t\tUpdating prospectus and privacy notice") + stmt = ( + update(Round) + .where(Round.id == round["id"]) + .values( + prospectus=round["prospectus"], + privacy_notice=round["privacy_notice"], + ) + ) + + db.session.execute(stmt) + else: + current_app.logger.warning("\t\tNo links defined") + db.session.commit() + + +def main() -> None: + current_app.logger.warning("Updating prospectus and privacy links for NSTF R2") + update_rounds_with_links(ns_r2.round_config) + current_app.logger.warning("Updates complete") + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/data_updates/FS2956_ns_weightings.py b/fund_store/scripts/data_updates/FS2956_ns_weightings.py new file mode 100644 index 000000000..5e3e8c276 --- /dev/null +++ b/fund_store/scripts/data_updates/FS2956_ns_weightings.py @@ -0,0 +1,42 @@ +from flask import current_app +from sqlalchemy import update +from sqlalchemy_utils import Ltree + +import config.fund_loader_config.night_shelter.ns_r2 as ns_r2 +from db import db +from db.models.section import Section + + +def update_section_weightings(section): + current_app.logger.warning( + "\tSection: {section_tree_path} ({section_name})", + extra=dict( + section_tree_path=str(section["tree_path"]), + section_name=str(section["section_name"]["en"]), + ), + ) + current_app.logger.warning("\t\tUpdating weighting") + stmt = update(Section).where(Section.path == Ltree(section["tree_path"])).values(weighting=section["weighting"]) + + db.session.execute(stmt) + db.session.commit() + + +def main() -> None: + current_app.logger.warning("Updating section weightings for NSTF R2") + ns_sections = ns_r2.r2_application_sections + sections_to_update = [ + f"{ns_r2.APPLICATION_BASE_PATH}.3", + f"{ns_r2.APPLICATION_BASE_PATH}.4", + ] + for section in ns_sections: + if section["tree_path"] in sections_to_update: + update_section_weightings(section) + current_app.logger.warning("Updates complete") + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/data_updates/README.md b/fund_store/scripts/data_updates/README.md new file mode 100644 index 000000000..aa8841fd2 --- /dev/null +++ b/fund_store/scripts/data_updates/README.md @@ -0,0 +1,26 @@ +# Data Updates +## Expected Usage +The configuration of data used to load funds, rounds, sections etc lives [here](/config/fund_loader_config). +Once this data has been loaded, if it needs to change, there are 2 options: +1. Truncate all data in the tables and rerun the [import scripts](/README.md#seeding-fund-data) +1. Update the data in the existing rows. + +This folder is intended to serve option 2. + +## Script format +When data needs to be updated (after it has already been inserted in UAT/production - for changes during development use option 1 and truncate the data then reinsert). + +For each change, +1. Update the main [fund loader config](/config/fund_loader_config/) with any new values. +1. Create a new python script in [data_updates](.). This script should read through the fund loader config and make any required updates. + +This approach ensures that data is only defined in one place (fund_loader_config) and then stored in the database. + +## Running +To run the script, find the container ID for the fund-store and execute + + docker exec -it python -m scripts.data_updates. + +To run on Copilot environments, use: + + copilot svc exec --command "launcher python -m scripts.data_updates." diff --git a/fund_store/scripts/data_updates/__init__.py b/fund_store/scripts/data_updates/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/fund_store/scripts/data_updates/patch_cof_r3w1_section_names.py b/fund_store/scripts/data_updates/patch_cof_r3w1_section_names.py new file mode 100644 index 000000000..7644a3509 --- /dev/null +++ b/fund_store/scripts/data_updates/patch_cof_r3w1_section_names.py @@ -0,0 +1,16 @@ +# flake8: noqa +from config.fund_loader_config.cof.cof_r3 import COF_ROUND_3_WINDOW_1_ID +from config.fund_loader_config.cof.cof_r3 import cof_r3_sections +from db.queries import update_application_section_names + + +def main() -> None: + print("Updating section names to sentance case.") + update_application_section_names(COF_ROUND_3_WINDOW_1_ID, cof_r3_sections) + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/data_updates/patch_cyp_name.py b/fund_store/scripts/data_updates/patch_cyp_name.py new file mode 100644 index 000000000..b9759e707 --- /dev/null +++ b/fund_store/scripts/data_updates/patch_cyp_name.py @@ -0,0 +1,28 @@ +from flask import current_app +from sqlalchemy import update + +import config.fund_loader_config.cyp.cyp_r1 as cyp_r1 +from db import db +from db.models.fund import Fund + + +def update_fund_name(fund_config): + current_app.logger.info("Fund: {fund_id}", extra=dict(fund_id=str(fund_config["id"]))) + current_app.logger.info("\t\tUpdating fund name") + stmt = update(Fund).where(Fund.id == fund_config["id"]).values(name_json=fund_config["name_json"]) + + db.session.execute(stmt) + db.session.commit() + + +def main() -> None: + current_app.logger.info("Updating fund name for CYP") + update_fund_name(cyp_r1.fund_config) + current_app.logger.info("Updates complete") + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/data_updates/patch_cypr1_guidance_201023.py b/fund_store/scripts/data_updates/patch_cypr1_guidance_201023.py new file mode 100644 index 000000000..919508fdb --- /dev/null +++ b/fund_store/scripts/data_updates/patch_cypr1_guidance_201023.py @@ -0,0 +1,34 @@ +from flask import current_app +from sqlalchemy import update + +import config.fund_loader_config.cyp.cyp_r1 as cyp_r1 +from db import db +from db.models.round import Round + + +def update_round_guidance(round_config): + current_app.logger.info( + "Round: {round_short_name}, id: {round_id}", + extra=dict( + round_short_name=round_config["short_name"], + round_id=str(round_config["id"]), + ), + ) + current_app.logger.info("\t\tUpdating round guidance") + stmt = update(Round).where(Round.id == round_config["id"]).values(guidance_url=round_config["guidance_url"]) + + db.session.execute(stmt) + db.session.commit() + + +def main() -> None: + current_app.logger.info("Updating guidance url for CYP R1") + update_round_guidance(cyp_r1.round_config[0]) + current_app.logger.info("Updates complete") + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/fund_round_loaders/load_cof_eoi.py b/fund_store/scripts/fund_round_loaders/load_cof_eoi.py new file mode 100644 index 000000000..3d4179f5e --- /dev/null +++ b/fund_store/scripts/fund_round_loaders/load_cof_eoi.py @@ -0,0 +1,33 @@ +# flake8: noqa +from config.fund_loader_config.cof.eoi import APPLICATION_BASE_PATH_COF_EOI +from config.fund_loader_config.cof.eoi import ASSESSMENT_BASE_PATH_COF_EOI +from config.fund_loader_config.cof.eoi import COF_EOI_ROUND_ID +from config.fund_loader_config.cof.eoi import cof_eoi_sections +from config.fund_loader_config.cof.eoi import fund_config +from config.fund_loader_config.cof.eoi import round_config_eoi +from db.queries import insert_base_sections +from db.queries import insert_fund_data +from db.queries import insert_or_update_application_sections +from db.queries import upsert_round_data + + +def main() -> None: + print("Inserting fund and round data.") + insert_fund_data(fund_config) + upsert_round_data(round_config_eoi) + + print("Inserting base sections config.") + insert_base_sections( + APPLICATION_BASE_PATH_COF_EOI, + ASSESSMENT_BASE_PATH_COF_EOI, + COF_EOI_ROUND_ID, + ) + print("Inserting sections.") + insert_or_update_application_sections(COF_EOI_ROUND_ID, cof_eoi_sections) + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/fund_round_loaders/load_cof_r2.py b/fund_store/scripts/fund_round_loaders/load_cof_r2.py new file mode 100644 index 000000000..85aeb2d8c --- /dev/null +++ b/fund_store/scripts/fund_round_loaders/load_cof_r2.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# flake8: noqa +from config.fund_loader_config.cof.cof_r2 import APPLICATION_BASE_PATH +from config.fund_loader_config.cof.cof_r2 import ASSESSMENT_BASE_PATH +from config.fund_loader_config.cof.cof_r2 import COF_ROUND_2_WINDOW_2_ID +from config.fund_loader_config.cof.cof_r2 import COF_ROUND_2_WINDOW_3_ID +from config.fund_loader_config.cof.cof_r2 import cof_r2_sections +from config.fund_loader_config.cof.cof_r2 import fund_config +from config.fund_loader_config.cof.cof_r2 import rounds_config +from db.queries import insert_base_sections +from db.queries import insert_fund_data +from db.queries import insert_or_update_application_sections +from db.queries import upsert_round_data + + +def main() -> None: + inserted_fund = insert_fund_data(fund_config) + print("Fund inserted:") + print(inserted_fund) + inserted_rounds = upsert_round_data(rounds_config) + print("Rounds inserted:") + print(inserted_rounds) + + print("Inserting base sections config.") + # Insert base sections + insert_base_sections(APPLICATION_BASE_PATH, ASSESSMENT_BASE_PATH, COF_ROUND_2_WINDOW_2_ID) + insert_base_sections(APPLICATION_BASE_PATH, ASSESSMENT_BASE_PATH, COF_ROUND_2_WINDOW_3_ID) + print("Inserting sections.") + # only need to do it for one round as they have identical section sorts + insert_or_update_application_sections(COF_ROUND_2_WINDOW_2_ID, cof_r2_sections) + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/fund_round_loaders/load_cof_r3w1.py b/fund_store/scripts/fund_round_loaders/load_cof_r3w1.py new file mode 100644 index 000000000..3bc3bd965 --- /dev/null +++ b/fund_store/scripts/fund_round_loaders/load_cof_r3w1.py @@ -0,0 +1,34 @@ +# flake8: noqa +from config.fund_loader_config.cof.cof_r3 import APPLICATION_BASE_PATH_COF_R3_W1 +from config.fund_loader_config.cof.cof_r3 import ASSESSMENT_BASE_PATH_COF_R3_W1 +from config.fund_loader_config.cof.cof_r3 import COF_ROUND_3_WINDOW_1_ID +from config.fund_loader_config.cof.cof_r3 import cof_r3_sections +from config.fund_loader_config.cof.cof_r3 import fund_config +from config.fund_loader_config.cof.cof_r3 import round_config +from db.queries import insert_assessment_sections +from db.queries import insert_base_sections +from db.queries import insert_fund_data +from db.queries import insert_or_update_application_sections +from db.queries import upsert_round_data + + +def main() -> None: + print("Inserting fund and round data.") + insert_fund_data(fund_config) + upsert_round_data(round_config) + + print("Inserting base sections config.") + insert_base_sections( + APPLICATION_BASE_PATH_COF_R3_W1, + ASSESSMENT_BASE_PATH_COF_R3_W1, + COF_ROUND_3_WINDOW_1_ID, + ) + print("Inserting sections.") + insert_or_update_application_sections(COF_ROUND_3_WINDOW_1_ID, cof_r3_sections) + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/fund_round_loaders/load_cof_r3w2.py b/fund_store/scripts/fund_round_loaders/load_cof_r3w2.py new file mode 100644 index 000000000..e469f4b5b --- /dev/null +++ b/fund_store/scripts/fund_round_loaders/load_cof_r3w2.py @@ -0,0 +1,31 @@ +# flake8: noqa +from config.fund_loader_config.cof.cof_r3 import APPLICATION_BASE_PATH_COF_R3_W2 +from config.fund_loader_config.cof.cof_r3 import ASSESSMENT_BASE_PATH_COF_R3_W2 +from config.fund_loader_config.cof.cof_r3 import COF_ROUND_3_WINDOW_2_ID +from config.fund_loader_config.cof.cof_r3 import cof_r3w2_sections +from config.fund_loader_config.cof.cof_r3 import round_config_w2 +from db.queries import insert_base_sections +from db.queries import insert_or_update_application_sections +from db.queries import upsert_round_data + + +def main() -> None: + print("'insert_fund_config(...)' not required as COFR3W2 shares the same fund config from COFR3W1.") + print("Inserting round data.") + upsert_round_data(round_config_w2) + + print("Inserting base sections config.") + insert_base_sections( + APPLICATION_BASE_PATH_COF_R3_W2, + ASSESSMENT_BASE_PATH_COF_R3_W2, + COF_ROUND_3_WINDOW_2_ID, + ) + print("Inserting sections.") + insert_or_update_application_sections(COF_ROUND_3_WINDOW_2_ID, cof_r3w2_sections) + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/fund_round_loaders/load_cof_r3w3.py b/fund_store/scripts/fund_round_loaders/load_cof_r3w3.py new file mode 100644 index 000000000..69ab340ec --- /dev/null +++ b/fund_store/scripts/fund_round_loaders/load_cof_r3w3.py @@ -0,0 +1,31 @@ +# flake8: noqa +from config.fund_loader_config.cof.cof_r3 import APPLICATION_BASE_PATH_COF_R3_W3 +from config.fund_loader_config.cof.cof_r3 import ASSESSMENT_BASE_PATH_COF_R3_W3 +from config.fund_loader_config.cof.cof_r3 import COF_ROUND_3_WINDOW_3_ID +from config.fund_loader_config.cof.cof_r3 import cof_r3w3_sections +from config.fund_loader_config.cof.cof_r3 import round_config_w3 +from db.queries import insert_base_sections +from db.queries import insert_or_update_application_sections +from db.queries import upsert_round_data + + +def main() -> None: + print("'insert_fund_config(...)' not required as COFR3W3 shares the same fund config from COFR3w1.") + print("Inserting round data.") + upsert_round_data(round_config_w3) + + print("Inserting base sections config.") + insert_base_sections( + APPLICATION_BASE_PATH_COF_R3_W3, + ASSESSMENT_BASE_PATH_COF_R3_W3, + COF_ROUND_3_WINDOW_3_ID, + ) + print("Inserting sections.") + insert_or_update_application_sections(COF_ROUND_3_WINDOW_3_ID, cof_r3w3_sections) + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/fund_round_loaders/load_cof_r4w1.py b/fund_store/scripts/fund_round_loaders/load_cof_r4w1.py new file mode 100644 index 000000000..900b53c4b --- /dev/null +++ b/fund_store/scripts/fund_round_loaders/load_cof_r4w1.py @@ -0,0 +1,33 @@ +# flake8: noqa +from config.fund_loader_config.cof.cof_r4 import APPLICATION_BASE_PATH_COF_R4_W1 +from config.fund_loader_config.cof.cof_r4 import ASSESSMENT_BASE_PATH_COF_R4_W1 +from config.fund_loader_config.cof.cof_r4 import COF_ROUND_4_WINDOW_1_ID +from config.fund_loader_config.cof.cof_r4 import cof_r4w1_sections +from config.fund_loader_config.cof.cof_r4 import fund_config +from config.fund_loader_config.cof.cof_r4 import round_config_w1 +from db.queries import insert_base_sections +from db.queries import insert_fund_data +from db.queries import insert_or_update_application_sections +from db.queries import upsert_round_data + + +def main() -> None: + print("Inserting fund and round data.") + insert_fund_data(fund_config) + upsert_round_data(round_config_w1) + + print("Inserting base sections config.") + insert_base_sections( + APPLICATION_BASE_PATH_COF_R4_W1, + ASSESSMENT_BASE_PATH_COF_R4_W1, + COF_ROUND_4_WINDOW_1_ID, + ) + print("Inserting sections.") + insert_or_update_application_sections(COF_ROUND_4_WINDOW_1_ID, cof_r4w1_sections) + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/fund_round_loaders/load_cof_r4w2.py b/fund_store/scripts/fund_round_loaders/load_cof_r4w2.py new file mode 100644 index 000000000..1bb21150c --- /dev/null +++ b/fund_store/scripts/fund_round_loaders/load_cof_r4w2.py @@ -0,0 +1,31 @@ +# flake8: noqa +from config.fund_loader_config.cof.cof_r4 import APPLICATION_BASE_PATH_COF_R4_W2 +from config.fund_loader_config.cof.cof_r4 import ASSESSMENT_BASE_PATH_COF_R4_W2 +from config.fund_loader_config.cof.cof_r4 import COF_ROUND_4_WINDOW_2_ID +from config.fund_loader_config.cof.cof_r4 import cof_r4w2_sections +from config.fund_loader_config.cof.cof_r4 import round_config_w2 +from db.queries import insert_base_sections +from db.queries import insert_or_update_application_sections +from db.queries import upsert_round_data + + +def main() -> None: + print("'insert_fund_config(...)' not required as COFR4W2 shares the same fund config from COFR4w1.") + print("Inserting round data.") + upsert_round_data(round_config_w2) + + print("Inserting base sections config.") + insert_base_sections( + APPLICATION_BASE_PATH_COF_R4_W2, + ASSESSMENT_BASE_PATH_COF_R4_W2, + COF_ROUND_4_WINDOW_2_ID, + ) + print("Inserting sections.") + insert_or_update_application_sections(COF_ROUND_4_WINDOW_2_ID, cof_r4w2_sections) + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/fund_round_loaders/load_cyp_r1.py b/fund_store/scripts/fund_round_loaders/load_cyp_r1.py new file mode 100644 index 000000000..e7bfc7e91 --- /dev/null +++ b/fund_store/scripts/fund_round_loaders/load_cyp_r1.py @@ -0,0 +1,31 @@ +# flake8: noqa +from config.fund_loader_config.cyp.cyp_r1 import APPLICATION_BASE_PATH +from config.fund_loader_config.cyp.cyp_r1 import ASSESSMENT_BASE_PATH +from config.fund_loader_config.cyp.cyp_r1 import CYP_ROUND_1_ID +from config.fund_loader_config.cyp.cyp_r1 import fund_config +from config.fund_loader_config.cyp.cyp_r1 import r1_application_sections +from config.fund_loader_config.cyp.cyp_r1 import round_config +from db.queries import insert_assessment_sections +from db.queries import insert_base_sections +from db.queries import insert_fund_data +from db.queries import insert_or_update_application_sections +from db.queries import upsert_round_data + + +def main() -> None: + print("Inserting fund data for the CYP fund.") + insert_fund_data(fund_config) + print("Inserting round data for round 1 of the CYP fund.") + upsert_round_data(round_config) + + print("Inserting base sections for CYP Round 1.") + insert_base_sections(APPLICATION_BASE_PATH, ASSESSMENT_BASE_PATH, CYP_ROUND_1_ID) + print("Inserting application sections for CYP Round 1.") + insert_or_update_application_sections(CYP_ROUND_1_ID, r1_application_sections) + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/fund_round_loaders/load_dpi_r2.py b/fund_store/scripts/fund_round_loaders/load_dpi_r2.py new file mode 100644 index 000000000..5495fed1d --- /dev/null +++ b/fund_store/scripts/fund_round_loaders/load_dpi_r2.py @@ -0,0 +1,31 @@ +# flake8: noqa +from config.fund_loader_config.digital_planning.dpi_r2 import APPLICATION_BASE_PATH +from config.fund_loader_config.digital_planning.dpi_r2 import ASSESSMENT_BASE_PATH +from config.fund_loader_config.digital_planning.dpi_r2 import DPI_ROUND_2_ID +from config.fund_loader_config.digital_planning.dpi_r2 import fund_config +from config.fund_loader_config.digital_planning.dpi_r2 import r2_application_sections +from config.fund_loader_config.digital_planning.dpi_r2 import round_config +from db.queries import insert_assessment_sections +from db.queries import insert_base_sections +from db.queries import insert_fund_data +from db.queries import insert_or_update_application_sections +from db.queries import upsert_round_data + + +def main() -> None: + print("Inserting fund data for the DPI fund.") + insert_fund_data(fund_config) + print("Inserting round data for round 2 of the DPI fund.") + upsert_round_data(round_config) + + print("Inserting base sections for DPI Round 2.") + insert_base_sections(APPLICATION_BASE_PATH, ASSESSMENT_BASE_PATH, DPI_ROUND_2_ID) + print("Inserting application sections for DPI Round 2.") + insert_or_update_application_sections(DPI_ROUND_2_ID, r2_application_sections) + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/fund_round_loaders/load_fund_round_from_fab.py b/fund_store/scripts/fund_round_loaders/load_fund_round_from_fab.py new file mode 100644 index 000000000..dae6e49c9 --- /dev/null +++ b/fund_store/scripts/fund_round_loaders/load_fund_round_from_fab.py @@ -0,0 +1,54 @@ +# flake8: noqa +import click + +from config.fund_loader_config.FAB import FAB_FUND_ROUND_CONFIGS +from db import db +from db.queries import insert_base_sections +from db.queries import insert_fund_data +from db.queries import insert_or_update_application_sections +from db.queries import upsert_round_data + + +@click.command() +@click.option("--fund_short_code", default="COF25-EOI", help="Fund short code", prompt=True) +def load_fund_from_fab(fund_short_code) -> None: + """ + Insert the FAB fund and round data into the database. + See required schema for import here: file_location + """ + + FUND_CONFIG = FAB_FUND_ROUND_CONFIGS.get(fund_short_code, None) + if not FUND_CONFIG: + raise ValueError(f"Config for fund {fund_short_code} does not exist") + + if FUND_CONFIG: + print(f"Preparing fund data for the {fund_short_code} fund.") + insert_fund_data(FUND_CONFIG, commit=False) + + for round_short_name, round in FUND_CONFIG["rounds"].items(): + + round_base_path = round["base_path"] + + APPLICATION_BASE_PATH = ".".join([str(round_base_path), str(1)]) + ASSESSMENT_BASE_PATH = ".".join([str(round_base_path), str(2)]) + + print(f"Preparing round data for the '{round_short_name}' round.") + upsert_round_data([round], commit=False) + + # Section config is per round, not per fund + print(f"Preparing base sections for {round_short_name}.") + insert_base_sections(APPLICATION_BASE_PATH, ASSESSMENT_BASE_PATH, round["id"]) + + print(f"Preparing application sections for {round_short_name}.") + insert_or_update_application_sections(round["id"], round["sections_config"]) + + print(f"All config has been successfully prepared, now committing to the database.") + db.session.commit() + print(f"Config has now been committed to the database.") + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + load_fund_from_fab() diff --git a/fund_store/scripts/fund_round_loaders/load_hsra_r1.py b/fund_store/scripts/fund_round_loaders/load_hsra_r1.py new file mode 100644 index 000000000..6a5ab843e --- /dev/null +++ b/fund_store/scripts/fund_round_loaders/load_hsra_r1.py @@ -0,0 +1,33 @@ +# flake8: noqa +from config.fund_loader_config.hsra.hsra import APPLICATION_BASE_PATH_HSRA +from config.fund_loader_config.hsra.hsra import ASSESSMENT_BASE_PATH_HSRA +from config.fund_loader_config.hsra.hsra import HSRA_ROUND_ID +from config.fund_loader_config.hsra.hsra import fund_config +from config.fund_loader_config.hsra.hsra import hsra_sections +from config.fund_loader_config.hsra.hsra import round_config +from db.queries import insert_base_sections +from db.queries import insert_fund_data +from db.queries import insert_or_update_application_sections +from db.queries import upsert_round_data + + +def main() -> None: + print("Inserting fund and round data for HSRA.") + insert_fund_data(fund_config) + upsert_round_data(round_config) + + print("Inserting base sections config.") + insert_base_sections( + APPLICATION_BASE_PATH_HSRA, + ASSESSMENT_BASE_PATH_HSRA, + HSRA_ROUND_ID, + ) + print("Inserting sections.") + insert_or_update_application_sections(HSRA_ROUND_ID, hsra_sections) + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/fund_round_loaders/load_ns_r2.py b/fund_store/scripts/fund_round_loaders/load_ns_r2.py new file mode 100644 index 000000000..111a603a7 --- /dev/null +++ b/fund_store/scripts/fund_round_loaders/load_ns_r2.py @@ -0,0 +1,31 @@ +# flake8: noqa +from config.fund_loader_config.night_shelter.ns_r2 import APPLICATION_BASE_PATH +from config.fund_loader_config.night_shelter.ns_r2 import ASSESSMENT_BASE_PATH +from config.fund_loader_config.night_shelter.ns_r2 import NIGHT_SHELTER_ROUND_2_ID +from config.fund_loader_config.night_shelter.ns_r2 import fund_config +from config.fund_loader_config.night_shelter.ns_r2 import r2_application_sections +from config.fund_loader_config.night_shelter.ns_r2 import round_config +from db.queries import insert_assessment_sections +from db.queries import insert_base_sections +from db.queries import insert_fund_data +from db.queries import insert_or_update_application_sections +from db.queries import upsert_round_data + + +def main() -> None: + print("Inserting fund data for the Night Shelter fund.") + insert_fund_data(fund_config) + print("Inserting round data for round 2 of the Night Shelter fund.") + upsert_round_data(round_config) + + print("Inserting base sections for Night Shelter Round 2.") + insert_base_sections(APPLICATION_BASE_PATH, ASSESSMENT_BASE_PATH, NIGHT_SHELTER_ROUND_2_ID) + print("Inserting application sections for Night Shelter Round 2.") + insert_or_update_application_sections(NIGHT_SHELTER_ROUND_2_ID, r2_application_sections) + + +if __name__ == "__main__": + from app import app + + with app.app.app_context(): + main() diff --git a/fund_store/scripts/generate_all_questions.py b/fund_store/scripts/generate_all_questions.py new file mode 100755 index 000000000..514c450bb --- /dev/null +++ b/fund_store/scripts/generate_all_questions.py @@ -0,0 +1,225 @@ +import json +import os +import sys + +import click + +sys.path.insert(1, ".") + +from airium import Airium # noqa: E402 + +from app import create_app # noqa: E402 +from db.models.section import Section # noqa: E402 +from db.queries import ( + get_application_sections_for_round, # noqa: E402 + get_round_by_short_name, # noqa: E402 +) +from scripts.all_questions.metadata_utils import ( # noqa: E402 + generate_print_data_for_sections, +) +from scripts.all_questions.read_forms import ( # noqa: E402; , build_form # noqa: E402 + find_forms_dir, +) + +# Initialise Airium html printer +air = Airium() + +# Define start and end html +BOILERPLATE_START = """ +{% extends "base.html" %} +{%- from 'govuk_frontend_jinja/components/inset-text/macro.html' import govukInsetText -%} +{%- from "govuk_frontend_jinja/components/button/macro.html" import govukButton -%} + +{% from "partials/file-formats.html" import file_formats %} +{% set pageHeading %}{% trans %}Full list of application questions{% endtrans %}{% endset %} +{% block content %} +
    +
    + {% trans %}{{fund_title}}{% endtrans %} {% trans %}{{round_title}}{% endtrans %} + +

    {{pageHeading}}

    +""" + +BOILERPLATE_END = """ +
    +
    +{% endblock %} +""" + + +def print_html_toc(air: Airium, sections: dict): + """Prints a table of contents for the supplied sections to the supplied `Airium` instance + + Args: + air (Airium): Instance to write html to + sections (dict): Sections for this TOC + """ + with air.h2(klass="govuk-heading-m "): + air("{% trans %}Table of contents{% endtrans %}") + with air.ol(klass="govuk-list govuk-list--number"): + for anchor, details in sections.items(): + with air.li(): + with air.a(klass="govuk-link", href=f"#{anchor}"): + air(details["title_text"]) + + +def print_components(air: Airium, components: list): + """Prints the components within a page + + Args: + air (Airium): Instance to print html + components (list): List of components to print + """ + for c in components: + # Print the title + if not c["hide_title"] and c["title"] is not None: + with air.p(klass="govuk-body"): + air(f"{c['title']}") + + for t in c["text"]: + # Print lists as
      bullet lists + if isinstance(t, list): + with air.ul(klass="govuk-list govuk-list--bullet"): + for bullet in t: + with air.li(klass=""): + air(bullet) + else: + # Just print the text + with air.p(klass="govuk-body"): + air(t) + + +def print_html(sections: dict) -> str: + """Prints the html for the supplied sections + + Args: + sections (dict): All sections to print, as generated by `metadata_utils.generate_print_data_for_sections` + + Returns: + str: HTML string + """ + with air.div(klass="govuk-!-margin-bottom-8"): + # Print Table of Contents + print_html_toc(air, sections) + idx_section = 1 + + for anchor, details in sections.items(): + if anchor == "assessment_display_info": + continue + air.hr(klass="govuk-section-break govuk-section-break--l govuk-section-break--visible") + + # Print each section header, with anchor + with air.h2(klass="govuk-heading-l", id=anchor): + air(f"{idx_section}. {details['title_text']}") + + form_print_data = details["form_print_data"] + # Sort in order of numbered headings + for heading in sorted( + form_print_data, + key=lambda item: str((form_print_data[item])["heading_number"]), + ): + header_info = form_print_data[heading] + # Print header for this form + if header_info["is_form_heading"]: + with air.h3(klass="govuk-heading-m"): + air(f"{header_info['heading_number']}. {header_info['title']}") + + else: + # Print header for this form page + with air.h4(klass="govuk-heading-s"): + air(f"{header_info['heading_number']}. {header_info['title']}") + + # Print components within this form + print_components(air, header_info["components"]) + + idx_section += 1 + + # Concatenate start html, generated html and end html to one string and return + html = f"{BOILERPLATE_START}{str(air)}{BOILERPLATE_END}" + print(html) + return html + + +@click.command() +@click.option("--fund_short_code", default="cyp", help="Fund short code", prompt=True) +@click.option("--round_short_code", default="R1", help="Round short code", prompt=True) +@click.option( + "--lang", + default="en", + help="Language - used when a round supports english and welsh", + prompt=True, + type=click.Choice(["en", "cy"]), +) +@click.option( + "--output_location", + default="../funding-service-design-frontend/app/templates/all_questions/", + help="Folder to write output html to (language code will be appended as an intermediate path)", + prompt=True, +) +@click.option( + "--forms_dir", + default="../digital-form-builder/fsd_config/form_jsons/", + help="Local, absolute path, to the form JSONs to use to generate question lists", + prompt=True, +) +@click.option( + "--include_assessment_display_info", + default=True, + help="Whether to output the assessment display field info", + prompt=True, +) +@click.option( + "--assessment_display_output_path", + default=".", + help="Where to store assessment field display info", + prompt=True, +) +def generate_all_questions( + fund_short_code: str = None, + round_short_code: str = None, + lang: str = None, + output_location: str = None, + forms_dir: str = None, + include_assessment_display_info: bool = False, + assessment_display_output_path: str = None, +): + app = create_app() + with app.app.app_context(): + round = get_round_by_short_name(fund_short_code, round_short_code) + if not round: + raise NameError(f"Round {round_short_code} does not exist in fund {fund_short_code}") + sections: list[Section] = get_application_sections_for_round(round.fund_id, round.id) + + path_to_form_jsons = find_forms_dir(forms_dir, fund_short_code, round_short_code, lang) + + section_map = generate_print_data_for_sections( + sections=sections, + path_to_form_jsons=path_to_form_jsons, + lang=lang, + include_assessment_field_details=include_assessment_display_info, + ) + if include_assessment_display_info: + to_write = {} + for _anchor, section in section_map.items(): + for form_name, fields in section["assessment_display_info"].items(): + to_write[form_name] = fields + with open( + os.path.join( + assessment_display_output_path, + f"{fund_short_code.casefold()}_{round_short_code.casefold()}_field_info_{lang}.json", + ), + "w", + ) as f: + json.dump(to_write, f) + + html_str = print_html( + sections=section_map, + ) + filename = f"{fund_short_code.casefold()}_{round_short_code.casefold()}_all_questions_{lang}.html" + out_path = os.path.join(output_location, lang, filename) + with open(out_path, "w") as f: + f.write(html_str) + + +if __name__ == "__main__": + generate_all_questions() diff --git a/fund_store/scripts/load_all_fund_rounds.py b/fund_store/scripts/load_all_fund_rounds.py new file mode 100755 index 000000000..b08da5145 --- /dev/null +++ b/fund_store/scripts/load_all_fund_rounds.py @@ -0,0 +1,25 @@ +import importlib +import os + +from app import app + + +def load_all_fund_rounds() -> None: + # Get a list of all Python files in the fund_round_loaders directory + loader_module_names = [f for f in os.listdir("scripts/fund_round_loaders") if f.endswith(".py")] + for module_name in loader_module_names: + # Remove the ".py" extension to get the module name + module_name_without_extension = module_name[:-3] + + # Dynamically import the module + loader_module = importlib.import_module(f"scripts.fund_round_loaders.{module_name_without_extension}") + + # Call the main function from the imported module + if hasattr(loader_module, "main"): + print(f"Calling main from {module_name_without_extension}") + loader_module.main() + + +if __name__ == "__main__": + with app.app.app_context(): + load_all_fund_rounds() diff --git a/fund_store/scripts/migration-task-script.py b/fund_store/scripts/migration-task-script.py new file mode 100755 index 000000000..f0b358cf1 --- /dev/null +++ b/fund_store/scripts/migration-task-script.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +import re +import subprocess +import sys + +command_to_run = "flask db upgrade" +environment = sys.argv[1] +service = sys.argv[2] + +try: + command = subprocess.run( + args=[ + "copilot", + "task", + "run", + "--generate-cmd", + f"pre-award/{environment}/{service}", + ], + capture_output=True, + check=True, + text=True, + ) + # Strip image argument as we want to build a new image to pick up new migrations + command_with_image_removed = re.sub(r"--image \S+", "", command.stderr) +except subprocess.CalledProcessError as e: + print(e.stderr) + raise e + +# Remove final line break and append arguments +try: + subprocess.run( + args=command_with_image_removed[:-1] + f" \\\n--follow \\\n--command '{command_to_run}'", + shell=True, + check=True, + ) +except subprocess.CalledProcessError as e: + # Don't want to leak the command output here so just exit with the command's return code + sys.exit(e.returncode) diff --git a/fund_store/tasks.py b/fund_store/tasks.py new file mode 100644 index 000000000..1dfab4a48 --- /dev/null +++ b/fund_store/tasks.py @@ -0,0 +1,46 @@ +import os +from urllib.parse import urlparse + +from colored import attr, fg, stylize +from invoke import task + +ECHO_STYLE = fg("light_gray") + attr("bold") + + +@task +def recreate_local_db(c): + """Create a clean database for testing""" + + # As we assume "db_name" is not yet created. First we need to connect to psql with an existing database + # Replace database in database_url with "postgres" db + database_url = os.environ.get("DATABASE_URL") + if not database_url: + raise Exception("Please set the environmental variable 'DATABASE_URL'!") + parsed_db_url = urlparse(database_url) + database_host = parsed_db_url.hostname + db_name = parsed_db_url.path.lstrip("/") + parsed_db_url = parsed_db_url._replace(path="/postgres") + database_url = parsed_db_url.geturl() + + c.run(f'psql {database_url} -c "DROP DATABASE IF EXISTS {db_name} WITH (FORCE);"') + print( + stylize( + f"{db_name} db dropped from {database_host}...", + ECHO_STYLE, + ) + ) + c.run(f'psql {database_url} -c "CREATE DATABASE {db_name};"') + c.run(f'psql {database_url} -c "create extension if not exists ltree;"') + print(stylize(f"{db_name} db created on {database_host}...", ECHO_STYLE)) + + +@task +def truncate_data(c): + database_url = os.environ.get("DATABASE_URL") + if not database_url: + raise Exception("Please set the environmental variable 'DATABASE_URL'!") + c.run( + f'psql {database_url} -c "TRUNCATE TABLE section, round, fund,' + " assessment_field, form_name, section_field CASCADE;ALTER SEQUENCE" + ' section_id_seq RESTART WITH 1;";' + ) diff --git a/fund_store/tests/__init__.py b/fund_store/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/fund_store/tests/conftest.py b/fund_store/tests/conftest.py new file mode 100644 index 000000000..ca1df0489 --- /dev/null +++ b/fund_store/tests/conftest.py @@ -0,0 +1,254 @@ +""" +Contains test configuration. +""" + +from datetime import datetime +from uuid import uuid4 + +import pytest +from flask import Flask +from sqlalchemy_utils import Ltree + +from app import create_app +from db.models.fund import Fund, FundingType +from db.models.round import Round +from db.models.section import Section +from db.queries import insert_fund_data, insert_sections, upsert_round_data + +pytest_plugins = ["fsd_test_utils.fixtures.db_fixtures"] + + +@pytest.fixture(scope="session") +def seed_dynamic_data(request, app, clear_test_data, _db): + marker = request.node.get_closest_marker("seed_config") + fund_count = 0 + round_count = 0 + if marker is None: + fund_id = str(uuid4()) + round_id_1 = str(uuid4()) + seed_config = { + "funds": [ + { + "id": fund_id, + "short_name": "FUND", + "funding_type": "COMPETITIVE", + "ggis_scheme_reference_number": "", + "rounds": [ + { + "id": round_id_1, + "sections": [ + Section( + id=1000, + round_id=round_id_1, + title_json={"en": "Application"}, + path=Ltree("3.1"), + ), + Section( + id=1008, + round_id=round_id_1, + title_json={"en": "skills"}, + path=Ltree("3.1.3"), + ), + Section( + id=1001, + round_id=round_id_1, + title_json={"en": "Middle1"}, + path=Ltree("3.1.1"), + ), + Section( + id=1002, + round_id=round_id_1, + title_json={"en": "Bottom1"}, + path=Ltree("3.1.1.1"), + ), + Section( + id=1003, + round_id=round_id_1, + title_json={"en": "Middle2"}, + path=Ltree("3.1.2"), + ), + Section( + id=1004, + round_id=round_id_1, + title_json={"en": "Bottom2"}, + path=Ltree("3.1.2.1"), + ), + Section( + id=1005, + round_id=round_id_1, + title_json={"en": "Assessment"}, + path=Ltree("0.2"), + ), + Section( + id=1006, + round_id=round_id_1, + title_json={"en": "assess section 1"}, + path=Ltree("0.2.1"), + ), + Section( + id=1007, + round_id=round_id_1, + title_json={"en": "assess section 1 a"}, + path=Ltree("0.2.1.1"), + ), + ], + }, + { + "id": str(uuid4()), + "sections": [], + # "fund_id": fund_id, + # "short_name": "RND2" + }, + ], + } + ] + } + else: + seed_config = marker.args[0] + inserted_data = {"funds": []} + for fund in seed_config["funds"]: + fund_count += 1 + # fund_id = str(uuid4()) + # short_suffix = fund_id[0:4] + fund_config = { + "id": fund["id"], + "name_json": {"en": f"Unit Test Fund {fund_count}"}, # fund['short_name']}", + "title_json": {"en": f"Unit test fund title {fund_count}"}, # {fund['short_name']}", + "short_name": f"FND{fund_count}", # fund["short_name"], + "description_json": {"en": "testing description"}, + "welsh_available": True, + "owner_organisation_name": "testing org name", + "owner_organisation_shortname": "TON", + "owner_organisation_logo_uri": "...", + "funding_type": fund["funding_type"] or FundingType.COMPETITIVE, + "ggis_scheme_reference_number": fund["ggis_scheme_reference_number"] or "", + } + insert_fund_data(fund_config) + rounds = [] + for round in fund["rounds"]: + round_count += 1 + # round_id = str(uuid4()) + # round_short_suffix = round_id[0:4] + round_config = { + "id": round["id"], + "title_json": {"en": f"Unit Test Round {round_count}"}, # {round['short_name']}", + "short_name": f"RND{round_count}", # round["short_name"], + "opens": "2023-01-01 12:00:00", + "assessment_start": None, + "deadline": "2023-12-31 12:00:00", + "application_reminder_sent": True, + "reminder_date": None, + "fund_id": fund["id"], + "assessment_deadline": "2024-02-28 12:00:00", + "prospectus": "http://google.com", + "privacy_notice": "http://google.com", + "reference_contact_page_over_email": False, + "contact_us_banner_json": {"en": "", "cy": ""}, + "contact_email": "contact@example.com", + "contact_phone": "01234567890", + "contact_textphone": "1234", + "support_times": "8am - 12:30pm", + "support_days": "Monday and Tuesday", + "instructions_json": {"en": "Instructions to fill out the form"}, + "project_name_field_id": "abc123", + "feedback_link": "www.feedback.link", + "application_guidance_json": {"en": "help text"}, + "guidance_url": "guidance link", + "all_uploaded_documents_section_available": False, + "application_fields_download_available": False, + "display_logo_on_pdf_exports": False, + "mark_as_complete_enabled": False, + "is_expression_of_interest": False, + "feedback_survey_config": { + "has_feedback_survey": False, + "has_section_feedback": False, + "is_feedback_survey_optional": True, + "is_section_feedback_optional": True, + }, + "eligibility_config": {"has_eligibility": False}, + "eoi_decision_schema": None, + } + rounds.append(round_config) + + upsert_round_data(rounds) + inserted_data["funds"].append({"rounds": rounds, "id": fund_id, "short_name": fund["short_name"]}) + + for fund in seed_config["funds"]: + for round in fund["rounds"]: + insert_sections(round["sections"]) + + yield inserted_data + + +@pytest.fixture(scope="session") +def app() -> Flask: + 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 mock_get_fund_round(mocker): + mock_fund: Fund = Fund( + id=uuid4(), + short_name="FND1", + name_json={"en": "Fund Name 1"}, + title_json={"en": "Fund 1"}, + description_json={"en": "description text"}, + funding_type=FundingType.COMPETITIVE, + ggis_scheme_reference_number="G2-SCH-0000092414", + ) + round_config = { + "id": uuid4(), + "assessment_deadline": datetime.now(), + "deadline": datetime.now(), + "reminder_date": None, + "fund_id": "", + "opens": datetime.now(), + "assessment_start": None, + "prospectus": "", + "privacy_notice": "", + "contact_email": "", + "contact_phone": "", + "contact_textphone": "", + "support_days": "", + "support_times": "", + } + mock_round: Round = Round(title_json={"en": "Round 1"}, short_name="RND1", **round_config) + mocker.patch("api.routes.get_all_funds", return_value=[mock_fund]) + mocker.patch("api.routes.get_fund_by_id", return_value=mock_fund) + mocker.patch("api.routes.get_fund_by_short_name", return_value=mock_fund) + mocker.patch("api.routes.get_round_by_id", return_value=mock_round) + mocker.patch("api.routes.get_round_by_short_name", return_value=mock_round) + mocker.patch("api.routes.get_rounds_for_fund_by_id", return_value=[mock_round]) + mocker.patch("api.routes.get_rounds_for_fund_by_short_name", return_value=[mock_round]) + + +@pytest.fixture(scope="function") +def mock_get_sections(mocker): + mock_sections = Section( + id=0, + title_json={"en": "Top"}, + path="0", + children=[ + Section( + id=1, + title_json={"en": "Middle"}, + path="0.1", + children=[Section(id=2, title_json={"en": "Bottom"}, path="0.1.1", children=[])], + ), + Section( + id=3, + title_json={"en": "Middle2"}, + path="0.2", + children=[Section(id=4, title_json={"en": "Bottom2"}, path="0.2.1", children=[])], + ), + ], + ) + mocker.patch("api.routes.get_application_sections_for_round", return_value=[mock_sections]) + mocker.patch("api.routes.get_assessment_sections_for_round", return_value=[mock_sections]) diff --git a/fund_store/tests/test_data/all_questions/forms/about-your-organisation-cyp.json b/fund_store/tests/test_data/all_questions/forms/about-your-organisation-cyp.json new file mode 100644 index 000000000..e8020cd29 --- /dev/null +++ b/fund_store/tests/test_data/all_questions/forms/about-your-organisation-cyp.json @@ -0,0 +1,952 @@ +{ + "metadata": {}, + "startPage": "/intro-about-your-organisation", + "backLinkText": "Go back to application overview", + "pages": [ + { + "title": "Organisation details", + "path": "/organisation-details", + "components": [ + { + "name": "JbmcJE", + "options": { + "classes": "govuk-input" + }, + "type": "TextField", + "title": "Organisation name", + "hint": "This must match your registered legal organisation name", + "schema": {} + }, + { + "name": "KUdOhN", + "options": {}, + "type": "YesNoField", + "title": "Does your organisation use any other names?", + "schema": {} + } + ], + "next": [ + { + "path": "/alternative-organisation-name", + "condition": "GZIoHg" + }, + { + "path": "/tell-us-about-your-organisations-main-activities", + "condition": "dfsBce" + } + ], + "section": "uLwBuz" + }, + { + "title": "Tell us about your organisation's main activities", + "path": "/tell-us-about-your-organisations-main-activities", + "components": [ + { + "name": "jcjKhe", + "options": {}, + "type": "Para", + "content": "
      \n

      Include any activities you undertake in order to achieve the organisation's purpose.

      \n\n

      You must list at least one, and can include up to three

      " + }, + { + "name": "kxgOne", + "options": { + "maxWords": "500", + "hideTitle": true + }, + "type": "FreeTextField", + "title": "Activity 1", + "hint": "" + }, + { + "name": "kxgTwo", + "options": { + "maxWords": "500", + "required": false, + "hideTitle": true + }, + "type": "FreeTextField", + "title": "Activity 2 (optional)", + "hint": "" + }, + { + "name": "kxgThr", + "options": { + "maxWords": "500", + "required": false, + "hideTitle": true + }, + "type": "FreeTextField", + "title": "Activity 3 (optional)", + "hint": "" + } + ], + "next": [ + { + "path": "/how-is-your-organisation-classified" + } + ], + "section": "uLwBuz" + }, + { + "title": "How is your organisation classified?", + "path": "/how-is-your-organisation-classified", + "components": [ + { + "name": "GNpQfE", + "options": {}, + "type": "Details", + "title": "Help with organisation types", + "content": "

      A registered community interest company is a special type of limited company. It exists to benefit the community rather than private shareholders.

      \n

      A social enterprise is a business with social objectives. It reinvests its surpluses in the business or community, rather than driving the need to maximise profit for shareholders and owners.

      \n
      \nCommunity groups must:\n
        \n
      • be established for charitable, benevolent, or philanthropic purposes
      • \n
      • have a governing body with at least 3 members
      • \n
      • have a governing document they can produce
      • \n
      • provide accounts for the last 2 financial years
      • \n
      " + }, + { + "name": "jcmcJE", + "options": { + "hideTitle": true + }, + "type": "RadiosField", + "title": "Organisation classification", + "hint": "

      Select one option

      ", + "list": "jcXaBR" + } + ], + "next": [ + { + "path": "/how-is-your-organisation-classified-other", + "condition": "aJJVVz" + }, + { + "path": "/charity-number", + "condition": "aVVJJz" + }, + { + "path": "/company-registration-number" + } + ], + "section": "uLwBuz" + }, + { + "title": "How is your organisation classified? (other)", + "path": "/how-is-your-organisation-classified-other", + "components": [ + { + "name": "jemcJE", + "options": { + "classes": "govuk-input", + "hideTitle": true + }, + "type": "TextField", + "title": "How is your organisation classified?", + "schema": {} + } + ], + "next": [ + { + "path": "/organisation-address" + } + ], + "section": "uLwBuz" + }, + { + "title": "Company registration number (optional)", + "path": "/company-registration-number", + "components": [ + { + "name": "jcjKhd", + "options": {}, + "type": "Para", + "content": "

      If you have a registration number, you can find this by searching Companies House.

      It is usually 8 numbers, or 2 letters followed by 6 numbers

      " + }, + { + "name": "jencJE", + "options": { + "hideTitle": true, + "required":false + }, + "type": "TextField", + "title": "Company registration number", + "schema": {} + } + ], + "next": [ + { + "path": "/organisation-address" + } + ], + "section": "uLwBuz" + }, + { + "title": "Charity number", + "path": "/charity-number", + "components": [ + { + "name": "jcjKhc", + "options": {}, + "type": "Para", + "content": "
      \n

      You can find this by searching the register of charities on the Charity Commission website.

      It is usually 7 numbers, or 8 numbers if your charity is recently registered

      " + }, + { + "name": "jeocJE", + "options": { + "hideTitle": true + }, + "type": "TextField", + "title": "Charity number", + "schema": {} + } + ], + "next": [ + { + "path": "/organisation-address" + } + ], + "section": "uLwBuz" + }, + { + "path": "/alternative-organisation-name", + "title": "Alternative names of your organisation", + "components": [ + { + "name": "NcnYCn", + "options": { + "classes": "govuk-input" + }, + "type": "TextField", + "title": "Alternative name 1", + "schema": {} + }, + { + "name": "rtFyqT", + "options": { + "required": false, + "classes": "govuk-input" + }, + "type": "TextField", + "title": "Alternative name 2", + "schema": {} + }, + { + "name": "DYUbGM", + "options": { + "required": false, + "classes": "govuk-input" + }, + "type": "TextField", + "title": "Alternative name 3", + "schema": {} + } + ], + "next": [ + { + "path": "/tell-us-about-your-organisations-main-activities" + } + ], + "section": "uLwBuz" + }, + { + "path": "/organisation-address", + "title": "Registered organisation address", + "components": [ + { + "name": "rmBPvK", + "options": { + "hideTitle": true + }, + "type": "UkAddressField", + "title": "Registered organisation address", + "schema": {} + } + ], + "next": [ + { + "path": "/alternative-organisation-address" + } + ], + "section": "uLwBuz" + }, + { + "path": "/alternative-organisation-address", + "title": "Alternative organisation address (optional)", + "components": [ + { + "name": "smBPvK", + "options": { + "hideTitle": true, + "required": false + }, + "type": "UkAddressField", + "title": "Alternative organisation address", + "schema": {} + } + ], + "next": [ + { + "path": "/joint-bid" + } + ], + "section": "uLwBuz" + }, + { + "path": "/joint-bid", + "title": "Is your application a joint bid in partnership with other organisations?", + "components": [ + { + "name": "MRdGKt", + "options": { + "hideTitle": true + }, + "type": "YesNoField", + "title": "Is your application a joint bid in partnership with other organisations?", + "schema": {} + } + ], + "next": [ + { + "path": "/partner-organisation-details", + "condition": "nMJAdu" + }, + { + "path": "/website-and-social-media", + "condition": "MbgmFQ" + } + ], + "section": "uLwBuz" + }, + { + "path": "/partner-organisation-details", + "title": "Partner organisation details", + "options": { + "summaryDisplayMode": { + "samePage": false, + "separatePage": true, + "hideRowTitles": false + }, + "customText": { + "separatePageTitle": "Your partner organisation details", + "samePageTitle": "", + "removeText": "Delete" + } + }, + "components": [ + { + "name": "jcjKhd", + "options": {}, + "type": "Para", + "content": "
      \n

      You can add more partner organisations on the next step

      " + }, + { + "name": "tZoOKx", + "options": { + "columnTitles": [ + "Organisation name", + "Address", + "Website and social media link 1", + "Website and social media link 2", + "Website and social media link 3", + "Action" + ] + }, + "type": "MultiInputField", + "title": "Partner organisation details", + "hint": "The MultiInputField needed", + "schema": {}, + "children": [ + { + "name": "GpLJDu", + "options": { + "classes": "govuk-!-width-full" + }, + "type": "TextField", + "title": "Partner organisation name" + }, + { + "name": "IXjMWp", + "options": {}, + "type": "UkAddressField", + "title": "Partner organisation address", + "hint": "Partner organisation address" + }, + { + "name": "MKbOlA", + "options": { + "classes": "govuk-!-width-full" + }, + "type": "WebsiteField", + "title": "Website and social media links", + "hint": "Link 1", + "schema": {} + }, + { + "name": "OghGGr", + "options": { + "classes": "govuk-!-width-full", + "hideTitle": true, + "required": false + }, + "type": "WebsiteField", + "title": "Website and social media", + "hint": "Link 2 (optional)", + "schema": {} + }, + { + "name": "RphKTp", + "options": { + "classes": "govuk-!-width-full", + "hideTitle": true, + "required": false + }, + "type": "WebsiteField", + "title": "Website and social media", + "hint": "Link 3 (optional)", + "schema": {} + } + ] + } + ], + "next": [ + { + "path": "/work-with-partner-organisations" + } + ], + "controller": "RepeatingFieldPageController", + "section": "uLwBuz" + }, + { + "path": "/website-and-social-media", + "title": "Website and social media", + "options": { + "summaryDisplayMode": { + "samePage": false, + "separatePage": true, + "hideRowTitles": false + }, + "customText": { + "separatePageTitle": "Your links", + "samePageTitle": "" + } + }, + "components": [ + { + "name": "tYoOqx", + "options": { "columnTitles": ["Link", "Action"] }, + "type": "MultiInputField", + "title": "Website and social media", + "schema": {}, + "children": [ + { + "name": "EShKlA", + "options": { + "classes": "govuk-!-width-full" + }, + "type": "WebsiteField", + "title": "Link", + "hint": "

      For example, your company's Facebook, Instagram or Twitter accounts (if applicable)

      You can add more links on the next step

      ", + "schema": {} + } + ] + } + ], + "next": [ + { + "path": "/summary" + } + ], + "controller": "RepeatingFieldPageController", + "section": "uLwBuz" + }, + { + "path": "/summary", + "title": "Check your answers", + "components": [], + "next": [], + "section": "uLwBuz", + "controller": "./pages/summary.js" + }, + { + "path": "/intro-about-your-organisation", + "title": "2.1 About your organisation", + "section": "uLwBuz", + "components": [ + { + "name": "sPbqXm", + "options": {}, + "type": "Html", + "content": "

      In this section, we'll ask about:

        \n
      • organisation name
      • \n
      • alternative organisation names (if applicable)
      • \n
      • organisation address
      • \n
      • alternative organisation address (if applicable)
      • \n
      • the organisation's main activities
      • \n
      • organisation type
      • \n
      • charity registration number (if applicable)
      • \n
      • company registration number (if applicable)
      • \n
      • joint bids with partner organisations (if applicable)
      • \n
      • partner organisation addresses (if applicable)
      • \n
      • partner organisations website and social media links (if applicable)
      • \n
      • how you plan to work with partner organisations (if applicable)
      • \n
      • if an agreement currently exist between your organisations and the partnership organisations
      • \n
      • organisation website and social media details
      • \n
      ", + "schema": {} + } + ], + "next": [ + { + "path": "/organisation-details" + } + ], + "controller": "./pages/start.js" + }, + { + "path": "/work-with-partner-organisations", + "title": "Tell us about how you plan to work with the partner organisations", + "components": [ + { + "name": "xPcbJX", + "options": { + "maxWords": "500", + "hideTitle": true + }, + "type": "FreeTextField", + "title": "Tell us about how you plan to work with the partner organisations", + "hint": "

      Explain what their role is in the delivery of the project and how they'll assist with delivery

      " + } + ], + "next": [ + { + "path": "/agreement-exists" + } + ], + "section": "uLwBuz" + }, + { + "path": "/agreement-exists", + "title": "Does an agreement currently exist between your organisation and the partnership organisations?", + "components": [ + { + "name": "RUdOhN", + "options": { + "hideTitle": true + }, + "type": "YesNoField", + "title": "Does an agreement currently exist between your organisation and the partnership organisations?", + "hint": "

      We may ask for a copy of the agreement if your application is successful

      ", + "schema": {} + } + ], + "next": [ + { + "path": "/website-and-social-media" + } + ], + "section": "uLwBuz" + } + ], + "lists": [ + { + "title": "Organisation classification", + "name": "jcXaBR", + "type": "string", + "items": [ + { + "text": "Upper or lower tier local authority", + "value": "upper-or-lower-tier-local-authority" + }, + { + "text": "Charity with a registered charity number", + "value": "charity-with-a-registered-charity-number" + }, + { + "text": "Registered community interest company", + "value": "registered-community-interest-company" + }, + { + "text": "Social enterprise", + "value": "social-enterprise" + }, + { + "text": "Community group", + "value": "community-group" + }, + { + "text": "Other", + "value": "other" + } + ] + }, + { + "title": "Focus of the project", + "name": "hfQISg", + "type": "string", + "items": [ + { + "text": "Trauma support", + "value": "trauma-support" + }, + { + "text": "Mental health and wellbeing", + "value": "mental-health-and-wellbeing" + }, + { + "text": "Teaching English as a language", + "value": "teaching-english-as-a-language" + }, + { + "text": "Peer groups and befriending", + "value": "peer-groups-and-befriending" + }, + { + "text": "Supporting hosts and sponsors", + "value": "supporting-hosts-and-sponsors" + }, + { + "text": "Maintaining links to culture", + "value": "maintaining-links-to-culture" + } + ] + }, + { + "title": "Cohort focus", + "name": "YSgfSN", + "type": "string", + "items": [ + { + "text": "Ukrainian schemes", + "value": "ukrainian-schemes" + }, + { + "text": "Hong Kong British Nationals (Overseas)", + "value": "hong-kong-british-nationals" + }, + { + "text": "Afghan citizens resettlement scheme", + "value": "afghan-citizens-resettlement-scheme" + } + ] + }, + { + "title": "Eligibility", + "name": "qwnTyS", + "type": "string", + "items": [ + { + "text": "Upper or lower tier local authority", + "value": "1" + }, + { + "text": "Charity with a registered charity number", + "value": "2" + }, + { + "text": "Registered community interest company", + "value": "3" + }, + { + "text": "Social enterprise", + "value": "4" + }, + { + "text": "Community group", + "value": "5" + }, + { + "text": "None of these", + "value": "6", + "description": "" + } + ] + } + ], + "sections": [ + { + "name": "uLwBuz", + "title": "2.1 About your organisation" + } + ], + "conditions": [ + { + "displayName": "org other name yes", + "name": "aHJVVz", + "value": { + "name": "org other name yes", + "conditions": [ + { + "field": { + "name": "KUdOhN", + "type": "YesNoField", + "display": "Does your organisation use any other names?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "true", + "display": "true" + } + } + ] + } + }, + { + "displayName": "org other yes", + "name": "aJJVVz", + "value": { + "name": "org other yes", + "conditions": [ + { + "field": { + "name": "jcmcJE", + "type": "RadiosField", + "display": "How is your organisation classified?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "other", + "display": "other" + } + } + ] + } + }, + { + "displayName": "org charity yes", + "name": "aVVJJz", + "value": { + "name": "org charity yes", + "conditions": [ + { + "field": { + "name": "jcmcJE", + "type": "RadiosField", + "display": "How is your organisation classified?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "charity-with-a-registered-charity-number", + "display": "charity" + } + } + ] + } + }, + { + "displayName": "org company yes", + "name": "aVJVJz", + "value": { + "name": "org company yes", + "conditions": [ + { + "field": { + "name": "jcmcJE", + "type": "RadiosField", + "display": "How is your organisation classified?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "registered-community-interest-company", + "display": "company" + } + } + ] + } + }, + { + "displayName": "Org other name no", + "name": "tOevBV", + "value": { + "name": "Org other name no", + "conditions": [ + { + "field": { + "name": "KUdOhN", + "type": "YesNoField", + "display": "Does your organisation use any other names?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "false", + "display": "false" + } + } + ] + } + }, + { + "displayName": "joint bid yes", + "name": "nMJAdu", + "value": { + "name": "joint bid yes", + "conditions": [ + { + "field": { + "name": "MRdGKt", + "type": "YesNoField", + "display": "Is your application a joint bid in partnership with other organisations?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "true", + "display": "true" + } + } + ] + } + }, + { + "displayName": "joint bid no", + "name": "ZhSWyC", + "value": { + "name": "joint bid no", + "conditions": [ + { + "field": { + "name": "MRdGKt", + "type": "YesNoField", + "display": "Is your application a joint bid in partnership with other organisations?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "false", + "display": "false" + } + } + ] + } + }, + { + "displayName": "Ukraine support", + "name": "btRdYE", + "value": { + "name": "Ukraine support", + "conditions": [ + { + "field": { + "name": "vYYoAC", + "type": "CheckboxesField", + "display": "Which cohort will your project focus on?" + }, + "operator": "contains", + "value": { + "type": "Value", + "value": "ukrainian-schemes", + "display": "ukrainian-schemes" + } + } + ] + } + }, + { + "displayName": "Organisation no", + "name": "dfsBce", + "value": { + "name": "Organisation no", + "conditions": [ + { + "field": { + "name": "KUdOhN", + "type": "YesNoField", + "display": "Does your organisation use any other names?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "false", + "display": "false" + } + } + ] + } + }, + { + "displayName": "Organisation yes", + "name": "GZIoHg", + "value": { + "name": "Organisation yes", + "conditions": [ + { + "field": { + "name": "KUdOhN", + "type": "YesNoField", + "display": "Does your organisation use any other names?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "true", + "display": "true" + } + } + ] + } + }, + { + "displayName": "Not a joint bid", + "name": "MbgmFQ", + "value": { + "name": "Not a joint bid", + "conditions": [ + { + "field": { + "name": "MRdGKt", + "type": "YesNoField", + "display": "Is your application a joint bid in partnership with other organisations?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "false", + "display": "false" + } + } + ] + } + }, + { + "displayName": "Based in england no", + "name": "JZGvHK", + "value": { + "name": "Based in england no", + "conditions": [ + { + "field": { + "name": "qGbSWS", + "type": "YesNoField", + "display": "Is your organisation based in England?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "false", + "display": "false" + } + } + ] + } + }, + { + "displayName": "Take place in England no", + "name": "LOPfHT", + "value": { + "name": "Take place in England no", + "conditions": [ + { + "field": { + "name": "mknqUV", + "type": "YesNoField", + "display": "Will the project take place in England?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "false", + "display": "false" + } + } + ] + } + } + ], + "fees": [], + "outputs": [ + { + "name": "update-form", + "title": "Update form in application store", + "type": "savePerPage", + "outputConfiguration": { + "savePerPageUrl": "True" + } + } + ], + "version": 2, + "skipSummary": false, + "name": "Apply for funding to support children and young people on pathways to the UK from Ukraine, Hong Kong and Afghanistan" +} diff --git a/fund_store/tests/test_data/all_questions/forms/name-your-application.json b/fund_store/tests/test_data/all_questions/forms/name-your-application.json new file mode 100644 index 000000000..fc203e4bd --- /dev/null +++ b/fund_store/tests/test_data/all_questions/forms/name-your-application.json @@ -0,0 +1,59 @@ +{ + "metadata": {}, + "startPage": "/11-name-your-application", + "pages": [ + { + "title": "1.1 Name your application", + "path": "/11-name-your-application", + "components": [ + { + "name": "JAAhRP", + "options": { + "hideTitle": true, + "classes": "govuk-!-width-full" + }, + "type": "TextField", + "title": "Name your application", + "hint": "
      This is what your application will be called and will help you find it when you save and return.
      \n
      Choose a memorable name that describes your application.
      " + } + ], + "next": [ + { + "path": "/summary" + } + ], + "section": "ScxYon" + }, + { + "title": "Check your answers", + "path": "/summary", + "controller": "./pages/summary.js", + "components": [], + "next": [], + "section": "ScxYon" + } + ], + "lists": [], + "sections": [ + { + "name": "ScxYon", + "title": "Name your application" + } + ], + "conditions": [], + "fees": [], + "outputs": [ + { + "name": "update-form", + "title": "Update form in application store", + "type": "savePerPage", + "outputConfiguration": { + "savePerPageUrl": "True" + } + } + ], + "version": 2, + "skipSummary": false, + "markAsComplete": false, + "name":"Apply for funding to begin your digital planning improvement journey" +} diff --git a/fund_store/tests/test_data/all_questions/forms/risk-and-deliverability-cyp.json b/fund_store/tests/test_data/all_questions/forms/risk-and-deliverability-cyp.json new file mode 100644 index 000000000..f2dbce5a9 --- /dev/null +++ b/fund_store/tests/test_data/all_questions/forms/risk-and-deliverability-cyp.json @@ -0,0 +1,221 @@ +{ + "metadata": {}, + "startPage": "/intro-risk-and-deliverability", + "backLinkText": "Go back to application overview", + "pages": [ + { + "path": "/summary", + "title": "Check your answers", + "components": [], + "next": [], + "section": "hhbMar", + "controller": "./pages/summary.js" + }, + { + "path": "/intro-risk-and-deliverability", + "title": "5.1 Risk and deliverability", + "components": [ + { + "name": "gMoQAm", + "options": {}, + "type": "Html", + "content": "

      In this section, we'll ask about:

        \n
      • risks to the project
      • \n
      • the likelihood of the risks
      • \n
      • the impact of the risks
      • \n
      • how you'll mitigate the risks
      • \n
      • a person accountable for the risk register
      • \n
      • your organisation's governance structure
      • \n
      ", + "schema": {} + } + ], + "next": [ + { + "path": "/risks-to-the-project" + } + ], + "controller": "./pages/start.js" + }, + { + "path": "/risks-to-the-project", + "title": "Risks to the project", + "options": { + "summaryDisplayMode": { + "samePage": false, + "separatePage": true, + "hideRowTitles": false + }, + "customText": { + "separatePageTitle": "Your project risks", + "removeText": "Delete" + } + }, + "components": [ + { + "name": "EaxCuu", + "options": {}, + "type": "Para", + "content": "

      Risks to the project

      \n\n

      Tell us about:

      \n\n
        \n
      • any risks to the project
      • \n
      • the liklihood of them happening
      • \n
      • the impact they'll have on the project
      • \n
      • how you plan to mitigate them
      • \n
      \n

      \n

      You can add more risks on the next step.

      \n\n\n\n" + }, + { + "name": "qQLYzL", + "options": { + "columnTitles": [ + "Risk", + "Likelihood", + "Impact", + "Proposed mitigation", + "Action" + ], + "required": true + }, + "type": "MultiInputField", + "title": "Risks to the project", + "hint": "The MultiInputField needed", + "schema": {}, + "children": [ + { + "name": "CzoasH", + "options": { + "maxWords": "500" + }, + "type": "MultilineTextField", + "title": "Risk", + "schema": {} + }, + { + "name": "eADHGN", + "options": {}, + "type": "RadiosField", + "title": "Likelihood", + "list": "zOwfxe" + }, + { + "name": "MPHvIr", + "options": {}, + "type": "RadiosField", + "title": "Impact", + "list": "yOwfxe" + }, + { + "name": "SKQluJ", + "options": { + "maxWords": "500" + }, + "type": "MultilineTextField", + "title": "Proposed mitigation", + "schema": {} + } + ] + } + ], + "next": [ + { + "path": "/who-is-responsible-risk-register" + } + ], + "section": "hhbMar", + "controller": "RepeatingFieldPageController" + }, + { + "path": "/who-is-responsible-risk-register", + "title": "Who owns the overall risk register?", + "components": [ + { + "name": "KHESdE", + "options": { + "hideTitle": true + }, + "type": "TextField", + "title": "Who owns the overall risk register?", + "hint": "
      Enter a full name
      ", + "schema": {} + } + ], + "next": [ + { + "path": "/organisation-governance-structure" + } + ], + "section": "hhbMar" + }, + { + "path": "/organisation-governance-structure", + "title": "Tell us about your organisation's governance structure", + "components": [ + { + "name": "KHESFr", + "options": { + "hideTitle": true, + "maxWords": 500 + }, + "type": "FreeTextField", + "title": "Tell us about your organisation's governance structure", + "hint": "

      Include members of your board, their roles and main responsibilities

      ", + "schema": {} + } + ], + "next": [ + { + "path": "/summary" + } + ], + "section": "hhbMar" + } + ], + "lists": [ + { + "title": "Likelihood", + "name": "zOwfxe", + "type": "string", + "items": [ + { + "text": "High", + "value": "High" + }, + { + "text": "Medium", + "value": "Medium" + }, + { + "text": "Low", + "value": "Low" + } + ] + }, + { + "title": "Impact", + "name": "yOwfxe", + "type": "string", + "items": [ + { + "text": "High", + "value": "High" + }, + { + "text": "Medium", + "value": "Medium" + }, + { + "text": "Low", + "value": "Low" + } + ] + } + ], + "sections": [ + { + "name": "hhbMar", + "title": "5.1 Risk and deliverability" + } + ], + "conditions": [], + "fees": [], + "outputs": [ + { + "name": "update-form", + "title": "Update form in application store", + "type": "savePerPage", + "outputConfiguration": { + "savePerPageUrl": "True" + } + } + ], + "version": 2, + "skipSummary": false, + "name": "Apply for funding to support children and young people on pathways to the UK from Ukraine, Hong Kong and Afghanistan" +} diff --git a/fund_store/tests/test_data/all_questions/forms/your-skills-and-experience-dpi.json b/fund_store/tests/test_data/all_questions/forms/your-skills-and-experience-dpi.json new file mode 100644 index 000000000..1e4abd593 --- /dev/null +++ b/fund_store/tests/test_data/all_questions/forms/your-skills-and-experience-dpi.json @@ -0,0 +1,147 @@ +{ + "metadata": {}, + "startPage": "/31-your-skills-and-experience", + "pages": [ + { + "title": "3.1 Your skills and experience", + "path": "/31-your-skills-and-experience", + "components": [ + { + "name": "MbtXtU", + "options": {}, + "type": "Html", + "content": "

      In this section, we'll ask about:

      \n
        \n
      • whether your organisation has delivered projects like this before 
      • \n
      • information about the projects you've delivered before (if applicable) 
      • \n
      " + } + ], + "next": [ + { + "path": "/has-your-organisation-delivered-projects-like-this-before" + } + ], + "controller": "./pages/start.js" + }, + { + "path": "/has-your-organisation-delivered-projects-like-this-before", + "title": "Has your organisation delivered projects like this before?", + "components": [ + { + "name": "rGADMs", + "options": { + "hideTitle": true + }, + "type": "YesNoField", + "title": "Has your organisation delivered projects like this before?" + } + ], + "next": [ + { + "path": "/summary", + "condition": "QOCrNa" + }, + { + "path": "/similar-previous-projects", + "condition": "MAfLtk" + } + ], + "section": "akREtH" + }, + { + "title": "Check your answers", + "path": "/summary", + "controller": "./pages/summary.js", + "components": [], + "next": [], + "section": "akREtH" + }, + { + "path": "/similar-previous-projects", + "title": "Tell us about how your organisation has worked on similar previous projects", + "components": [ + { + "name": "zHgZBx", + "options": { + "hideTitle": true, + "maxWords": "250" + }, + "type": "FreeTextField", + "title": "Tell us about how your organisation has worked on similar previous projects", + "hint": "
      Tell us about:\n
        \n
      • the scale of the project
      • \n
      • the value of the project
      • \n
      • who was involved
      • \n
      \n
      " + } + ], + "next": [ + { + "path": "/summary" + } + ], + "section": "akREtH" + } + ], + "lists": [], + "sections": [ + { + "name": "akREtH", + "title": "Your skills and experience" + } + ], + "conditions": [ + { + "displayName": "previous projects", + "name": "MAfLtk", + "value": { + "name": "previous projects", + "conditions": [ + { + "field": { + "name": "rGADMs", + "type": "YesNoField", + "display": "Has your organisation delivered projects like this before?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "true", + "display": "true" + } + } + ] + } + }, + { + "displayName": "Previous project no", + "name": "QOCrNa", + "value": { + "name": "Previous project no", + "conditions": [ + { + "field": { + "name": "rGADMs", + "type": "YesNoField", + "display": "Has your organisation delivered projects like this before?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "false", + "display": "false" + } + } + ] + } + } + ], + "fees": [], + "outputs": [ + { + "name": "update-form", + "title": "Update form in application store", + "type": "savePerPage", + "outputConfiguration": { + "savePerPageUrl": "True" + } + } + ], + "version": 2, + "skipSummary": false, + "markAsComplete": false, + "name": "Apply for funding to begin your digital planning improvement journey" +} diff --git a/fund_store/tests/test_data/all_questions/metadata/how_is_org_classified.json b/fund_store/tests/test_data/all_questions/metadata/how_is_org_classified.json new file mode 100644 index 000000000..bcfc693e7 --- /dev/null +++ b/fund_store/tests/test_data/all_questions/metadata/how_is_org_classified.json @@ -0,0 +1 @@ +{"start_page": "/how-is-your-organisation-classified", "all_pages": [{"path": "/how-is-your-organisation-classified", "next_paths": ["/how-is-your-organisation-classified-other", "/charity-number", "/company-registration-number"], "all_direct_previous": ["/tell-us-about-your-organisations-main-activities"], "direct_next_of_direct_previous": ["/how-is-your-organisation-classified"], "all_possible_next_of_siblings": ["/how-is-your-organisation-classified-other", "/summary", "/joint-bid", "/partner-organisation-details", "/organisation-address", "/charity-number", "/agreement-exists", "/work-with-partner-organisations", "/alternative-organisation-address", "/company-registration-number", "/website-and-social-media"], "all_possible_previous": ["/intro-about-your-organisation", "/alternative-organisation-name", "/organisation-details", "/tell-us-about-your-organisations-main-activities"], "all_possible_previous_direct_next": ["/alternative-organisation-name", "/how-is-your-organisation-classified", "/organisation-details", "/tell-us-about-your-organisations-main-activities"], "all_possible_after": ["/how-is-your-organisation-classified-other", "/summary", "/joint-bid", "/partner-organisation-details", "/organisation-address", "/charity-number", "/agreement-exists", "/work-with-partner-organisations", "/alternative-organisation-address", "/company-registration-number", "/website-and-social-media"]}, {"path": "/how-is-your-organisation-classified-other", "next_paths": ["/organisation-address"], "all_direct_previous": ["/how-is-your-organisation-classified"], "direct_next_of_direct_previous": ["/company-registration-number", "/how-is-your-organisation-classified-other", "/charity-number"], "all_possible_next_of_siblings": ["/summary", "/joint-bid", "/partner-organisation-details", "/organisation-address", "/agreement-exists", "/work-with-partner-organisations", "/alternative-organisation-address", "/website-and-social-media"], "all_possible_previous": ["/tell-us-about-your-organisations-main-activities", "/intro-about-your-organisation", "/organisation-details", "/alternative-organisation-name", "/how-is-your-organisation-classified"], "all_possible_previous_direct_next": ["/how-is-your-organisation-classified-other", "/tell-us-about-your-organisations-main-activities", "/organisation-details", "/charity-number", "/alternative-organisation-name", "/how-is-your-organisation-classified", "/company-registration-number"], "all_possible_after": ["/summary", "/joint-bid", "/partner-organisation-details", "/organisation-address", "/agreement-exists", "/work-with-partner-organisations", "/alternative-organisation-address", "/website-and-social-media"]}, {"path": "/charity-number", "next_paths": ["/organisation-address"], "all_direct_previous": ["/how-is-your-organisation-classified"], "direct_next_of_direct_previous": ["/company-registration-number", "/how-is-your-organisation-classified-other", "/charity-number"], "all_possible_next_of_siblings": ["/summary", "/joint-bid", "/partner-organisation-details", "/organisation-address", "/agreement-exists", "/work-with-partner-organisations", "/alternative-organisation-address", "/website-and-social-media"], "all_possible_previous": ["/tell-us-about-your-organisations-main-activities", "/intro-about-your-organisation", "/organisation-details", "/alternative-organisation-name", "/how-is-your-organisation-classified"], "all_possible_previous_direct_next": ["/how-is-your-organisation-classified-other", "/tell-us-about-your-organisations-main-activities", "/organisation-details", "/charity-number", "/alternative-organisation-name", "/how-is-your-organisation-classified", "/company-registration-number"], "all_possible_after": ["/summary", "/joint-bid", "/partner-organisation-details", "/organisation-address", "/agreement-exists", "/work-with-partner-organisations", "/alternative-organisation-address", "/website-and-social-media"]}, {"path": "/company-registration-number", "next_paths": ["/organisation-address"], "all_direct_previous": ["/how-is-your-organisation-classified"], "direct_next_of_direct_previous": ["/company-registration-number", "/how-is-your-organisation-classified-other", "/charity-number"], "all_possible_next_of_siblings": ["/summary", "/joint-bid", "/partner-organisation-details", "/organisation-address", "/agreement-exists", "/work-with-partner-organisations", "/alternative-organisation-address", "/website-and-social-media"], "all_possible_previous": ["/tell-us-about-your-organisations-main-activities", "/intro-about-your-organisation", "/organisation-details", "/alternative-organisation-name", "/how-is-your-organisation-classified"], "all_possible_previous_direct_next": ["/how-is-your-organisation-classified-other", "/tell-us-about-your-organisations-main-activities", "/organisation-details", "/charity-number", "/alternative-organisation-name", "/how-is-your-organisation-classified", "/company-registration-number"], "all_possible_after": ["/summary", "/joint-bid", "/partner-organisation-details", "/organisation-address", "/agreement-exists", "/work-with-partner-organisations", "/alternative-organisation-address", "/website-and-social-media"]}, {"path": "/organisation-address", "next_paths": [], "all_direct_previous": ["/how-is-your-organisation-classified-other", "/company-registration-number", "/charity-number"], "direct_next_of_direct_previous": ["/organisation-address"], "all_possible_next_of_siblings": ["/summary", "/joint-bid", "/partner-organisation-details", "/agreement-exists", "/work-with-partner-organisations", "/alternative-organisation-address", "/website-and-social-media"], "all_possible_previous": ["/how-is-your-organisation-classified-other", "/tell-us-about-your-organisations-main-activities", "/intro-about-your-organisation", "/organisation-details", "/charity-number", "/alternative-organisation-name", "/how-is-your-organisation-classified", "/company-registration-number"], "all_possible_previous_direct_next": ["/how-is-your-organisation-classified-other", "/tell-us-about-your-organisations-main-activities", "/organisation-details", "/organisation-address", "/charity-number", "/alternative-organisation-name", "/how-is-your-organisation-classified", "/company-registration-number"], "all_possible_after": []}]} diff --git a/fund_store/tests/test_data/all_questions/metadata/joint_bid_out_and_back.json b/fund_store/tests/test_data/all_questions/metadata/joint_bid_out_and_back.json new file mode 100644 index 000000000..e2b19929e --- /dev/null +++ b/fund_store/tests/test_data/all_questions/metadata/joint_bid_out_and_back.json @@ -0,0 +1 @@ +{"start_page": "/joint-bid", "all_pages": [{"path": "/partner-organisation-details", "next_paths": ["/work-with-partner-organisations"], "all_direct_previous": ["/joint-bid"], "direct_next_of_direct_previous": ["/partner-organisation-details", "/website-and-social-media"], "all_possible_next_of_siblings": ["/work-with-partner-organisations", "/summary", "/website-and-social-media", "/agreement-exists"], "all_possible_previous": ["/how-is-your-organisation-classified-other", "/tell-us-about-your-organisations-main-activities", "/joint-bid", "/intro-about-your-organisation", "/organisation-details", "/organisation-address", "/charity-number", "/alternative-organisation-name", "/how-is-your-organisation-classified", "/alternative-organisation-address", "/company-registration-number"], "all_possible_previous_direct_next": ["/how-is-your-organisation-classified-other", "/tell-us-about-your-organisations-main-activities", "/joint-bid", "/partner-organisation-details", "/organisation-details", "/organisation-address", "/charity-number", "/alternative-organisation-name", "/how-is-your-organisation-classified", "/alternative-organisation-address", "/company-registration-number", "/website-and-social-media"], "all_possible_after": ["/work-with-partner-organisations", "/summary", "/website-and-social-media", "/agreement-exists"]}, {"path": "/work-with-partner-organisations", "next_paths": ["/agreement-exists"], "all_direct_previous": ["/partner-organisation-details"], "direct_next_of_direct_previous": ["/work-with-partner-organisations"], "all_possible_next_of_siblings": ["/summary", "/website-and-social-media", "/agreement-exists"], "all_possible_previous": ["/how-is-your-organisation-classified-other", "/tell-us-about-your-organisations-main-activities", "/joint-bid", "/partner-organisation-details", "/intro-about-your-organisation", "/organisation-details", "/organisation-address", "/charity-number", "/alternative-organisation-name", "/how-is-your-organisation-classified", "/alternative-organisation-address", "/company-registration-number"], "all_possible_previous_direct_next": ["/how-is-your-organisation-classified-other", "/tell-us-about-your-organisations-main-activities", "/joint-bid", "/partner-organisation-details", "/organisation-details", "/organisation-address", "/charity-number", "/alternative-organisation-name", "/work-with-partner-organisations", "/how-is-your-organisation-classified", "/alternative-organisation-address", "/company-registration-number", "/website-and-social-media"], "all_possible_after": ["/summary", "/website-and-social-media", "/agreement-exists"]}, {"path": "/agreement-exists", "next_paths": ["/website-and-social-media"], "all_direct_previous": ["/work-with-partner-organisations"], "direct_next_of_direct_previous": ["/agreement-exists"], "all_possible_next_of_siblings": ["/summary", "/website-and-social-media"], "all_possible_previous": ["/how-is-your-organisation-classified-other", "/tell-us-about-your-organisations-main-activities", "/joint-bid", "/partner-organisation-details", "/intro-about-your-organisation", "/organisation-details", "/organisation-address", "/charity-number", "/alternative-organisation-name", "/work-with-partner-organisations", "/how-is-your-organisation-classified", "/alternative-organisation-address", "/company-registration-number"], "all_possible_previous_direct_next": ["/how-is-your-organisation-classified-other", "/tell-us-about-your-organisations-main-activities", "/joint-bid", "/partner-organisation-details", "/organisation-details", "/organisation-address", "/agreement-exists", "/charity-number", "/alternative-organisation-name", "/work-with-partner-organisations", "/how-is-your-organisation-classified", "/alternative-organisation-address", "/company-registration-number", "/website-and-social-media"], "all_possible_after": ["/summary", "/website-and-social-media"]}, {"path": "/website-and-social-media", "next_paths": [], "all_direct_previous": ["/joint-bid", "/agreement-exists"], "direct_next_of_direct_previous": ["/partner-organisation-details", "/website-and-social-media"], "all_possible_next_of_siblings": ["/work-with-partner-organisations", "/summary", "/website-and-social-media", "/agreement-exists"], "all_possible_previous": ["/how-is-your-organisation-classified-other", "/tell-us-about-your-organisations-main-activities", "/joint-bid", "/intro-about-your-organisation", "/partner-organisation-details", "/organisation-details", "/organisation-address", "/agreement-exists", "/charity-number", "/alternative-organisation-name", "/how-is-your-organisation-classified", "/work-with-partner-organisations", "/alternative-organisation-address", "/company-registration-number"], "all_possible_previous_direct_next": ["/how-is-your-organisation-classified-other", "/tell-us-about-your-organisations-main-activities", "/joint-bid", "/partner-organisation-details", "/organisation-details", "/organisation-address", "/charity-number", "/agreement-exists", "/alternative-organisation-name", "/how-is-your-organisation-classified", "/work-with-partner-organisations", "/alternative-organisation-address", "/company-registration-number", "/website-and-social-media"], "all_possible_after": []}, {"path": "/joint-bid", "next_paths": ["/partner-organisation-details", "/website-and-social-media"], "all_direct_previous": ["/alternative-organisation-address"], "direct_next_of_direct_previous": ["/joint-bid"], "all_possible_next_of_siblings": ["/summary", "/partner-organisation-details", "/agreement-exists", "/work-with-partner-organisations", "/website-and-social-media"], "all_possible_previous": ["/how-is-your-organisation-classified-other", "/tell-us-about-your-organisations-main-activities", "/intro-about-your-organisation", "/organisation-details", "/organisation-address", "/charity-number", "/alternative-organisation-name", "/how-is-your-organisation-classified", "/alternative-organisation-address", "/company-registration-number"], "all_possible_previous_direct_next": ["/how-is-your-organisation-classified-other", "/tell-us-about-your-organisations-main-activities", "/joint-bid", "/organisation-details", "/organisation-address", "/charity-number", "/alternative-organisation-name", "/how-is-your-organisation-classified", "/alternative-organisation-address", "/company-registration-number"], "all_possible_after": ["/summary", "/partner-organisation-details", "/agreement-exists", "/work-with-partner-organisations", "/website-and-social-media"]}]} diff --git a/fund_store/tests/test_data/all_questions/metadata/metadata_about_your_org_cyp.json b/fund_store/tests/test_data/all_questions/metadata/metadata_about_your_org_cyp.json new file mode 100644 index 000000000..fb2436ae8 --- /dev/null +++ b/fund_store/tests/test_data/all_questions/metadata/metadata_about_your_org_cyp.json @@ -0,0 +1,799 @@ +{ + "start_page": "/intro-about-your-organisation", + "all_pages": [ + { + "path": "/organisation-details", + "next_paths": [ + "/alternative-organisation-name", + "/tell-us-about-your-organisations-main-activities" + ], + "all_direct_previous": [ + "/intro-about-your-organisation" + ], + "direct_next_of_direct_previous": [ + "/organisation-details" + ], + "all_possible_next_of_siblings": [ + "/how-is-your-organisation-classified-other", + "/summary", + "/tell-us-about-your-organisations-main-activities", + "/joint-bid", + "/partner-organisation-details", + "/charity-number", + "/organisation-address", + "/agreement-exists", + "/alternative-organisation-name", + "/how-is-your-organisation-classified", + "/work-with-partner-organisations", + "/alternative-organisation-address", + "/company-registration-number", + "/website-and-social-media" + ], + "all_possible_previous": [ + "/intro-about-your-organisation" + ], + "all_possible_previous_direct_next": [ + "/organisation-details" + ], + "all_possible_after": [ + "/how-is-your-organisation-classified-other", + "/summary", + "/tell-us-about-your-organisations-main-activities", + "/joint-bid", + "/partner-organisation-details", + "/charity-number", + "/organisation-address", + "/agreement-exists", + "/alternative-organisation-name", + "/how-is-your-organisation-classified", + "/work-with-partner-organisations", + "/alternative-organisation-address", + "/company-registration-number", + "/website-and-social-media" + ] + }, + { + "path": "/tell-us-about-your-organisations-main-activities", + "next_paths": [ + "/how-is-your-organisation-classified" + ], + "all_direct_previous": [ + "/organisation-details", + "/alternative-organisation-name" + ], + "direct_next_of_direct_previous": [ + "/alternative-organisation-name", + "/tell-us-about-your-organisations-main-activities" + ], + "all_possible_next_of_siblings": [ + "/how-is-your-organisation-classified-other", + "/summary", + "/tell-us-about-your-organisations-main-activities", + "/joint-bid", + "/partner-organisation-details", + "/charity-number", + "/organisation-address", + "/agreement-exists", + "/how-is-your-organisation-classified", + "/work-with-partner-organisations", + "/alternative-organisation-address", + "/company-registration-number", + "/website-and-social-media" + ], + "all_possible_previous": [ + "/intro-about-your-organisation", + "/alternative-organisation-name", + "/organisation-details" + ], + "all_possible_previous_direct_next": [ + "/alternative-organisation-name", + "/organisation-details", + "/tell-us-about-your-organisations-main-activities" + ], + "all_possible_after": [ + "/how-is-your-organisation-classified-other", + "/summary", + "/joint-bid", + "/partner-organisation-details", + "/organisation-address", + "/charity-number", + "/agreement-exists", + "/how-is-your-organisation-classified", + "/work-with-partner-organisations", + "/alternative-organisation-address", + "/company-registration-number", + "/website-and-social-media" + ] + }, + { + "path": "/how-is-your-organisation-classified", + "next_paths": [ + "/how-is-your-organisation-classified-other", + "/charity-number", + "/company-registration-number" + ], + "all_direct_previous": [ + "/tell-us-about-your-organisations-main-activities" + ], + "direct_next_of_direct_previous": [ + "/how-is-your-organisation-classified" + ], + "all_possible_next_of_siblings": [ + "/how-is-your-organisation-classified-other", + "/summary", + "/joint-bid", + "/partner-organisation-details", + "/organisation-address", + "/charity-number", + "/agreement-exists", + "/work-with-partner-organisations", + "/alternative-organisation-address", + "/company-registration-number", + "/website-and-social-media" + ], + "all_possible_previous": [ + "/intro-about-your-organisation", + "/alternative-organisation-name", + "/organisation-details", + "/tell-us-about-your-organisations-main-activities" + ], + "all_possible_previous_direct_next": [ + "/alternative-organisation-name", + "/how-is-your-organisation-classified", + "/organisation-details", + "/tell-us-about-your-organisations-main-activities" + ], + "all_possible_after": [ + "/how-is-your-organisation-classified-other", + "/summary", + "/joint-bid", + "/partner-organisation-details", + "/organisation-address", + "/charity-number", + "/agreement-exists", + "/work-with-partner-organisations", + "/alternative-organisation-address", + "/company-registration-number", + "/website-and-social-media" + ] + }, + { + "path": "/how-is-your-organisation-classified-other", + "next_paths": [ + "/organisation-address" + ], + "all_direct_previous": [ + "/how-is-your-organisation-classified" + ], + "direct_next_of_direct_previous": [ + "/company-registration-number", + "/how-is-your-organisation-classified-other", + "/charity-number" + ], + "all_possible_next_of_siblings": [ + "/summary", + "/joint-bid", + "/partner-organisation-details", + "/organisation-address", + "/agreement-exists", + "/work-with-partner-organisations", + "/alternative-organisation-address", + "/website-and-social-media" + ], + "all_possible_previous": [ + "/tell-us-about-your-organisations-main-activities", + "/intro-about-your-organisation", + "/organisation-details", + "/alternative-organisation-name", + "/how-is-your-organisation-classified" + ], + "all_possible_previous_direct_next": [ + "/how-is-your-organisation-classified-other", + "/tell-us-about-your-organisations-main-activities", + "/organisation-details", + "/charity-number", + "/alternative-organisation-name", + "/how-is-your-organisation-classified", + "/company-registration-number" + ], + "all_possible_after": [ + "/summary", + "/joint-bid", + "/partner-organisation-details", + "/organisation-address", + "/agreement-exists", + "/work-with-partner-organisations", + "/alternative-organisation-address", + "/website-and-social-media" + ] + }, + { + "path": "/company-registration-number", + "next_paths": [ + "/organisation-address" + ], + "all_direct_previous": [ + "/how-is-your-organisation-classified" + ], + "direct_next_of_direct_previous": [ + "/company-registration-number", + "/how-is-your-organisation-classified-other", + "/charity-number" + ], + "all_possible_next_of_siblings": [ + "/summary", + "/joint-bid", + "/partner-organisation-details", + "/organisation-address", + "/agreement-exists", + "/work-with-partner-organisations", + "/alternative-organisation-address", + "/website-and-social-media" + ], + "all_possible_previous": [ + "/tell-us-about-your-organisations-main-activities", + "/intro-about-your-organisation", + "/organisation-details", + "/alternative-organisation-name", + "/how-is-your-organisation-classified" + ], + "all_possible_previous_direct_next": [ + "/how-is-your-organisation-classified-other", + "/tell-us-about-your-organisations-main-activities", + "/organisation-details", + "/charity-number", + "/alternative-organisation-name", + "/how-is-your-organisation-classified", + "/company-registration-number" + ], + "all_possible_after": [ + "/summary", + "/joint-bid", + "/partner-organisation-details", + "/organisation-address", + "/agreement-exists", + "/work-with-partner-organisations", + "/alternative-organisation-address", + "/website-and-social-media" + ] + }, + { + "path": "/charity-number", + "next_paths": [ + "/organisation-address" + ], + "all_direct_previous": [ + "/how-is-your-organisation-classified" + ], + "direct_next_of_direct_previous": [ + "/company-registration-number", + "/how-is-your-organisation-classified-other", + "/charity-number" + ], + "all_possible_next_of_siblings": [ + "/summary", + "/joint-bid", + "/partner-organisation-details", + "/organisation-address", + "/agreement-exists", + "/work-with-partner-organisations", + "/alternative-organisation-address", + "/website-and-social-media" + ], + "all_possible_previous": [ + "/tell-us-about-your-organisations-main-activities", + "/intro-about-your-organisation", + "/organisation-details", + "/alternative-organisation-name", + "/how-is-your-organisation-classified" + ], + "all_possible_previous_direct_next": [ + "/how-is-your-organisation-classified-other", + "/tell-us-about-your-organisations-main-activities", + "/organisation-details", + "/charity-number", + "/alternative-organisation-name", + "/how-is-your-organisation-classified", + "/company-registration-number" + ], + "all_possible_after": [ + "/summary", + "/joint-bid", + "/partner-organisation-details", + "/organisation-address", + "/agreement-exists", + "/work-with-partner-organisations", + "/alternative-organisation-address", + "/website-and-social-media" + ] + }, + { + "path": "/alternative-organisation-name", + "next_paths": [ + "/tell-us-about-your-organisations-main-activities" + ], + "all_direct_previous": [ + "/organisation-details" + ], + "direct_next_of_direct_previous": [ + "/alternative-organisation-name", + "/tell-us-about-your-organisations-main-activities" + ], + "all_possible_next_of_siblings": [ + "/how-is-your-organisation-classified-other", + "/summary", + "/tell-us-about-your-organisations-main-activities", + "/joint-bid", + "/partner-organisation-details", + "/charity-number", + "/organisation-address", + "/agreement-exists", + "/how-is-your-organisation-classified", + "/work-with-partner-organisations", + "/alternative-organisation-address", + "/company-registration-number", + "/website-and-social-media" + ], + "all_possible_previous": [ + "/intro-about-your-organisation", + "/organisation-details" + ], + "all_possible_previous_direct_next": [ + "/alternative-organisation-name", + "/organisation-details", + "/tell-us-about-your-organisations-main-activities" + ], + "all_possible_after": [ + "/how-is-your-organisation-classified-other", + "/summary", + "/tell-us-about-your-organisations-main-activities", + "/joint-bid", + "/partner-organisation-details", + "/charity-number", + "/organisation-address", + "/agreement-exists", + "/how-is-your-organisation-classified", + "/work-with-partner-organisations", + "/alternative-organisation-address", + "/company-registration-number", + "/website-and-social-media" + ] + }, + { + "path": "/organisation-address", + "next_paths": [ + "/alternative-organisation-address" + ], + "all_direct_previous": [ + "/how-is-your-organisation-classified-other", + "/company-registration-number", + "/charity-number" + ], + "direct_next_of_direct_previous": [ + "/organisation-address" + ], + "all_possible_next_of_siblings": [ + "/summary", + "/joint-bid", + "/partner-organisation-details", + "/agreement-exists", + "/work-with-partner-organisations", + "/alternative-organisation-address", + "/website-and-social-media" + ], + "all_possible_previous": [ + "/how-is-your-organisation-classified-other", + "/tell-us-about-your-organisations-main-activities", + "/intro-about-your-organisation", + "/organisation-details", + "/charity-number", + "/alternative-organisation-name", + "/how-is-your-organisation-classified", + "/company-registration-number" + ], + "all_possible_previous_direct_next": [ + "/how-is-your-organisation-classified-other", + "/tell-us-about-your-organisations-main-activities", + "/organisation-details", + "/organisation-address", + "/charity-number", + "/alternative-organisation-name", + "/how-is-your-organisation-classified", + "/company-registration-number" + ], + "all_possible_after": [ + "/summary", + "/joint-bid", + "/partner-organisation-details", + "/agreement-exists", + "/work-with-partner-organisations", + "/alternative-organisation-address", + "/website-and-social-media" + ] + }, + { + "path": "/alternative-organisation-address", + "next_paths": [ + "/joint-bid" + ], + "all_direct_previous": [ + "/organisation-address" + ], + "direct_next_of_direct_previous": [ + "/alternative-organisation-address" + ], + "all_possible_next_of_siblings": [ + "/summary", + "/joint-bid", + "/partner-organisation-details", + "/agreement-exists", + "/work-with-partner-organisations", + "/website-and-social-media" + ], + "all_possible_previous": [ + "/how-is-your-organisation-classified-other", + "/tell-us-about-your-organisations-main-activities", + "/intro-about-your-organisation", + "/organisation-details", + "/charity-number", + "/organisation-address", + "/alternative-organisation-name", + "/how-is-your-organisation-classified", + "/company-registration-number" + ], + "all_possible_previous_direct_next": [ + "/how-is-your-organisation-classified-other", + "/tell-us-about-your-organisations-main-activities", + "/organisation-details", + "/organisation-address", + "/charity-number", + "/alternative-organisation-name", + "/how-is-your-organisation-classified", + "/alternative-organisation-address", + "/company-registration-number" + ], + "all_possible_after": [ + "/summary", + "/joint-bid", + "/partner-organisation-details", + "/agreement-exists", + "/work-with-partner-organisations", + "/website-and-social-media" + ] + }, + { + "path": "/joint-bid", + "next_paths": [ + "/partner-organisation-details", + "/website-and-social-media" + ], + "all_direct_previous": [ + "/alternative-organisation-address" + ], + "direct_next_of_direct_previous": [ + "/joint-bid" + ], + "all_possible_next_of_siblings": [ + "/summary", + "/partner-organisation-details", + "/agreement-exists", + "/work-with-partner-organisations", + "/website-and-social-media" + ], + "all_possible_previous": [ + "/how-is-your-organisation-classified-other", + "/tell-us-about-your-organisations-main-activities", + "/intro-about-your-organisation", + "/organisation-details", + "/organisation-address", + "/charity-number", + "/alternative-organisation-name", + "/how-is-your-organisation-classified", + "/alternative-organisation-address", + "/company-registration-number" + ], + "all_possible_previous_direct_next": [ + "/how-is-your-organisation-classified-other", + "/tell-us-about-your-organisations-main-activities", + "/joint-bid", + "/organisation-details", + "/organisation-address", + "/charity-number", + "/alternative-organisation-name", + "/how-is-your-organisation-classified", + "/alternative-organisation-address", + "/company-registration-number" + ], + "all_possible_after": [ + "/summary", + "/partner-organisation-details", + "/agreement-exists", + "/work-with-partner-organisations", + "/website-and-social-media" + ] + }, + { + "path": "/partner-organisation-details", + "next_paths": [ + "/work-with-partner-organisations" + ], + "all_direct_previous": [ + "/joint-bid" + ], + "direct_next_of_direct_previous": [ + "/partner-organisation-details", + "/website-and-social-media" + ], + "all_possible_next_of_siblings": [ + "/work-with-partner-organisations", + "/summary", + "/website-and-social-media", + "/agreement-exists" + ], + "all_possible_previous": [ + "/how-is-your-organisation-classified-other", + "/tell-us-about-your-organisations-main-activities", + "/joint-bid", + "/intro-about-your-organisation", + "/organisation-details", + "/organisation-address", + "/charity-number", + "/alternative-organisation-name", + "/how-is-your-organisation-classified", + "/alternative-organisation-address", + "/company-registration-number" + ], + "all_possible_previous_direct_next": [ + "/how-is-your-organisation-classified-other", + "/tell-us-about-your-organisations-main-activities", + "/joint-bid", + "/partner-organisation-details", + "/organisation-details", + "/organisation-address", + "/charity-number", + "/alternative-organisation-name", + "/how-is-your-organisation-classified", + "/alternative-organisation-address", + "/company-registration-number", + "/website-and-social-media" + ], + "all_possible_after": [ + "/work-with-partner-organisations", + "/summary", + "/website-and-social-media", + "/agreement-exists" + ] + }, + { + "path": "/website-and-social-media", + "next_paths": [ + "/summary" + ], + "all_direct_previous": [ + "/joint-bid", + "/agreement-exists" + ], + "direct_next_of_direct_previous": [ + "/partner-organisation-details", + "/website-and-social-media" + ], + "all_possible_next_of_siblings": [ + "/work-with-partner-organisations", + "/summary", + "/website-and-social-media", + "/agreement-exists" + ], + "all_possible_previous": [ + "/how-is-your-organisation-classified-other", + "/tell-us-about-your-organisations-main-activities", + "/joint-bid", + "/intro-about-your-organisation", + "/partner-organisation-details", + "/organisation-details", + "/organisation-address", + "/agreement-exists", + "/charity-number", + "/alternative-organisation-name", + "/how-is-your-organisation-classified", + "/work-with-partner-organisations", + "/alternative-organisation-address", + "/company-registration-number" + ], + "all_possible_previous_direct_next": [ + "/how-is-your-organisation-classified-other", + "/tell-us-about-your-organisations-main-activities", + "/joint-bid", + "/partner-organisation-details", + "/organisation-details", + "/organisation-address", + "/charity-number", + "/agreement-exists", + "/alternative-organisation-name", + "/how-is-your-organisation-classified", + "/work-with-partner-organisations", + "/alternative-organisation-address", + "/company-registration-number", + "/website-and-social-media" + ], + "all_possible_after": [ + "/summary" + ] + }, + { + "path": "/summary", + "next_paths": [], + "all_direct_previous": [ + "/website-and-social-media" + ], + "direct_next_of_direct_previous": [ + "/summary" + ], + "all_possible_next_of_siblings": [], + "all_possible_previous": [ + "/how-is-your-organisation-classified-other", + "/tell-us-about-your-organisations-main-activities", + "/joint-bid", + "/intro-about-your-organisation", + "/partner-organisation-details", + "/organisation-details", + "/organisation-address", + "/agreement-exists", + "/charity-number", + "/alternative-organisation-name", + "/how-is-your-organisation-classified", + "/work-with-partner-organisations", + "/alternative-organisation-address", + "/company-registration-number", + "/website-and-social-media" + ], + "all_possible_previous_direct_next": [ + "/how-is-your-organisation-classified-other", + "/summary", + "/tell-us-about-your-organisations-main-activities", + "/joint-bid", + "/partner-organisation-details", + "/organisation-details", + "/organisation-address", + "/charity-number", + "/agreement-exists", + "/alternative-organisation-name", + "/how-is-your-organisation-classified", + "/work-with-partner-organisations", + "/alternative-organisation-address", + "/company-registration-number", + "/website-and-social-media" + ], + "all_possible_after": [] + }, + { + "path": "/intro-about-your-organisation", + "next_paths": [ + "/organisation-details" + ], + "all_direct_previous": [], + "direct_next_of_direct_previous": [], + "all_possible_next_of_siblings": [], + "all_possible_previous": [], + "all_possible_previous_direct_next": [], + "all_possible_after": [ + "/how-is-your-organisation-classified-other", + "/summary", + "/tell-us-about-your-organisations-main-activities", + "/joint-bid", + "/partner-organisation-details", + "/organisation-details", + "/charity-number", + "/organisation-address", + "/agreement-exists", + "/alternative-organisation-name", + "/how-is-your-organisation-classified", + "/work-with-partner-organisations", + "/alternative-organisation-address", + "/company-registration-number", + "/website-and-social-media" + ] + }, + { + "path": "/work-with-partner-organisations", + "next_paths": [ + "/agreement-exists" + ], + "all_direct_previous": [ + "/partner-organisation-details" + ], + "direct_next_of_direct_previous": [ + "/work-with-partner-organisations" + ], + "all_possible_next_of_siblings": [ + "/summary", + "/website-and-social-media", + "/agreement-exists" + ], + "all_possible_previous": [ + "/how-is-your-organisation-classified-other", + "/tell-us-about-your-organisations-main-activities", + "/joint-bid", + "/partner-organisation-details", + "/intro-about-your-organisation", + "/organisation-details", + "/organisation-address", + "/charity-number", + "/alternative-organisation-name", + "/how-is-your-organisation-classified", + "/alternative-organisation-address", + "/company-registration-number" + ], + "all_possible_previous_direct_next": [ + "/how-is-your-organisation-classified-other", + "/tell-us-about-your-organisations-main-activities", + "/joint-bid", + "/partner-organisation-details", + "/organisation-details", + "/organisation-address", + "/charity-number", + "/alternative-organisation-name", + "/work-with-partner-organisations", + "/how-is-your-organisation-classified", + "/alternative-organisation-address", + "/company-registration-number", + "/website-and-social-media" + ], + "all_possible_after": [ + "/summary", + "/website-and-social-media", + "/agreement-exists" + ] + }, + { + "path": "/agreement-exists", + "next_paths": [ + "/website-and-social-media" + ], + "all_direct_previous": [ + "/work-with-partner-organisations" + ], + "direct_next_of_direct_previous": [ + "/agreement-exists" + ], + "all_possible_next_of_siblings": [ + "/summary", + "/website-and-social-media" + ], + "all_possible_previous": [ + "/how-is-your-organisation-classified-other", + "/tell-us-about-your-organisations-main-activities", + "/joint-bid", + "/partner-organisation-details", + "/intro-about-your-organisation", + "/organisation-details", + "/organisation-address", + "/charity-number", + "/alternative-organisation-name", + "/work-with-partner-organisations", + "/how-is-your-organisation-classified", + "/alternative-organisation-address", + "/company-registration-number" + ], + "all_possible_previous_direct_next": [ + "/how-is-your-organisation-classified-other", + "/tell-us-about-your-organisations-main-activities", + "/joint-bid", + "/partner-organisation-details", + "/organisation-details", + "/organisation-address", + "/agreement-exists", + "/charity-number", + "/alternative-organisation-name", + "/work-with-partner-organisations", + "/how-is-your-organisation-classified", + "/alternative-organisation-address", + "/company-registration-number", + "/website-and-social-media" + ], + "all_possible_after": [ + "/summary", + "/website-and-social-media" + ] + } + ] +} diff --git a/fund_store/tests/test_data/all_questions/metadata/metadata_applicant_ns.json b/fund_store/tests/test_data/all_questions/metadata/metadata_applicant_ns.json new file mode 100644 index 000000000..261eec759 --- /dev/null +++ b/fund_store/tests/test_data/all_questions/metadata/metadata_applicant_ns.json @@ -0,0 +1 @@ +{"start_page": "/13-applicant-information", "all_pages": [{"path": "/13-applicant-information", "next_paths": ["/lead-contact-details"], "all_direct_previous": [], "direct_next_of_direct_previous": [], "all_possible_next_of_siblings": [], "all_possible_previous": [], "all_possible_previous_direct_next": [], "all_possible_after": ["/lead-contact-details", "/summary", "/authorised-signatory-details"]}, {"path": "/lead-contact-details", "next_paths": ["/authorised-signatory-details", "/summary"], "all_direct_previous": ["/13-applicant-information"], "direct_next_of_direct_previous": ["/lead-contact-details"], "all_possible_next_of_siblings": ["/summary", "/authorised-signatory-details"], "all_possible_previous": ["/13-applicant-information"], "all_possible_previous_direct_next": ["/lead-contact-details"], "all_possible_after": ["/summary", "/authorised-signatory-details"]}, {"path": "/summary", "next_paths": [], "all_direct_previous": ["/lead-contact-details", "/authorised-signatory-details"], "direct_next_of_direct_previous": ["/summary", "/authorised-signatory-details"], "all_possible_next_of_siblings": ["/summary"], "all_possible_previous": ["/13-applicant-information", "/lead-contact-details", "/authorised-signatory-details"], "all_possible_previous_direct_next": ["/lead-contact-details", "/summary", "/authorised-signatory-details"], "all_possible_after": []}, {"path": "/authorised-signatory-details", "next_paths": ["/summary"], "all_direct_previous": ["/lead-contact-details"], "direct_next_of_direct_previous": ["/summary", "/authorised-signatory-details"], "all_possible_next_of_siblings": ["/summary"], "all_possible_previous": ["/13-applicant-information", "/lead-contact-details"], "all_possible_previous_direct_next": ["/lead-contact-details", "/summary", "/authorised-signatory-details"], "all_possible_after": ["/summary"]}]} diff --git a/fund_store/tests/test_data/all_questions/metadata/metadata_name_app_cyp.json b/fund_store/tests/test_data/all_questions/metadata/metadata_name_app_cyp.json new file mode 100644 index 000000000..8f4e95da6 --- /dev/null +++ b/fund_store/tests/test_data/all_questions/metadata/metadata_name_app_cyp.json @@ -0,0 +1 @@ +{"start_page": "/name-your-application", "all_pages": [{"path": "/name-your-application", "next_paths": ["/summary"], "all_direct_previous": [], "direct_next_of_direct_previous": [], "all_possible_next_of_siblings": [], "all_possible_previous": [], "all_possible_previous_direct_next": [], "all_possible_after": ["/summary"]}, {"path": "/summary", "next_paths": [], "all_direct_previous": ["/name-your-application"], "direct_next_of_direct_previous": ["/summary"], "all_possible_next_of_siblings": [], "all_possible_previous": ["/name-your-application"], "all_possible_previous_direct_next": ["/summary"], "all_possible_after": []}]} diff --git a/fund_store/tests/test_data/all_questions/metadata/metadata_org_info_cof_r3w2.json b/fund_store/tests/test_data/all_questions/metadata/metadata_org_info_cof_r3w2.json new file mode 100644 index 000000000..5c131d75a --- /dev/null +++ b/fund_store/tests/test_data/all_questions/metadata/metadata_org_info_cof_r3w2.json @@ -0,0 +1 @@ +{"start_page": "/organisation-information", "all_pages": [{"path": "/organisation-information", "next_paths": ["/organisation-names"], "all_direct_previous": [], "direct_next_of_direct_previous": [], "all_possible_next_of_siblings": [], "all_possible_previous": [], "all_possible_previous_direct_next": [], "all_possible_after": ["/alternative-names-of-your-organisation", "/organisation-address", "/joint-applications", "/how-your-organisation-is-classified", "/company-registration-details", "/registration-details", "/partner-organisation-details", "/purpose-and-activities", "/about-your-organisation-eBkQGy", "/previous-projects-similar-to-this-one", "/correspondence-address", "/summary", "/charity-registration-details", "/organisation-names", "/parent-organisation-details", "/trading-subsidiaries", "/how-your-organisation-is-classified-other"]}, {"path": "/organisation-names", "next_paths": ["/alternative-names-of-your-organisation", "/purpose-and-activities"], "all_direct_previous": ["/organisation-information"], "direct_next_of_direct_previous": ["/organisation-names"], "all_possible_next_of_siblings": ["/alternative-names-of-your-organisation", "/organisation-address", "/joint-applications", "/how-your-organisation-is-classified", "/company-registration-details", "/registration-details", "/partner-organisation-details", "/purpose-and-activities", "/about-your-organisation-eBkQGy", "/previous-projects-similar-to-this-one", "/correspondence-address", "/summary", "/charity-registration-details", "/how-your-organisation-is-classified-other", "/parent-organisation-details", "/trading-subsidiaries"], "all_possible_previous": ["/organisation-information"], "all_possible_previous_direct_next": ["/organisation-names"], "all_possible_after": ["/alternative-names-of-your-organisation", "/organisation-address", "/joint-applications", "/how-your-organisation-is-classified", "/company-registration-details", "/registration-details", "/partner-organisation-details", "/purpose-and-activities", "/about-your-organisation-eBkQGy", "/previous-projects-similar-to-this-one", "/correspondence-address", "/summary", "/charity-registration-details", "/how-your-organisation-is-classified-other", "/parent-organisation-details", "/trading-subsidiaries"]}, {"path": "/alternative-names-of-your-organisation", "next_paths": ["/purpose-and-activities"], "all_direct_previous": ["/organisation-names"], "direct_next_of_direct_previous": ["/alternative-names-of-your-organisation", "/purpose-and-activities"], "all_possible_next_of_siblings": ["/organisation-address", "/joint-applications", "/how-your-organisation-is-classified", "/company-registration-details", "/registration-details", "/partner-organisation-details", "/purpose-and-activities", "/about-your-organisation-eBkQGy", "/previous-projects-similar-to-this-one", "/correspondence-address", "/summary", "/charity-registration-details", "/how-your-organisation-is-classified-other", "/parent-organisation-details", "/trading-subsidiaries"], "all_possible_previous": ["/organisation-information", "/organisation-names"], "all_possible_previous_direct_next": ["/alternative-names-of-your-organisation", "/organisation-names", "/purpose-and-activities"], "all_possible_after": ["/organisation-address", "/joint-applications", "/how-your-organisation-is-classified", "/company-registration-details", "/registration-details", "/partner-organisation-details", "/purpose-and-activities", "/about-your-organisation-eBkQGy", "/previous-projects-similar-to-this-one", "/correspondence-address", "/summary", "/charity-registration-details", "/how-your-organisation-is-classified-other", "/parent-organisation-details", "/trading-subsidiaries"]}, {"path": "/purpose-and-activities", "next_paths": ["/previous-projects-similar-to-this-one", "/how-your-organisation-is-classified"], "all_direct_previous": ["/organisation-names", "/alternative-names-of-your-organisation"], "direct_next_of_direct_previous": ["/alternative-names-of-your-organisation", "/purpose-and-activities"], "all_possible_next_of_siblings": ["/organisation-address", "/joint-applications", "/how-your-organisation-is-classified", "/company-registration-details", "/registration-details", "/partner-organisation-details", "/purpose-and-activities", "/about-your-organisation-eBkQGy", "/previous-projects-similar-to-this-one", "/correspondence-address", "/summary", "/charity-registration-details", "/how-your-organisation-is-classified-other", "/parent-organisation-details", "/trading-subsidiaries"], "all_possible_previous": ["/alternative-names-of-your-organisation", "/organisation-names", "/organisation-information"], "all_possible_previous_direct_next": ["/alternative-names-of-your-organisation", "/organisation-names", "/purpose-and-activities"], "all_possible_after": ["/organisation-address", "/joint-applications", "/how-your-organisation-is-classified", "/company-registration-details", "/registration-details", "/partner-organisation-details", "/about-your-organisation-eBkQGy", "/summary", "/previous-projects-similar-to-this-one", "/correspondence-address", "/charity-registration-details", "/how-your-organisation-is-classified-other", "/parent-organisation-details", "/trading-subsidiaries"]}, {"path": "/previous-projects-similar-to-this-one", "next_paths": ["/how-your-organisation-is-classified"], "all_direct_previous": ["/purpose-and-activities"], "direct_next_of_direct_previous": ["/how-your-organisation-is-classified", "/previous-projects-similar-to-this-one"], "all_possible_next_of_siblings": ["/organisation-address", "/joint-applications", "/company-registration-details", "/how-your-organisation-is-classified", "/partner-organisation-details", "/about-your-organisation-eBkQGy", "/summary", "/correspondence-address", "/charity-registration-details", "/registration-details", "/parent-organisation-details", "/trading-subsidiaries", "/how-your-organisation-is-classified-other"], "all_possible_previous": ["/alternative-names-of-your-organisation", "/organisation-names", "/organisation-information", "/purpose-and-activities"], "all_possible_previous_direct_next": ["/alternative-names-of-your-organisation", "/how-your-organisation-is-classified", "/purpose-and-activities", "/previous-projects-similar-to-this-one", "/organisation-names"], "all_possible_after": ["/organisation-address", "/joint-applications", "/how-your-organisation-is-classified", "/company-registration-details", "/registration-details", "/partner-organisation-details", "/about-your-organisation-eBkQGy", "/summary", "/correspondence-address", "/charity-registration-details", "/how-your-organisation-is-classified-other", "/parent-organisation-details", "/trading-subsidiaries"]}, {"path": "/how-your-organisation-is-classified", "next_paths": ["/how-your-organisation-is-classified-other", "/company-registration-details", "/charity-registration-details", "/trading-subsidiaries"], "all_direct_previous": ["/purpose-and-activities", "/previous-projects-similar-to-this-one"], "direct_next_of_direct_previous": ["/how-your-organisation-is-classified", "/previous-projects-similar-to-this-one"], "all_possible_next_of_siblings": ["/organisation-address", "/joint-applications", "/company-registration-details", "/how-your-organisation-is-classified", "/partner-organisation-details", "/about-your-organisation-eBkQGy", "/summary", "/correspondence-address", "/charity-registration-details", "/registration-details", "/parent-organisation-details", "/trading-subsidiaries", "/how-your-organisation-is-classified-other"], "all_possible_previous": ["/alternative-names-of-your-organisation", "/organisation-information", "/purpose-and-activities", "/previous-projects-similar-to-this-one", "/organisation-names"], "all_possible_previous_direct_next": ["/alternative-names-of-your-organisation", "/how-your-organisation-is-classified", "/purpose-and-activities", "/previous-projects-similar-to-this-one", "/organisation-names"], "all_possible_after": ["/organisation-address", "/joint-applications", "/company-registration-details", "/partner-organisation-details", "/about-your-organisation-eBkQGy", "/summary", "/correspondence-address", "/charity-registration-details", "/registration-details", "/parent-organisation-details", "/trading-subsidiaries", "/how-your-organisation-is-classified-other"]}, {"path": "/how-your-organisation-is-classified-other", "next_paths": ["/registration-details"], "all_direct_previous": ["/how-your-organisation-is-classified"], "direct_next_of_direct_previous": ["/company-registration-details", "/how-your-organisation-is-classified-other", "/charity-registration-details", "/trading-subsidiaries"], "all_possible_next_of_siblings": ["/organisation-address", "/joint-applications", "/partner-organisation-details", "/about-your-organisation-eBkQGy", "/summary", "/correspondence-address", "/registration-details", "/parent-organisation-details", "/trading-subsidiaries"], "all_possible_previous": ["/alternative-names-of-your-organisation", "/how-your-organisation-is-classified", "/organisation-information", "/purpose-and-activities", "/previous-projects-similar-to-this-one", "/organisation-names"], "all_possible_previous_direct_next": ["/alternative-names-of-your-organisation", "/organisation-names", "/company-registration-details", "/how-your-organisation-is-classified", "/purpose-and-activities", "/previous-projects-similar-to-this-one", "/charity-registration-details", "/how-your-organisation-is-classified-other", "/trading-subsidiaries"], "all_possible_after": ["/organisation-address", "/joint-applications", "/partner-organisation-details", "/about-your-organisation-eBkQGy", "/summary", "/correspondence-address", "/registration-details", "/parent-organisation-details", "/trading-subsidiaries"]}, {"path": "/registration-details", "next_paths": ["/about-your-organisation-eBkQGy", "/trading-subsidiaries"], "all_direct_previous": ["/how-your-organisation-is-classified-other", "/company-registration-details"], "direct_next_of_direct_previous": ["/registration-details"], "all_possible_next_of_siblings": ["/organisation-address", "/joint-applications", "/partner-organisation-details", "/about-your-organisation-eBkQGy", "/summary", "/correspondence-address", "/parent-organisation-details", "/trading-subsidiaries"], "all_possible_previous": ["/alternative-names-of-your-organisation", "/organisation-names", "/company-registration-details", "/how-your-organisation-is-classified", "/organisation-information", "/purpose-and-activities", "/previous-projects-similar-to-this-one", "/how-your-organisation-is-classified-other"], "all_possible_previous_direct_next": ["/alternative-names-of-your-organisation", "/organisation-names", "/company-registration-details", "/how-your-organisation-is-classified", "/purpose-and-activities", "/previous-projects-similar-to-this-one", "/charity-registration-details", "/registration-details", "/trading-subsidiaries", "/how-your-organisation-is-classified-other"], "all_possible_after": ["/organisation-address", "/joint-applications", "/partner-organisation-details", "/about-your-organisation-eBkQGy", "/summary", "/correspondence-address", "/parent-organisation-details", "/trading-subsidiaries"]}, {"path": "/charity-registration-details", "next_paths": ["/trading-subsidiaries"], "all_direct_previous": ["/how-your-organisation-is-classified"], "direct_next_of_direct_previous": ["/company-registration-details", "/how-your-organisation-is-classified-other", "/charity-registration-details", "/trading-subsidiaries"], "all_possible_next_of_siblings": ["/organisation-address", "/joint-applications", "/partner-organisation-details", "/about-your-organisation-eBkQGy", "/summary", "/correspondence-address", "/registration-details", "/parent-organisation-details", "/trading-subsidiaries"], "all_possible_previous": ["/alternative-names-of-your-organisation", "/how-your-organisation-is-classified", "/organisation-information", "/purpose-and-activities", "/previous-projects-similar-to-this-one", "/organisation-names"], "all_possible_previous_direct_next": ["/alternative-names-of-your-organisation", "/organisation-names", "/company-registration-details", "/how-your-organisation-is-classified", "/purpose-and-activities", "/previous-projects-similar-to-this-one", "/charity-registration-details", "/how-your-organisation-is-classified-other", "/trading-subsidiaries"], "all_possible_after": ["/organisation-address", "/joint-applications", "/partner-organisation-details", "/summary", "/correspondence-address", "/parent-organisation-details", "/trading-subsidiaries"]}, {"path": "/trading-subsidiaries", "next_paths": ["/parent-organisation-details", "/organisation-address"], "all_direct_previous": ["/how-your-organisation-is-classified", "/registration-details", "/charity-registration-details", "/about-your-organisation-eBkQGy"], "direct_next_of_direct_previous": ["/company-registration-details", "/about-your-organisation-eBkQGy", "/charity-registration-details", "/how-your-organisation-is-classified-other", "/trading-subsidiaries"], "all_possible_next_of_siblings": ["/organisation-address", "/joint-applications", "/partner-organisation-details", "/about-your-organisation-eBkQGy", "/summary", "/correspondence-address", "/registration-details", "/parent-organisation-details", "/trading-subsidiaries"], "all_possible_previous": ["/alternative-names-of-your-organisation", "/organisation-names", "/how-your-organisation-is-classified", "/organisation-information", "/company-registration-details", "/purpose-and-activities", "/about-your-organisation-eBkQGy", "/previous-projects-similar-to-this-one", "/charity-registration-details", "/registration-details", "/how-your-organisation-is-classified-other"], "all_possible_previous_direct_next": ["/alternative-names-of-your-organisation", "/organisation-names", "/company-registration-details", "/registration-details", "/how-your-organisation-is-classified", "/purpose-and-activities", "/about-your-organisation-eBkQGy", "/previous-projects-similar-to-this-one", "/charity-registration-details", "/how-your-organisation-is-classified-other", "/trading-subsidiaries"], "all_possible_after": ["/organisation-address", "/joint-applications", "/partner-organisation-details", "/summary", "/correspondence-address", "/parent-organisation-details"]}, {"path": "/parent-organisation-details", "next_paths": ["/organisation-address"], "all_direct_previous": ["/trading-subsidiaries"], "direct_next_of_direct_previous": ["/organisation-address", "/parent-organisation-details"], "all_possible_next_of_siblings": ["/organisation-address", "/joint-applications", "/partner-organisation-details", "/summary", "/correspondence-address"], "all_possible_previous": ["/alternative-names-of-your-organisation", "/organisation-names", "/how-your-organisation-is-classified", "/organisation-information", "/company-registration-details", "/about-your-organisation-eBkQGy", "/purpose-and-activities", "/previous-projects-similar-to-this-one", "/charity-registration-details", "/registration-details", "/trading-subsidiaries", "/how-your-organisation-is-classified-other"], "all_possible_previous_direct_next": ["/alternative-names-of-your-organisation", "/organisation-names", "/organisation-address", "/company-registration-details", "/registration-details", "/how-your-organisation-is-classified", "/purpose-and-activities", "/about-your-organisation-eBkQGy", "/previous-projects-similar-to-this-one", "/charity-registration-details", "/how-your-organisation-is-classified-other", "/parent-organisation-details", "/trading-subsidiaries"], "all_possible_after": ["/organisation-address", "/joint-applications", "/partner-organisation-details", "/summary", "/correspondence-address"]}, {"path": "/organisation-address", "next_paths": ["/correspondence-address", "/joint-applications"], "all_direct_previous": ["/trading-subsidiaries", "/parent-organisation-details"], "direct_next_of_direct_previous": ["/organisation-address", "/parent-organisation-details"], "all_possible_next_of_siblings": ["/organisation-address", "/joint-applications", "/partner-organisation-details", "/summary", "/correspondence-address"], "all_possible_previous": ["/alternative-names-of-your-organisation", "/organisation-names", "/how-your-organisation-is-classified", "/organisation-information", "/company-registration-details", "/about-your-organisation-eBkQGy", "/purpose-and-activities", "/previous-projects-similar-to-this-one", "/charity-registration-details", "/registration-details", "/parent-organisation-details", "/trading-subsidiaries", "/how-your-organisation-is-classified-other"], "all_possible_previous_direct_next": ["/alternative-names-of-your-organisation", "/organisation-names", "/organisation-address", "/company-registration-details", "/registration-details", "/how-your-organisation-is-classified", "/purpose-and-activities", "/about-your-organisation-eBkQGy", "/previous-projects-similar-to-this-one", "/charity-registration-details", "/how-your-organisation-is-classified-other", "/parent-organisation-details", "/trading-subsidiaries"], "all_possible_after": ["/summary", "/correspondence-address", "/partner-organisation-details", "/joint-applications"]}, {"path": "/correspondence-address", "next_paths": ["/joint-applications"], "all_direct_previous": ["/organisation-address"], "direct_next_of_direct_previous": ["/correspondence-address", "/joint-applications"], "all_possible_next_of_siblings": ["/summary", "/partner-organisation-details", "/joint-applications"], "all_possible_previous": ["/alternative-names-of-your-organisation", "/organisation-names", "/organisation-address", "/how-your-organisation-is-classified", "/organisation-information", "/company-registration-details", "/about-your-organisation-eBkQGy", "/purpose-and-activities", "/previous-projects-similar-to-this-one", "/charity-registration-details", "/registration-details", "/parent-organisation-details", "/trading-subsidiaries", "/how-your-organisation-is-classified-other"], "all_possible_previous_direct_next": ["/alternative-names-of-your-organisation", "/organisation-names", "/organisation-address", "/joint-applications", "/company-registration-details", "/registration-details", "/how-your-organisation-is-classified", "/purpose-and-activities", "/about-your-organisation-eBkQGy", "/correspondence-address", "/previous-projects-similar-to-this-one", "/charity-registration-details", "/how-your-organisation-is-classified-other", "/parent-organisation-details", "/trading-subsidiaries"], "all_possible_after": ["/summary", "/partner-organisation-details", "/joint-applications"]}, {"path": "/joint-applications", "next_paths": ["/partner-organisation-details", "/summary"], "all_direct_previous": ["/organisation-address", "/correspondence-address"], "direct_next_of_direct_previous": ["/correspondence-address", "/joint-applications"], "all_possible_next_of_siblings": ["/summary", "/partner-organisation-details", "/joint-applications"], "all_possible_previous": ["/alternative-names-of-your-organisation", "/organisation-names", "/organisation-address", "/how-your-organisation-is-classified", "/organisation-information", "/company-registration-details", "/about-your-organisation-eBkQGy", "/purpose-and-activities", "/correspondence-address", "/previous-projects-similar-to-this-one", "/charity-registration-details", "/registration-details", "/parent-organisation-details", "/trading-subsidiaries", "/how-your-organisation-is-classified-other"], "all_possible_previous_direct_next": ["/alternative-names-of-your-organisation", "/organisation-names", "/organisation-address", "/joint-applications", "/company-registration-details", "/registration-details", "/how-your-organisation-is-classified", "/purpose-and-activities", "/about-your-organisation-eBkQGy", "/correspondence-address", "/previous-projects-similar-to-this-one", "/charity-registration-details", "/how-your-organisation-is-classified-other", "/parent-organisation-details", "/trading-subsidiaries"], "all_possible_after": ["/summary", "/partner-organisation-details"]}, {"path": "/partner-organisation-details", "next_paths": ["/summary"], "all_direct_previous": ["/joint-applications"], "direct_next_of_direct_previous": ["/summary", "/partner-organisation-details"], "all_possible_next_of_siblings": ["/summary"], "all_possible_previous": ["/alternative-names-of-your-organisation", "/organisation-names", "/organisation-address", "/joint-applications", "/how-your-organisation-is-classified", "/organisation-information", "/company-registration-details", "/about-your-organisation-eBkQGy", "/purpose-and-activities", "/correspondence-address", "/previous-projects-similar-to-this-one", "/charity-registration-details", "/registration-details", "/parent-organisation-details", "/trading-subsidiaries", "/how-your-organisation-is-classified-other"], "all_possible_previous_direct_next": ["/alternative-names-of-your-organisation", "/organisation-names", "/organisation-address", "/joint-applications", "/company-registration-details", "/registration-details", "/how-your-organisation-is-classified", "/partner-organisation-details", "/purpose-and-activities", "/summary", "/correspondence-address", "/previous-projects-similar-to-this-one", "/about-your-organisation-eBkQGy", "/charity-registration-details", "/how-your-organisation-is-classified-other", "/parent-organisation-details", "/trading-subsidiaries"], "all_possible_after": ["/summary"]}, {"path": "/summary", "next_paths": [], "all_direct_previous": ["/joint-applications", "/partner-organisation-details"], "direct_next_of_direct_previous": ["/summary", "/partner-organisation-details"], "all_possible_next_of_siblings": ["/summary"], "all_possible_previous": ["/alternative-names-of-your-organisation", "/organisation-names", "/organisation-address", "/joint-applications", "/how-your-organisation-is-classified", "/organisation-information", "/company-registration-details", "/partner-organisation-details", "/about-your-organisation-eBkQGy", "/purpose-and-activities", "/correspondence-address", "/previous-projects-similar-to-this-one", "/charity-registration-details", "/registration-details", "/parent-organisation-details", "/trading-subsidiaries", "/how-your-organisation-is-classified-other"], "all_possible_previous_direct_next": ["/alternative-names-of-your-organisation", "/organisation-names", "/organisation-address", "/joint-applications", "/company-registration-details", "/registration-details", "/how-your-organisation-is-classified", "/partner-organisation-details", "/purpose-and-activities", "/summary", "/correspondence-address", "/previous-projects-similar-to-this-one", "/about-your-organisation-eBkQGy", "/charity-registration-details", "/how-your-organisation-is-classified-other", "/parent-organisation-details", "/trading-subsidiaries"], "all_possible_after": []}, {"path": "/company-registration-details", "next_paths": ["/registration-details"], "all_direct_previous": ["/how-your-organisation-is-classified"], "direct_next_of_direct_previous": ["/company-registration-details", "/how-your-organisation-is-classified-other", "/charity-registration-details", "/trading-subsidiaries"], "all_possible_next_of_siblings": ["/organisation-address", "/joint-applications", "/partner-organisation-details", "/about-your-organisation-eBkQGy", "/summary", "/correspondence-address", "/registration-details", "/parent-organisation-details", "/trading-subsidiaries"], "all_possible_previous": ["/alternative-names-of-your-organisation", "/how-your-organisation-is-classified", "/organisation-information", "/purpose-and-activities", "/previous-projects-similar-to-this-one", "/organisation-names"], "all_possible_previous_direct_next": ["/alternative-names-of-your-organisation", "/organisation-names", "/company-registration-details", "/how-your-organisation-is-classified", "/purpose-and-activities", "/previous-projects-similar-to-this-one", "/charity-registration-details", "/how-your-organisation-is-classified-other", "/trading-subsidiaries"], "all_possible_after": ["/organisation-address", "/joint-applications", "/partner-organisation-details", "/about-your-organisation-eBkQGy", "/summary", "/correspondence-address", "/registration-details", "/parent-organisation-details", "/trading-subsidiaries"]}, {"path": "/about-your-organisation-eBkQGy", "next_paths": ["/trading-subsidiaries"], "all_direct_previous": ["/registration-details"], "direct_next_of_direct_previous": ["/trading-subsidiaries", "/about-your-organisation-eBkQGy"], "all_possible_next_of_siblings": ["/organisation-address", "/joint-applications", "/partner-organisation-details", "/summary", "/correspondence-address", "/parent-organisation-details", "/trading-subsidiaries"], "all_possible_previous": ["/alternative-names-of-your-organisation", "/organisation-names", "/company-registration-details", "/how-your-organisation-is-classified", "/organisation-information", "/purpose-and-activities", "/previous-projects-similar-to-this-one", "/registration-details", "/how-your-organisation-is-classified-other"], "all_possible_previous_direct_next": ["/alternative-names-of-your-organisation", "/organisation-names", "/company-registration-details", "/how-your-organisation-is-classified", "/purpose-and-activities", "/about-your-organisation-eBkQGy", "/previous-projects-similar-to-this-one", "/charity-registration-details", "/registration-details", "/trading-subsidiaries", "/how-your-organisation-is-classified-other"], "all_possible_after": ["/organisation-address", "/joint-applications", "/partner-organisation-details", "/summary", "/correspondence-address", "/parent-organisation-details", "/trading-subsidiaries"]}]} diff --git a/fund_store/tests/test_data/all_questions/metadata/metadata_risk_cyp.json b/fund_store/tests/test_data/all_questions/metadata/metadata_risk_cyp.json new file mode 100644 index 000000000..60d1e4469 --- /dev/null +++ b/fund_store/tests/test_data/all_questions/metadata/metadata_risk_cyp.json @@ -0,0 +1,130 @@ +{ + "start_page": "/intro-risk-and-deliverability", + "all_pages": [ + { + "path": "/summary", + "next_paths": [], + "all_direct_previous": [ + "/organisation-governance-structure" + ], + "direct_next_of_direct_previous": [ + "/summary" + ], + "all_possible_next_of_siblings": [], + "all_possible_previous": [ + "/organisation-governance-structure", + "/risks-to-the-project", + "/who-is-responsible-risk-register", + "/intro-risk-and-deliverability" + ], + "all_possible_previous_direct_next": [ + "/organisation-governance-structure", + "/risks-to-the-project", + "/who-is-responsible-risk-register", + "/summary" + ], + "all_possible_after": [] + }, + { + "path": "/intro-risk-and-deliverability", + "next_paths": [ + "/risks-to-the-project" + ], + "all_direct_previous": [], + "direct_next_of_direct_previous": [], + "all_possible_next_of_siblings": [], + "all_possible_previous": [], + "all_possible_previous_direct_next": [], + "all_possible_after": [ + "/organisation-governance-structure", + "/risks-to-the-project", + "/who-is-responsible-risk-register", + "/summary" + ] + }, + { + "path": "/risks-to-the-project", + "next_paths": [ + "/who-is-responsible-risk-register" + ], + "all_direct_previous": [ + "/intro-risk-and-deliverability" + ], + "direct_next_of_direct_previous": [ + "/risks-to-the-project" + ], + "all_possible_next_of_siblings": [ + "/organisation-governance-structure", + "/who-is-responsible-risk-register", + "/summary" + ], + "all_possible_previous": [ + "/intro-risk-and-deliverability" + ], + "all_possible_previous_direct_next": [ + "/risks-to-the-project" + ], + "all_possible_after": [ + "/organisation-governance-structure", + "/who-is-responsible-risk-register", + "/summary" + ] + }, + { + "path": "/who-is-responsible-risk-register", + "next_paths": [ + "/organisation-governance-structure" + ], + "all_direct_previous": [ + "/risks-to-the-project" + ], + "direct_next_of_direct_previous": [ + "/who-is-responsible-risk-register" + ], + "all_possible_next_of_siblings": [ + "/organisation-governance-structure", + "/summary" + ], + "all_possible_previous": [ + "/risks-to-the-project", + "/intro-risk-and-deliverability" + ], + "all_possible_previous_direct_next": [ + "/risks-to-the-project", + "/who-is-responsible-risk-register" + ], + "all_possible_after": [ + "/organisation-governance-structure", + "/summary" + ] + }, + { + "path": "/organisation-governance-structure", + "next_paths": [ + "/summary" + ], + "all_direct_previous": [ + "/who-is-responsible-risk-register" + ], + "direct_next_of_direct_previous": [ + "/organisation-governance-structure" + ], + "all_possible_next_of_siblings": [ + "/summary" + ], + "all_possible_previous": [ + "/risks-to-the-project", + "/who-is-responsible-risk-register", + "/intro-risk-and-deliverability" + ], + "all_possible_previous_direct_next": [ + "/organisation-governance-structure", + "/risks-to-the-project", + "/who-is-responsible-risk-register" + ], + "all_possible_after": [ + "/summary" + ] + } + ] +} diff --git a/fund_store/tests/test_data/all_questions/metadata/start_to_main_activites.json b/fund_store/tests/test_data/all_questions/metadata/start_to_main_activites.json new file mode 100644 index 000000000..84ea9f8c4 --- /dev/null +++ b/fund_store/tests/test_data/all_questions/metadata/start_to_main_activites.json @@ -0,0 +1 @@ +{"start_page": "/intro-about-your-organisation", "all_pages": [{"path": "/intro-about-your-organisation", "next_paths": ["/organisation-details"], "all_direct_previous": [], "direct_next_of_direct_previous": [], "all_possible_next_of_siblings": [], "all_possible_previous": [], "all_possible_previous_direct_next": [], "all_possible_after": ["/how-is-your-organisation-classified-other", "/summary", "/tell-us-about-your-organisations-main-activities", "/joint-bid", "/partner-organisation-details", "/organisation-details", "/charity-number", "/organisation-address", "/agreement-exists", "/alternative-organisation-name", "/how-is-your-organisation-classified", "/work-with-partner-organisations", "/alternative-organisation-address", "/company-registration-number", "/website-and-social-media"]}, {"path": "/alternative-organisation-name", "next_paths": ["/tell-us-about-your-organisations-main-activities"], "all_direct_previous": ["/organisation-details"], "direct_next_of_direct_previous": ["/alternative-organisation-name", "/tell-us-about-your-organisations-main-activities"], "all_possible_next_of_siblings": ["/how-is-your-organisation-classified-other", "/summary", "/tell-us-about-your-organisations-main-activities", "/joint-bid", "/partner-organisation-details", "/charity-number", "/organisation-address", "/agreement-exists", "/how-is-your-organisation-classified", "/work-with-partner-organisations", "/alternative-organisation-address", "/company-registration-number", "/website-and-social-media"], "all_possible_previous": ["/intro-about-your-organisation", "/organisation-details"], "all_possible_previous_direct_next": ["/alternative-organisation-name", "/organisation-details", "/tell-us-about-your-organisations-main-activities"], "all_possible_after": ["/how-is-your-organisation-classified-other", "/summary", "/tell-us-about-your-organisations-main-activities", "/joint-bid", "/partner-organisation-details", "/charity-number", "/organisation-address", "/agreement-exists", "/how-is-your-organisation-classified", "/work-with-partner-organisations", "/alternative-organisation-address", "/company-registration-number", "/website-and-social-media"]}, {"path": "/organisation-details", "next_paths": ["/alternative-organisation-name", "/tell-us-about-your-organisations-main-activities"], "all_direct_previous": ["/intro-about-your-organisation"], "direct_next_of_direct_previous": ["/organisation-details"], "all_possible_next_of_siblings": ["/how-is-your-organisation-classified-other", "/summary", "/tell-us-about-your-organisations-main-activities", "/joint-bid", "/partner-organisation-details", "/charity-number", "/organisation-address", "/agreement-exists", "/alternative-organisation-name", "/how-is-your-organisation-classified", "/work-with-partner-organisations", "/alternative-organisation-address", "/company-registration-number", "/website-and-social-media"], "all_possible_previous": ["/intro-about-your-organisation"], "all_possible_previous_direct_next": ["/organisation-details"], "all_possible_after": ["/how-is-your-organisation-classified-other", "/summary", "/tell-us-about-your-organisations-main-activities", "/joint-bid", "/partner-organisation-details", "/charity-number", "/organisation-address", "/agreement-exists", "/alternative-organisation-name", "/how-is-your-organisation-classified", "/work-with-partner-organisations", "/alternative-organisation-address", "/company-registration-number", "/website-and-social-media"]}, {"path": "/tell-us-about-your-organisations-main-activities", "next_paths": [], "all_direct_previous": ["/organisation-details", "/alternative-organisation-name"], "direct_next_of_direct_previous": ["/alternative-organisation-name", "/tell-us-about-your-organisations-main-activities"], "all_possible_next_of_siblings": ["/how-is-your-organisation-classified-other", "/summary", "/tell-us-about-your-organisations-main-activities", "/joint-bid", "/partner-organisation-details", "/charity-number", "/organisation-address", "/agreement-exists", "/how-is-your-organisation-classified", "/work-with-partner-organisations", "/alternative-organisation-address", "/company-registration-number", "/website-and-social-media"], "all_possible_previous": ["/intro-about-your-organisation", "/alternative-organisation-name", "/organisation-details"], "all_possible_previous_direct_next": ["/alternative-organisation-name", "/organisation-details", "/tell-us-about-your-organisations-main-activities"], "all_possible_after": []}]} diff --git a/fund_store/tests/test_data/cof_eoi.json b/fund_store/tests/test_data/cof_eoi.json new file mode 100644 index 000000000..66f7dade7 --- /dev/null +++ b/fund_store/tests/test_data/cof_eoi.json @@ -0,0 +1,410 @@ +[ + { + "name": "Access funding cof-eoi-declaration", + "questions": [ + { + "question": "Declaration", + "fields": [ + { + "key": "YRDRQa", + "title": "I confirm", + "type": "list", + "answer": "confirm" + } + ] + } + ], + "metadata": { + "isSummaryPageSubmit": true, + "paymentSkipped": false + } + }, + { + "name": "Access funding cof-eoi-details", + "questions": [ + { + "category": "eAEGhH", + "question": "Organisation names", + "fields": [ + { + "key": "SMRWjl", + "title": "Organisation name", + "type": "text", + "answer": "test org name" + }, + { + "key": "SxkwhF", + "title": "Does your organisation have any alternative names?", + "type": "list", + "answer": false + } + ] + }, + { + "category": "eAEGhH", + "question": "Organisation address", + "fields": [ + { + "key": "OpeSdM", + "title": "Organisation address", + "type": "text", + "answer": "Line 1, null, Newport, null, NP10 8QQ" + } + ] + }, + { + "category": "eAEGhH", + "question": "How your organisation is classified", + "fields": [ + { + "key": "uYiLsv", + "title": "Organisation classification", + "type": "list", + "answer": "Charitable incorporated organisation" + } + ] + }, + { + "category": "eAEGhH", + "question": "Is your organisation subject to any insolvency actions?", + "fields": [ + { + "key": "NcQSbU", + "title": "Is your organisation subject to any insolvency actions?", + "type": "list", + "answer": false + } + ] + } + ], + "metadata": { + "isSummaryPageSubmit": true, + "paymentSkipped": false + } + }, + { + "name": "Access funding cof-eoi-funding", + "questions": [ + { + "category": "seZPbt", + "question": "What do you plan to use COF's funding for?", + "fields": [ + { + "key": "aocRmv", + "title": "What do you plan to use COF's funding for?", + "type": "list", + "answer": [ + "Renovate a leasehold" + ] + } + ] + }, + { + "category": "seZPbt", + "question": "Will the leasehold have at least 15 years when your organisation submits a full application?", + "fields": [ + { + "key": "foQgiy", + "title": "Will the leasehold have at least 15 years when your organisation submits a full application?", + "type": "list", + "answer": true + } + ] + }, + { + "category": "seZPbt", + "question": "How much capital funding are you requesting from COF?", + "fields": [ + { + "key": "fZAMFv", + "title": "How much capital funding are you requesting from COF?", + "type": "text", + "answer": "123123" + } + ] + }, + { + "category": "seZPbt", + "question": "Do you plan to request any revenue funding?", + "fields": [ + { + "key": "oblxxv", + "title": "Do you plan to request any revenue funding?", + "type": "list", + "answer": true + } + ] + }, + { + "category": "seZPbt", + "question": "Do you plan to secure match funding? (either cash or in-kind)", + "fields": [ + { + "key": "eOWKoO", + "title": "Do you plan to secure match funding?", + "type": "list", + "answer": true + } + ] + }, + { + "category": "seZPbt", + "question": "Where do you plan to source match funding?", + "fields": [ + { + "key": "BykoQQ", + "title": "In-kind match funding", + "type": "list", + "answer": [ + "Donation of services" + ] + }, + { + "key": "qgytim", + "title": "Cash match funding", + "type": "list", + "answer": [ + "Your own financial resources" + ] + }, + { + "key": "daJkaD", + "title": "Or", + "type": "list", + "answer": [ + "Not sure" + ] + } + ] + }, + { + "category": "seZPbt", + "question": "Does your project include an element of housing?", + "fields": [ + { + "key": "yZxdeJ", + "title": "Does your project include an element of housing?", + "type": "list", + "answer": true + } + ] + }, + { + "category": "seZPbt", + "question": "Will you need planning permission for your project?", + "fields": [ + { + "key": "UORyaF", + "title": "Will you need planning permission for your project?", + "type": "list", + "answer": "Yes" + } + ] + } + ], + "metadata": { + "isSummaryPageSubmit": true, + "paymentSkipped": false + } + }, + { + "name": "Access funding cof-eoi-support", + "questions": [ + { + "category": "IEwDfr", + "question": "Do you wish to be contacted by the development support provider, if eligible for in-depth support?", + "fields": [ + { + "key": "iXmKyq", + "title": "Do you wish to be contacted by the development support provider, if eligible for in-depth support?", + "type": "list", + "answer": true + } + ] + }, + { + "category": "IEwDfr", + "question": "What are the main things you feel you need support with to submit a good COF application?", + "fields": [ + { + "key": "ObIBSx", + "title": "What are the main things you feel you need support with to submit a good COF application?", + "type": "freeText", + "answer": "

      adsfasdfasdf

      \r\n

      need some support with filling out the form

      " + } + ] + }, + { + "category": "IEwDfr", + "question": "Describe your project and its aims", + "fields": [ + { + "key": "MxzEYq", + "title": "Describe your project and its aims", + "type": "freeText", + "answer": "

      To build stuff

      " + } + ] + }, + { + "category": "IEwDfr", + "question": "Lead contact details", + "fields": [ + { + "key": "xWnVof", + "title": "Name of lead contact", + "type": "text", + "answer": "Bandit Healer" + }, + { + "key": "NQoGIm", + "title": "Lead contact email address", + "type": "text", + "answer": "bandit@bluey.com" + }, + { + "key": "srxZmv", + "title": "Lead contact telephone number", + "type": "text", + "answer": "0123123123" + } + ] + } + ], + "metadata": { + "isSummaryPageSubmit": true, + "paymentSkipped": false + } + }, + { + "name": "Access funding cof-eoi-asset", + "questions": [ + { + "category": "McpePG", + "question": "Does your organisation plan both to receive the funding and run the project?", + "fields": [ + { + "key": "eEaDGz", + "title": "Does your organisation plan both to receive the funding and run the project?", + "type": "list", + "answer": true + } + ] + }, + { + "category": "McpePG", + "question": "Type of asset", + "fields": [ + { + "key": "Ihjjyi", + "title": "Type of asset", + "type": "list", + "answer": "Community centre" + } + ] + }, + { + "category": "McpePG", + "question": "Is the asset based in the UK?", + "fields": [ + { + "key": "zurxox", + "title": "Is the asset based in the UK?", + "type": "list", + "answer": true + } + ] + }, + { + "category": "McpePG", + "question": "Address of the asset", + "fields": [ + { + "key": "dnqIdW", + "title": "Address of the asset", + "type": "text", + "answer": "asdf, null, asdf, null, NP11 3RD" + } + ] + }, + { + "category": "McpePG", + "question": "Is the asset at risk?", + "fields": [ + { + "key": "lLQmNb", + "title": "Is the asset at risk?", + "type": "list", + "answer": true + } + ] + }, + { + "category": "McpePG", + "question": "What is the risk to the asset?", + "fields": [ + { + "key": "ilMbMH", + "title": "What is the risk to the asset?", + "type": "list", + "answer": [ + "Closure or end of lease" + ] + } + ] + }, + { + "category": "McpePG", + "question": "Has the asset ever been used by or had significance to the community?", + "fields": [ + { + "key": "fBhSNc", + "title": "Has the asset ever been used by or had significance to the community?", + "type": "list", + "answer": true + } + ] + }, + { + "category": "McpePG", + "question": "Do you already own the asset?", + "fields": [ + { + "key": "cPcZos", + "title": "Do you already own the asset?", + "type": "list", + "answer": false + } + ] + }, + { + "category": "McpePG", + "question": "Does the asset belong to a public authority?", + "fields": [ + { + "key": "XuAyrs", + "title": "Does the asset belong to a public authority?", + "type": "list", + "answer": "Yes, a town, parish or community council" + } + ] + }, + { + "category": "McpePG", + "question": "Select the option which best represents the stage you are at in purchasing the asset", + "fields": [ + { + "key": "oDhZlw", + "title": "Stage purchasing asset", + "type": "list", + "answer": "Do not know who owner is" + } + ] + } + ], + "metadata": { + "isSummaryPageSubmit": true, + "paymentSkipped": false + } + } +] diff --git a/fund_store/tests/test_data/test_fab_round_config.py b/fund_store/tests/test_data/test_fab_round_config.py new file mode 100644 index 000000000..d4ae85d80 --- /dev/null +++ b/fund_store/tests/test_data/test_fab_round_config.py @@ -0,0 +1,63 @@ +LOADER_CONFIG = { + "base_path": 0, + "sections_config": [ + {"section_name": {"en": "1. Test Section", "cy": ""}, "tree_path": "0.1.1", "requires_feedback": None}, + { + "section_name": {"en": "1.1 Name your application", "cy": ""}, + "tree_path": "0.1.1.1", + "form_name_json": {"en": "organisation-information", "cy": ""}, + }, + {"section_name": {"en": "2. Test Section 2", "cy": ""}, "tree_path": "0.1.2", "requires_feedback": None}, + { + "section_name": {"en": "2.1 Asset information", "cy": ""}, + "tree_path": "0.1.2.1", + "form_name_json": {"en": "asset-information-cof-r3-w2", "cy": ""}, + }, + ], + "fund_config": { + "id": "b33ea578-b35b-4508-994a-7589a9c9c060", + "short_name": "T", + "welsh_available": False, + "owner_organisation_name": "", + "owner_organisation_shortname": "", + "owner_organisation_logo_uri": "", + "name_json": {"en": "Test"}, + "title_json": {"en": "Test Fund"}, + "description_json": {"en": "test"}, + "ggis_scheme_reference_number": "", + }, + "round_config": { + "id": "1a2d6043-689b-4472-a09c-fd8fcfd20151", + "fund_id": "b33ea578-b35b-4508-994a-7589a9c9c060", + "short_name": "T", + "opens": "2025-12-12T12:00:00", + "assessment_start": "2026-01-12T12:00:00", + "deadline": "2025-12-24T12:00:00", + "application_reminder_sent": False, + "reminder_date": "2026-01-23T12:00:00", + "assessment_deadline": "2026-01-24T12:00:00", + "prospectus": "https://www.google.com", + "privacy_notice": "https://www.google.com", + "reference_contact_page_over_email": False, + "contact_email": "test@hotmail.com", + "contact_phone": "07555094188", + "contact_textphone": "07555094188", + "support_times": "no", + "support_days": "Mon", + "instructions_json": {"en": "None", "cy": None}, + "feedback_link": "https://www.google.com", + "project_name_field_id": "Test", + "application_guidance_json": {"en": "None", "cy": None}, + "guidance_url": "https://www.google.com", + "all_uploaded_documents_section_available": False, + "application_fields_download_available": False, + "display_logo_on_pdf_exports": False, + "mark_as_complete_enabled": False, + "is_expression_of_interest": False, + "eoi_decision_schema": None, + "feedback_survey_config": "None", + "eligibility_config": {"has_eligibility": False}, + "title_json": {"en": "Tree"}, + "contact_us_banner_json": {"en": "None", "cy": None}, + }, +} diff --git a/fund_store/tests/test_data_fund_round.py b/fund_store/tests/test_data_fund_round.py new file mode 100644 index 000000000..89347ee4e --- /dev/null +++ b/fund_store/tests/test_data_fund_round.py @@ -0,0 +1,86 @@ +import pytest + +from db.models.fund import FundingType +from db.queries import ( + get_all_funds, + get_fund_by_id, + get_fund_by_short_name, + get_round_by_id, + get_round_by_short_name, + get_rounds_for_fund_by_id, + get_rounds_for_fund_by_short_name, +) + + +def test_get_fund_by_id(seed_dynamic_data): + f = get_fund_by_id(seed_dynamic_data["funds"][0]["id"]) + assert f.name_json["en"] == "Unit Test Fund 1" + assert f.short_name == "FND1" + assert hasattr(f, "ggis_scheme_reference_number") + assert f.ggis_scheme_reference_number is None + + +def test_get_fund_by_short_name(seed_dynamic_data): + f = get_fund_by_short_name("FND1") + assert f.name_json["en"] == "Unit Test Fund 1" + assert f.short_name == "FND1" + + +def test_get_round_by_id(seed_dynamic_data): + r = get_round_by_id( + seed_dynamic_data["funds"][0]["id"], + seed_dynamic_data["funds"][0]["rounds"][0]["id"], + ) + assert r.title_json["en"] == "Unit Test Round 1" + assert r.short_name == "RND1" + assert str(r.id) == seed_dynamic_data["funds"][0]["rounds"][0]["id"] + + +def test_get_rounds_for_fund_by_id(seed_dynamic_data): + r = get_rounds_for_fund_by_id(seed_dynamic_data["funds"][0]["id"]) + assert len(r) == 2 + # assert r[0].title_json["en"] == "Unit Test Round 1" + assert r[0].short_name == "RND1" + assert r[1].short_name == "RND2" + assert str(r[0].id) == seed_dynamic_data["funds"][0]["rounds"][0]["id"] + + +def test_get_rounds_for_fund_by_short_name(seed_dynamic_data): + r = get_rounds_for_fund_by_short_name("FND1") + assert len(r) == 2 + # assert r[0].title_json["en"] == "Unit Test Round 1" + assert r[0].short_name == "RND1" + assert r[1].short_name == "RND2" + assert str(r[0].id) == seed_dynamic_data["funds"][0]["rounds"][0]["id"] + + +@pytest.mark.parametrize( + "fund_short_name, round_short_name, expected_none, expected_title", + [ + ("FND1", "RND1", False, "Unit Test Round 1"), + ("fnd1", "rnd1", False, "Unit Test Round 1"), + ("bad", "rnd1", True, None), + ("fund", "bad", True, None), + ("FND1", "RND2", False, "Unit Test Round 2"), + ], +) +def test_get_round_by_short_name( + fund_short_name, + round_short_name, + expected_none, + expected_title, + seed_dynamic_data, +): + r = get_round_by_short_name(fund_short_name, round_short_name) + + if expected_none: + assert r is None + else: + assert r.title_json["en"] == expected_title + + +def test_get_all_funds(seed_dynamic_data): + result = get_all_funds() + assert len(result) == 1 + assert result[0].name_json["en"] == "Unit Test Fund 1" + assert result[0].funding_type == FundingType.COMPETITIVE diff --git a/fund_store/tests/test_data_sections.py b/fund_store/tests/test_data_sections.py new file mode 100644 index 000000000..96f1254c4 --- /dev/null +++ b/fund_store/tests/test_data_sections.py @@ -0,0 +1,64 @@ +from typing import List + +import pytest + +from config.fund_loader_config.cof.cof_r2 import ( + APPLICATION_BASE_PATH, + ASSESSMENT_BASE_PATH, + COF_ROUND_2_WINDOW_2_ID, + cof_r2_sections, + fund_config, + rounds_config, +) +from db.models.section import Section +from db.queries import ( + get_application_sections_for_round, + get_assessment_sections_for_round, + insert_base_sections, + insert_fund_data, + insert_or_update_application_sections, + upsert_round_data, +) + + +def test_get_application_sections(seed_dynamic_data): + sections: List[Section] = get_application_sections_for_round( + seed_dynamic_data["funds"][0]["id"], + seed_dynamic_data["funds"][0]["rounds"][0]["id"], + ) + assert len(sections) == 3 + first, second, third = sections + assert first.title_json == {"en": "Middle1"} + assert len(first.children) == 1 + assert second.title_json == {"en": "Middle2"} + assert len(second.children) == 1 + assert third.title_json == {"en": "skills"} + assert len(third.children) == 0 + + +def test_get_assessment_sections(seed_dynamic_data): + sections: List[Section] = get_assessment_sections_for_round( + seed_dynamic_data["funds"][0]["id"], + seed_dynamic_data["funds"][0]["rounds"][0]["id"], + "", + ) + assert len(sections) == 1 + assert sections[0].title_json == {"en": "assess section 1"} + assert len(sections[0].children) == 1 + assert sections[0].children[0].title_json == {"en": "assess section 1 a"} + assert len(sections[0].children[0].children) == 0 + + +def test_load_application_sections(clear_test_data): + insert_fund_data(fund_config) + upsert_round_data(rounds_config) + + base_sections = insert_base_sections(APPLICATION_BASE_PATH, ASSESSMENT_BASE_PATH, COF_ROUND_2_WINDOW_2_ID) + application_sections = insert_or_update_application_sections(COF_ROUND_2_WINDOW_2_ID, cof_r2_sections) + assert len(base_sections) == 2 + assert len(application_sections) == 28 + + +@pytest.mark.skip(reason="not implemented assessment loading yet") +def test_load_assessment_sections(): + pass diff --git a/fund_store/tests/test_data_update_scripts.py b/fund_store/tests/test_data_update_scripts.py new file mode 100644 index 000000000..f36718ace --- /dev/null +++ b/fund_store/tests/test_data_update_scripts.py @@ -0,0 +1,87 @@ +from scripts.data_updates.FS2910_ns_links import update_rounds_with_links +from scripts.data_updates.FS2956_ns_weightings import update_section_weightings +from scripts.data_updates.patch_cyp_name import update_fund_name + +from db.queries import get_application_sections_for_round, get_fund_by_id, get_round_by_id +from db.schemas.fund import FundSchema +from db.schemas.round import RoundSchema +from db.schemas.section import SectionSchema + + +def test_update_section_weightings(seed_dynamic_data): + sections = get_application_sections_for_round( + seed_dynamic_data["funds"][0]["id"], + seed_dynamic_data["funds"][0]["rounds"][0]["id"], + ) + section_to_update = None + for s in sections: + if "skill" in s.title_json["en"]: + section_to_update = s + assert section_to_update is not None, "Unable to find expected test data before updates" + + section_data = SectionSchema().dump(section_to_update) + section_data["tree_path"] = section_to_update.path + section_data["section_name"] = section_to_update.title_json + section_data["weighting"] = "12" + update_section_weightings(section_data) + + sections = get_application_sections_for_round( + seed_dynamic_data["funds"][0]["id"], + seed_dynamic_data["funds"][0]["rounds"][0]["id"], + ) + + for s in sections: + if "skill" in s.title_json["en"]: + section = s + assert section.weighting == 12 + + +def test_update_links_present(seed_dynamic_data): + r = get_round_by_id( + seed_dynamic_data["funds"][0]["id"], + seed_dynamic_data["funds"][0]["rounds"][0]["id"], + ) + round_data = RoundSchema().dump(r) + round_data["privacy_notice"] = "new privacy notice" + round_data["prospectus"] = "new prospectus" + + update_rounds_with_links([round_data]) + + r = get_round_by_id( + seed_dynamic_data["funds"][0]["id"], + seed_dynamic_data["funds"][0]["rounds"][0]["id"], + ) + + assert r.privacy_notice == "new privacy notice" + assert r.prospectus == "new prospectus" + + +def test_update_links_not_present(seed_dynamic_data): + r = get_round_by_id( + seed_dynamic_data["funds"][0]["id"], + seed_dynamic_data["funds"][0]["rounds"][1]["id"], + ) + round_data = RoundSchema().dump(r) + round_data["privacy_notice"] = "" + round_data["prospectus"] = "" + + update_rounds_with_links([round_data]) + + r = get_round_by_id( + seed_dynamic_data["funds"][0]["id"], + seed_dynamic_data["funds"][0]["rounds"][1]["id"], + ) + + assert r.privacy_notice == "http://google.com" + assert r.prospectus == "http://google.com" + + +def test_update_fund_name(seed_dynamic_data): + f = get_fund_by_id(seed_dynamic_data["funds"][0]["id"]) + fund_data = FundSchema().dump(f) + fund_data["name_json"] = "new name json" + + update_fund_name(fund_config=fund_data) + + f = get_fund_by_id(seed_dynamic_data["funds"][0]["id"]) + assert f.name_json == "new name json" diff --git a/fund_store/tests/test_db_routes.py b/fund_store/tests/test_db_routes.py new file mode 100644 index 000000000..4b2ef6775 --- /dev/null +++ b/fund_store/tests/test_db_routes.py @@ -0,0 +1,339 @@ +from copy import deepcopy +from datetime import datetime +from unittest.mock import patch + +from api.routes import is_valid_uuid +from fsd_test_utils.test_config.useful_config import UsefulConfig + +from db.models.event import EventType + + +def test_valid_uuid(): + uuid = "a357e264-7ef1-4f9a-be1b-6228f80c65ea" + assert is_valid_uuid(uuid) is True + uuid = "A357E264-7EF1-4F9A-BE1B-6228F80C65EA" + assert is_valid_uuid(uuid) is True + # Test Wrong UUID characters + uuid = "A357E264-7EF1-4F9A-BE1B-6228FBBBBBBBB" + assert is_valid_uuid(uuid) is False + + +def test_invalid_random(): + uuid = "abc123" + assert is_valid_uuid(uuid) is False + + +def test_invalid_None_uuid(): + uuid = None + assert is_valid_uuid(uuid) is False + uuid = "" + assert is_valid_uuid(uuid) is False + + +def test_get_fund_by_id(flask_test_client, mock_get_fund_round, mocker): + mocker.patch("api.routes.is_valid_uuid", return_value=True) + response = flask_test_client.get("/funds/123") + assert response.status_code == 200 + result = response.json() + assert result["name"] == "Fund Name 1" + assert result["funding_type"] == "COMPETITIVE" + + +def test_get_fund_by_invalid_id(flask_test_client, mocker): + mocker.patch("api.routes.get_fund_by_id", return_value=None) + response = flask_test_client.get("/funds/None") + assert response.status_code == 404 + + +def test_get_fund_by_short_name(flask_test_client, mock_get_fund_round, mocker): + mocker.patch("api.routes.is_valid_uuid", return_value=True) + response = flask_test_client.get("/funds/ABC?use_short_name=True") + assert response.status_code == 200 + result = response.json() + assert result["name"] == "Fund Name 1" + + +def test_get_round_by_short_name(flask_test_client, mock_get_fund_round, mocker): + mocker.patch("api.routes.is_valid_uuid", return_value=True) + response = flask_test_client.get("/funds/FND1/rounds/RND1?use_short_name=True") + assert response.status_code == 200 + result = response.json() + assert result["title"] == "Round 1" + + +def test_get_eoi_decision_schema(flask_test_client, mock_get_fund_round, mocker): + mocker.patch("api.routes.is_valid_uuid", return_value=True) + response = flask_test_client.get("/funds/FND1/rounds/RND1/eoi_decision_schema?use_short_name=True") + assert response.status_code == 200 + result = response.json() + assert result == {} + + +def test_get_round_by_id(flask_test_client, mock_get_fund_round, mocker): + mocker.patch("api.routes.is_valid_uuid", return_value=True) + response = flask_test_client.get("/funds/FND1/rounds/RND1") + assert response.status_code == 200 + result = response.json() + assert result["title"] == "Round 1" + assert "eoi_decision_schema" not in result + + +def test_get_round_by_bad_id(flask_test_client, mocker): + mocker.patch("api.routes.is_valid_uuid", return_value=True) + mocker.patch("api.routes.get_round_by_id", return_value=None) + response = flask_test_client.get("/funds/FND1/rounds/RND1") + assert response.status_code == 404 + + +def test_get_eoi_decision_schema_bad_id(flask_test_client, mocker): + mocker.patch("api.routes.is_valid_uuid", return_value=True) + mocker.patch("api.routes.get_round_by_id", return_value=None) + response = flask_test_client.get("/funds/xxxxx/rounds/xxxxx/eoi_decision_schema") + assert response.status_code == 404 + + +def test_get_all_funds(flask_test_client, mock_get_fund_round): + response = flask_test_client.get("/funds") + assert response.status_code == 200 + result = response.json() + assert result[0]["name"] == "Fund Name 1" + + +def test_get_all_funds_no_data(flask_test_client, mocker): + mocker.patch("api.routes.get_all_funds", return_value=[]) + response = flask_test_client.get("/funds") + assert response.status_code == 200 + + +def test_get_app_sections_for_round(flask_test_client, mock_get_sections, mocker): + mocker.patch("api.routes.is_valid_uuid", return_value=True) + response = flask_test_client.get( + f"/funds/{UsefulConfig.COF_FUND_ID}/rounds/{UsefulConfig.COF_ROUND_2_ID}/sections/application" + ) + assert response.status_code == 200 + result = response.json() + assert result[0]["title"] == "Top" + + +def test_get_assess_sections_for_round(flask_test_client, mock_get_sections, mocker): + mocker.patch("api.routes.is_valid_uuid", return_value=True) + response = flask_test_client.get( + f"/funds/{UsefulConfig.COF_FUND_ID}/rounds/{UsefulConfig.COF_ROUND_2_ID}/sections/assessment" + ) + assert response.status_code == 200 + result = response.json() + assert result[0]["title"] == "Top" + + +def test_get_events_for_round(flask_test_client, mocker): + mock_events = [ + { + "id": "1", + "round_id": "9", + "type": EventType.APPLICATION_DEADLINE_REMINDER, + "activation_date": datetime(2000, 10, 1), + "processed": None, + }, + { + "id": "2", + "round_id": "9", + "type": EventType.APPLICATION_DEADLINE_REMINDER, + "activation_date": datetime(2001, 7, 8), + "processed": datetime(2001, 8, 8), + }, + ] + mocker.patch("api.routes.is_valid_uuid", return_value=True) + expected_response = deepcopy(mock_events) + for response in expected_response: + response["activation_date"] = response["activation_date"].isoformat() + response["processed"] = response["processed"].isoformat() if response["processed"] else None + response["type"] = response["type"].value + with patch("api.routes.get_events_from_db", return_value=mock_events) as mock_get_events_for_round_from_db: + response = flask_test_client.get("/funds/some_fund_id/rounds/some_round_id/events?only_unprocessed=true") + + assert response.status_code == 200 + assert response.json() == expected_response + mock_get_events_for_round_from_db.assert_called_once_with(round_id="some_round_id", only_unprocessed=True) + + +def test_get_events_for_round_not_found(flask_test_client, mocker): + mocker.patch("api.routes.is_valid_uuid", return_value=True) + with patch("api.routes.get_events_from_db", return_value=None) as mock_get_events_for_round_from_db: + response = flask_test_client.get("/funds/some_fund_id/rounds/some_round_id/events") + + assert response.status_code == 404 + mock_get_events_for_round_from_db.assert_called_once_with(round_id="some_round_id", only_unprocessed=False) + + +def test_get_event(flask_test_client, mocker): + mock_event = { + "id": "1", + "round_id": "9", + "type": EventType.APPLICATION_DEADLINE_REMINDER, + "activation_date": datetime(2000, 10, 1), + "processed": None, + } + expected_response = deepcopy(mock_event) + expected_response["activation_date"] = expected_response["activation_date"].isoformat() + expected_response["processed"] = ( + expected_response["processed"].isoformat() if expected_response["processed"] else None + ) + expected_response["type"] = expected_response["type"].value + mocker.patch("api.routes.is_valid_uuid", return_value=True) + with patch("api.routes.get_event_from_db", return_value=mock_event) as mock_get_event_from_db: + response = flask_test_client.get("/funds/some_fund_id/rounds/some_round_id/event/123") + + assert response.status_code == 200 + assert response.json() == expected_response + mock_get_event_from_db.assert_called_once_with(round_id="some_round_id", event_id="123") + + +def test_get_event_not_found(flask_test_client, mocker): + mocker.patch("api.routes.is_valid_uuid", return_value=True) + with patch("api.routes.get_event_from_db", return_value=None) as mock_get_events_for_round_from_db: + response = flask_test_client.get("/funds/some_fund_id/rounds/some_round_id/event/123") + + assert response.status_code == 404 + mock_get_events_for_round_from_db.assert_called_once_with(round_id="some_round_id", event_id="123") + + +def test_set_event_to_processed(flask_test_client, mocker): + mock_event = { + "id": "1", + "round_id": "9", + "type": EventType.APPLICATION_DEADLINE_REMINDER, + "activation_date": datetime(2000, 10, 1), + "processed": datetime(2000, 11, 1), + } + expected_response = deepcopy(mock_event) + expected_response["activation_date"] = expected_response["activation_date"].isoformat() + expected_response["type"] = expected_response["type"].value + expected_response["processed"] = expected_response["processed"].isoformat() + mocker.patch("api.routes.is_valid_uuid", return_value=True) + with patch( + "api.routes.set_event_to_processed_in_db", return_value=mock_event + ) as mock_set_round_event_to_processed_in_db: + response = flask_test_client.put("/funds/some_fund_id/rounds/some_round_id/event/123?processed=true") + + assert response.status_code == 200 + assert response.json() == expected_response + mock_set_round_event_to_processed_in_db.assert_called_once_with(event_id="123", processed=True) + + with patch( + "api.routes.set_event_to_processed_in_db", return_value=mock_event + ) as mock_set_round_event_to_processed_in_db: + response = flask_test_client.put("/event/123?processed=true") + + assert response.status_code == 200 + assert response.json() == expected_response + mock_set_round_event_to_processed_in_db.assert_called_once_with(event_id="123", processed=True) + + +def test_get_events_by_type(flask_test_client): + mock_expected_events = [ + { + "id": "1", + "type": EventType.APPLICATION_DEADLINE_REMINDER, + "round_id": "9", + "activation_date": datetime(2000, 10, 1), + "processed": None, + }, + ] + + expected_response = deepcopy(mock_expected_events) + for response in expected_response: + response["activation_date"] = response["activation_date"].isoformat() + response["processed"] = response["processed"].isoformat() if response["processed"] else None + response["type"] = response["type"].value + with patch("api.routes.get_events_from_db", return_value=mock_expected_events) as mock_get_events_by_type_from_db: + response = flask_test_client.get("/events/APPLICATION_DEADLINE_REMINDER?only_unprocessed=true") + + assert response.status_code == 200 + assert response.json() == expected_response + mock_get_events_by_type_from_db.assert_called_once_with( + type="APPLICATION_DEADLINE_REMINDER", only_unprocessed=True + ) + + +def test_get_events_by_type_not_recognised(flask_test_client, mocker): + with patch("api.routes.get_events_from_db", return_value=None): + response = flask_test_client.get("/events/INVALID_TYPE") + + assert response.status_code == 400 + + +def test_get_events_by_type_not_found(flask_test_client, mocker): + with patch("api.routes.get_events_from_db", return_value=None) as mock_get_events_by_type_from_db: + response = flask_test_client.get("/events/APPLICATION_DEADLINE_REMINDER") + + assert response.status_code == 404 + mock_get_events_by_type_from_db.assert_called_once_with( + type="APPLICATION_DEADLINE_REMINDER", only_unprocessed=False + ) + + +def test_get_event_by_id(flask_test_client, mocker): + mock_event = { + "id": "1", + "type": EventType.APPLICATION_DEADLINE_REMINDER, + "round_id": "9", + "activation_date": datetime(2000, 10, 1), + "processed": None, + } + expected_response = deepcopy(mock_event) + expected_response["activation_date"] = expected_response["activation_date"].isoformat() + expected_response["processed"] = ( + expected_response["processed"].isoformat() if expected_response["processed"] else None + ) + expected_response["type"] = expected_response["type"].value + mocker.patch("api.routes.is_valid_uuid", return_value=True) + with patch("api.routes.get_event_from_db", return_value=mock_event) as mock_get_event_from_db: + response = flask_test_client.get("/event/1") + + assert response.status_code == 200 + assert response.json() == expected_response + mock_get_event_from_db.assert_called_once_with(event_id="1") + + +def test_get_event_by_id_not_found(flask_test_client, mocker): + mocker.patch("api.routes.is_valid_uuid", return_value=True) + with patch("api.routes.get_event_from_db", return_value=None) as mock_get_event_by_id_from_db: + response = flask_test_client.get("/event/123") + + assert response.status_code == 404 + mock_get_event_by_id_from_db.assert_called_once_with(event_id="123") + + +def test_create_event(flask_test_client, mocker): + mock_events = { + "id": "1", + "type": EventType.ACCOUNT_IMPORT, + "activation_date": datetime(2000, 10, 1), + "processed": datetime(2000, 10, 1), + "round_id": None, + } + new_event_payload = { + "type": EventType.ACCOUNT_IMPORT.value, + "activation_date": datetime(2000, 10, 1).isoformat(), + "processed": datetime(2000, 10, 1).isoformat(), + } + + expected_response = {"id": "1", "round_id": None, **new_event_payload} + mocker.patch("api.routes.create_event_in_db", return_value=mock_events) + with patch("api.routes.create_event_in_db", return_value=mock_events) as mock_create_event_in_db: + response = flask_test_client.post("/event", json=new_event_payload) + mock_create_event_in_db.assert_called_once_with(**new_event_payload, round_id=None) + + assert response.status_code == 201 + assert response.json() == expected_response + + +def test_create_event_missing_type(flask_test_client): + new_event_payload = { + "round_id": "9", + "activation_date": datetime(2000, 10, 1).isoformat(), + } + response = flask_test_client.post("/event", json=new_event_payload) + + assert response.status_code == 400 + assert response.json()["detail"] == "Post body must contain event type field" diff --git a/fund_store/tests/test_eoi_schema.py b/fund_store/tests/test_eoi_schema.py new file mode 100644 index 000000000..f510461d2 --- /dev/null +++ b/fund_store/tests/test_eoi_schema.py @@ -0,0 +1,172 @@ +import json + +import pytest +from fsd_utils import Decision, evaluate_response + +from config.fund_loader_config.cof.eoi_r1_schema import ( + COF_PLANNING_PERMISSION_CAVEAT_EN, + COF_PLANNING_PERMISSION_IF_NEEDED_CAVEAT_EN, + COF_R3_EOI_SCHEMA_EN, + COF_SECURE_MATCH_FUNDING_CAVEAT_EN, +) + + +def test_eoi_schema_throws_no_errors_with_all_forms(): + with open("tests/test_data/cof_eoi.json", "r") as f: + forms = json.loads(f.read()) + result = evaluate_response(schema=COF_R3_EOI_SCHEMA_EN, forms=forms) + + assert result + + +@pytest.mark.parametrize( + "question_key,supplied_answer,exp_decision,exp_caveats", + [ + ("non-existant-question", "anything", Decision.PASS, []), + ( + "uYiLsv", + "not-yet-incorporated", + Decision.PASS_WITH_CAVEATS, + [ + "Incorporate your organisation: You must have incorporated your" + " organisation by the time you submit a full application. If you remain" + " unincorporated, your application will be ineligible." + ], + ), + ("NcQSbU", True, Decision.FAIL, []), + ("eEaDGz", False, Decision.FAIL, []), + ("zurxox", False, Decision.FAIL, []), + ("lLQmNb", False, Decision.FAIL, []), + ("fBhSNc", False, Decision.FAIL, []), + ("eOWKoO", False, Decision.FAIL, []), + ("foQgiy", False, Decision.FAIL, []), + ( + "XuAyrs", + "Yes, a town, parish or community council", + Decision.PASS_WITH_CAVEATS, + [COF_R3_EOI_SCHEMA_EN["XuAyrs"][0]["caveat"]], + ), + ( + "XuAyrs", + "Yes, another type of public authority", + Decision.PASS_WITH_CAVEATS, + [COF_R3_EOI_SCHEMA_EN["XuAyrs"][1]["caveat"]], + ), + ( + "BykoQQ", + ["Not sure"], + Decision.PASS_WITH_CAVEATS, + [COF_R3_EOI_SCHEMA_EN["BykoQQ"][0]["caveat"]], + ), + ( + "oblxxv", + False, + Decision.PASS_WITH_CAVEATS, + [COF_R3_EOI_SCHEMA_EN["oblxxv"][0]["caveat"]], + ), + ( + "kWRuac", + "Not yet approached any funders", + Decision.PASS_WITH_CAVEATS, + [COF_SECURE_MATCH_FUNDING_CAVEAT_EN], + ), + ( + "kWRuac", + "Approached some funders but not yet secured", + Decision.PASS_WITH_CAVEATS, + [COF_SECURE_MATCH_FUNDING_CAVEAT_EN], + ), + ( + "kWRuac", + "Secured some match funding", + Decision.PASS_WITH_CAVEATS, + [COF_SECURE_MATCH_FUNDING_CAVEAT_EN], + ), + ( + "kWRuac", + "Approached all funders but not yet secured", + Decision.PASS_WITH_CAVEATS, + [COF_SECURE_MATCH_FUNDING_CAVEAT_EN], + ), + ( + "yZxdeJ", + True, + Decision.PASS_WITH_CAVEATS, + [COF_R3_EOI_SCHEMA_EN["yZxdeJ"][0]["caveat"]], + ), + ( + "UORyaF", + "Not sure", + Decision.PASS_WITH_CAVEATS, + [COF_PLANNING_PERMISSION_IF_NEEDED_CAVEAT_EN], + ), + ( + "jICagT", + "Not yet started", + Decision.PASS_WITH_CAVEATS, + [COF_PLANNING_PERMISSION_CAVEAT_EN], + ), + ( + "jICagT", + "Early stage", + Decision.PASS_WITH_CAVEATS, + [COF_PLANNING_PERMISSION_CAVEAT_EN], + ), + ("fZAMFv", "2000001", Decision.FAIL, []), + ], +) +def test_answer_and_result(question_key, supplied_answer, exp_decision, exp_caveats): + # Construct a dummy form with the supplied question and answer + forms = [ + { + "name": "COF EOI Test question", + "questions": [ + { + "question": "Test question", + "fields": [ + { + "key": question_key, + "title": "test question", + "type": "test", + "answer": supplied_answer, + } + ], + } + ], + }, + ] + # evaluate a response + result = evaluate_response(schema=COF_R3_EOI_SCHEMA_EN, forms=forms) + + assert result + assert result["decision"] == exp_decision + assert result["caveats"] == exp_caveats + + +@pytest.mark.parametrize("question_key", COF_R3_EOI_SCHEMA_EN.keys()) +def test_answers_with_non_conditioned_values(question_key): + # Construct a dummy form with the supplied question and answer + forms = [ + { + "name": "COF EOI Test question", + "questions": [ + { + "question": "Test question", + "fields": [ + { + "key": question_key, + "title": "test question", + "type": "test", + "answer": "123", + } + ], + } + ], + }, + ] + # evaluate a response + result = evaluate_response(schema=COF_R3_EOI_SCHEMA_EN, forms=forms) + + assert result + assert result["decision"] == Decision.PASS + assert result["caveats"] == [] diff --git a/fund_store/tests/test_generate_all_questions.py b/fund_store/tests/test_generate_all_questions.py new file mode 100644 index 000000000..b5ade46f0 --- /dev/null +++ b/fund_store/tests/test_generate_all_questions.py @@ -0,0 +1,393 @@ +import json +import os + +import pytest +from scripts.all_questions.generate_test_data import ( + HOW_IS_ORG_CLASSIFIED, + JOINT_BID, + START_TO_MAIN_ACTIVITIES, + generate_test_data, +) +from scripts.all_questions.metadata_utils import ( + build_components_from_page, + build_hierarchy_levels_for_page, + build_section_header, + generate_metadata, + update_wording_for_multi_input_fields, +) +from scripts.all_questions.read_forms import ( + increment_lowest_in_hierarchy, + remove_lowest_in_hierarchy, + strip_leading_numbers, +) +from scripts.generate_all_questions import find_forms_dir + +from db.models.section import Section + +TEST_METADATA_FOLDER = "./tests/test_data/all_questions/metadata/" +TEST_FORMS_FOLDER = "./tests/test_data/all_questions/forms/" + + +@pytest.mark.skip(reason="Generates test data") +def test_generate_metadata(): + """Used to save generated metadata to a file, so taht file can be used for static test data""" + filename = "organisation-information-cof-r3-w2.json" + path_to_form = os.path.join( + "/path/to/digital-form-builder/fsd_config/form_jsons/cof_r3w2/en/", + filename, + ) + with open(path_to_form, "r") as f: + form_data = json.load(f) + metadata = generate_metadata(full_form_data=form_data) + with open(os.path.join(TEST_METADATA_FOLDER, f"metadata_{filename}"), "w") as f: + json.dump(metadata, f) + + +@pytest.mark.skip(reason="Generates test data") +def test_generate_test_data(): + """Used to extract a small part of metadata for easier testing.""" + output_folder = "/some/temp/folder" + files_to_generate = [START_TO_MAIN_ACTIVITIES, HOW_IS_ORG_CLASSIFIED, JOINT_BID] + generate_test_data( + target_test_files=files_to_generate, + in_path=os.path.join(TEST_METADATA_FOLDER, "some_file_name.json"), + out_folder=output_folder, + ) + + +def test_generate_index_org_info_cof_r3w2(): + with open(os.path.join(TEST_METADATA_FOLDER, "metadata_org_info_cof_r3w2.json"), "r") as f: + form_data = json.load(f) + + results = {} + + first_page = next(p for p in form_data["all_pages"] if p["path"] == form_data["start_page"]) + build_hierarchy_levels_for_page(first_page, results, 1, form_data["all_pages"], start_page=True) + + assert len(results) == 18 + org_details_level = results["/organisation-names"] + assert results["/alternative-names-of-your-organisation"] == org_details_level + 1 + assert results["/purpose-and-activities"] == org_details_level + assert results["/previous-projects-similar-to-this-one"] == org_details_level + 1 + + assert results["/how-your-organisation-is-classified"] == org_details_level + assert results["/how-your-organisation-is-classified-other"] == org_details_level + 1 + # TODO why does this fail assert results["/registration-details"] == org_details_level + assert results["/trading-subsidiaries"] == org_details_level + assert results["/parent-organisation-details"] == org_details_level + 1 + + assert results["/organisation-address"] == org_details_level + assert results["/correspondence-address"] == org_details_level + 1 + assert results["/joint-applications"] == org_details_level + assert results["/partner-organisation-details"] == org_details_level + 1 + + +def test_generate_index_applicant_ns(): + with open(os.path.join(TEST_METADATA_FOLDER, "metadata_applicant_ns.json"), "r") as f: + form_data = json.load(f) + + results = {} + + first_page = next(p for p in form_data["all_pages"] if p["path"] == form_data["start_page"]) + build_hierarchy_levels_for_page(first_page, results, 1, form_data["all_pages"]) + + assert len(results) == 4 + start_level = results["/13-applicant-information"] + assert results["/lead-contact-details"] == start_level + assert results["/authorised-signatory-details"] == start_level + 1 + assert results["/summary"] == start_level + + +def test_generate_index_risk_cyp(): + with open(os.path.join(TEST_METADATA_FOLDER, "metadata_risk_cyp.json"), "r") as f: + form_data = json.load(f) + + results = {} + + first_page = next(p for p in form_data["all_pages"] if p["path"] == form_data["start_page"]) + build_hierarchy_levels_for_page(first_page, results, 1, form_data["all_pages"]) + + assert len(results) == 5 + start_level = results["/intro-risk-and-deliverability"] + for _, value in results.items(): + assert value == start_level + + +def test_generate_index_name_app_cyp(): + with open(os.path.join(TEST_METADATA_FOLDER, "metadata_name_app_cyp.json"), "r") as f: + form_data = json.load(f) + + results = {} + + first_page = next(p for p in form_data["all_pages"] if p["path"] == form_data["start_page"]) + build_hierarchy_levels_for_page(first_page, results, 1, form_data["all_pages"]) + + assert len(results) == 2 + start_level = results["/name-your-application"] + assert results["/summary"] == start_level + + +def test_generate_index_branch_out_multi_pages_back_to_parent_sibling(): + with open(os.path.join(TEST_METADATA_FOLDER, "joint_bid_out_and_back.json"), "r") as f: + form_data = json.load(f) + + results = {} + + first_page = next(p for p in form_data["all_pages"] if p["path"] == form_data["start_page"]) + build_hierarchy_levels_for_page(first_page, results, 1, form_data["all_pages"]) + + assert len(results) == 5 + start_level = results["/joint-bid"] + assert results["/partner-organisation-details"] == start_level + 1 + assert results["/work-with-partner-organisations"] == start_level + 1 + assert results["/agreement-exists"] == start_level + 1 + assert results["/website-and-social-media"] == start_level + + +def test_generate_index_branch_out_all_back_to_new(): + with open(os.path.join(TEST_METADATA_FOLDER, "how_is_org_classified.json"), "r") as f: + form_data = json.load(f) + + results = {} + + first_page = next(p for p in form_data["all_pages"] if p["path"] == form_data["start_page"]) + build_hierarchy_levels_for_page(first_page, results, 1, form_data["all_pages"]) + + assert len(results) == 5 + start_level = results["/how-is-your-organisation-classified"] + assert results["/how-is-your-organisation-classified-other"] == start_level + 1 + assert results["/charity-number"] == start_level + 1 + assert results["/company-registration-number"] == start_level + 1 + assert results["/organisation-address"] == start_level + + +def test_generate_index_simple_branch(): + with open(os.path.join(TEST_METADATA_FOLDER, "start_to_main_activites.json"), "r") as f: + form_data = json.load(f) + + results = {} + + first_page = next(p for p in form_data["all_pages"] if p["path"] == form_data["start_page"]) + build_hierarchy_levels_for_page(first_page, results, 1, form_data["all_pages"]) + + assert len(results) == 4 + org_details_level = results["/organisation-details"] + assert results["/alternative-organisation-name"] == org_details_level + 1 + assert results["/tell-us-about-your-organisations-main-activities"] == org_details_level + + +def test_generate_index_about_your_org_cyp(): + with open(os.path.join(TEST_METADATA_FOLDER, "metadata_about_your_org_cyp.json"), "r") as f: + form_data = json.load(f) + + results = {} + + first_page = next(p for p in form_data["all_pages"] if p["path"] == form_data["start_page"]) + build_hierarchy_levels_for_page(first_page, results, 1, form_data["all_pages"], start_page=True) + + assert len(results) == 16 + org_details_level = results["/organisation-details"] + assert results["/alternative-organisation-name"] == org_details_level + 1 + assert results["/tell-us-about-your-organisations-main-activities"] == org_details_level + assert results["/how-is-your-organisation-classified"] == org_details_level + assert results["/how-is-your-organisation-classified-other"] == org_details_level + 1 + assert results["/organisation-address"] == org_details_level + + +@pytest.mark.parametrize( + "number,exp", + [ + ("5.1", "5"), + ("5.0.0", "5.0"), + ("5.1.2", "5.1"), + ("5", ""), + ("5.1.2.3.4.6", "5.1.2.3.4"), + ], +) +def test_remove_lowest_in_hierarchy(number, exp): + assert remove_lowest_in_hierarchy(number) == exp + + +@pytest.mark.parametrize( + "number,exp", + [ + ("5.1", "5.2"), + ("5.1.", "5.2"), + ("5.1.2", "5.1.3"), + ("5", "6"), + ("5.1.2.3.4.5", "5.1.2.3.4.6"), + ], +) +def test_increment_lowest_in_hierarchy(number, exp): + assert increment_lowest_in_hierarchy(number) == exp + + +@pytest.mark.parametrize( + "text,exp", + [ + ("5.1 hello", "hello"), + ("5.1. hello", "hello"), + ("55.1. hello", "hello"), + ("5.13. hello", "hello"), + ("hello", "hello"), + ], +) +def test_strip_leading_numbers(text, exp): + assert strip_leading_numbers(text) == exp + + +@pytest.mark.parametrize( + "section,lang,exp_anchor,exp_text", + [ + ( + Section(title_json={"en": "Before You Start"}), + "en", + "before-you-start", + "Before You Start", + ), + ( + Section(title_json={"en": "1. Before You Start"}), + "en", + "before-you-start", + "Before You Start", + ), + ( + Section(title_json={"en": "Before You Start", "cy": "Welsh"}), + "cy", + "welsh", + "Welsh", + ), + ( + Section(title_json={"en": "Before You Start", "cy": "10. Welsh"}), + "cy", + "welsh", + "Welsh", + ), + ], +) +def test_build_section_headers(section, lang, exp_anchor, exp_text): + res_anchor, res_text = build_section_header(section, lang) + assert res_anchor == exp_anchor + assert res_text == exp_text + + +def test_find_forms_dir_no_lang(tmp_path): + temp_json_dir = os.path.join(tmp_path, "form_jsons") + os.mkdir(temp_json_dir) + round_dir = os.path.join(temp_json_dir, "f1_r1") + os.mkdir(round_dir) + result = find_forms_dir(temp_json_dir, "f1", "r1", "en") + assert result == round_dir + + +def test_find_forms_dir_with_lang(tmp_path): + temp_json_dir = os.path.join(tmp_path, "form_jsons") + os.mkdir(temp_json_dir) + round_dir = os.path.join(temp_json_dir, "f1_r1") + os.mkdir(round_dir) + round_dir = os.path.join(round_dir, "en") + os.mkdir(round_dir) + result = find_forms_dir(temp_json_dir, "f1", "r1", "en") + assert result == round_dir + + +def test_generate_component_display_name_your_app(): + with open( + os.path.join(TEST_FORMS_FOLDER, "name-your-application.json"), + "r", + ) as f: + form_json = json.load(f) + page_json = next(p for p in form_json["pages"] if p["path"] == "/11-name-your-application") + components = build_components_from_page(page_json, include_html_components=True) + assert len(components) == 1 + assert components[0]["title"] == "Name your application" + + +def test_build_components_empty_text_and_title(): + with open( + os.path.join(TEST_FORMS_FOLDER, "about-your-organisation-cyp.json"), + "r", + ) as f: + form_json = json.load(f) + + # Test intro has no text + page_json = next(p for p in form_json["pages"] if p["path"] == "/intro-about-your-organisation") + components = build_components_from_page(page_json, include_html_components=False) + assert len(components) == 0 + + +def test_build_components_include_options_from_radios_and_branching_text(): + with open( + os.path.join(TEST_FORMS_FOLDER, "about-your-organisation-cyp.json"), + "r", + ) as f: + form_json = json.load(f) + + # Test if for all options in how classified + page_json = next(p for p in form_json["pages"] if p["path"] == "/how-is-your-organisation-classified") + components = build_components_from_page( + page_json, + include_html_components=False, + form_lists=form_json["lists"], + form_conditions=form_json["conditions"], + index_of_printed_headers={ + "/how-is-your-organisation-classified-other": {"heading_number": "1"}, + "/charity-number": {"heading_number": "2"}, + "/company-registration-number": {"heading_number": "3"}, + }, + ) + + assert len(components) == 1 + assert components[0]["hide_title"] is True + assert len(components[0]["text"]) == 4 + assert components[0]["text"][0] == "Select one option" + assert isinstance(components[0]["text"][1], list) + assert len(components[0]["text"][1]) == 6 + assert components[0]["text"][2] == "If 'Other', go to 1" + + +def test_build_components_bullets_in_hint(): + with open( + os.path.join(TEST_FORMS_FOLDER, "your-skills-and-experience-dpi.json"), + "r", + ) as f: + form_json = json.load(f) + + page_json = next(p for p in form_json["pages"] if p["path"] == "/similar-previous-projects") + components = build_components_from_page(page_json, include_html_components=False) + assert len(components) == 1 + assert len(components[0]["text"]) == 3 + assert components[0]["text"][2] == "(Max 250 words)" + assert len(components[0]["text"][1]) == 3 + + +def test_build_components_multi_input(): + with open(os.path.join(TEST_FORMS_FOLDER, "risk-and-deliverability-cyp.json"), "r") as f: + form_json = json.load(f) + page_json = next(p for p in form_json["pages"] if p["path"] == "/risks-to-the-project") + components = build_components_from_page( + page_json, + include_html_components=True, + form_lists=form_json["lists"], + ) + + # TODO - print header with bullet lists, remove 'multiinput needed' text + assert len(components) == 2 + assert len(components[1]["text"]) == 6 + + +@pytest.mark.parametrize( + "input_text,exp_result_length", + [ + (["You can add more stuff on the next step"], 0), + (["You can add more stuff on the next step", "something else"], 1), + (["You can add more stuff on the next step", ["a list"], "something else"], 2), + (["asdfasdfasdf"], 1), + (["You can add more stuff on the next step. And do something else"], 0), + (["You can add more on the next step"], 1), + (["You can add DIFFERENT the next step"], 1), + ], +) +def test_update_wording_for_multi_input_fields(input_text, exp_result_length): + result = update_wording_for_multi_input_fields(input_text) + assert len(result) == exp_result_length diff --git a/fund_store/tests/test_healthcheck.py b/fund_store/tests/test_healthcheck.py new file mode 100644 index 000000000..74cb67f6a --- /dev/null +++ b/fund_store/tests/test_healthcheck.py @@ -0,0 +1,17 @@ +""" +A file containing all tests related to the fund endpoint. +""" + +from flask import Flask + + +def test_healthchecks_endpoint(client: Flask): + response = client.get("/healthcheck") + + expected_dict = { + "checks": [{"check_flask_running": "OK"}, {"check_db": "OK"}], + "version": "123123", + } + + assert 200 == response.status_code, "Unexpected status code" + assert expected_dict == response.json, "Unexpected json body" diff --git a/fund_store/tests/test_routes.py b/fund_store/tests/test_routes.py new file mode 100644 index 000000000..3a6cb24be --- /dev/null +++ b/fund_store/tests/test_routes.py @@ -0,0 +1,380 @@ +import pytest +from api.routes import filter_fund_by_lang, filter_round_by_lang + + +@pytest.mark.parametrize( + "fund_data, lang_key, expected", + [ + ( + { + "name_json": {"en": "English Name", "fr": "French Name"}, + "title_json": {"en": "English Title", "fr": "French Title"}, + "description_json": { + "en": "English Description", + "fr": "French Description", + }, + }, + "en", + { + "description": "English Description", + "description_json": { + "en": "English Description", + "fr": "French Description", + }, + "name": "English Name", + "name_json": {"en": "English Name", "fr": "French Name"}, + "title": "English Title", + "title_json": {"en": "English Title", "fr": "French Title"}, + }, + ), + ( + { + "name_json": {"en": "English Name", "fr": "French Name"}, + "title_json": {"en": "English Title", "fr": "French Title"}, + "description_json": { + "en": "English Description", + "fr": "French Description", + }, + }, + "fr", + { + "description": "French Description", + "description_json": { + "en": "English Description", + "fr": "French Description", + }, + "name": "French Name", + "name_json": {"en": "English Name", "fr": "French Name"}, + "title": "French Title", + "title_json": {"en": "English Title", "fr": "French Title"}, + }, + ), + ( + [ + { + "name_json": {"en": "English Name", "fr": "French Name"}, + "title_json": {"en": "English Title", "fr": "French Title"}, + "description_json": { + "en": "English Description", + "fr": "French Description", + }, + }, + { + "name_json": { + "en": "Another English Name", + "fr": "Another French Name", + }, + "title_json": { + "en": "Another English Title", + "fr": "Another French Title", + }, + "description_json": { + "en": "Another English Description", + "fr": "Another French Description", + }, + }, + ], + "en", + [ + { + "description": "English Description", + "description_json": { + "en": "English Description", + "fr": "French Description", + }, + "name": "English Name", + "name_json": {"en": "English Name", "fr": "French Name"}, + "title": "English Title", + "title_json": {"en": "English Title", "fr": "French Title"}, + }, + { + "description": "Another English Description", + "description_json": { + "en": "Another English Description", + "fr": "Another French Description", + }, + "name": "Another English Name", + "name_json": { + "en": "Another English Name", + "fr": "Another French Name", + }, + "title": "Another English Title", + "title_json": { + "en": "Another English Title", + "fr": "Another French Title", + }, + }, + ], + ), + ( + [ + { + "name_json": {"en": "English Name", "fr": "French Name"}, + "title_json": {"en": "English Title", "fr": "French Title"}, + "description_json": { + "en": "English Description", + "fr": "French Description", + }, + }, + { + "name_json": { + "en": "Another English Name", + "fr": "Another French Name", + }, + "title_json": { + "en": "Another English Title", + "fr": "Another French Title", + }, + "description_json": { + "en": "Another English Description", + "fr": "Another French Description", + }, + }, + ], + "fr", + [ + { + "description": "French Description", + "description_json": { + "en": "English Description", + "fr": "French Description", + }, + "name": "French Name", + "name_json": {"en": "English Name", "fr": "French Name"}, + "title": "French Title", + "title_json": {"en": "English Title", "fr": "French Title"}, + }, + { + "description": "Another French Description", + "description_json": { + "en": "Another English Description", + "fr": "Another French Description", + }, + "name": "Another French Name", + "name_json": { + "en": "Another English Name", + "fr": "Another French Name", + }, + "title": "Another French Title", + "title_json": { + "en": "Another English Title", + "fr": "Another French Title", + }, + }, + ], + ), + ("Not a dictionary or a list", "en", "Not a dictionary or a list"), + ], +) +def test_filter_fund_by_lang(fund_data, lang_key, expected): + assert filter_fund_by_lang(fund_data, lang_key) == expected + + +@pytest.mark.parametrize( + "round_data, lang_key, expected", + [ + ( + { + "title_json": {"en": "English Title", "fr": "French Title"}, + "instructions_json": {"en": "English Instructions", "fr": "French Instructions"}, + "application_guidance_json": { + "en": "English Application Guidance", + "fr": "French Application Guidance", + }, + "contact_us_banner_json": { + "en": "English banner", + "fr": "French banner", + }, + }, + "en", + { + "title": "English Title", + "title_json": {"en": "English Title", "fr": "French Title"}, + "instructions": "English Instructions", + "instructions_json": {"en": "English Instructions", "fr": "French Instructions"}, + "application_guidance": "English Application Guidance", + "application_guidance_json": { + "en": "English Application Guidance", + "fr": "French Application Guidance", + }, + "contact_us_banner": "English banner", + "contact_us_banner_json": { + "en": "English banner", + "fr": "French banner", + }, + }, + ), + ( + { + "title_json": {"en": "English Title", "fr": "French Title"}, + "instructions_json": {"en": "English Instructions", "fr": "French Instructions"}, + "application_guidance_json": { + "en": "English Application Guidance", + "fr": "French Application Guidance", + }, + "contact_us_banner_json": { + "en": "English banner", + "fr": "French banner", + }, + }, + "fr", + { + "title": "French Title", + "title_json": {"en": "English Title", "fr": "French Title"}, + "instructions": "French Instructions", + "instructions_json": {"en": "English Instructions", "fr": "French Instructions"}, + "application_guidance": "French Application Guidance", + "application_guidance_json": { + "en": "English Application Guidance", + "fr": "French Application Guidance", + }, + "contact_us_banner": "French banner", + "contact_us_banner_json": { + "en": "English banner", + "fr": "French banner", + }, + }, + ), + ( + [ + { + "title_json": {"en": "English Title", "fr": "French Title"}, + "instructions_json": {"en": "English Instructions", "fr": "French Instructions"}, + "application_guidance_json": { + "en": "English Application Guidance", + "fr": "French Application Guidance", + }, + "contact_us_banner_json": { + "en": "English banner", + "fr": "French banner", + }, + }, + { + "title_json": { + "en": "Another English Title", + "fr": "Another French Title", + }, + "instructions_json": {"en": "Another English Instructions", "fr": "Another French Instructions"}, + "application_guidance_json": { + "en": "Another English Application Guidance", + "fr": "Another French Application Guidance", + }, + "contact_us_banner_json": { + "en": "Another English banner", + "fr": "Another French banner", + }, + }, + ], + "en", + [ + { + "title": "English Title", + "title_json": {"en": "English Title", "fr": "French Title"}, + "instructions": "English Instructions", + "instructions_json": {"en": "English Instructions", "fr": "French Instructions"}, + "application_guidance": "English Application Guidance", + "application_guidance_json": { + "en": "English Application Guidance", + "fr": "French Application Guidance", + }, + "contact_us_banner": "English banner", + "contact_us_banner_json": { + "en": "English banner", + "fr": "French banner", + }, + }, + { + "title": "Another English Title", + "title_json": { + "en": "Another English Title", + "fr": "Another French Title", + }, + "instructions": "Another English Instructions", + "instructions_json": {"en": "Another English Instructions", "fr": "Another French Instructions"}, + "application_guidance": "Another English Application Guidance", + "application_guidance_json": { + "en": "Another English Application Guidance", + "fr": "Another French Application Guidance", + }, + "contact_us_banner": "Another English banner", + "contact_us_banner_json": { + "en": "Another English banner", + "fr": "Another French banner", + }, + }, + ], + ), + ( + [ + { + "title_json": {"en": "English Title", "fr": "French Title"}, + "instructions_json": {"en": "English Instructions", "fr": "French Instructions"}, + "application_guidance_json": { + "en": "English Application Guidance", + "fr": "French Application Guidance", + }, + "contact_us_banner_json": { + "en": "English banner", + "fr": "French banner", + }, + }, + { + "title_json": { + "en": "Another English Title", + "fr": "Another French Title", + }, + "instructions_json": {"en": "Another English Instructions", "fr": "Another French Instructions"}, + "application_guidance_json": { + "en": "Another English Application Guidance", + "fr": "Another French Application Guidance", + }, + "contact_us_banner_json": { + "en": "Another English banner", + "fr": "Another French banner", + }, + }, + ], + "fr", + [ + { + "title": "French Title", + "title_json": {"en": "English Title", "fr": "French Title"}, + "instructions": "French Instructions", + "instructions_json": {"en": "English Instructions", "fr": "French Instructions"}, + "application_guidance": "French Application Guidance", + "application_guidance_json": { + "en": "English Application Guidance", + "fr": "French Application Guidance", + }, + "contact_us_banner": "French banner", + "contact_us_banner_json": { + "en": "English banner", + "fr": "French banner", + }, + }, + { + "title": "Another French Title", + "title_json": { + "en": "Another English Title", + "fr": "Another French Title", + }, + "instructions": "Another French Instructions", + "instructions_json": {"en": "Another English Instructions", "fr": "Another French Instructions"}, + "application_guidance": "Another French Application Guidance", + "application_guidance_json": { + "en": "Another English Application Guidance", + "fr": "Another French Application Guidance", + }, + "contact_us_banner": "Another French banner", + "contact_us_banner_json": { + "en": "Another English banner", + "fr": "Another French banner", + }, + }, + ], + ), + ("Not a dictionary or a list", "en", "Not a dictionary or a list"), + ], +) +def test_filter_round_by_lang(round_data, lang_key, expected): + assert filter_round_by_lang(round_data, lang_key) == expected diff --git a/fund_store/tests/test_schemas.py b/fund_store/tests/test_schemas.py new file mode 100644 index 000000000..8b27a2547 --- /dev/null +++ b/fund_store/tests/test_schemas.py @@ -0,0 +1,156 @@ +import pytest + +from db.models import FormName +from db.models.section import Section +from db.schemas.section import SECTION_SCHEMA_MAP, EnglishSectionSchema, WelshSectionSchema + +section = Section( + id=1, + title_json={"en": "English Title", "cy": "Welsh Title"}, + form_name=[FormName(form_name_json={"en": "English Form Name", "cy": "Welsh Form Name"})], + children=[ + Section( + id=1, + title_json={"en": "English Child Section 1", "cy": "Welsh Child Section"}, + path="1.1", + ), + Section( + id=3, + title_json={"en": "English Child Section 3", "cy": "Welsh Child Section"}, + path="1.3", + ), + Section( + id=2, + title_json={"en": "English Child Section 2", "cy": "Welsh Child Section"}, + path="1.2", + ), + ], +) + +expected_en = { + "children": [ + { + "children": [], + "fields": [], + "form_name": None, + "id": 1, + "path": "1.1", + "requires_feedback": None, + "title": "English Child Section 1", + "title_json": { + "cy": "Welsh Child Section", + "en": "English Child Section 1", + }, + "weighting": None, + }, + { + "children": [], + "fields": [], + "form_name": None, + "id": 2, + "path": "1.2", + "requires_feedback": None, + "title": "English Child Section 2", + "title_json": { + "cy": "Welsh Child Section", + "en": "English Child Section 2", + }, + "weighting": None, + }, + { + "children": [], + "fields": [], + "form_name": None, + "id": 3, + "path": "1.3", + "requires_feedback": None, + "title": "English Child Section 3", + "title_json": { + "cy": "Welsh Child Section", + "en": "English Child Section 3", + }, + "weighting": None, + }, + ], + "fields": [], + "form_name": "English Form Name", + "id": 1, + "path": None, + "requires_feedback": None, + "title": "English Title", + "title_json": {"cy": "Welsh Title", "en": "English Title"}, + "weighting": None, +} + +expected_cy = { + "children": [ + { + "children": [], + "fields": [], + "form_name": None, + "id": 1, + "path": "1.1", + "requires_feedback": None, + "title": "Welsh Child Section", + "title_json": { + "cy": "Welsh Child Section", + "en": "English Child Section 1", + }, + "weighting": None, + }, + { + "children": [], + "fields": [], + "form_name": None, + "id": 2, + "path": "1.2", + "requires_feedback": None, + "title": "Welsh Child Section", + "title_json": { + "cy": "Welsh Child Section", + "en": "English Child Section 2", + }, + "weighting": None, + }, + { + "children": [], + "fields": [], + "form_name": None, + "id": 3, + "path": "1.3", + "requires_feedback": None, + "title": "Welsh Child Section", + "title_json": { + "cy": "Welsh Child Section", + "en": "English Child Section 3", + }, + "weighting": None, + }, + ], + "fields": [], + "form_name": "Welsh Form Name", + "id": 1, + "path": None, + "requires_feedback": None, + "title": "Welsh Title", + "title_json": {"cy": "Welsh Title", "en": "English Title"}, + "weighting": None, +} + + +@pytest.mark.parametrize( + "section, lang_code, expected", + [ + (section, "en", expected_en), + (section, "cy", expected_cy), + ], +) +def test_dump(section, lang_code, expected): + schema = SECTION_SCHEMA_MAP[lang_code]() + result = schema.dump(section) + assert result == expected + + +def test_section_schema_map(): + assert SECTION_SCHEMA_MAP["en"] == EnglishSectionSchema + assert SECTION_SCHEMA_MAP["cy"] == WelshSectionSchema diff --git a/fund_store/uv.lock b/fund_store/uv.lock new file mode 100644 index 000000000..f61bbdb23 --- /dev/null +++ b/fund_store/uv.lock @@ -0,0 +1,1702 @@ +version = 1 +requires-python = "==3.10.*" +resolution-markers = [ + "platform_python_implementation == 'CPython' and sys_platform != 'cygwin' and sys_platform != 'win32'", + "platform_python_implementation != 'CPython' and platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'", + "(platform_python_implementation == 'CPython' and sys_platform == 'cygwin') or (platform_python_implementation == 'CPython' and sys_platform == 'win32')", + "(platform_python_implementation != 'CPython' and platform_python_implementation != 'PyPy' and sys_platform == 'cygwin') or (platform_python_implementation != 'CPython' and platform_python_implementation != 'PyPy' and sys_platform == 'win32')", + "platform_python_implementation == 'PyPy'", +] + +[[package]] +name = "a2wsgi" +version = "1.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/1c/07d91da0c8618ecc146a811ab01985ca95ad07221483625dc00a024ea5cb/a2wsgi-1.10.4.tar.gz", hash = "sha256:50e81ac55aa609fa2c666e42bacc25c424c8884ce6072f1a7e902114b7ee5d63", size = 18186 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/a6/73b02f52206f7bc3600a702726bd75b0cda229a23c4a7ea6189bbd9ae528/a2wsgi-1.10.4-py3-none-any.whl", hash = "sha256:f17da93bf5952e0b0938c87f261c52b7305ddfab1ff3c70dd10b4b76db3851d3", size = 16812 }, +] + +[[package]] +name = "airium" +version = "0.2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/98/f843cd8969409e913b0535ae15771f86d35aed87484372de6fa4e48b283f/airium-0.2.6.tar.gz", hash = "sha256:ccab36b798b6cce3d0c5074e52ce8059f6e82991caae4985f42cadfad80b1de4", size = 19496 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/bc/d174ebf44e4cf8cd967bca5e181e254bd2f95afbb5367471382713736bf5/airium-0.2.6-py3-none-any.whl", hash = "sha256:50af5cf491e084f27909e29a93550b4170e587cde01334d58c6249644ee8c6c2", size = 13313 }, +] + +[[package]] +name = "alembic" +version = "1.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/24/ddce068e2ac9b5581bd58602edb2a1be1b0752e1ff2963c696ecdbe0470d/alembic-1.13.1.tar.gz", hash = "sha256:4932c8558bf68f2ee92b9bbcb8218671c627064d5b08939437af6d77dc05e595", size = 1213288 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/50/9fb3a5c80df6eb6516693270621676980acd6d5a9a7efdbfa273f8d616c7/alembic-1.13.1-py3-none-any.whl", hash = "sha256:2edcc97bed0bd3272611ce3a98d98279e9c209e7186e43e75bbb1b2bdfdbcc43", size = 233424 }, +] + +[[package]] +name = "anyio" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/e3/c4c8d473d6780ef1853d630d581f70d655b4f8d7553c6997958c283039a2/anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94", size = 163930 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/a2/10639a79341f6c019dedc95bd48a4928eed9f1d1197f4c04f546fc7ae0ff/anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7", size = 86780 }, +] + +[[package]] +name = "asgiref" +version = "3.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828 }, +] + +[[package]] +name = "asserts" +version = "0.11.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/ef/a02b2af8228be2a08ffd7e7630084e441030fbd3e6426483ddcdf905ac34/asserts-0.11.1-py2.py3-none-any.whl", hash = "sha256:dfe3a45a311c727f516e9375eac85e5f5dd1df846b60ae52f559d8534b24df5d", size = 12454 }, +] + +[[package]] +name = "async-timeout" +version = "4.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/d6/21b30a550dafea84b1b8eee21b5e23fa16d010ae006011221f33dcd8d7f8/async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", size = 8345 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/fa/e01228c2938de91d47b307831c62ab9e4001e747789d0b05baf779a6488c/async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028", size = 5721 }, +] + +[[package]] +name = "attrs" +version = "23.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/fc/f800d51204003fa8ae392c4e8278f256206e7a919b708eef054f5f4b650d/attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", size = 780820 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/44/827b2a91a5816512fcaf3cc4ebc465ccd5d598c45cefa6703fcf4a79018f/attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1", size = 60752 }, +] + +[[package]] +name = "babel" +version = "2.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/d2/9671b93d623300f0aef82cde40e25357f11330bdde91743891b22a555bed/babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413", size = 9390000 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/45/377f7e32a5c93d94cd56542349b34efab5ca3f9e2fd5a68c5e93169aa32d/Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb", size = 9634913 }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/ca/824b1195773ce6166d388573fc106ce56d4a805bd7427b624e063596ec58/beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", size = 581181 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/fe/e8c672695b37eecc5cbf43e1d0638d88d66ba3a44c4d321c796f4e59167f/beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed", size = 147925 }, +] + +[[package]] +name = "black" +version = "24.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/47/c9997eb470a7f48f7aaddd3d9a828244a2e4199569e38128715c48059ac1/black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d", size = 642299 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/f6/3adc48c210527a7b651aaed43824a9b8bd04b3fb361a5227bad046e1c876/black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce", size = 1631487 }, + { url = "https://files.pythonhosted.org/packages/a2/25/70aa1bec12c841a03e333e312daa0cf2fee50ea6336ac4851c93c0e2b411/black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021", size = 1456317 }, + { url = "https://files.pythonhosted.org/packages/e0/7d/7f8df0fdbbbefc4362d3eca6b69b7a8a4249a8a88dabc00a207d31fddcd7/black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063", size = 1822765 }, + { url = "https://files.pythonhosted.org/packages/5c/21/1ee97841c469c1551133cbe47448cdba9628c7d9431f74f114f02e3b233c/black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96", size = 1409336 }, + { url = "https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c", size = 205925 }, +] + +[[package]] +name = "blinker" +version = "1.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/57/a6a1721eff09598fb01f3c7cda070c1b6a0f12d63c83236edf79a440abcc/blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83", size = 23161 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/2a/10164ed1f31196a2f7f3799368a821765c62851ead0e630ab52b8e14b4d0/blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01", size = 9456 }, +] + +[[package]] +name = "boto3" +version = "1.34.142" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/1c/e7108fac4b3fd5bd2c2eb48abc2b07922b15f48f64445d86af940377cf2f/boto3-1.34.142.tar.gz", hash = "sha256:72daee953cfa0631c584c9e3aef594079e1fe6a2f64c81ff791dab9a7b25c013", size = 108696 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/4f/68ea33322927e8bf5c5b854cbec883d97f172b58f3f41094fb64f8031ea7/boto3-1.34.142-py3-none-any.whl", hash = "sha256:cae11cb54f79795e44248a9e53ec5c7328519019df1ba54bc01413f51c548626", size = 139174 }, +] + +[[package]] +name = "botocore" +version = "1.34.142" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/ad/b97d3de3ebbd953ff32bbcdbb5ceb2c8413db3d89b53bceef590f376c8a9/botocore-1.34.142.tar.gz", hash = "sha256:2eeb8e6be729c1f8ded723970ed6c6ac29cc3014d86a99e73428fa8bdca81f63", size = 12584267 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/b8/db573582b0f80f2ab7b37ade176a769bf93e217827f8424c230328db2106/botocore-1.34.142-py3-none-any.whl", hash = "sha256:9d8095bab0b93b9064e856730a7ffbbb4f897353d3170bec9ddccc5f4a3753bc", size = 12372481 }, +] + +[[package]] +name = "bs4" +version = "0.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/aa/4acaf814ff901145da37332e05bb510452ebed97bc9602695059dd46ef39/bs4-0.0.2.tar.gz", hash = "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925", size = 698 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/bb/bf7aab772a159614954d84aa832c129624ba6c32faa559dfb200a534e50b/bs4-0.0.2-py2.py3-none-any.whl", hash = "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc", size = 1189 }, +] + +[[package]] +name = "certifi" +version = "2024.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/b3/e02f4f397c81077ffc52a538e0aec464016f1860c472ed33bd2a1d220cc5/certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516", size = 165550 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/11/1e78951465b4a225519b8c3ad29769c49e0d8d157a070f681d5b6d64737f/certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56", size = 164433 }, +] + +[[package]] +name = "cffi" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/ce/95b0bae7968c65473e1298efb042e10cafc7bafc14d9e4f154008241c91d/cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", size = 512873 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/aa/1c43e48a6f361d1529f9e4602d6992659a0107b5f21cae567e2eddcf8d66/cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088", size = 182457 }, + { url = "https://files.pythonhosted.org/packages/c4/01/f5116266fe80c04d4d1cc96c3d355606943f9fb604a810e0b02228a0ce19/cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9", size = 176792 }, + { url = "https://files.pythonhosted.org/packages/57/3a/c263cf4d5b02880274866968fa2bf196a02c4486248bc164732319b4a4c0/cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673", size = 423848 }, + { url = "https://files.pythonhosted.org/packages/f0/31/a6503a5c4874fb4d4c2053f73f09a957cb427b6943fab5a43b8e156df397/cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896", size = 446005 }, + { url = "https://files.pythonhosted.org/packages/22/05/43cfda378da7bb0aa19b3cf34fe54f8867b0d581294216339d87deefd69c/cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684", size = 452639 }, + { url = "https://files.pythonhosted.org/packages/54/49/b8875986beef2e74fc668b95f2df010e354f78e009d33d95b375912810c3/cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7", size = 434140 }, + { url = "https://files.pythonhosted.org/packages/c9/7c/43d81bdd5a915923c3bad5bb4bff401ea00ccc8e28433fb6083d2e3bf58e/cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614", size = 443865 }, + { url = "https://files.pythonhosted.org/packages/eb/de/4f644fc78a1144a897e1f908abfb2058f7be05a8e8e4fe90b7f41e9de36b/cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743", size = 436867 }, + { url = "https://files.pythonhosted.org/packages/ee/68/74a2b9f9432b70d97d1184cdabf32d7803124c228adef9481d280864a4a7/cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d", size = 465830 }, + { url = "https://files.pythonhosted.org/packages/20/18/76e26bcfa6a7a62f880791122261575b3048ac57dd72f300ba0827629ab8/cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a", size = 172955 }, + { url = "https://files.pythonhosted.org/packages/be/3e/0b197d1bfbf386a90786b251dbf2634a15f2ea3d4e4070e99c7d1c7689cf/cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1", size = 181616 }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + +[[package]] +name = "chardet" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/61/095a0aa1a84d1481998b534177c8566fdc50bb1233ea9a0478cd3cc075bd/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", size = 194219 }, + { url = "https://files.pythonhosted.org/packages/cc/94/f7cf5e5134175de79ad2059edf2adce18e0685ebdb9227ff0139975d0e93/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", size = 122521 }, + { url = "https://files.pythonhosted.org/packages/46/6a/d5c26c41c49b546860cc1acabdddf48b0b3fb2685f4f5617ac59261b44ae/charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", size = 120383 }, + { url = "https://files.pythonhosted.org/packages/b8/60/e2f67915a51be59d4539ed189eb0a2b0d292bf79270410746becb32bc2c3/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", size = 138223 }, + { url = "https://files.pythonhosted.org/packages/05/8c/eb854996d5fef5e4f33ad56927ad053d04dc820e4a3d39023f35cad72617/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", size = 148101 }, + { url = "https://files.pythonhosted.org/packages/f6/93/bb6cbeec3bf9da9b2eba458c15966658d1daa8b982c642f81c93ad9b40e1/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", size = 140699 }, + { url = "https://files.pythonhosted.org/packages/da/f1/3702ba2a7470666a62fd81c58a4c40be00670e5006a67f4d626e57f013ae/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", size = 142065 }, + { url = "https://files.pythonhosted.org/packages/3f/ba/3f5e7be00b215fa10e13d64b1f6237eb6ebea66676a41b2bcdd09fe74323/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", size = 144505 }, + { url = "https://files.pythonhosted.org/packages/33/c3/3b96a435c5109dd5b6adc8a59ba1d678b302a97938f032e3770cc84cd354/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", size = 139425 }, + { url = "https://files.pythonhosted.org/packages/43/05/3bf613e719efe68fb3a77f9c536a389f35b95d75424b96b426a47a45ef1d/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", size = 145287 }, + { url = "https://files.pythonhosted.org/packages/58/78/a0bc646900994df12e07b4ae5c713f2b3e5998f58b9d3720cce2aa45652f/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", size = 149929 }, + { url = "https://files.pythonhosted.org/packages/eb/5c/97d97248af4920bc68687d9c3b3c0f47c910e21a8ff80af4565a576bd2f0/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", size = 141605 }, + { url = "https://files.pythonhosted.org/packages/a8/31/47d018ef89f95b8aded95c589a77c072c55e94b50a41aa99c0a2008a45a4/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", size = 142646 }, + { url = "https://files.pythonhosted.org/packages/ae/d5/4fecf1d58bedb1340a50f165ba1c7ddc0400252d6832ff619c4568b36cc0/charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", size = 92846 }, + { url = "https://files.pythonhosted.org/packages/a2/a0/4af29e22cb5942488cf45630cbdd7cefd908768e69bdd90280842e4e8529/charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", size = 100343 }, + { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543 }, +] + +[[package]] +name = "click" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "colored" +version = "2.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/98/4d4546307039955eec131cf9538732fb7a28d2db2090cd1d4e4a135829e1/colored-2.2.4.tar.gz", hash = "sha256:595e1dd7f3b472ea5f12af21d2fec8a2ea2cf8f9d93e67180197330b26df9b61", size = 13202 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/d1/548f697f88872321525e294f8863efbdd1c313964b7f94e49ab0dc4f2895/colored-2.2.4-py3-none-any.whl", hash = "sha256:a7069673bd90a35f46cb748d012c17284a0668d2f1c06bc7a51822a2d5ad2112", size = 16109 }, +] + +[[package]] +name = "commonmark" +version = "0.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/60/48/a60f593447e8f0894ebb7f6e6c1f25dafc5e89c5879fdc9360ae93ff83f0/commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60", size = 95764 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/92/dfd892312d822f36c55366118b95d914e5f16de11044a27cf10a7d71bbbf/commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9", size = 51068 }, +] + +[[package]] +name = "connexion" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "httpx" }, + { name = "inflection" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "python-multipart" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/74/a6d4aa579c8afc9acb0f9cefc1ae2f1da1564cd5b244bdd434407081535f/connexion-3.1.0.tar.gz", hash = "sha256:66a44580991f53955b6e409a84fa9fa65c7ca4db52dc217b49cd35c201066083", size = 88189 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/55/e943e94b123b72e181c26a20205639f1f680578dd78deef903b1349994ae/connexion-3.1.0-py3-none-any.whl", hash = "sha256:e92b6d0412eb54b3b69f2516b93d982a06b0e23f6d5c1ab94257c55d365f63ce", size = 113099 }, +] + +[package.optional-dependencies] +flask = [ + { name = "a2wsgi" }, + { name = "flask", extra = ["async"] }, +] +swagger-ui = [ + { name = "swagger-ui-bundle" }, +] +uvicorn = [ + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "cryptography" +version = "42.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/a7/1498799a2ea06148463a9a2c10ab2f6a921a74fb19e231b27dc412a748e2/cryptography-42.0.8.tar.gz", hash = "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2", size = 671250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/8b/1b929ba8139430e09e140e6939c2b29c18df1f2fc2149e41bdbdcdaf5d1f/cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e", size = 5899961 }, + { url = "https://files.pythonhosted.org/packages/fa/5d/31d833daa800e4fab33209843095df7adb4a78ea536929145534cbc15026/cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d", size = 3114353 }, + { url = "https://files.pythonhosted.org/packages/5d/32/f6326c70a9f0f258a201d3b2632bca586ea24d214cec3cf36e374040e273/cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902", size = 3647773 }, + { url = "https://files.pythonhosted.org/packages/35/66/2d87e9ca95c82c7ee5f2c09716fc4c4242c1ae6647b9bd27e55e920e9f10/cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801", size = 3839763 }, + { url = "https://files.pythonhosted.org/packages/c2/de/8083fa2e68d403553a01a9323f4f8b9d7ffed09928ba25635c29fb28c1e7/cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949", size = 3632661 }, + { url = "https://files.pythonhosted.org/packages/07/40/d6f6819c62e808ea74639c3c640f7edd636b86cce62cb14943996a15df92/cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9", size = 3851536 }, + { url = "https://files.pythonhosted.org/packages/5c/46/de71d48abf2b6d3c808f4fbb0f4dc44a4e72786be23df0541aa2a3f6fd7e/cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583", size = 3754209 }, + { url = "https://files.pythonhosted.org/packages/25/c9/86f04e150c5d5d5e4a731a2c1e0e43da84d901f388e3fea3d5de98d689a7/cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7", size = 3923551 }, + { url = "https://files.pythonhosted.org/packages/53/c2/903014dafb7271fb148887d4355b2e90319cad6e810663be622b0c933fc9/cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b", size = 3739265 }, + { url = "https://files.pythonhosted.org/packages/95/26/82d704d988a193cbdc69ac3b41c687c36eaed1642cce52530ad810c35645/cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7", size = 3937371 }, + { url = "https://files.pythonhosted.org/packages/cf/71/4e0d05c9acd638a225f57fb6162aa3d03613c11b76893c23ea4675bb28c5/cryptography-42.0.8-cp37-abi3-win32.whl", hash = "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2", size = 2438849 }, + { url = "https://files.pythonhosted.org/packages/06/0f/78da3cad74f2ba6c45321dc90394d70420ea846730dc042ef527f5a224b5/cryptography-42.0.8-cp37-abi3-win_amd64.whl", hash = "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba", size = 2889090 }, + { url = "https://files.pythonhosted.org/packages/60/12/f064af29190cdb1d38fe07f3db6126091639e1dece7ec77c4ff037d49193/cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28", size = 5901232 }, + { url = "https://files.pythonhosted.org/packages/43/c2/4a3eef67e009a522711ebd8ac89424c3a7fe591ece7035d964419ad52a1d/cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e", size = 3648711 }, + { url = "https://files.pythonhosted.org/packages/49/1c/9f6d13cc8041c05eebff1154e4e71bedd1db8e174fff999054435994187a/cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70", size = 3841968 }, + { url = "https://files.pythonhosted.org/packages/5f/f9/c3d4f19b82bdb25a3d857fe96e7e571c981810e47e3f299cc13ac429066a/cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c", size = 3633032 }, + { url = "https://files.pythonhosted.org/packages/fa/e2/b7e6e8c261536c489d9cf908769880d94bd5d9a187e166b0dc838d2e6a56/cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7", size = 3852478 }, + { url = "https://files.pythonhosted.org/packages/a2/68/e16751f6b859bc120f53fddbf3ebada5c34f0e9689d8af32884d8b2e4b4c/cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e", size = 3754102 }, + { url = "https://files.pythonhosted.org/packages/0f/38/85c74d0ac4c540780e072b1e6f148ecb718418c1062edcb20d22f3ec5bbb/cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961", size = 3925042 }, + { url = "https://files.pythonhosted.org/packages/89/f4/a8b982e88eb5350407ebdbf4717b55043271d878705329e107f4783555f2/cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1", size = 3738833 }, + { url = "https://files.pythonhosted.org/packages/fd/2b/be327b580645927bb1a1f32d5a175b897a9b956bc085b095e15c40bac9ed/cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14", size = 3938751 }, + { url = "https://files.pythonhosted.org/packages/3c/d5/c6a78ffccdbe4516711ebaa9ed2c7eb6ac5dfa3dc920f2c7e920af2418b0/cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c", size = 2439281 }, + { url = "https://files.pythonhosted.org/packages/a2/7b/b0d330852dd5953daee6b15f742f15d9f18e9c0154eb4cfcc8718f0436da/cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a", size = 2886038 }, + { url = "https://files.pythonhosted.org/packages/a3/fe/1e21699f0a7904e8a30d4fc6db262958f1edf5e505a02e7d97a5b419e482/cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe", size = 3014449 }, + { url = "https://files.pythonhosted.org/packages/d5/f3/61b398b5ec61f4b6ffbf746227df7ebb421696458d9625d634043f236a13/cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c", size = 3558533 }, + { url = "https://files.pythonhosted.org/packages/c1/e2/60b05e720766e185ef097d07068bd878a51d613ef91e4c241750f9c9192b/cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71", size = 3759330 }, + { url = "https://files.pythonhosted.org/packages/10/38/2c8dae407d301eaf942e377a5b2b30485cfa0df03c6c2dcc2ac044054ed9/cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d", size = 2801764 }, +] + +[[package]] +name = "debugpy" +version = "1.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/c7/a18e15ed2e53f86de2e1c4162a54ddf1c4f4cee5ca40270c14725ccdd8ff/debugpy-1.8.1.zip", hash = "sha256:f696d6be15be87aef621917585f9bb94b1dc9e8aced570db1b8a6fc14e8f9b42", size = 4619053 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/00/629fd2ba18483496482fd4b640c59b904238472c004036f331729753c63c/debugpy-1.8.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:3bda0f1e943d386cc7a0e71bfa59f4137909e2ed947fb3946c506e113000f741", size = 1714331 }, + { url = "https://files.pythonhosted.org/packages/7a/27/78d5cf9c7aba43f8341e78273ab776913d2d33beb581ec39b65e56a0db77/debugpy-1.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dda73bf69ea479c8577a0448f8c707691152e6c4de7f0c4dec5a4bc11dee516e", size = 2998681 }, + { url = "https://files.pythonhosted.org/packages/ee/28/69b62b9e21d0e8d5ad45e5c0323f921a00ccc436bf144940b084fb898d86/debugpy-1.8.1-cp310-cp310-win32.whl", hash = "sha256:3a79c6f62adef994b2dbe9fc2cc9cc3864a23575b6e387339ab739873bea53d0", size = 4771701 }, + { url = "https://files.pythonhosted.org/packages/a2/81/408eecc856b931b4db262bd3402534d815e22dc7ebcfdc333cb57e69e9f9/debugpy-1.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:7eb7bd2b56ea3bedb009616d9e2f64aab8fc7000d481faec3cd26c98a964bcdd", size = 4796491 }, + { url = "https://files.pythonhosted.org/packages/57/ab/6df7e24db51e1db642a5ea1759d44fb656251253995a27deb37af9b192ae/debugpy-1.8.1-py2.py3-none-any.whl", hash = "sha256:28acbe2241222b87e255260c76741e1fbf04fdc3b6d094fcf57b6c6f75ce1242", size = 4832569 }, +] + +[[package]] +name = "deepdiff" +version = "7.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ordered-set" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/10/6f4b0bd0627d542f63a24f38e29d77095dc63d5f45bc1a7b4a6ca8750fa9/deepdiff-7.0.1.tar.gz", hash = "sha256:260c16f052d4badbf60351b4f77e8390bee03a0b516246f6839bc813fb429ddf", size = 421718 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/e6/d27d37dc55dbf40cdbd665aa52844b065ac760c9a02a02265f97ea7a4256/deepdiff-7.0.1-py3-none-any.whl", hash = "sha256:447760081918216aa4fd4ca78a4b6a848b81307b2ea94c810255334b759e1dc3", size = 80825 }, +] + +[[package]] +name = "distlib" +version = "0.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/91/e2df406fb4efacdf46871c25cde65d3c6ee5e173b7e5a4547a47bae91920/distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64", size = 609931 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/41/9307e4f5f9976bc8b7fea0b66367734e8faf3ec84bc0d412d8cfabbb66cd/distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", size = 468850 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/65/d66b7fbaef021b3c954b3bbb196d21d8a4b97918ea524f82cfae474215af/exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16", size = 28717 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/90/79fe92dd413a9cab314ef5c591b5aa9b9ba787ae4cadab75055b0ae00b33/exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad", size = 16458 }, +] + +[[package]] +name = "filelock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/7d/73d36db6955bde2ed495ce40ce02c9a2533b8c7b64fd42a38b1ee879ea18/filelock-3.15.1.tar.gz", hash = "sha256:58a2549afdf9e02e10720eaa4d4470f56386d7a6f72edd7d0596337af8ed7ad8", size = 17564 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/aa/edf5205465b70cee020b711f1f4b6179a0ae369cc13aadb8f8ec6fd7d2f5/filelock-3.15.1-py3-none-any.whl", hash = "sha256:71b3102950e91dfc1bb4209b64be4dc8854f40e5f534428d8684f953ac847fac", size = 15946 }, +] + +[[package]] +name = "flake8" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mccabe" }, + { name = "pycodestyle" }, + { name = "pyflakes" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/34/64f8a43736d9862ced7dd0ea5c3ed99815b8ff4b826a4f3bfd3a1b0639b1/flake8-7.1.0.tar.gz", hash = "sha256:48a07b626b55236e0fb4784ee69a465fbf59d79eec1f5b4785c3d3bc57d17aa5", size = 48240 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/43/d5147aadaa52558e94e024811f2f9543b4bd7203b3a9659eeb5dff9c61b3/flake8-7.1.0-py2.py3-none-any.whl", hash = "sha256:2e416edcc62471a64cea09353f4e7bdba32aeb079b6e360554c659a122b1bc6a", size = 57569 }, +] + +[[package]] +name = "flake8-pyproject" +version = "1.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flake8" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/1d/635e86f9f3a96b7ea9e9f19b5efe17a987e765c39ca496e4a893bb999112/flake8_pyproject-1.2.3-py3-none-any.whl", hash = "sha256:6249fe53545205af5e76837644dc80b4c10037e73a0e5db87ff562d75fb5bd4a", size = 4756 }, +] + +[[package]] +name = "flask" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/e1/d104c83026f8d35dfd2c261df7d64738341067526406b40190bc063e829a/flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842", size = 676315 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/80/ffe1da13ad9300f87c93af113edd0638c75138c42a0994becfacac078c06/flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3", size = 101735 }, +] + +[package.optional-dependencies] +async = [ + { name = "asgiref" }, +] + +[[package]] +name = "flask-babel" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "flask" }, + { name = "jinja2" }, + { name = "pytz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/1a/4c65e3b90bda699a637bfb7fb96818b0a9bbff7636ea91aade67f6020a31/flask_babel-4.0.0.tar.gz", hash = "sha256:dbeab4027a3f4a87678a11686496e98e1492eb793cbdd77ab50f4e9a2602a593", size = 10178 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/c2/e0ab5abe37882e118482884f2ec660cd06da644ddfbceccf5f88f546b574/flask_babel-4.0.0-py3-none-any.whl", hash = "sha256:638194cf91f8b301380f36d70e2034c77ee25b98cb5d80a1626820df9a6d4625", size = 9602 }, +] + +[[package]] +name = "flask-json" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/4a/6046e195241772f4b6add3adcd4e820004c6151afc587d8c7d0d4ffeb473/Flask-JSON-0.4.0.tar.gz", hash = "sha256:07945d66024f3b77694ce1db5d1fe83940f2aa3bcad8a608535686be67e4bc48", size = 15899 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/87/3b3ae3ca23dd90b15faa1128c473c1cf16d42e9f95efc5e4a1b77bf22260/Flask_JSON-0.4.0-py3-none-any.whl", hash = "sha256:1c1b87a657daa2179fc19f1ffc78204a716c7c5139673dc5038772db4d9f1988", size = 8714 }, +] + +[[package]] +name = "flask-migrate" +version = "4.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alembic" }, + { name = "flask" }, + { name = "flask-sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/e2/4008fc0d298d7ce797021b194bbe151d4d12db670691648a226d4fc8aefc/Flask-Migrate-4.0.7.tar.gz", hash = "sha256:dff7dd25113c210b069af280ea713b883f3840c1e3455274745d7355778c8622", size = 21770 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/01/587023575286236f95d2ab8a826c320375ed5ea2102bb103ed89704ffa6b/Flask_Migrate-4.0.7-py3-none-any.whl", hash = "sha256:5c532be17e7b43a223b7500d620edae33795df27c75811ddf32560f7d48ec617", size = 21127 }, +] + +[[package]] +name = "flask-redis" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "redis" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/d1/6e5a087e2fd99782451312dd467bbf5f9f64d999de900047dc0854a7d175/flask-redis-0.4.0.tar.gz", hash = "sha256:e1fccc11e7ea35c2a4d68c0b9aa58226a098e45e834d615c7b6c4928b01ddd6c", size = 9906 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/9c/cead8fff1c8da2bd31a83ec476c3364812ee74f3c7c3445d070555f681d1/flask_redis-0.4.0-py2.py3-none-any.whl", hash = "sha256:8d79eef4eb1217095edab603acc52f935b983ae4b7655ee7c82c0dfd87315d17", size = 8550 }, +] + +[[package]] +name = "flask-sqlalchemy" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/53/b0a9fcc1b1297f51e68b69ed3b7c3c40d8c45be1391d77ae198712914392/flask_sqlalchemy-3.1.1.tar.gz", hash = "sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312", size = 81899 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/6a/89963a5c6ecf166e8be29e0d1bf6806051ee8fe6c82e232842e3aeac9204/flask_sqlalchemy-3.1.1-py3-none-any.whl", hash = "sha256:4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0", size = 25125 }, +] + +[[package]] +name = "funding-service-design-fund-store" +version = "0.1.1" +source = { virtual = "." } +dependencies = [ + { name = "airium" }, + { name = "bs4" }, + { name = "connexion", extra = ["flask", "swagger-ui", "uvicorn"] }, + { name = "flask" }, + { name = "flask-json" }, + { name = "flask-migrate" }, + { name = "flask-sqlalchemy" }, + { name = "funding-service-design-utils" }, + { name = "marshmallow-sqlalchemy" }, + { name = "openapi-spec-validator" }, + { name = "prance" }, + { name = "psycopg2-binary" }, + { name = "pytest-html" }, + { name = "pytest-mock" }, + { name = "sqlalchemy", extra = ["mypy"] }, + { name = "sqlalchemy-json" }, + { name = "sqlalchemy-utils" }, + { name = "swagger-ui-bundle" }, + { name = "uvicorn" }, +] + +[package.dependency-groups] +dev = [ + { name = "asserts" }, + { name = "black" }, + { name = "colored" }, + { name = "debugpy" }, + { name = "deepdiff" }, + { name = "flake8-pyproject" }, + { name = "invoke" }, + { name = "json2html" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-env" }, + { name = "pytest-flask" }, + { name = "pytest-mock" }, +] + +[package.metadata] +requires-dist = [ + { name = "airium", specifier = ">=0.2.6" }, + { name = "bs4", specifier = ">=0.0.2" }, + { name = "connexion", extras = ["flask", "swagger-ui", "uvicorn"], specifier = ">=3.1.0" }, + { name = "flask", specifier = "==3.0.3" }, + { name = "flask-json", specifier = "==0.4.0" }, + { name = "flask-migrate", specifier = "==4.0.7" }, + { name = "flask-sqlalchemy", specifier = "==3.1.1" }, + { name = "funding-service-design-utils", specifier = ">=5.0.8,<6.0.0" }, + { name = "marshmallow-sqlalchemy", specifier = "==1.0.0" }, + { name = "openapi-spec-validator", specifier = ">=0.7.1" }, + { name = "prance", specifier = ">=23.6.21.0" }, + { name = "psycopg2-binary", specifier = "==2.9.9" }, + { name = "pytest-html", specifier = ">=3.2.0" }, + { name = "pytest-mock", specifier = "==3.14.0" }, + { name = "sqlalchemy", extras = ["mypy"], specifier = ">=2.0.30" }, + { name = "sqlalchemy-json", specifier = "==0.7.0" }, + { name = "sqlalchemy-utils", specifier = "==0.41.2" }, + { name = "swagger-ui-bundle", specifier = "==1.1.0" }, + { name = "uvicorn", specifier = "==0.30.1" }, +] + +[package.metadata.dependency-groups] +dev = [ + { name = "asserts", specifier = "==0.11.1" }, + { name = "black", specifier = ">=24.4.2" }, + { name = "colored", specifier = ">=2.2.4" }, + { name = "debugpy", specifier = ">=1.8.1" }, + { name = "deepdiff", specifier = ">=7.0.1" }, + { name = "flake8-pyproject", specifier = ">=1.2.3" }, + { name = "invoke", specifier = ">=2.2.0" }, + { name = "json2html", specifier = "==1.3.0" }, + { name = "pre-commit", specifier = "~=4.0.0" }, + { name = "pytest", specifier = ">=8.2.2" }, + { name = "pytest-env", specifier = ">=1.1.3" }, + { name = "pytest-flask", specifier = ">=1.3.0" }, + { name = "pytest-mock", specifier = "==3.14.0" }, +] + +[[package]] +name = "funding-service-design-utils" +version = "5.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "boto3" }, + { name = "flask" }, + { name = "flask-babel" }, + { name = "flask-migrate" }, + { name = "flask-redis" }, + { name = "flask-sqlalchemy" }, + { name = "gunicorn" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-dotenv" }, + { name = "python-json-logger" }, + { name = "pytz" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "rich" }, + { name = "sentry-sdk", extra = ["flask"] }, + { name = "sqlalchemy-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/54/051e40877eaf86a80763aa635c212bc26d5b6b59893f297851124e181613/funding_service_design_utils-5.0.8.tar.gz", hash = "sha256:a7c0c0acf4031f375f0cfc4974b2fa4ba10e68fcea7e5a38b50a88c407916e2a", size = 67134 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/1b/4c3a8f89a3307d8381de2fae28d6322aea11b4a580cbdb3c7f48f6633cac/funding_service_design_utils-5.0.8-py3-none-any.whl", hash = "sha256:4582eef625c13c4059648b916040cf21e39e928c5e57326f8fc461e84eb34982", size = 81075 }, +] + +[[package]] +name = "greenlet" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/90/5234a78dc0ef6496a6eb97b67a42a8e96742a56f7dc808cb954a85390448/greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", size = 271235 }, + { url = "https://files.pythonhosted.org/packages/7c/16/cd631fa0ab7d06ef06387135b7549fdcc77d8d859ed770a0d28e47b20972/greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", size = 637168 }, + { url = "https://files.pythonhosted.org/packages/2f/b1/aed39043a6fec33c284a2c9abd63ce191f4f1a07319340ffc04d2ed3256f/greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", size = 648826 }, + { url = "https://files.pythonhosted.org/packages/76/25/40e0112f7f3ebe54e8e8ed91b2b9f970805143efef16d043dfc15e70f44b/greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", size = 644443 }, + { url = "https://files.pythonhosted.org/packages/fb/2f/3850b867a9af519794784a7eeed1dd5bc68ffbcc5b28cef703711025fd0a/greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", size = 643295 }, + { url = "https://files.pythonhosted.org/packages/cf/69/79e4d63b9387b48939096e25115b8af7cd8a90397a304f92436bcb21f5b2/greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", size = 599544 }, + { url = "https://files.pythonhosted.org/packages/46/1d/44dbcb0e6c323bd6f71b8c2f4233766a5faf4b8948873225d34a0b7efa71/greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", size = 1125456 }, + { url = "https://files.pythonhosted.org/packages/e0/1d/a305dce121838d0278cee39d5bb268c657f10a5363ae4b726848f833f1bb/greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", size = 1149111 }, + { url = "https://files.pythonhosted.org/packages/96/28/d62835fb33fb5652f2e98d34c44ad1a0feacc8b1d3f1aecab035f51f267d/greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", size = 298392 }, +] + +[[package]] +name = "gunicorn" +version = "22.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/88/e2f93c5738a4c1f56a458fc7a5b1676fc31dcdbb182bef6b40a141c17d66/gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63", size = 3639760 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/97/6d610ae77b5633d24b69c2ff1ac3044e0e565ecbd1ec188f02c45073054c/gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9", size = 84443 }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "httpcore" +version = "1.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/b0/5e8b8674f8d203335a62fdfcfa0d11ebe09e23613c3391033cbba35f7926/httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61", size = 83234 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/d4/e5d7e4f2174f8a4d63c8897d79eb8fe2503f7ecc03282fee1fa2719c2704/httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5", size = 77926 }, +] + +[[package]] +name = "httptools" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/1d/d77686502fced061b3ead1c35a2d70f6b281b5f723c4eff7a2277c04e4a2/httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a", size = 191228 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/6a/80bce0216b63babf51cdc34814c3f0f10489e13ab89fb6bc91202736a8a2/httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f", size = 149778 }, + { url = "https://files.pythonhosted.org/packages/bd/7d/4cd75356dfe0ed0b40ca6873646bf9ff7b5138236c72338dc569dc57d509/httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563", size = 77604 }, + { url = "https://files.pythonhosted.org/packages/4e/74/6348ce41fb5c1484f35184c172efb8854a288e6090bb54e2210598268369/httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58", size = 346717 }, + { url = "https://files.pythonhosted.org/packages/65/e7/dd5ba95c84047118a363f0755ad78e639e0529be92424bb020496578aa3b/httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185", size = 341442 }, + { url = "https://files.pythonhosted.org/packages/d8/97/b37d596bc32be291477a8912bf9d1508d7e8553aa11a30cd871fd89cbae4/httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142", size = 354531 }, + { url = "https://files.pythonhosted.org/packages/99/c9/53ed7176583ec4b4364d941a08624288f2ae55b4ff58b392cdb68db1e1ed/httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658", size = 347754 }, + { url = "https://files.pythonhosted.org/packages/1e/fc/8a26c2adcd3f141e4729897633f03832b71ebea6f4c31cce67a92ded1961/httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b", size = 58165 }, +] + +[[package]] +name = "httpx" +version = "0.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/3da5bdf4408b8b2800061c339f240c1802f2e82d55e50bd39c5a881f47f0/httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5", size = 126413 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/7b/ddacf6dcebb42466abd03f368782142baa82e08fc0c1f8eaa05b4bae87d5/httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5", size = 75590 }, +] + +[[package]] +name = "identify" +version = "2.5.36" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/9a/83775a4e09de8b9d774a2217bfe03038c488778e58561e6970daa39b4801/identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d", size = 99049 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/d3/d31b7fe744a3b2e6c51ea04af6575d1583deb09eb33cecfc99fa7644a725/identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa", size = 98970 }, +] + +[[package]] +name = "idna" +version = "3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/ed/f86a79a07470cb07819390452f178b3bef1d375f2ec021ecfc709fc7cf07/idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", size = 189575 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/3e/741d8c82801c347547f8a2a06aa57dbb1992be9e948df2ea0eda2c8b79e8/idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0", size = 66836 }, +] + +[[package]] +name = "inflection" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/7e/691d061b7329bc8d54edbf0ec22fbfb2afe61facb681f9aaa9bff7a27d04/inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", size = 15091 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "invoke" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/42/127e6d792884ab860defc3f4d80a8f9812e48ace584ffc5a346de58cdc6c/invoke-2.2.0.tar.gz", hash = "sha256:ee6cbb101af1a859c7fe84f2a264c059020b0cb7fe3535f9424300ab568f6bd5", size = 299835 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/66/7f8c48009c72d73bc6bbe6eb87ac838d6a526146f7dab14af671121eb379/invoke-2.2.0-py3-none-any.whl", hash = "sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820", size = 160274 }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 }, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256 }, +] + +[[package]] +name = "json2html" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/d5/40b617ee19d2d79f606ed37f8a81e51158f126d2af67270c68f2b47ae0d5/json2html-1.3.0.tar.gz", hash = "sha256:8951a53662ae9cfd812685facdba693fc950ffc1c1fd1a8a2d3cf4c34600689c", size = 6977 } + +[[package]] +name = "jsonschema" +version = "4.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/f1/1c1dc0f6b3bf9e76f7526562d29c320fa7d6a2f35b37a1392cc0acd58263/jsonschema-4.22.0.tar.gz", hash = "sha256:5b22d434a45935119af990552c862e5d6d564e8f6601206b305a61fdf661a2b7", size = 325490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/2f/324fab4be6fe37fb7b521546e8a557e6cf08c1c1b3d0b4839a00f589d9ef/jsonschema-4.22.0-py3-none-any.whl", hash = "sha256:ff4cfd6b1367a40e7bc6411caec72effadd3db0bbe5017de188f2d6108335802", size = 88316 }, +] + +[[package]] +name = "jsonschema-path" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathable" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/17/47bf2da4582a6d35a1254bc058258835a452698f97dade2ce9ed3dabd512/jsonschema_path-0.3.2.tar.gz", hash = "sha256:4d0dababf341e36e9b91a5fb2a3e3fd300b0150e7fe88df4e55cc8253c5a3989", size = 11597 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/5a/f405ced79c55191e460fc6d17a14845fddf09f601e39cfcab28cc1d3ff1c/jsonschema_path-0.3.2-py3-none-any.whl", hash = "sha256:271aedfefcd161a0f467bdf23e1d9183691a61eaabf4b761046a914e369336c7", size = 14813 }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2023.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b9/cc0cc592e7c195fb8a650c1d5990b10175cf13b4c97465c72ec841de9e4b/jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc", size = 13983 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/07/44bd408781594c4d0a027666ef27fab1e441b109dc3b76b4f836f8fd04fe/jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c", size = 18482 }, +] + +[[package]] +name = "lazy-object-proxy" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/f0/f02e2d150d581a294efded4020094a371bbab42423fe78625ac18854d89b/lazy-object-proxy-1.10.0.tar.gz", hash = "sha256:78247b6d45f43a52ef35c25b5581459e85117225408a4128a3daf8bf9648ac69", size = 43271 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/42/a96d9d153f6ea38b925494cb9b42cf4a9f98fd30cad3124fc22e9d04ec34/lazy_object_proxy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:855e068b0358ab916454464a884779c7ffa312b8925c6f7401e952dcf3b89977", size = 27432 }, + { url = "https://files.pythonhosted.org/packages/4a/0d/b325461e43dde8d7644e9b9e9dd57f2a4af472b588c51ccbc92778e60ea4/lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab7004cf2e59f7c2e4345604a3e6ea0d92ac44e1c2375527d56492014e690c3", size = 69133 }, + { url = "https://files.pythonhosted.org/packages/8b/fc/83711d743fb5aaca5747bbf225fe3b5cbe085c7f6c115856b5cce80f3224/lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc0d2fc424e54c70c4bc06787e4072c4f3b1aa2f897dfdc34ce1013cf3ceef05", size = 68272 }, + { url = "https://files.pythonhosted.org/packages/8d/b5/ea47215abd4da45791664d7bbfe2976ca0de2c37af38b5e9e6cf89e0e65e/lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e2adb09778797da09d2b5ebdbceebf7dd32e2c96f79da9052b2e87b6ea495895", size = 70891 }, + { url = "https://files.pythonhosted.org/packages/8b/9b/908e12e5fa265ea1579261ff80f7b2136fd2ba254bc7f4f7e3dba83fd0f2/lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1f711e2c6dcd4edd372cf5dec5c5a30d23bba06ee012093267b3376c079ec83", size = 70451 }, + { url = "https://files.pythonhosted.org/packages/16/ab/d9a47f2e70767af5ee311d71109be6ef2991c66c77bfa18e66707edd9f8c/lazy_object_proxy-1.10.0-cp310-cp310-win32.whl", hash = "sha256:76a095cfe6045c7d0ca77db9934e8f7b71b14645f0094ffcd842349ada5c5fb9", size = 25778 }, + { url = "https://files.pythonhosted.org/packages/74/d6/0104e4154d2c30227eb54491dda8a4132be046b4cb37fb4ce915a5abc0d5/lazy_object_proxy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:b4f87d4ed9064b2628da63830986c3d2dca7501e6018347798313fcf028e2fd4", size = 27551 }, + { url = "https://files.pythonhosted.org/packages/31/8b/94dc8d58704ab87b39faed6f2fc0090b9d90e2e2aa2bbec35c79f3d2a054/lazy_object_proxy-1.10.0-pp310.pp311.pp312.pp38.pp39-none-any.whl", hash = "sha256:80fa48bd89c8f2f456fc0765c11c23bf5af827febacd2f523ca5bc1893fcc09d", size = 16405 }, +] + +[[package]] +name = "mako" +version = "1.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/03/fb5ba97ff65ce64f6d35b582aacffc26b693a98053fa831ab43a437cbddb/Mako-1.3.5.tar.gz", hash = "sha256:48dbc20568c1d276a2698b36d968fa76161bf127194907ea6fc594fa81f943bc", size = 392738 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/62/70f5a0c2dd208f9f3f2f9afd103aec42ee4d9ad2401d78342f75e9b8da36/Mako-1.3.5-py3-none-any.whl", hash = "sha256:260f1dbc3a519453a9c856dedfe4beb4e50bd5a26d96386cb6c80856556bb91a", size = 78565 }, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/54/ad5eb37bf9d51800010a74e4665425831a9db4e7c4e0fde4352e391e808e/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", size = 18206 }, + { url = "https://files.pythonhosted.org/packages/6a/4a/a4d49415e600bacae038c67f9fecc1d5433b9d3c71a4de6f33537b89654c/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", size = 14079 }, + { url = "https://files.pythonhosted.org/packages/0a/7b/85681ae3c33c385b10ac0f8dd025c30af83c78cec1c37a6aa3b55e67f5ec/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", size = 26620 }, + { url = "https://files.pythonhosted.org/packages/7c/52/2b1b570f6b8b803cef5ac28fdf78c0da318916c7d2fe9402a84d591b394c/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", size = 25818 }, + { url = "https://files.pythonhosted.org/packages/29/fe/a36ba8c7ca55621620b2d7c585313efd10729e63ef81e4e61f52330da781/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", size = 25493 }, + { url = "https://files.pythonhosted.org/packages/60/ae/9c60231cdfda003434e8bd27282b1f4e197ad5a710c14bee8bea8a9ca4f0/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", size = 30630 }, + { url = "https://files.pythonhosted.org/packages/65/dc/1510be4d179869f5dafe071aecb3f1f41b45d37c02329dfba01ff59e5ac5/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", size = 29745 }, + { url = "https://files.pythonhosted.org/packages/30/39/8d845dd7d0b0613d86e0ef89549bfb5f61ed781f59af45fc96496e897f3a/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", size = 30021 }, + { url = "https://files.pythonhosted.org/packages/c7/5c/356a6f62e4f3c5fbf2602b4771376af22a3b16efa74eb8716fb4e328e01e/MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", size = 16659 }, + { url = "https://files.pythonhosted.org/packages/69/48/acbf292615c65f0604a0c6fc402ce6d8c991276e16c80c46a8f758fbd30c/MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", size = 17213 }, +] + +[[package]] +name = "marshmallow" +version = "3.21.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/31/0881962e77efa2d524ca80566ba1fb7cab000edaa9f4152b97a39b8d9a2d/marshmallow-3.21.3.tar.gz", hash = "sha256:4f57c5e050a54d66361e826f94fba213eb10b67b2fdb02c3e0343ce207ba1662", size = 176279 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/d7/f318261e6ccbba86bdf626e07cd850981508fdaec52cfcdc4ac1030327ab/marshmallow-3.21.3-py3-none-any.whl", hash = "sha256:86ce7fb914aa865001a4b2092c4c2872d13bc347f3d42673272cabfdbad386f1", size = 49201 }, +] + +[[package]] +name = "marshmallow-sqlalchemy" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "marshmallow" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/e5/6ed1255b8b252cbc063082c85db3690ff40118c891ebda3cf633ec065322/marshmallow_sqlalchemy-1.0.0.tar.gz", hash = "sha256:20a0f2fcdd5bddc86444fa01461f17f9b6a12a8ddd4ca8c9b34fe2f2e35d00a2", size = 49747 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/32/95b3e03d41480e5e8963034ed569e94cd5febe64bc23240936b108592bbb/marshmallow_sqlalchemy-1.0.0-py3-none-any.whl", hash = "sha256:f415d57809e3555b6323356589aba91e36e4470f35953d3a10c755ac5c3307df", size = 14427 }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350 }, +] + +[[package]] +name = "mypy" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b6/297734bb9f20ddf5e831cf4a83f422ddef5a29a33463999f0959d9cdc2df/mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131", size = 3022145 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/82/2081dbfbbf1071e1370e57f9e327adeda060113688ec0d6bf7bbf4d7a5ad/mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2", size = 10819193 }, + { url = "https://files.pythonhosted.org/packages/e8/1b/b7c9caa89955a7d9c89eac79f31550f48f2c8233b5e12fe48ef55cd2e953/mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99", size = 9970689 }, + { url = "https://files.pythonhosted.org/packages/15/ae/03d3f767f1ca5576970720ea551b43b79254d12998484d8f3e63fc07561e/mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2", size = 12728098 }, + { url = "https://files.pythonhosted.org/packages/96/ba/8f5db8bd94c18d86033d09bbe634d471c1e9d7014cc621585973183ad1d0/mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9", size = 12798838 }, + { url = "https://files.pythonhosted.org/packages/0e/ad/d476f1055deea6e63a91e065ba046a7ee494705574c4f9730de439172810/mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051", size = 9365995 }, + { url = "https://files.pythonhosted.org/packages/e9/39/0148f7ee1b7f3a86d378a23b88cb85c432f83914ceb60364efa1769c598f/mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee", size = 2580084 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "openapi-schema-validator" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema" }, + { name = "jsonschema-specifications" }, + { name = "rfc3339-validator" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/b2/7d5bdf2b26b6a95ebf4fbec294acaf4306c713f3a47c2453962511110248/openapi_schema_validator-0.6.2.tar.gz", hash = "sha256:11a95c9c9017912964e3e5f2545a5b11c3814880681fcacfb73b1759bb4f2804", size = 11860 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/dc/9aefae8891454130968ff079ece851d1ae9ccf6fb7965761f47c50c04853/openapi_schema_validator-0.6.2-py3-none-any.whl", hash = "sha256:c4887c1347c669eb7cded9090f4438b710845cd0f90d1fb9e1b3303fb37339f8", size = 8750 }, +] + +[[package]] +name = "openapi-spec-validator" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema" }, + { name = "jsonschema-path" }, + { name = "lazy-object-proxy" }, + { name = "openapi-schema-validator" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/fe/21954ff978239dc29ebb313f5c87eeb4ec929b694b9667323086730998e2/openapi_spec_validator-0.7.1.tar.gz", hash = "sha256:8577b85a8268685da6f8aa30990b83b7960d4d1117e901d451b5d572605e5ec7", size = 37985 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/4d/e744fff95aaf3aeafc968d5ba7297c8cda0d1ecb8e3acd21b25adae4d835/openapi_spec_validator-0.7.1-py3-none-any.whl", hash = "sha256:3c81825043f24ccbcd2f4b149b11e8231abce5ba84f37065e14ec947d8f4e959", size = 38998 }, +] + +[[package]] +name = "ordered-set" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/ca/bfac8bc689799bcca4157e0e0ced07e70ce125193fc2e166d2e685b7e2fe/ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8", size = 12826 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/55/af02708f230eb77084a299d7b08175cff006dea4f2721074b92cdb0296c0/ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562", size = 7634 }, +] + +[[package]] +name = "packaging" +version = "24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 }, +] + +[[package]] +name = "pathable" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/ed/e0e29300253b61dea3b7ec3a31f5d061d577c2a6fd1e35c5cfd0e6f2cd6d/pathable-0.4.3.tar.gz", hash = "sha256:5c869d315be50776cc8a993f3af43e0c60dc01506b399643f919034ebf4cdcab", size = 8679 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/0a/acfb251ba01009d3053f04f4661e96abf9d485266b04a0a4deebc702d9cb/pathable-0.4.3-py3-none-any.whl", hash = "sha256:cdd7b1f9d7d5c8b8d3315dbf5a86b2596053ae845f056f57d97c0eefff84da14", size = 9587 }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + +[[package]] +name = "platformdirs" +version = "4.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/52/0763d1d976d5c262df53ddda8d8d4719eedf9594d046f117c25a27261a19/platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3", size = 20916 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/13/2aa1f0e1364feb2c9ef45302f387ac0bd81484e9c9a4c5688a322fbdfd08/platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee", size = 18146 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "prance" +version = "23.6.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "chardet" }, + { name = "packaging" }, + { name = "requests" }, + { name = "ruamel-yaml" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/f0/bcb5ffc8b7ab8e3d02dbef3bd945cf8fd6e12c146774f900659406b9fce1/prance-23.6.21.0.tar.gz", hash = "sha256:d8c15f8ac34019751cc4945f866d8d964d7888016d10de3592e339567177cabe", size = 2798776 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/db/4fb4901ee61274d0ab97746461fc5f2637e5d73aa73f34ee28e941a699a1/prance-23.6.21.0-py3-none-any.whl", hash = "sha256:6a4276fa07ed9f22feda4331097d7503c4adc3097e46ffae97425f2c1026bd9f", size = 36279 }, +] + +[[package]] +name = "pre-commit" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/e22c292035f1bac8b9f5237a2622305bc0304e776080b246f3df57c4ff9f/pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", size = 191678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713 }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/07/e720e53bfab016ebcc34241695ccc06a9e3d91ba19b40ca81317afbdc440/psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c", size = 384973 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7c/6aaf8c3cb05d86d2c3f407b95bac0c71a43f2718e38c1091972aacb5e1b2/psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202", size = 2822503 }, + { url = "https://files.pythonhosted.org/packages/72/3d/acab427845198794aafd963dd073ee35810e2c52606e8a28c12db39821fb/psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7", size = 2552645 }, + { url = "https://files.pythonhosted.org/packages/ed/be/6c787962d706e55a528ef1693dd7251de657ae60e4d9d767ed61e8e2975c/psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b", size = 2850980 }, + { url = "https://files.pythonhosted.org/packages/83/50/a054076c6358753661cd1da59f4dabc03e83d51690371f3fd1edb9e2cf72/psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9", size = 3080543 }, + { url = "https://files.pythonhosted.org/packages/9c/02/826dc5cdfc9515423ec912ba00cc2e4eb09f69e0339b177c9c742f2a09a2/psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84", size = 3264316 }, + { url = "https://files.pythonhosted.org/packages/bc/0d/486e3fa27f39a00168abfcf14a3d8444f437f4b755cc34316da1124f293d/psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e", size = 3019508 }, + { url = "https://files.pythonhosted.org/packages/41/af/bce37630c525d2b9cf93f930110fc98616d6aca308d59b833b83b3a38176/psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98", size = 2355821 }, + { url = "https://files.pythonhosted.org/packages/3b/76/e46dae1b2273814ef80231f86d59cadf94ec36fd757045ed713c5b75cde7/psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245", size = 2534855 }, + { url = "https://files.pythonhosted.org/packages/0e/6d/e97245eabff29d7c2de5fc1fc17cf7ef427beee93d20a5ae114c6e6718bd/psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e", size = 2486614 }, + { url = "https://files.pythonhosted.org/packages/70/a7/2cd2c9d5e23b556c11e3b7da41895808d9b056f8f34f50de4375a35b4951/psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f", size = 2454928 }, + { url = "https://files.pythonhosted.org/packages/63/41/815d19767e2adb1a585213b801c954f46102f305c352c4a4f96287342d44/psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682", size = 1025249 }, + { url = "https://files.pythonhosted.org/packages/5e/4c/9233e0e206634a5387f3ab40f334a5325fb8bef2ca4e12ee7dbdeaf96afc/psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0", size = 1163645 }, +] + +[[package]] +name = "pycodestyle" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/56/52d8283e1a1c85695291040192776931782831e21117c84311cbdd63f70c/pycodestyle-2.12.0.tar.gz", hash = "sha256:442f950141b4f43df752dd303511ffded3a04c2b6fb7f65980574f0c31e6e79c", size = 39055 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/c4/bf8ede2d1641e0a2e027c6d0c7060e00332851ea772cc5cee42a4a207707/pycodestyle-2.12.0-py2.py3-none-any.whl", hash = "sha256:949a39f6b86c3e1515ba1787c2022131d165a8ad271b11370a8819aa070269e4", size = 31221 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pyflakes" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/f9/669d8c9c86613c9d568757c7f5824bd3197d7b1c6c27553bc5618a27cce2/pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", size = 63788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/d7/f1b7db88d8e4417c5d47adad627a93547f44bdc9028372dbd2313f34a855/pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a", size = 62725 }, +] + +[[package]] +name = "pygments" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, +] + +[[package]] +name = "pyjwt" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/72/8259b2bccfe4673330cea843ab23f86858a419d8f1493f66d413a76c7e3b/PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de", size = 78313 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/4f/e04a8067c7c96c364cef7ef73906504e2f40d690811c021e1a1901473a19/PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320", size = 22591 }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pytest" +version = "8.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/58/e993ca5357553c966b9e73cb3475d9c935fe9488746e13ebdf9b80fae508/pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977", size = 1427980 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/e7/81ebdd666d3bff6670d27349b5053605d83d55548e6bd5711f3b0ae7dd23/pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343", size = 339873 }, +] + +[[package]] +name = "pytest-env" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/cc/df6940b2527bfa634c00940dfb6e3ec873bdfb7507b55894c93283fa3178/pytest_env-1.1.3.tar.gz", hash = "sha256:fcd7dc23bb71efd3d35632bde1bbe5ee8c8dc4489d6617fb010674880d96216b", size = 8627 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/b2/bdc663a5647ce2034f7e8420122af340df87c01ba97745fc753b8c917acb/pytest_env-1.1.3-py3-none-any.whl", hash = "sha256:aada77e6d09fcfb04540a6e462c58533c37df35fa853da78707b17ec04d17dfc", size = 6154 }, +] + +[[package]] +name = "pytest-flask" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "pytest" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/23/32b36d2f769805c0f3069ca8d9eeee77b27fcf86d41d40c6061ddce51c7d/pytest-flask-1.3.0.tar.gz", hash = "sha256:58be1c97b21ba3c4d47e0a7691eb41007748506c36bf51004f78df10691fa95e", size = 35816 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/03/7a917fda3d0e96b4e80ab1f83a6628ec4ee4a882523b49417d3891bacc9e/pytest_flask-1.3.0-py3-none-any.whl", hash = "sha256:c0e36e6b0fddc3b91c4362661db83fa694d1feb91fa505475be6732b5bc8c253", size = 13105 }, +] + +[[package]] +name = "pytest-html" +version = "4.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "pytest" }, + { name = "pytest-metadata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/ab/4862dcb5a8a514bd87747e06b8d55483c0c9e987e1b66972336946e49b49/pytest_html-4.1.1.tar.gz", hash = "sha256:70a01e8ae5800f4a074b56a4cb1025c8f4f9b038bba5fe31e3c98eb996686f07", size = 150773 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c7/c160021cbecd956cc1a6f79e5fe155f7868b2e5b848f1320dad0b3e3122f/pytest_html-4.1.1-py3-none-any.whl", hash = "sha256:c8152cea03bd4e9bee6d525573b67bbc6622967b72b9628dda0ea3e2a0b5dd71", size = 23491 }, +] + +[[package]] +name = "pytest-metadata" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/85/8c969f8bec4e559f8f2b958a15229a35495f5b4ce499f6b865eac54b878d/pytest_metadata-3.1.1.tar.gz", hash = "sha256:d2a29b0355fbc03f168aa96d41ff88b1a3b44a3b02acbe491801c98a048017c8", size = 9952 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/43/7e7b2ec865caa92f67b8f0e9231a798d102724ca4c0e1f414316be1c1ef2/pytest_metadata-3.1.1-py3-none-any.whl", hash = "sha256:c8e0844db684ee1c798cfa38908d20d67d0463ecb6137c72e91f418558dd5f4b", size = 11428 }, +] + +[[package]] +name = "pytest-mock" +version = "3.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, +] + +[[package]] +name = "python-json-logger" +version = "2.0.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/da/95963cebfc578dabd323d7263958dfb68898617912bb09327dd30e9c8d13/python-json-logger-2.0.7.tar.gz", hash = "sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c", size = 10508 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/a6/145655273568ee78a581e734cf35beb9e33a370b29c5d3c8fee3744de29f/python_json_logger-2.0.7-py3-none-any.whl", hash = "sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd", size = 8067 }, +] + +[[package]] +name = "python-multipart" +version = "0.0.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/0f/9c55ac6c84c0336e22a26fa84ca6c51d58d7ac3a2d78b0dfa8748826c883/python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026", size = 31516 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/47/444768600d9e0ebc82f8e347775d24aef8f6348cf00e9fa0e81910814e6d/python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215", size = 22299 }, +] + +[[package]] +name = "pytz" +version = "2024.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/26/9f1f00a5d021fff16dee3de13d43e5e978f3d58928e129c3a62cf7eb9738/pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812", size = 316214 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/3d/a121f284241f08268b21359bd425f7d4825cffc5ac5cd0e1b3d82ffd2b10/pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319", size = 505474 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/e5/af35f7ea75cf72f2cd079c95ee16797de7cd71f29ea7c68ae5ce7be1eda0/PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", size = 125201 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/06/4beb652c0fe16834032e54f0956443d4cc797fe645527acee59e7deaa0a2/PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", size = 189447 }, + { url = "https://files.pythonhosted.org/packages/5b/07/10033a403b23405a8fc48975444463d3d10a5c2736b7eb2550b07b367429/PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f", size = 169264 }, + { url = "https://files.pythonhosted.org/packages/f1/26/55e4f21db1f72eaef092015d9017c11510e7e6301c62a6cfee91295d13c6/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", size = 677003 }, + { url = "https://files.pythonhosted.org/packages/ba/91/090818dfa62e85181f3ae23dd1e8b7ea7f09684864a900cab72d29c57346/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", size = 699070 }, + { url = "https://files.pythonhosted.org/packages/29/61/bf33c6c85c55bc45a29eee3195848ff2d518d84735eb0e2d8cb42e0d285e/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", size = 705525 }, + { url = "https://files.pythonhosted.org/packages/07/91/45dfd0ef821a7f41d9d0136ea3608bb5b1653e42fd56a7970532cb5c003f/PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", size = 707514 }, + { url = "https://files.pythonhosted.org/packages/b6/a0/b6700da5d49e9fed49dc3243d3771b598dad07abb37cc32e524607f96adc/PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", size = 130488 }, + { url = "https://files.pythonhosted.org/packages/24/97/9b59b43431f98d01806b288532da38099cc6f2fea0f3d712e21e269c0279/PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", size = 145338 }, +] + +[[package]] +name = "redis" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/88/63d802c2b18dd9eaa5b846cbf18917c6b2882f20efda398cc16a7500b02c/redis-4.6.0.tar.gz", hash = "sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d", size = 4561721 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/2e/409703d645363352a20c944f5d119bdae3eb3034051a53724a7c5fee12b8/redis-4.6.0-py3-none-any.whl", hash = "sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c", size = 241149 }, +] + +[[package]] +name = "referencing" +version = "0.31.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/ce/e99def6196f53af8de12a9c36968de32f80b7871084d677d0dfcd2762d0b/referencing-0.31.1.tar.gz", hash = "sha256:81a1471c68c9d5e3831c30ad1dd9815c45b558e596653db751a2bfdd17b3b9ec", size = 54177 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/d8/e826b3f743d97e45d3ace674a5c6f026069428e608c5fde3d08d072c87f2/referencing-0.31.1-py3-none-any.whl", hash = "sha256:c19c4d006f1757e3dd75c4f784d38f8698d87b649c54f9ace14e5e8c9667c01d", size = 25842 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490 }, +] + +[[package]] +name = "rich" +version = "12.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "commonmark" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/23/814edf09ec6470d52022b9e95c23c1bef77f0bc451761e1504ebd09606d3/rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0", size = 220114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/60/81ac2e7d1e3b861ab478a72e3b20fc91c4302acd2274822e493758941829/rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e", size = 237505 }, +] + +[[package]] +name = "rpds-py" +version = "0.18.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/aa/e7c404bdee1db7be09860dff423d022ffdce9269ec8e6532cce09ee7beea/rpds_py-0.18.1.tar.gz", hash = "sha256:dc48b479d540770c811fbd1eb9ba2bb66951863e448efec2e2c102625328e92f", size = 25388 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/eb/5b7591bb8d9f710df243a3b6304a2b70db5a426a2bd478c2912f8b81b806/rpds_py-0.18.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:d31dea506d718693b6b2cffc0648a8929bdc51c70a311b2770f09611caa10d53", size = 327723 }, + { url = "https://files.pythonhosted.org/packages/b9/9a/f1cce2481968d0ff1301d6da02bb977d7c7dc2ad9d218281ead38cc7f1ae/rpds_py-0.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:732672fbc449bab754e0b15356c077cc31566df874964d4801ab14f71951ea80", size = 322269 }, + { url = "https://files.pythonhosted.org/packages/f3/16/7ddc46210ec4b52614c5d5ed72ca0afd12b6c6af1d7cb3859b2e7faa8ffe/rpds_py-0.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a98a1f0552b5f227a3d6422dbd61bc6f30db170939bd87ed14f3c339aa6c7c9", size = 1114128 }, + { url = "https://files.pythonhosted.org/packages/fd/6a/e67b83791863db607a297b822fe95813c6cbff979608496f47d81ad45fea/rpds_py-0.18.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f1944ce16401aad1e3f7d312247b3d5de7981f634dc9dfe90da72b87d37887d", size = 1123687 }, + { url = "https://files.pythonhosted.org/packages/d7/a9/b25013071a61f008a8266a208e701cf8ec2c2946feb6005b3ec454f63a66/rpds_py-0.18.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38e14fb4e370885c4ecd734f093a2225ee52dc384b86fa55fe3f74638b2cfb09", size = 1145179 }, + { url = "https://files.pythonhosted.org/packages/57/65/b9769f891d0f2f915151f6d172f82ce182cedf950bcc65af4e853d794421/rpds_py-0.18.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08d74b184f9ab6289b87b19fe6a6d1a97fbfea84b8a3e745e87a5de3029bf944", size = 1309609 }, + { url = "https://files.pythonhosted.org/packages/e5/20/10c12b1acb102c4981a7e1dc86b60e36c1d5c940a7bda48643542f80dbff/rpds_py-0.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d70129cef4a8d979caa37e7fe957202e7eee8ea02c5e16455bc9808a59c6b2f0", size = 1114172 }, + { url = "https://files.pythonhosted.org/packages/73/5b/bf77d1fe5025eeec85d62e389edacf073b93553b4837f8221093acc3ed7e/rpds_py-0.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce0bb20e3a11bd04461324a6a798af34d503f8d6f1aa3d2aa8901ceaf039176d", size = 1139248 }, + { url = "https://files.pythonhosted.org/packages/c2/b9/dcb20646cda07b4e9db3b346c19a4685623c9a9aa8ad8a566776def0da33/rpds_py-0.18.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81c5196a790032e0fc2464c0b4ab95f8610f96f1f2fa3d4deacce6a79852da60", size = 1277001 }, + { url = "https://files.pythonhosted.org/packages/55/5c/f59ed857a85d6713d936d70e3235a7c9bc51bc83ab7c1b4e9b4f9371abbc/rpds_py-0.18.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f3027be483868c99b4985fda802a57a67fdf30c5d9a50338d9db646d590198da", size = 1304195 }, + { url = "https://files.pythonhosted.org/packages/4f/3c/2807bb396f1d940813d1ec39efb8984ec01e84e2064db9a06bf314f3658d/rpds_py-0.18.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d44607f98caa2961bab4fa3c4309724b185b464cdc3ba6f3d7340bac3ec97cc1", size = 1282968 }, + { url = "https://files.pythonhosted.org/packages/d2/13/495eea6921b280ac04602fc3cc4b385ab985a2eb3e450281d02ce98872bc/rpds_py-0.18.1-cp310-none-win32.whl", hash = "sha256:c273e795e7a0f1fddd46e1e3cb8be15634c29ae8ff31c196debb620e1edb9333", size = 196635 }, + { url = "https://files.pythonhosted.org/packages/1b/bf/c8f8b5d1da7f0673998c63d2246987773c3422e1c2482bbf511b7fffe184/rpds_py-0.18.1-cp310-none-win_amd64.whl", hash = "sha256:8352f48d511de5f973e4f2f9412736d7dea76c69faa6d36bcf885b50c758ab9a", size = 209023 }, + { url = "https://files.pythonhosted.org/packages/97/04/966a1b2286d6af7ab00bf66ccd18cac38c59b0c2973be18975edb19c639f/rpds_py-0.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cbfbea39ba64f5e53ae2915de36f130588bba71245b418060ec3330ebf85678e", size = 326661 }, + { url = "https://files.pythonhosted.org/packages/75/e6/3a04f482d8c6d602d6d848ce18e6195510504fed6ff32928052321fcca72/rpds_py-0.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a3d456ff2a6a4d2adcdf3c1c960a36f4fd2fec6e3b4902a42a384d17cf4e7a65", size = 321065 }, + { url = "https://files.pythonhosted.org/packages/c0/96/edfe0d2cb019aab199344d19a2c0e2e3514ffeb67a04236933630c8a4090/rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7700936ef9d006b7ef605dc53aa364da2de5a3aa65516a1f3ce73bf82ecfc7ae", size = 1114055 }, + { url = "https://files.pythonhosted.org/packages/05/48/b578893a32290c9011e93e340264fdefa0df0f074d793a8c419e707fe346/rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:51584acc5916212e1bf45edd17f3a6b05fe0cbb40482d25e619f824dccb679de", size = 1123145 }, + { url = "https://files.pythonhosted.org/packages/a6/71/f4e8ac7a833ff6f70e18f6d2496b1a1d3a08272c777624359d2aa785de45/rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:942695a206a58d2575033ff1e42b12b2aece98d6003c6bc739fbf33d1773b12f", size = 1144650 }, + { url = "https://files.pythonhosted.org/packages/cb/61/90bb60a78c7c5da7155fed66b6cc875b9b402108565a00057f45391f3dcc/rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b906b5f58892813e5ba5c6056d6a5ad08f358ba49f046d910ad992196ea61397", size = 1306828 }, + { url = "https://files.pythonhosted.org/packages/79/f4/e91e3d9c462387c08b833687c7095967461b785ac52e95eaa4d928a459d8/rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6f8e3fecca256fefc91bb6765a693d96692459d7d4c644660a9fff32e517843", size = 1116549 }, + { url = "https://files.pythonhosted.org/packages/47/c2/c711866156543ada46d5977383235d4c7821bb27db108014f4895d18fc9c/rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7732770412bab81c5a9f6d20aeb60ae943a9b36dcd990d876a773526468e7163", size = 1138719 }, + { url = "https://files.pythonhosted.org/packages/a9/60/cc3d345d125998ecbccb9ab394193243c66903d53b02beade693810563fa/rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:bd1105b50ede37461c1d51b9698c4f4be6e13e69a908ab7751e3807985fc0346", size = 1275986 }, + { url = "https://files.pythonhosted.org/packages/92/00/426001ad8c36f1a9a76cc414489f3eab6750f34cf1fee5ec054dba8af07f/rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:618916f5535784960f3ecf8111581f4ad31d347c3de66d02e728de460a46303c", size = 1306612 }, + { url = "https://files.pythonhosted.org/packages/07/e9/89e1f70ee6e32fd2c7f0829d9264b28683bcb4ddb54bcfff0fa4506bf629/rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:17c6d2155e2423f7e79e3bb18151c686d40db42d8645e7977442170c360194d4", size = 1281640 }, +] + +[[package]] +name = "ruamel-yaml" +version = "0.18.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ruamel-yaml-clib", marker = "platform_python_implementation == 'CPython' and python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/81/4dfc17eb6ebb1aac314a3eb863c1325b907863a1b8b1382cdffcb6ac0ed9/ruamel.yaml-0.18.6.tar.gz", hash = "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b", size = 143362 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/67/8ece580cc363331d9a53055130f86b096bf16e38156e33b1d3014fffda6b/ruamel.yaml-0.18.6-py3-none-any.whl", hash = "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636", size = 117761 }, +] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/ab/bab9eb1566cd16f060b54055dd39cf6a34bfa0240c53a7218c43e974295b/ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512", size = 213824 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/01/37ac131614f71b98e9b148b2d7790662dcee92217d2fb4bac1aa377def33/ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d", size = 148236 }, + { url = "https://files.pythonhosted.org/packages/61/ee/4874c9fc96010fce85abefdcbe770650c5324288e988d7a48b527a423815/ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462", size = 133996 }, + { url = "https://files.pythonhosted.org/packages/d3/62/c60b034d9a008bbd566eeecf53a5a4c73d191c8de261290db6761802b72d/ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412", size = 526680 }, + { url = "https://files.pythonhosted.org/packages/90/8c/6cdb44f548b29eb6328b9e7e175696336bc856de2ff82e5776f860f03822/ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f", size = 605853 }, + { url = "https://files.pythonhosted.org/packages/88/30/fc45b45d5eaf2ff36cffd215a2f85e9b90ac04e70b97fd4097017abfb567/ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334", size = 655206 }, + { url = "https://files.pythonhosted.org/packages/af/dc/133547f90f744a0c827bac5411d84d4e81da640deb3af1459e38c5f3b6a0/ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d", size = 689649 }, + { url = "https://files.pythonhosted.org/packages/23/1d/589139191b187a3c750ae8d983c42fd799246d5f0dd84451a0575c9bdbe9/ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d", size = 100044 }, + { url = "https://files.pythonhosted.org/packages/4f/5b/744df20285a75ac4c606452ce9a0fcc42087d122f42294518ded1017697c/ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31", size = 117825 }, +] + +[[package]] +name = "s3transfer" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/67/94c6730ee4c34505b14d94040e2f31edf144c230b6b49e971b4f25ff8fab/s3transfer-0.10.2.tar.gz", hash = "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6", size = 144095 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/4a/b221409913760d26cf4498b7b1741d510c82d3ad38381984a3ddc135ec66/s3transfer-0.10.2-py3-none-any.whl", hash = "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69", size = 82716 }, +] + +[[package]] +name = "sentry-sdk" +version = "2.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/8c/fa54692542bc11649a3590d1ba1f455fba9986758048b2ecfee8498cfaf9/sentry_sdk-2.9.0.tar.gz", hash = "sha256:4c85bad74df9767976afb3eeddc33e0e153300e887d637775a753a35ef99bee6", size = 276392 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/22/249d158f9497231cd24e2829049b2c3e82841ec81b0a98b6f722357d5fed/sentry_sdk-2.9.0-py2.py3-none-any.whl", hash = "sha256:0bea5fa8b564cc0d09f2e6f55893e8f70286048b0ffb3a341d5b695d1af0e6ee", size = 301811 }, +] + +[package.optional-dependencies] +flask = [ + { name = "blinker" }, + { name = "flask" }, + { name = "markupsafe" }, +] + +[[package]] +name = "six" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "soupsieve" +version = "2.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/21/952a240de1c196c7e3fbcd4e559681f0419b1280c617db21157a0390717b/soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690", size = 100943 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/f3/038b302fdfbe3be7da016777069f26ceefe11a681055ea1f7817546508e3/soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7", size = 36131 }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.30" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/d0/0137ebcf0dc230c2e82a621b3af755b8788a2a9dd6fd1b8cd6d5e7f6b00d/SQLAlchemy-2.0.30.tar.gz", hash = "sha256:2b1708916730f4830bc69d6f49d37f7698b5bd7530aca7f04f785f8849e95255", size = 9579500 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/d0/aec1421ff832da60badef9cf01fdc795b2ea399c5d65e2b8c37d801d06ff/SQLAlchemy-2.0.30-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3b48154678e76445c7ded1896715ce05319f74b1e73cf82d4f8b59b46e9c0ddc", size = 2082300 }, + { url = "https://files.pythonhosted.org/packages/be/86/25faae6b5c9920a7954bf7c68a7ff8f3436e9f140ac7dfccc8fc213bce66/SQLAlchemy-2.0.30-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2753743c2afd061bb95a61a51bbb6a1a11ac1c44292fad898f10c9839a7f75b2", size = 2073500 }, + { url = "https://files.pythonhosted.org/packages/22/da/90e8d421836c2d265b7a72a7923348705d1e0124321bb2b3f2de307b91d0/SQLAlchemy-2.0.30-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7bfc726d167f425d4c16269a9a10fe8630ff6d14b683d588044dcef2d0f6be7", size = 3055759 }, + { url = "https://files.pythonhosted.org/packages/c2/83/2ed47c5b841496d4e106f8ed04316c6193ba8615ab70fe769a593237b20b/SQLAlchemy-2.0.30-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4f61ada6979223013d9ab83a3ed003ded6959eae37d0d685db2c147e9143797", size = 3064230 }, + { url = "https://files.pythonhosted.org/packages/a5/3f/b3f1bc1f14ab65ab1d8b4030ba0787b529bfde654b2b2cf9eb7cdb26b28c/SQLAlchemy-2.0.30-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a365eda439b7a00732638f11072907c1bc8e351c7665e7e5da91b169af794af", size = 3098327 }, + { url = "https://files.pythonhosted.org/packages/8c/e4/3550fba0561560cb8fae78d3636813f7a2b83eb7b55c76703ac34143cd3c/SQLAlchemy-2.0.30-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bba002a9447b291548e8d66fd8c96a6a7ed4f2def0bb155f4f0a1309fd2735d5", size = 3096450 }, + { url = "https://files.pythonhosted.org/packages/18/bc/e6117ae5cc3577fb3ee487f6321802ec72dcdd81fca13baf0db907818ad3/SQLAlchemy-2.0.30-cp310-cp310-win32.whl", hash = "sha256:0138c5c16be3600923fa2169532205d18891b28afa817cb49b50e08f62198bb8", size = 2052510 }, + { url = "https://files.pythonhosted.org/packages/f2/6b/18900a4df0d91397569f645105a4fb36f12033075622e3d131c456dc73f3/SQLAlchemy-2.0.30-cp310-cp310-win_amd64.whl", hash = "sha256:99650e9f4cf3ad0d409fed3eec4f071fadd032e9a5edc7270cd646a26446feeb", size = 2077661 }, + { url = "https://files.pythonhosted.org/packages/de/80/13fc9c003dffc169e03244e0ce23495ff54bbd77ba1245ef01c9a5c04a4c/SQLAlchemy-2.0.30-py3-none-any.whl", hash = "sha256:7108d569d3990c71e26a42f60474b4c02c8586c4681af5fd67e51a044fdea86a", size = 1873477 }, +] + +[package.optional-dependencies] +mypy = [ + { name = "mypy" }, +] + +[[package]] +name = "sqlalchemy-json" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/57/73ba3d0ee5efbec5a0d15ee3e21606edd33b1d1fd11b5d64e581c8b8a3f6/sqlalchemy-json-0.7.0.tar.gz", hash = "sha256:620d0b26f648f21a8fa9127df66f55f83a5ab4ae010e5397a5c6989a08238561", size = 8848 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/79/1fc7309ecf75756e9ad9280f19cd83ca7b79a0dae36cd025f668e8e2741f/sqlalchemy_json-0.7.0-py3-none-any.whl", hash = "sha256:27881d662ca18363a4ac28175cc47ea2a6f2bef997ae1159c151026b741818e6", size = 7688 }, +] + +[[package]] +name = "sqlalchemy-utils" +version = "0.41.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/bf/abfd5474cdd89ddd36dbbde9c6efba16bfa7f5448913eba946fed14729da/SQLAlchemy-Utils-0.41.2.tar.gz", hash = "sha256:bc599c8c3b3319e53ce6c5c3c471120bd325d0071fb6f38a10e924e3d07b9990", size = 138017 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/f0/dc4757b83ac1ab853cf222df8535ed73973e0c203d983982ba7b8bc60508/SQLAlchemy_Utils-0.41.2-py3-none-any.whl", hash = "sha256:85cf3842da2bf060760f955f8467b87983fb2e30f1764fd0e24a48307dc8ec6e", size = 93083 }, +] + +[[package]] +name = "starlette" +version = "0.37.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/b5/6bceb93ff20bd7ca36e6f7c540581abb18f53130fabb30ba526e26fd819b/starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823", size = 2843736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/18/31fa32ed6c68ba66220204ef0be798c349d0a20c1901f9d4a794e08c76d8/starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee", size = 71908 }, +] + +[[package]] +name = "swagger-ui-bundle" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/e6/d8ae21087a42627c2a04a738c947825b78c26b18595704b94bd3227197a2/swagger_ui_bundle-1.1.0.tar.gz", hash = "sha256:20673c3431c8733d5d1615ecf79d9acf30cff75202acaf21a7d9c7f489714529", size = 2599741 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/66/8fb11445940bde7ca328d6aa23dd36b6056197d862f4bd6bb51c820c50e5/swagger_ui_bundle-1.1.0-py3-none-any.whl", hash = "sha256:f7526f7bb99923e10594c54247265839bec97e96b0438561ac86faf40d40dd57", size = 2626591 }, +] + +[[package]] +name = "tomli" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "urllib3" +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/6d/fa469ae21497ddc8bc93e5877702dca7cb8f911e337aca7452b5724f1bb6/urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168", size = 292266 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/1c/89ffc63a9605b583d5df2be791a27bc1a42b7c32bab68d3c8f2f73a98cd4/urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", size = 121444 }, +] + +[[package]] +name = "uvicorn" +version = "0.30.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/16/9f5ccaa1a76e5bfbaa0c67640e2db8a5214ca08d92a1b427fa1677b3da88/uvicorn-0.30.1.tar.gz", hash = "sha256:d46cd8e0fd80240baffbcd9ec1012a712938754afcf81bce56c024c1656aece8", size = 42572 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/f9/e6f30ba6094733e4f9794fd098ca0543a19b07ac1fa3075d595bf0f1fb60/uvicorn-0.30.1-py3-none-any.whl", hash = "sha256:cd17daa7f3b9d7a24de3617820e634d0933b69eed8e33a516071174427238c81", size = 62393 }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/16/728cc5dde368e6eddb299c5aec4d10eaf25335a5af04e8c0abd68e2e9d32/uvloop-0.19.0.tar.gz", hash = "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd", size = 2318492 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/c2/27bf858a576b1fa35b5c2c2029c8cec424a8789e87545ed2f25466d1f21d/uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e", size = 1443484 }, + { url = "https://files.pythonhosted.org/packages/4e/35/05b6064b93f4113412d1fd92bdcb6018607e78ae94d1712e63e533f9b2fa/uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428", size = 793850 }, + { url = "https://files.pythonhosted.org/packages/aa/56/b62ab4e10458ce96bb30c98d327c127f989d3bb4ef899e4c410c739f7ef6/uvloop-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8", size = 3418601 }, + { url = "https://files.pythonhosted.org/packages/ab/ed/12729fba5e3b7e02ee70b3ea230b88e60a50375cf63300db22607694d2f0/uvloop-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849", size = 3416731 }, + { url = "https://files.pythonhosted.org/packages/a2/23/80381a2d728d2a0c36e2eef202f5b77428990004d8fbdd3865558ff49fa5/uvloop-0.19.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957", size = 4128572 }, + { url = "https://files.pythonhosted.org/packages/6b/23/1ee41a15e1ad15182e2bd12cbfd37bcb6802f01d6bbcaddf6ca136cbb308/uvloop-0.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd", size = 4129235 }, +] + +[[package]] +name = "virtualenv" +version = "20.26.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/5a/cabd5846cb550e2871d9532def625d0771f4e38f765c30dc0d101be33697/virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c", size = 7290363 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/02/085eee8570e807db9a1aa905e18c9123efec753ae9021b029115c6e0bb28/virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b", size = 3917916 }, +] + +[[package]] +name = "watchfiles" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/e1/666771f0746f95c4df767ff98ff17fe55bb0c32ac88ec14ce0615a789330/watchfiles-0.22.0.tar.gz", hash = "sha256:988e981aaab4f3955209e7e28c7794acdb690be1efa7f16f8ea5aba7ffdadacb", size = 37900 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/1e/040d62f23f02dea6903edd810ccbab04115fbc3862c2ce37fa31323b5d83/watchfiles-0.22.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:da1e0a8caebf17976e2ffd00fa15f258e14749db5e014660f53114b676e68538", size = 394988 }, + { url = "https://files.pythonhosted.org/packages/29/21/59afadc00e3f6cc18aa507f7ae5fef1d6bc864dcc35c595f06d012433fc0/watchfiles-0.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61af9efa0733dc4ca462347becb82e8ef4945aba5135b1638bfc20fad64d4f0e", size = 390956 }, + { url = "https://files.pythonhosted.org/packages/8f/f1/ea873aefbba72880e028c3efb7d846b5ed01143c4a4b08049453993e8a52/watchfiles-0.22.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d9188979a58a096b6f8090e816ccc3f255f137a009dd4bbec628e27696d67c1", size = 1196698 }, + { url = "https://files.pythonhosted.org/packages/bf/2e/c0d43c72b33d9520d6d9d10a65be0b680507df6c8316e28dd8f4a06b90f9/watchfiles-0.22.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2bdadf6b90c099ca079d468f976fd50062905d61fae183f769637cb0f68ba59a", size = 1210192 }, + { url = "https://files.pythonhosted.org/packages/dc/07/aada2dc1aa6b184aeee67ecd079d5a38edd5320f866ccc1539e337c82b8a/watchfiles-0.22.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:067dea90c43bf837d41e72e546196e674f68c23702d3ef80e4e816937b0a3ffd", size = 1229778 }, + { url = "https://files.pythonhosted.org/packages/b2/e6/c853384d143001f3f698aae27ce40eeaac3e5f09aa63e671212972843600/watchfiles-0.22.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbf8a20266136507abf88b0df2328e6a9a7c7309e8daff124dda3803306a9fdb", size = 1234220 }, + { url = "https://files.pythonhosted.org/packages/8b/57/92c29fec82104277454b6082824d83b434a6833ab0c87afc8b60970c4c98/watchfiles-0.22.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1235c11510ea557fe21be5d0e354bae2c655a8ee6519c94617fe63e05bca4171", size = 1366736 }, + { url = "https://files.pythonhosted.org/packages/3d/ae/e7eddbdca559f14a9a38cf04782a5d715cf350aad498d0862fb02b4ebe10/watchfiles-0.22.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2444dc7cb9d8cc5ab88ebe792a8d75709d96eeef47f4c8fccb6df7c7bc5be71", size = 1198604 }, + { url = "https://files.pythonhosted.org/packages/a7/f6/a2673bbbdb2943b37e8587719bf14d6f544d00660f059c66e5c034b279e0/watchfiles-0.22.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c5af2347d17ab0bd59366db8752d9e037982e259cacb2ba06f2c41c08af02c39", size = 1364604 }, + { url = "https://files.pythonhosted.org/packages/1b/46/7a4f7231ccf82f814282e5ca7fa1ab6bcd8efbbea98be75e0126e53edcad/watchfiles-0.22.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9624a68b96c878c10437199d9a8b7d7e542feddda8d5ecff58fdc8e67b460848", size = 1367627 }, + { url = "https://files.pythonhosted.org/packages/71/10/a020809df82b46f5d30e7a8cfdebcec1eadc572aa06a60f42e50495c52c3/watchfiles-0.22.0-cp310-none-win32.whl", hash = "sha256:4b9f2a128a32a2c273d63eb1fdbf49ad64852fc38d15b34eaa3f7ca2f0d2b797", size = 272397 }, + { url = "https://files.pythonhosted.org/packages/f8/79/9d7e16e805c721d5e5a3b1d1a3e798cc9b1282b799d7058a70ab506dc970/watchfiles-0.22.0-cp310-none-win_amd64.whl", hash = "sha256:2627a91e8110b8de2406d8b2474427c86f5a62bf7d9ab3654f541f319ef22bcb", size = 282074 }, + { url = "https://files.pythonhosted.org/packages/7c/86/9bc03462970d5002863919cdcbaa2d8be8c9a6ef59188cfe7a5f596c80a3/watchfiles-0.22.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b810a2c7878cbdecca12feae2c2ae8af59bea016a78bc353c184fa1e09f76b68", size = 395790 }, + { url = "https://files.pythonhosted.org/packages/50/c2/0022e988ce16c6dc4d2903de6ca26c9c9dbc8ee5658859f04f20146c7871/watchfiles-0.22.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f7e1f9c5d1160d03b93fc4b68a0aeb82fe25563e12fbcdc8507f8434ab6f823c", size = 392212 }, + { url = "https://files.pythonhosted.org/packages/ec/e5/f50e27bea4b408d1b902d4b0e67a1a2502c85eef91192178db6c0e8f2ccd/watchfiles-0.22.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:030bc4e68d14bcad2294ff68c1ed87215fbd9a10d9dea74e7cfe8a17869785ab", size = 1197263 }, + { url = "https://files.pythonhosted.org/packages/27/fe/5d9e484caade79e1b54aebe48eeae9f74df92826fffb797700e8bd78cd09/watchfiles-0.22.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ace7d060432acde5532e26863e897ee684780337afb775107c0a90ae8dbccfd2", size = 1199104 }, +] + +[[package]] +name = "websockets" +version = "12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/62/7a7874b7285413c954a4cca3c11fd851f11b2fe5b4ae2d9bee4f6d9bdb10/websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b", size = 104994 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/b9/360b86ded0920a93bff0db4e4b0aa31370b0208ca240b2e98d62aad8d082/websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374", size = 124025 }, + { url = "https://files.pythonhosted.org/packages/bb/d3/1eca0d8fb6f0665c96f0dc7c0d0ec8aa1a425e8c003e0c18e1451f65d177/websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be", size = 121261 }, + { url = "https://files.pythonhosted.org/packages/4e/e1/f6c3ecf7f1bfd9209e13949db027d7fdea2faf090c69b5f2d17d1d796d96/websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547", size = 121328 }, + { url = "https://files.pythonhosted.org/packages/74/4d/f88eeceb23cb587c4aeca779e3f356cf54817af2368cb7f2bd41f93c8360/websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2", size = 130925 }, + { url = "https://files.pythonhosted.org/packages/16/17/f63d9ee6ffd9afbeea021d5950d6e8db84cd4aead306c6c2ca523805699e/websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558", size = 129930 }, + { url = "https://files.pythonhosted.org/packages/9a/12/c7a7504f5bf74d6ee0533f6fc7d30d8f4b79420ab179d1df2484b07602eb/websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480", size = 130245 }, + { url = "https://files.pythonhosted.org/packages/e4/6a/3600c7771eb31116d2e77383d7345618b37bb93709d041e328c08e2a8eb3/websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c", size = 134966 }, + { url = "https://files.pythonhosted.org/packages/22/26/df77c4b7538caebb78c9b97f43169ef742a4f445e032a5ea1aaef88f8f46/websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8", size = 134196 }, + { url = "https://files.pythonhosted.org/packages/e5/18/18ce9a4a08203c8d0d3d561e3ea4f453daf32f099601fc831e60c8a9b0f2/websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603", size = 134822 }, + { url = "https://files.pythonhosted.org/packages/45/51/1f823a341fc20a880e67ae62f6c38c4880a24a4b60fbe544a38f516f39a1/websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f", size = 124454 }, + { url = "https://files.pythonhosted.org/packages/41/b0/5ec054cfcf23adfc88d39359b85e81d043af8a141e3ac8ce40f45a5ce5f4/websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf", size = 124974 }, + { url = "https://files.pythonhosted.org/packages/43/8b/554a8a8bb6da9dd1ce04c44125e2192af7b7beebf6e3dbfa5d0e285cc20f/websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd", size = 121110 }, + { url = "https://files.pythonhosted.org/packages/b0/8e/58b8812940d746ad74d395fb069497255cb5ef50748dfab1e8b386b1f339/websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870", size = 123216 }, + { url = "https://files.pythonhosted.org/packages/81/ee/272cb67ace1786ce6d9f39d47b3c55b335e8b75dd1972a7967aad39178b6/websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077", size = 122821 }, + { url = "https://files.pythonhosted.org/packages/a8/03/387fc902b397729df166763e336f4e5cec09fe7b9d60f442542c94a21be1/websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b", size = 122768 }, + { url = "https://files.pythonhosted.org/packages/50/f0/5939fbc9bc1979d79a774ce5b7c4b33c0cefe99af22fb70f7462d0919640/websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30", size = 125009 }, + { url = "https://files.pythonhosted.org/packages/79/4d/9cc401e7b07e80532ebc8c8e993f42541534da9e9249c59ee0139dcb0352/websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e", size = 118370 }, +] + +[[package]] +name = "werkzeug" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/51/2e0fc149e7a810d300422ab543f87f2bcf64d985eb6f1228c4efd6e4f8d4/werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18", size = 803342 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/6e/e792999e816d19d7fcbfa94c730936750036d65656a76a5a688b57a656c4/werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8", size = 227274 }, +] diff --git a/pyproject.toml b/pyproject.toml index b18de566d..3a9b6de24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ exclude = [ "venv*", ".venv*", "__pycache__", + "fund_store/config/fund_loader_config/FAB/" ] mccabe.max-complexity = 12 diff --git a/pytest.ini b/pytest.ini index d293c6dd4..65f2c33ce 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,3 +3,5 @@ env = FLASK_ENV=unit_test FLASK_DEBUG=1 GITHUB_SHA=123123 +testpaths = + tests