Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FS-5006: improvements for the all questions pages #272

Merged
merged 3 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/all_questions/metadata_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ def build_components_from_page(
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"]
destination = index_of_printed_headers[next_config["path"]]["title"]
text_with_coordinators = ""
for condition in [
cc for cc in condition_config["value"]["conditions"] if cc["field"]["name"] == c["name"]
Expand Down
17 changes: 12 additions & 5 deletions app/blueprints/application/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@
from app.db.queries.clone import clone_single_form
from app.db.queries.fund import get_all_funds, get_fund_by_id
from app.db.queries.round import get_round_by_id
from app.export_config.generate_all_questions import print_html
from app.export_config.generate_all_questions import generate_html
from app.export_config.generate_assessment_config import (
generate_assessment_config_for_round,
)
from app.export_config.generate_form import build_form_json
from app.export_config.generate_form import build_form_json, _find_page_by_controller
from app.export_config.generate_fund_round_config import generate_config_for_round
from app.export_config.generate_fund_round_form_jsons import (
generate_form_jsons_for_round,
Expand Down Expand Up @@ -120,13 +120,14 @@ def view_all_questions(round_id):
section_data,
lang="en",
)
html = print_html(print_data)
html = generate_html(print_data)
return render_template(
"view_questions.html",
round=round,
fund=fund,
question_html=html,
title=f"All Questions for {fund.short_name} - {round.short_name}",
all_questions_view=True
)


Expand Down Expand Up @@ -245,6 +246,7 @@ def view_form_questions(round_id, section_id, form_id):
round = get_round_by_id(round_id)
fund = get_fund_by_id(round.fund_id)
form = get_form_by_id(form_id=form_id)
start_page = _find_page_by_controller(form.pages, "start.js")
section_data = [
{
"section_title": f"Preview of form [{form.name_in_apply_json['en']}]",
Expand All @@ -256,7 +258,12 @@ def view_form_questions(round_id, section_id, form_id):
section_data,
lang="en",
)
html = print_html(print_data, True)
html = generate_html(print_data, False)
return render_template(
"view_questions.html", round=round, fund=fund, question_html=html, title=form.name_in_apply_json["en"]
"view_questions.html",
round=round,
fund=fund,
question_html=html,
title=start_page.name_in_apply_json["en"],
all_questions_view=False
)
19 changes: 12 additions & 7 deletions app/blueprints/application/templates/view_questions.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{% extends "base.html" %}
{% from "govuk_frontend_jinja/components/back-link/macro.html" import govukBackLink %}

{% set pageHeading %}{{title}} {% endset %}
{% set pageHeading %}{{ title }} {% endset %}
{% block beforeContent %}
{{ super() }}
<div class="govuk-grid-row">
Expand All @@ -14,11 +14,16 @@
</div>
{% endblock beforeContent %}
{% block content %}
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<span class="govuk-caption-l">[Preview] Application questions</span>
<h1 class="govuk-heading-xl">{{pageHeading}}</h1>
{{ question_html|safe}}
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
{% if all_questions_view %}
<span class="govuk-caption-l">[Preview] Application questions</span>
<h1 class="govuk-heading-xl">{{ pageHeading }}</h1>
{% else %}
<h1 class="govuk-heading-l govuk-!-margin-bottom-0">{{ pageHeading }}</h1>
<p class="govuk-body govuk-!-margin-bottom-9">This template contains the following questions.</p>
{% endif %}
{{ question_html | safe }}
</div>
</div>
</div>
{% endblock %}
4 changes: 2 additions & 2 deletions app/blueprints/template/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
get_form_by_template_name,
update_form,
)
from app.export_config.generate_all_questions import print_html
from app.export_config.generate_all_questions import generate_html
from app.export_config.generate_form import build_form_json
from app.export_config.helpers import human_to_kebab_case
from app.shared.helpers import flash_message
Expand Down Expand Up @@ -96,7 +96,7 @@ def template_questions(form_id):
section_data,
lang="en",
)
html = print_html(print_data, False, False, False)
html = generate_html(print_data, False)
return render_template("view_template_questions.html", question_html=html, form=form)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
{% block content %}
<div class="govuk-grid-row">
<div class="govuk-grid-column-full">
<h1 class="govuk-heading-l">{{ form.template_name }}</h1>
<p class="govuk-body">This template contains the following questions.</p>
<h1 class="govuk-heading-l govuk-!-margin-bottom-0">{{ form.template_name }}</h1>
<p class="govuk-body govuk-!-margin-bottom-9">This template contains the following questions.</p>
</div>
</div>
<div class="govuk-grid-row">
Expand Down
179 changes: 86 additions & 93 deletions app/export_config/generate_all_questions.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from airium import Airium # noqa: E402
from airium import Airium

# Initialise Airium html printer
air = Airium()

# Define start and end html
# --------------------------
# Boilerplate HTML Templates
# --------------------------
BOILERPLATE_START = """
{% extends "apply/base.html" %}
{%- from 'govuk_frontend_jinja/components/inset-text/macro.html' import govukInsetText -%}
Expand All @@ -14,8 +13,7 @@
{% block content %}
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<span class="govuk-caption-l">{% trans %}{{fund_title}}{% endtrans %} {% trans %}{{round_title}}{% endtrans %}
</span>
<span class="govuk-caption-l">{% trans %}{{fund_title}}{% endtrans %} {% trans %}{{round_title}}{% endtrans %}</span>
<h1 class="govuk-heading-xl">{{pageHeading}}</h1>
"""

Expand All @@ -26,14 +24,21 @@
"""


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
# --------------------------
# Table of Contents Section
# --------------------------
def generate_table_of_contents(air, sections):
"""
Generates a table of contents from the given sections.

Example Output:
<h2 class="govuk-heading-m">Table of contents</h2>
<ol class="govuk-list govuk-list--number">
<li><a class="govuk-link" href="#section1">Section 1</a></li>
<li><a class="govuk-link" href="#section2">Section 2</a></li>
</ol>
"""
with air.h2(klass="govuk-heading-m "):
with air.h2(klass="govuk-heading-m"):
air("Table of contents")
with air.ol(klass="govuk-list govuk-list--number"):
for anchor, details in sections.items():
Expand All @@ -42,92 +47,80 @@ def print_html_toc(air: Airium, sections: dict):
air(details["title_text"])


def print_components(air: Airium, components: list, show_field_types: bool = False):
"""Prints the components within a page

Args:
air (Airium): Instance to print html
components (list): List of components to print
# --------------------------
# Component Rendering Section
# --------------------------
def render_components(air, components):
"""
for c in components:
# Print the title
with air.div(klass="govuk-body all-questions-component"):
if not c["hide_title"] and c["title"] is not None:
with air.p(klass="govuk-body"):
air(f"{c['title']}")
if show_field_types:
air(f" [{c['type']}]")

for t in c["text"]:
# Print lists as <ul> 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
Renders components within a page.

Example Output:
<div class="govuk-body all-questions-component">
<p class="govuk-body">Component Title [Type]</p>
<ul class="govuk-list govuk-list--bullet">
<li>Bullet 1</li>
<li>Bullet 2</li>
</ul>
<p class="govuk-body">Some additional text.</p>
</div>
"""
for component in components:
title = component.get("title")
text_list = component.get("text", [])

if (title and not component.get("hide_title")) or text_list:
with air.div(klass="govuk-body"):
if title and not component.get("hide_title"):
with air.p(klass="govuk-body"):
air(t)


def print_html(sections: dict, show_field_types=False, allow_table_of_content=True, allow_heading_number=True) -> 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
:param sections : form sections
:param allow_heading_number: ignore the heading number
:param allow_table_of_content: ignore table of content
:param show_field_types: ignore field types
air(title)

for text in text_list:
if isinstance(text, list):
with air.ul(klass="govuk-list govuk-list--bullet"):
for bullet in text:
with air.li():
air(bullet)
else:
with air.p(klass="govuk-body"):
air(text)

# --------------------------
# Main HTML Generation Section
# --------------------------
def generate_html(sections, all_question_view=True):
"""
Generates an HTML document for the given sections.

Example Output:
<h2 class="govuk-heading-l" id="section1">1. Section 1 Title</h2>
<h3 class="govuk-heading-m">1.1 Subheading</h3>
<div class="govuk-body all-questions-component">
<p class="govuk-body">Question Text</p>
</div>
"""
air = Airium()
with air.div(klass="govuk-!-margin-bottom-8"):
if allow_table_of_content:
# Print Table of Contents
print_html_toc(air, sections)
idx_section = 1
if all_question_view:
generate_table_of_contents(air, sections)
air.hr(klass="govuk-section-break govuk-section-break--l govuk-section-break--visible")

for anchor, details in sections.items():
for idx, (anchor, details) in enumerate(sections.items(), start=1):
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"):
if allow_heading_number:
air(f"{header_info['heading_number']}. {header_info['title']}")
else:
air(header_info["title"])

else:
# Print header for this form page
with air.h4(klass="govuk-heading-s"):
if allow_heading_number:
air(f"{header_info['heading_number']}. {header_info['title']}")
else:
air(header_info["title"])

# Print components within this form
print_components(air, header_info["components"], show_field_types)

idx_section += 1

# Concatenate start html, generated html and end html to one string and return
# html = f"{BOILERPLATE_START}{str(air)}{BOILERPLATE_END}"
if all_question_view:
with air.h2(klass="govuk-heading-l", id=anchor):
air(f"{idx}. {details['title_text']}")

for heading, header_info in sorted(details["form_print_data"].items(),
key=lambda item: str(item[1]["heading_number"])):
tag = "h3" if header_info["is_form_heading"] else "h4"
heading_text = header_info["title"]

with getattr(air, tag)(klass=f"govuk-heading-{'m' if tag == 'h3' else 's'}"):
air(heading_text)

render_components(air, header_info["components"])
air.hr(klass="govuk-section-break govuk-section-break--l govuk-section-break--visible")

return str(air)
# print(html)
# return html
4 changes: 2 additions & 2 deletions app/export_config/generate_fund_round_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from app.all_questions.metadata_utils import generate_print_data_for_sections
from app.db.queries.fund import get_fund_by_id
from app.db.queries.round import get_round_by_id
from app.export_config.generate_all_questions import print_html
from app.export_config.generate_all_questions import generate_html
from app.export_config.generate_form import build_form_json
from app.export_config.helpers import write_config

Expand Down Expand Up @@ -68,7 +68,7 @@ def generate_all_round_html(round_id, base_output_dir=None):
lang="en",
)
html_content = frontend_html_prefix
html_content += print_html(print_data)
html_content += generate_html(print_data)
html_content += frontend_html_suffix
write_config(
html_content,
Expand Down
10 changes: 8 additions & 2 deletions tests/test_config_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
generate_all_round_html,
)
from app.export_config.helpers import validate_json
from bs4 import BeautifulSoup


def read_data_from_output_file(file):
Expand Down Expand Up @@ -268,7 +269,7 @@ def test_generate_fund_round_html(seed_dynamic_data, temp_output_dir):
/ "html"
/ f"{fund_short_name.casefold()}_{round_short_name.casefold()}_all_questions_en.html",
"expected_output": frontend_html_prefix
+ '<div class="govuk-!-margin-bottom-8">\n <h2 class="govuk-heading-m ">\n Table of contents\n </h2>\n <ol class="govuk-list govuk-list--number">\n <li>\n <a class="govuk-link" href="#organisation-information">\n Organisation Information\n </a>\n </li>\n </ol>\n <hr class="govuk-section-break govuk-section-break--l govuk-section-break--visible" />\n <h2 class="govuk-heading-l" id="organisation-information">\n 1. Organisation Information\n </h2>\n <h3 class="govuk-heading-m">\n 1.1. About your organisation\n </h3>\n <h4 class="govuk-heading-s">\n 1.1.1. Organisation Name\n </h4>\n <div class="govuk-body all-questions-component">\n <p class="govuk-body">\n What is your organisation\'s name?\n </p>\n <p class="govuk-body">\n This must match the registered legal organisation name\n </p>\n </div>\n <div class="govuk-body all-questions-component">\n <p class="govuk-body">\n How is your organisation classified?\n </p>\n <ul class="govuk-list govuk-list--bullet">\n <li class="">\n Charity\n </li>\n <li class="">\n Public Limited Company\n </li>\n </ul>\n </div>\n</div>' # noqa: E501
+ '<div class="govuk-!-margin-bottom-8">\n <h2 class="govuk-heading-m">Table of contents</h2>\n <ol class="govuk-list govuk-list--number">\n <li><a class="govuk-link" href="#organisation-information">Organisation Information</a></li>\n </ol>\n <hr class="govuk-section-break govuk-section-break--l govuk-section-break--visible" />\n <h2 class="govuk-heading-l" id="organisation-information">1. Organisation Information</h2>\n <h3 class="govuk-heading-m">About your organisation</h3>\n <hr class="govuk-section-break govuk-section-break--l govuk-section-break--visible" />\n <h4 class="govuk-heading-s">Organisation Name</h4>\n <div class="govuk-body">\n <p class="govuk-body">What is your organisation\'s name?</p>\n <p class="govuk-body">This must match the registered legal organisation name</p>\n </div>\n <div class="govuk-body">\n <p class="govuk-body">How is your organisation classified?</p>\n <ul class="govuk-list govuk-list--bullet">\n <li>Charity</li>\n <li>Public Limited Company</li>\n </ul>\n </div>\n <hr class="govuk-section-break govuk-section-break--l govuk-section-break--visible" />\n</div>' # noqa: E501
+ frontend_html_suffix,
}
]
Expand All @@ -278,7 +279,7 @@ def test_generate_fund_round_html(seed_dynamic_data, temp_output_dir):

with open(expected_file["path"], "r") as file:
data = file.read()
assert data == expected_file["expected_output"]
assert _normalize_html(data) == _normalize_html(expected_file["expected_output"]), "HTML outputs do not match"


def test_generate_fund_round_html_invalid_input(seed_dynamic_data):
Expand Down Expand Up @@ -328,3 +329,8 @@ def test_create_export_zip(temp_output_dir):
assert output
output_path = Path(output)
assert output_path.exists()


def _normalize_html(html):
"""Parses and normalizes HTML using BeautifulSoup to avoid formatting differences."""
return BeautifulSoup(html, "html.parser").prettify()