Skip to content

Commit

Permalink
add export scripts with tests and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
Adam-W1 committed Aug 8, 2024
1 parent b9da458 commit ebf7914
Show file tree
Hide file tree
Showing 35 changed files with 1,092 additions and 157 deletions.
86 changes: 48 additions & 38 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,41 +1,51 @@
{
// 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":"Python Debugger: Current File","type":"debugpy","request":"launch","program":"${file}","console":"integratedTerminal"},
// 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": "Python Debugger: Current File",
"type": "debugpy",
"request": "launch",
"env": {
"FLASK_ENV": "development"
},
"program": "${file}",
"console": "integratedTerminal"
},
{
"name": "Python Debugger: Flask",
"type": "debugpy",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "app.app.py",
"FLASK_DEBUG": "1"
},
"args": ["run", "--debug"],
"jinja": true,
"autoStartBrowser": false
},
{
"name": "Docker Runner FAB",
"type": "debugpy",
"env": {
"FLASK_APP": "app.app.py",
"FLASK_DEBUG": "1"
},
"request": "attach",
"connect": {
"host": "localhost",
"port": 5686
},
"pathMappings": [
{
"name": "Python Debugger: Flask",
"type": "debugpy",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "app.app.py",
"FLASK_DEBUG": "1"
},
"args": ["run", "--debug"],
"jinja": true,
"autoStartBrowser": false
},
{
"name": "Docker Runner FAB",
"type": "python",
"request": "attach",
"connect": {
"host": "localhost",
"port": 5686
},
"pathMappings": [
{
"localRoot": "${workspaceFolder:funding-service-design-fund-application-builder}",
"remoteRoot": "."
}
],
"justMyCode": true
}
]
}

]
"localRoot": "${workspaceFolder:funding-service-design-fund-application-builder}",
"remoteRoot": "."
}
],
"justMyCode": true
}
]
}
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ Run the app with `flask run` (include `--debug` for auto reloading on file chang
## Helper Tasks
Contained in [db_tasks.py](./tasks/db_tasks.py)

## Configuration output
The configuration output is generated by the [config_generator](./config_generator/README.md) module. This module contains functions to generate fund and round configuration, form JSONs, and HTML representations for a given funding round.

### Recreate Local DBs
For both `DATABASE_URL` and `DATABASE_URL_UNIT_TEST`, drops the database if it exists and then recreates it.

Expand Down
4 changes: 2 additions & 2 deletions app/blueprints/fund_builder/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
from app.all_questions.metadata_utils import generate_print_data_for_sections
from app.blueprints.fund_builder.forms.fund import FundForm
from app.blueprints.fund_builder.forms.round import RoundForm
from app.config_reuse.generate_all_questions import print_html
from app.config_reuse.generate_form import build_form_json
from app.config_generator.generate_all_questions import print_html
from app.config_generator.generate_form import build_form_json
from app.db.models.fund import Fund
from app.db.models.round import Round
from app.db.queries.application import clone_single_round
Expand Down
43 changes: 12 additions & 31 deletions app/blueprints/self_serve/routes.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import json
import os

import requests
from flask import Blueprint
from flask import Response
from flask import flash
Expand All @@ -23,8 +22,8 @@
from app.blueprints.self_serve.forms.page_form import PageForm
from app.blueprints.self_serve.forms.question_form import QuestionForm
from app.blueprints.self_serve.forms.section_form import SectionForm
from app.config_reuse.generate_all_questions import print_html
from app.config_reuse.generate_form import build_form_json
from app.config_generator.generate_all_questions import print_html
from app.config_generator.generate_form import build_form_json

FORM_RUNNER_URL = os.getenv("FORM_RUNNER_INTERNAL_HOST", "http://form-runner:3009")
FORM_RUNNER_URL_REDIRECT = os.getenv("FORM_RUNNER_EXTERNAL_HOST", "http://localhost:3009")
Expand Down Expand Up @@ -59,28 +58,18 @@ def human_to_kebab_case(word: str) -> str | None:
return word.replace(" ", "-").strip().lower()


def human_to_snake_case(word: str) -> str | None:
if word:
return word.replace(" ", "_").strip().lower()


def generate_form_config_from_request():
pages = request.form.getlist("selected_pages")
title = request.form.get("form_title", "My Form")
intro_content = request.form.get("startPageContent")
form_id = human_to_kebab_case(title)
input_data = {"title": form_id, "pages": pages, "intro_content": intro_content}
form_json = build_form_json(form_title=title, input_json=input_data, form_id=form_id)
form_json = build_form_json(form)
return {"form_json": form_json, "form_id": form_id, "title": title}


@self_serve_bp.route("/preview", methods=["POST"])
def preview_form():
form_config = generate_form_config_from_request()
form_config["form_json"]["outputs"][0]["outputConfiguration"][
"savePerPageUrl"
] = "http://fsd-self-serve:8080/dev/save"
requests.post(
url=f"{FORM_RUNNER_URL}/publish", json={"id": form_config["form_id"], "configuration": form_config["form_json"]}
)
return redirect(f"{FORM_RUNNER_URL_REDIRECT}/{form_config['form_id']}")


@self_serve_bp.route("/form_questions", methods=["POST"])
def view_form_questions():
form_config = generate_form_config_from_request()
Expand Down Expand Up @@ -121,15 +110,13 @@ def view_section_questions():
@self_serve_bp.route("section", methods=["GET", "POST", "PUT", "DELETE"])
def section():
# TODO: Create frontend routes and connect to middleware
if request.method == "GET":
pass
if request.method == "PUT":
pass
if request.method == "DELETE":
pass

form = SectionForm()
if form.validate_on_submit():
if request.method == "POST" and form.validate_on_submit():
save_template_section(form.as_dict())
flash(message=f"Section '{form['builder_display_name'].data}' was saved")
return redirect(url_for("self_serve_bp.index"))
Expand All @@ -151,15 +138,13 @@ def section():
@self_serve_bp.route("/form", methods=["GET", "POST", "PUT", "DELETE"])
def form():
# TODO: Create frontend routes and connect to middleware
if request.method == "GET":
pass
if request.method == "PUT":
pass
if request.method == "DELETE":
pass

form = FormForm()
if form.validate_on_submit():
if request.method == "POST" and form.validate_on_submit():
new_form = {
"builder_display_name": form.builder_display_name.data,
"start_page_guidance": form.start_page_guidance.data,
Expand Down Expand Up @@ -191,15 +176,13 @@ def form():
@self_serve_bp.route("/page", methods=["GET", "POST", "PUT", "DELETE"])
def page():
# TODO: Create frontend routes and connect to middleware
if request.method == "GET":
pass
if request.method == "PUT":
pass
if request.method == "DELETE":
pass

form = PageForm()
if form.validate_on_submit():
if request.method == "POST" and form.validate_on_submit():
new_page = {
"id": form.id.data,
"builder_display_name": form.builder_display_name.data,
Expand All @@ -225,16 +208,14 @@ def page():
@self_serve_bp.route("/question", methods=["GET", "PUT", "POST", "DELETE"])
def question():
# TODO: Create frontend routes and connect to middleware
if request.method == "GET":
pass
if request.method == "PUT":
pass
if request.method == "DELETE":
pass

form = QuestionForm()
question = form.as_dict()
if form.validate_on_submit():
if request.method == "POST" and form.validate_on_submit():
save_template_component(question)
flash(message=f"Question '{question['title']}' was saved")
return redirect(url_for("self_serve_bp.index"))
Expand Down
4 changes: 2 additions & 2 deletions app/blueprints/self_serve/templates/create_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ <h3 class="govuk-heading-s">

function previewForm() {
section_form = document.getElementById("form_form")
section_form.setAttribute("action", "{{url_for('self_serve_bp.preview_form')}}")
section_form.setAttribute("action", "{{url_for('build_fund_bp.preview_form', form_id=form.id)}}")
section_form.submit()
}
function allQuestions() {
Expand All @@ -208,7 +208,7 @@ <h3 class="govuk-heading-s">
}
function saveForm() {
section_form = document.getElementById("form_form")
section_form.setAttribute("action", "{{url_for('self_serve_bp.create_form')}}")
section_form.setAttribute("action", "{{url_for('self_serve_bp.form')}}")
section_form.submit()
}

Expand Down
109 changes: 109 additions & 0 deletions app/config_generator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# FAB Config output

This directory contains the scripts and output required to generate the FAB configuration files.

## Scripts
### Generate fund round configuration
Generates configuration for a specific funding round.

This function orchestrates the generation of various configurations needed for a funding round.
It calls three specific functions in sequence to generate the fund configuration, round configuration,
and application display configuration for the given round ID.

Args:
round_id (str): The unique identifier for the funding round.

The functions called within this function are:
- generate_fund_config: Generates the fund configuration for the given round ID.
- generate_round_config: Generates the round configuration for the given round ID.
- generate_application_display_config: Generates the application display configuration for the given round ID.

### Generate round form JSONs
Generates JSON configurations for all forms associated with a given funding round.

This function iterates through all sections of a specified funding round, and for each form
within those sections, it generates a JSON configuration. These configurations are then written
to files named after the forms, organized by the round's short name.

Args:
round_id (str): The unique identifier for the funding round.

The generated files are named after the form names and are stored in a directory
corresponding to the round's short name.

### Generate round html
Generates an HTML representation for a specific funding round.

This function creates an HTML document that represents all the sections and forms
associated with a given funding round. It retrieves the round and its related fund
information, iterates through each section of the round to collect form data, and
then generates HTML content based on this data.

Args:
round_id (str): The unique identifier for the funding round.

The generated HTML is intended to provide a comprehensive overview of the round,
including details of each section and form, for printing or web display purposes.

## Running the scripts
To run the scripts, execute the following commands:

```bash
inv generate-fund-and-round-config {roundid}

inv generate-round-form-jsons {roundid}

inv generate-round-html {roundid}
```

## Output
The scripts generate the following output file structure:
"""
app/
config_generator/
- scripts/
-- **
-- output/
-- round_short_name/
-- form_runner/
-- form_name.json
-- fund_store/
-- fund_config.py
-- round_config.py
-- sections_config.py
-- html/
-- full_aplication.html
"""

<!-- TODO: Is this now covered by cloning? >> -->
<!-- # SPIKE to look at Question Bank and answer config_reuse -->
<!-- By storing some reusable configuration outside the form jsons, we can allow parts of forms to be generated from minimal input information - making it more feasible for less technical colleagues to create this input information, or for it to be generated by a UI.
Creating forms from reusable questions means the answers to those questions will line up between applications, so we can more easily allow applicants to take information from one application and reuse in another.
Having reusable questions also means we can have reusable assessment config - eg. the organisation information can be reused in un-scored general information sections without duplicating the config.
## Reusable configuration
[Components](./config/components_to_reuse.py) Configuration for individual components (fields). Structure is as in the form json, except for conditions which are simplified
[Pages](./config/pages_to_reuse.py) Configuration for pages that can be inserted into forms. Basically each page is a list of component IDs that exist in `components_to_reuse.py` above.
[Sub Pages](./config/sub_pages_to_reuse.py) Contains full form json info for some pages that are constant when reused, eg. the summary page. But also ones that are needed for sub flows based on conditions - eg. the 'what alternative names does your org use' page is in here, as it will always be required if you add the component `reuse_organisation_other_names_yes_no`
[Assessment Themes](./config/themes_to_reuse.py) Specifies themes that can be reused across assessments, basically a list of the components in each theme. These component names are the same as in `components_to_reuse.py`
## Example inputs - Forms
These are examples of the inputs required from a fund to create forms based on reusable components. Once the form json is generated, it can always be edited to add non-reusable components/pages as well.
[Org Info Basic](./test_data/in/org-info_basic_name_address.json) Just asks for organisation name and address
[Org info with alternative names](./test_data/in/org-info_alt_name_address.json) As above but allows alternative names
[Full organisation info](./test_data/in/org-info_all.json) Uses all the components configured as part of the POC - org name, address, alternative names, purpose and web links
## Example inputs - Assessment
Example of input to generate assessment configuration for the `Full Organisation Info` example form above
[Un-scored Full org info](./test_data/in/assmnt_unscored.json) Lists the themes within each sub-criteria for the assessment sections
# Steps to generate form json
1. Create an input file, as per [example inputs](#example-inputs---forms) specifying the pages you want in your form
1. Execute the form generation script: `python -m config_reuse.generate_form` and complete the command prompts to generate the json from the input
# Steps to generate assessment config for a set of questions
1. Create an input file, as per [example inputs](#example-inputs---assessment) specifying the layout of themes etc that you need
1. Generate field info for the forms you are using - atm run `test_generate_assessment_fields_for_testing` in `test_generate_all_questions.py` in fund-store.
1. Run the assessment config generation script: `python -m config_reuse.generate_assessment_config` and answer the prompts, point it to the input file you created and the generated field info from the previous step. -->
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def build_assessment_config(criteria_list: list[Criteria]) -> dict:
@click.command()
@click.option(
"--input_folder",
default="./question_reuse/test_data/in/",
default="./config_reuse/test_data/in/",
help="Input configuration",
prompt=True,
)
Expand All @@ -171,7 +171,7 @@ def build_assessment_config(criteria_list: list[Criteria]) -> dict:
)
@click.option(
"--output_folder",
default="./question_reuse/test_data/out",
default="./config_reuse/test_data/out",
help="Output destination",
prompt=True,
)
Expand All @@ -183,7 +183,7 @@ def build_assessment_config(criteria_list: list[Criteria]) -> dict:
)
@click.option(
"--forms_dir",
default="./question_reuse/test_data/out/forms/",
default="./config_reuse/test_data/out/forms/",
help="Directory containing forms",
prompt=True,
)
Expand Down
Loading

0 comments on commit ebf7914

Please sign in to comment.