├── assets
└── .gitkeep
├── demo
├── __init__.py
├── components
│ ├── __init__.py
│ ├── models.py
│ ├── urls.py
│ ├── templates
│ │ └── components
│ │ │ ├── component.html
│ │ │ └── index.html
│ └── forms
│ │ ├── warning.py
│ │ ├── panel.py
│ │ ├── inset.py
│ │ ├── details.py
│ │ ├── __init__.py
│ │ ├── file_upload.py
│ │ ├── fieldset.py
│ │ ├── select.py
│ │ ├── textarea.py
│ │ ├── tag.py
│ │ └── accordion.py
├── context_processors.py
├── urls.py
├── templates
│ └── demo
│ │ └── index.html
└── static
│ └── css
│ └── demo.css
├── tests
├── unit
│ ├── __init__.py
│ ├── templatetags
│ │ └── results
│ │ │ ├── back_link.html
│ │ │ ├── button_link.html
│ │ │ ├── button_start.html
│ │ │ └── breadcrumbs.html
│ ├── urls.py
│ ├── conftest.py
│ ├── layout
│ │ ├── results
│ │ │ ├── fieldset
│ │ │ │ ├── css_id.html
│ │ │ │ ├── attributes.html
│ │ │ │ ├── css_class.html
│ │ │ │ ├── layout.html
│ │ │ │ ├── legend_size.html
│ │ │ │ └── legend_heading.html
│ │ │ ├── accordion
│ │ │ │ ├── css_id.html
│ │ │ │ ├── attributes.html
│ │ │ │ ├── css_class.html
│ │ │ │ ├── section_css_id.html
│ │ │ │ ├── section_attributes.html
│ │ │ │ ├── section_css_class.html
│ │ │ │ └── layout.html
│ │ │ ├── buttons
│ │ │ │ ├── css_id.html
│ │ │ │ ├── primary.html
│ │ │ │ ├── css_class.html
│ │ │ │ ├── secondary.html
│ │ │ │ ├── warning.html
│ │ │ │ ├── attributes.html
│ │ │ │ └── disabled.html
│ │ │ ├── tabs
│ │ │ │ ├── attributes.html
│ │ │ │ ├── css_id.html
│ │ │ │ ├── css_class.html
│ │ │ │ ├── panel_css_id.html
│ │ │ │ ├── panel_attributes.html
│ │ │ │ ├── panel_css_class.html
│ │ │ │ └── layout.html
│ │ │ ├── text_input
│ │ │ │ ├── no_help_text.html
│ │ │ │ ├── no_label.html
│ │ │ │ ├── label_size.html
│ │ │ │ ├── initial.html
│ │ │ │ ├── label_heading.html
│ │ │ │ ├── no_help_text_errors.html
│ │ │ │ ├── no_help_text_errors_aria_invalid.html
│ │ │ │ ├── validation_errors.html
│ │ │ │ └── validation_errors_aria_invalid.html
│ │ │ ├── file_upload
│ │ │ │ ├── no_help_text.html
│ │ │ │ ├── no_label.html
│ │ │ │ ├── initial.html
│ │ │ │ ├── label_size.html
│ │ │ │ ├── label_heading.html
│ │ │ │ ├── no_help_text_errors.html
│ │ │ │ ├── no_help_text_errors_aria_invalid.html
│ │ │ │ ├── validation_errors.html
│ │ │ │ └── validation_errors_aria_invalid.html
│ │ │ ├── textarea
│ │ │ │ ├── no_help_text.html
│ │ │ │ ├── no_label.html
│ │ │ │ ├── label_size.html
│ │ │ │ ├── initial.html
│ │ │ │ ├── label_heading.html
│ │ │ │ ├── no_help_text_errors.html
│ │ │ │ ├── no_help_text_errors_aria_invalid.html
│ │ │ │ ├── validation_errors.html
│ │ │ │ ├── validation_errors_aria_invalid.html
│ │ │ │ ├── word_count.html
│ │ │ │ ├── character_count.html
│ │ │ │ └── threshold.html
│ │ │ ├── checkbox
│ │ │ │ ├── no_help_text.html
│ │ │ │ ├── initial.html
│ │ │ │ ├── checkbox_size.html
│ │ │ │ ├── no_help_text_errors.html
│ │ │ │ ├── no_help_text_errors_aria_invalid.html
│ │ │ │ ├── validation_errors.html
│ │ │ │ └── validation_errors_aria_invalid.html
│ │ │ ├── select
│ │ │ │ ├── no_help_text.html
│ │ │ │ ├── no_label.html
│ │ │ │ ├── initial.html
│ │ │ │ ├── label_size.html
│ │ │ │ ├── label_heading.html
│ │ │ │ ├── no_help_text_errors.html
│ │ │ │ ├── no_help_text_errors_aria_invalid.html
│ │ │ │ ├── validation_errors.html
│ │ │ │ └── validation_errors_aria_invalid.html
│ │ │ ├── table
│ │ │ │ └── layout.html
│ │ │ ├── radios
│ │ │ │ ├── initial_overlapping.html
│ │ │ │ ├── no_help_text.html
│ │ │ │ └── no_legend.html
│ │ │ └── checkboxes
│ │ │ │ └── initial_overlapping.html
│ │ └── test_conditional_radios.py
│ ├── helpers
│ │ └── results
│ │ │ ├── label_size.html
│ │ │ ├── override_label_size.html
│ │ │ ├── error_summary.html
│ │ │ └── error_summary_aria_invalid.html
│ ├── settings.py
│ └── choices
│ │ └── test_choice.py
└── demo
│ ├── snapshots
│ ├── test_tabs
│ │ ├── test_tabs__layout[chromium][linux]-gds-5.0.0.png
│ │ ├── test_tabs__layout[chromium][linux]-gds-5.2.0.png
│ │ ├── test_tabs__layout[chromium][linux]-gds-5.4.0.png
│ │ ├── test_tabs__layout[chromium][linux]-gds-5.6.0.png
│ │ ├── test_tabs__layout[chromium][linux]-gds-5.8.0.png
│ │ ├── test_tabs__click_tab[chromium][linux]-gds-5.0.0.png
│ │ ├── test_tabs__click_tab[chromium][linux]-gds-5.2.0.png
│ │ ├── test_tabs__click_tab[chromium][linux]-gds-5.4.0.png
│ │ ├── test_tabs__click_tab[chromium][linux]-gds-5.6.0.png
│ │ └── test_tabs__click_tab[chromium][linux]-gds-5.8.0.png
│ ├── test_radios
│ │ ├── test_radios__layout[chromium][linux]-gds-5.0.0.png
│ │ ├── test_radios__layout[chromium][linux]-gds-5.2.0.png
│ │ ├── test_radios__layout[chromium][linux]-gds-5.4.0.png
│ │ ├── test_radios__layout[chromium][linux]-gds-5.6.0.png
│ │ ├── test_radios__layout[chromium][linux]-gds-5.8.0.png
│ │ ├── test_radios__click_and_submit[chromium][linux]-gds-5.0.0.png
│ │ ├── test_radios__click_and_submit[chromium][linux]-gds-5.2.0.png
│ │ ├── test_radios__click_and_submit[chromium][linux]-gds-5.4.0.png
│ │ ├── test_radios__click_and_submit[chromium][linux]-gds-5.6.0.png
│ │ ├── test_radios__click_and_submit[chromium][linux]-gds-5.8.0.png
│ │ ├── test_radios__leave_blank_and_submit[chromium][linux]-gds-5.0.0.png
│ │ ├── test_radios__leave_blank_and_submit[chromium][linux]-gds-5.2.0.png
│ │ ├── test_radios__leave_blank_and_submit[chromium][linux]-gds-5.4.0.png
│ │ ├── test_radios__leave_blank_and_submit[chromium][linux]-gds-5.6.0.png
│ │ └── test_radios__leave_blank_and_submit[chromium][linux]-gds-5.8.0.png
│ ├── test_select
│ │ ├── test_select__layout[chromium][linux]-gds-5.0.0.png
│ │ ├── test_select__layout[chromium][linux]-gds-5.2.0.png
│ │ ├── test_select__layout[chromium][linux]-gds-5.4.0.png
│ │ ├── test_select__layout[chromium][linux]-gds-5.6.0.png
│ │ ├── test_select__layout[chromium][linux]-gds-5.8.0.png
│ │ ├── test_select__select_item_and_submit[chromium][linux]-gds-5.0.0.png
│ │ ├── test_select__select_item_and_submit[chromium][linux]-gds-5.2.0.png
│ │ ├── test_select__select_item_and_submit[chromium][linux]-gds-5.4.0.png
│ │ ├── test_select__select_item_and_submit[chromium][linux]-gds-5.6.0.png
│ │ └── test_select__select_item_and_submit[chromium][linux]-gds-5.8.0.png
│ ├── test_buttons
│ │ ├── test_buttons__layout[chromium][linux]-gds-5.0.0.png
│ │ ├── test_buttons__layout[chromium][linux]-gds-5.2.0.png
│ │ ├── test_buttons__layout[chromium][linux]-gds-5.4.0.png
│ │ ├── test_buttons__layout[chromium][linux]-gds-5.6.0.png
│ │ ├── test_buttons__layout[chromium][linux]-gds-5.8.0.png
│ │ ├── test_buttons__click_add_button[chromium][linux]-gds-5.0.0.png
│ │ ├── test_buttons__click_add_button[chromium][linux]-gds-5.2.0.png
│ │ ├── test_buttons__click_add_button[chromium][linux]-gds-5.4.0.png
│ │ ├── test_buttons__click_add_button[chromium][linux]-gds-5.6.0.png
│ │ └── test_buttons__click_add_button[chromium][linux]-gds-5.8.0.png
│ ├── test_details
│ │ ├── test_details__layout[chromium][linux]-gds-5.0.0.png
│ │ ├── test_details__layout[chromium][linux]-gds-5.2.0.png
│ │ ├── test_details__layout[chromium][linux]-gds-5.4.0.png
│ │ ├── test_details__layout[chromium][linux]-gds-5.6.0.png
│ │ ├── test_details__layout[chromium][linux]-gds-5.8.0.png
│ │ ├── test_details__hide_summary[chromium][linux]-gds-5.0.0.png
│ │ ├── test_details__hide_summary[chromium][linux]-gds-5.2.0.png
│ │ ├── test_details__hide_summary[chromium][linux]-gds-5.4.0.png
│ │ ├── test_details__hide_summary[chromium][linux]-gds-5.6.0.png
│ │ ├── test_details__hide_summary[chromium][linux]-gds-5.8.0.png
│ │ ├── test_details__show_summary[chromium][linux]-gds-5.0.0.png
│ │ ├── test_details__show_summary[chromium][linux]-gds-5.2.0.png
│ │ ├── test_details__show_summary[chromium][linux]-gds-5.4.0.png
│ │ ├── test_details__show_summary[chromium][linux]-gds-5.6.0.png
│ │ └── test_details__show_summary[chromium][linux]-gds-5.8.0.png
│ ├── test_accordion
│ │ ├── test_accordion__layout[chromium][linux]-gds-5.0.0.png
│ │ ├── test_accordion__layout[chromium][linux]-gds-5.2.0.png
│ │ ├── test_accordion__layout[chromium][linux]-gds-5.4.0.png
│ │ ├── test_accordion__layout[chromium][linux]-gds-5.6.0.png
│ │ ├── test_accordion__layout[chromium][linux]-gds-5.8.0.png
│ │ ├── test_accordion__hide_all[chromium][linux]-gds-5.0.0.png
│ │ ├── test_accordion__hide_all[chromium][linux]-gds-5.2.0.png
│ │ ├── test_accordion__hide_all[chromium][linux]-gds-5.4.0.png
│ │ ├── test_accordion__hide_all[chromium][linux]-gds-5.6.0.png
│ │ ├── test_accordion__hide_all[chromium][linux]-gds-5.8.0.png
│ │ ├── test_accordion__show_all[chromium][linux]-gds-5.0.0.png
│ │ ├── test_accordion__show_all[chromium][linux]-gds-5.2.0.png
│ │ ├── test_accordion__show_all[chromium][linux]-gds-5.4.0.png
│ │ ├── test_accordion__show_all[chromium][linux]-gds-5.6.0.png
│ │ ├── test_accordion__show_all[chromium][linux]-gds-5.8.0.png
│ │ ├── test_accordion__hide_section[chromium][linux]-gds-5.0.0.png
│ │ ├── test_accordion__hide_section[chromium][linux]-gds-5.2.0.png
│ │ ├── test_accordion__hide_section[chromium][linux]-gds-5.4.0.png
│ │ ├── test_accordion__hide_section[chromium][linux]-gds-5.6.0.png
│ │ ├── test_accordion__hide_section[chromium][linux]-gds-5.8.0.png
│ │ ├── test_accordion__show_section[chromium][linux]-gds-5.0.0.png
│ │ ├── test_accordion__show_section[chromium][linux]-gds-5.2.0.png
│ │ ├── test_accordion__show_section[chromium][linux]-gds-5.4.0.png
│ │ ├── test_accordion__show_section[chromium][linux]-gds-5.6.0.png
│ │ └── test_accordion__show_section[chromium][linux]-gds-5.8.0.png
│ ├── test_tag
│ │ ├── test_tag_component__layout[chromium][linux]-gds-5.0.0.png
│ │ ├── test_tag_component__layout[chromium][linux]-gds-5.2.0.png
│ │ ├── test_tag_component__layout[chromium][linux]-gds-5.4.0.png
│ │ ├── test_tag_component__layout[chromium][linux]-gds-5.6.0.png
│ │ └── test_tag_component__layout[chromium][linux]-gds-5.8.0.png
│ ├── test_textarea
│ │ ├── test_textarea__layout[chromium][linux]-gds-5.0.0.png
│ │ ├── test_textarea__layout[chromium][linux]-gds-5.2.0.png
│ │ ├── test_textarea__layout[chromium][linux]-gds-5.4.0.png
│ │ ├── test_textarea__layout[chromium][linux]-gds-5.6.0.png
│ │ ├── test_textarea__layout[chromium][linux]-gds-5.8.0.png
│ │ ├── test_textarea__fill_and_submit[chromium][linux]-gds-5.0.0.png
│ │ ├── test_textarea__fill_and_submit[chromium][linux]-gds-5.2.0.png
│ │ ├── test_textarea__fill_and_submit[chromium][linux]-gds-5.4.0.png
│ │ ├── test_textarea__fill_and_submit[chromium][linux]-gds-5.6.0.png
│ │ ├── test_textarea__fill_and_submit[chromium][linux]-gds-5.8.0.png
│ │ ├── test_textarea__leave_blank_and_submit[chromium][linux]-gds-5.0.0.png
│ │ ├── test_textarea__leave_blank_and_submit[chromium][linux]-gds-5.2.0.png
│ │ ├── test_textarea__leave_blank_and_submit[chromium][linux]-gds-5.4.0.png
│ │ ├── test_textarea__leave_blank_and_submit[chromium][linux]-gds-5.6.0.png
│ │ └── test_textarea__leave_blank_and_submit[chromium][linux]-gds-5.8.0.png
│ ├── test_checkboxes
│ │ ├── test_checkboxes__layout[chromium][linux]-gds-5.0.0.png
│ │ ├── test_checkboxes__layout[chromium][linux]-gds-5.2.0.png
│ │ ├── test_checkboxes__layout[chromium][linux]-gds-5.4.0.png
│ │ ├── test_checkboxes__layout[chromium][linux]-gds-5.6.0.png
│ │ ├── test_checkboxes__layout[chromium][linux]-gds-5.8.0.png
│ │ ├── test_checkboxes__leave_blank_and_submit[chromium][linux]-gds-5.0.0.png
│ │ ├── test_checkboxes__leave_blank_and_submit[chromium][linux]-gds-5.2.0.png
│ │ ├── test_checkboxes__leave_blank_and_submit[chromium][linux]-gds-5.4.0.png
│ │ ├── test_checkboxes__leave_blank_and_submit[chromium][linux]-gds-5.6.0.png
│ │ ├── test_checkboxes__leave_blank_and_submit[chromium][linux]-gds-5.8.0.png
│ │ ├── test_checkboxes__select_option_and_submit[chromium][linux]-gds-5.0.0.png
│ │ ├── test_checkboxes__select_option_and_submit[chromium][linux]-gds-5.2.0.png
│ │ ├── test_checkboxes__select_option_and_submit[chromium][linux]-gds-5.4.0.png
│ │ ├── test_checkboxes__select_option_and_submit[chromium][linux]-gds-5.6.0.png
│ │ └── test_checkboxes__select_option_and_submit[chromium][linux]-gds-5.8.0.png
│ ├── test_date_input
│ │ ├── test_date_input__layout[chromium][linux]-gds-5.0.0.png
│ │ ├── test_date_input__layout[chromium][linux]-gds-5.2.0.png
│ │ ├── test_date_input__layout[chromium][linux]-gds-5.4.0.png
│ │ ├── test_date_input__layout[chromium][linux]-gds-5.6.0.png
│ │ ├── test_date_input__layout[chromium][linux]-gds-5.8.0.png
│ │ ├── test_date_input__enter_date_and_submit[chromium][linux]-gds-5.0.0.png
│ │ ├── test_date_input__enter_date_and_submit[chromium][linux]-gds-5.2.0.png
│ │ ├── test_date_input__enter_date_and_submit[chromium][linux]-gds-5.4.0.png
│ │ ├── test_date_input__enter_date_and_submit[chromium][linux]-gds-5.6.0.png
│ │ ├── test_date_input__enter_date_and_submit[chromium][linux]-gds-5.8.0.png
│ │ ├── test_date_input__leave_blank_and_submit[chromium][linux]-gds-5.0.0.png
│ │ ├── test_date_input__leave_blank_and_submit[chromium][linux]-gds-5.2.0.png
│ │ ├── test_date_input__leave_blank_and_submit[chromium][linux]-gds-5.4.0.png
│ │ ├── test_date_input__leave_blank_and_submit[chromium][linux]-gds-5.6.0.png
│ │ └── test_date_input__leave_blank_and_submit[chromium][linux]-gds-5.8.0.png
│ ├── test_inset
│ │ ├── test_inset_component__layout[chromium][linux]-gds-5.0.0.png
│ │ ├── test_inset_component__layout[chromium][linux]-gds-5.2.0.png
│ │ ├── test_inset_component__layout[chromium][linux]-gds-5.4.0.png
│ │ ├── test_inset_component__layout[chromium][linux]-gds-5.6.0.png
│ │ └── test_inset_component__layout[chromium][linux]-gds-5.8.0.png
│ ├── test_text_input
│ │ ├── test_text_input__layout[chromium][linux]-gds-5.0.0.png
│ │ ├── test_text_input__layout[chromium][linux]-gds-5.2.0.png
│ │ ├── test_text_input__layout[chromium][linux]-gds-5.4.0.png
│ │ ├── test_text_input__layout[chromium][linux]-gds-5.6.0.png
│ │ ├── test_text_input__layout[chromium][linux]-gds-5.8.0.png
│ │ ├── test_text_input__fill_fields_and_submit[chromium][linux]-gds-5.0.0.png
│ │ ├── test_text_input__fill_fields_and_submit[chromium][linux]-gds-5.2.0.png
│ │ ├── test_text_input__fill_fields_and_submit[chromium][linux]-gds-5.4.0.png
│ │ ├── test_text_input__fill_fields_and_submit[chromium][linux]-gds-5.6.0.png
│ │ ├── test_text_input__fill_fields_and_submit[chromium][linux]-gds-5.8.0.png
│ │ ├── test_text_input__leave_blank_and_submit[chromium][linux]-gds-5.0.0.png
│ │ ├── test_text_input__leave_blank_and_submit[chromium][linux]-gds-5.2.0.png
│ │ ├── test_text_input__leave_blank_and_submit[chromium][linux]-gds-5.4.0.png
│ │ ├── test_text_input__leave_blank_and_submit[chromium][linux]-gds-5.6.0.png
│ │ └── test_text_input__leave_blank_and_submit[chromium][linux]-gds-5.8.0.png
│ ├── test_panels
│ │ ├── test_panel_component__layout[chromium][linux]-gds-5.0.0.png
│ │ ├── test_panel_component__layout[chromium][linux]-gds-5.2.0.png
│ │ ├── test_panel_component__layout[chromium][linux]-gds-5.4.0.png
│ │ ├── test_panel_component__layout[chromium][linux]-gds-5.6.0.png
│ │ └── test_panel_component__layout[chromium][linux]-gds-5.8.0.png
│ ├── test_warning
│ │ ├── test_warning_component__layout[chromium][linux]-gds-5.0.0.png
│ │ ├── test_warning_component__layout[chromium][linux]-gds-5.2.0.png
│ │ ├── test_warning_component__layout[chromium][linux]-gds-5.4.0.png
│ │ ├── test_warning_component__layout[chromium][linux]-gds-5.6.0.png
│ │ └── test_warning_component__layout[chromium][linux]-gds-5.8.0.png
│ ├── test_fieldset
│ │ ├── test_fieldset_component__layout[chromium][linux]-gds-5.0.0.png
│ │ ├── test_fieldset_component__layout[chromium][linux]-gds-5.2.0.png
│ │ ├── test_fieldset_component__layout[chromium][linux]-gds-5.4.0.png
│ │ ├── test_fieldset_component__layout[chromium][linux]-gds-5.6.0.png
│ │ └── test_fieldset_component__layout[chromium][linux]-gds-5.8.0.png
│ ├── test_file_upload
│ │ ├── test_file_upload_component__layout[chromium][linux]-gds-5.0.0.png
│ │ ├── test_file_upload_component__layout[chromium][linux]-gds-5.2.0.png
│ │ ├── test_file_upload_component__layout[chromium][linux]-gds-5.4.0.png
│ │ ├── test_file_upload_component__layout[chromium][linux]-gds-5.6.0.png
│ │ └── test_file_upload_component__layout[chromium][linux]-gds-5.8.0.png
│ └── test_radios_conditional
│ │ ├── test_radios_conditional__layout[chromium][linux]-gds-5.0.0.png
│ │ ├── test_radios_conditional__layout[chromium][linux]-gds-5.2.0.png
│ │ ├── test_radios_conditional__layout[chromium][linux]-gds-5.4.0.png
│ │ ├── test_radios_conditional__layout[chromium][linux]-gds-5.6.0.png
│ │ ├── test_radios_conditional__layout[chromium][linux]-gds-5.8.0.png
│ │ ├── test_radios_conditional__fill_hidden_and_submit[chromium][linux]-gds-5.0.0.png
│ │ ├── test_radios_conditional__fill_hidden_and_submit[chromium][linux]-gds-5.2.0.png
│ │ ├── test_radios_conditional__fill_hidden_and_submit[chromium][linux]-gds-5.4.0.png
│ │ ├── test_radios_conditional__fill_hidden_and_submit[chromium][linux]-gds-5.6.0.png
│ │ ├── test_radios_conditional__fill_hidden_and_submit[chromium][linux]-gds-5.8.0.png
│ │ ├── test_radios_conditional__show_hidden_and_submit[chromium][linux]-gds-5.0.0.png
│ │ ├── test_radios_conditional__show_hidden_and_submit[chromium][linux]-gds-5.2.0.png
│ │ ├── test_radios_conditional__show_hidden_and_submit[chromium][linux]-gds-5.4.0.png
│ │ ├── test_radios_conditional__show_hidden_and_submit[chromium][linux]-gds-5.6.0.png
│ │ └── test_radios_conditional__show_hidden_and_submit[chromium][linux]-gds-5.8.0.png
│ ├── test_tag.py
│ ├── test_inset.py
│ ├── test_panels.py
│ ├── test_warning.py
│ ├── test_fieldset.py
│ ├── test_file_upload.py
│ ├── test_tabs.py
│ ├── test_select.py
│ ├── test_buttons.py
│ ├── test_checkboxes.py
│ ├── test_details.py
│ ├── test_radios.py
│ ├── test_date_input.py
│ ├── test_text_input.py
│ ├── test_radios_conditional.py
│ └── test_textarea.py
├── docs
├── _templates
│ └── .gitkeep
├── .gitignore
├── start
│ ├── form.png
│ ├── errors.png
│ └── install.rst
├── reference
│ ├── layout
│ │ ├── div.rst
│ │ ├── html.rst
│ │ ├── fieldset.rst
│ │ ├── button.rst
│ │ ├── tabs.rst
│ │ ├── index.rst
│ │ ├── accordion.rst
│ │ └── field.rst
│ ├── form
│ │ ├── fields.rst
│ │ ├── helper.rst
│ │ ├── widgets.rst
│ │ └── index.rst
│ └── templates.rst
├── screenshots
│ ├── accordion.png
│ ├── radio-buttons.png
│ └── validation-errors.png
├── urls.py
├── components
│ ├── skip_link.rst
│ ├── phase_banner.rst
│ ├── panel.rst
│ ├── index.rst
│ ├── inset.rst
│ ├── footer.rst
│ ├── select.rst
│ ├── file.rst
│ ├── header.rst
│ └── warning.rst
├── conf.rst
├── settings.py
├── Makefile
├── make.bat
├── index.rst
└── _static
│ └── css
│ └── gds.css
├── src
└── crispy_forms_gds
│ ├── templatetags
│ └── __init__.py
│ ├── models.py
│ ├── apps.py
│ ├── layout
│ ├── base.py
│ └── __init__.py
│ ├── templates
│ └── gds
│ │ ├── inputs.html
│ │ ├── layout
│ │ ├── tab-link.html
│ │ ├── _conditional_radios.html
│ │ ├── div.html
│ │ ├── conditional_radios.html
│ │ ├── field_errors.html
│ │ ├── help_text.html
│ │ ├── button.html
│ │ ├── field_errors_block.html
│ │ ├── tabs.html
│ │ ├── help_text_and_errors.html
│ │ ├── baseinput.html
│ │ ├── conditional_question.html
│ │ ├── breadcrumbs.html
│ │ ├── fieldset.html
│ │ ├── multifield.html
│ │ ├── radios.html
│ │ ├── error_summary.html
│ │ └── radio_item.html
│ │ ├── accordion.html
│ │ ├── uni_formset.html
│ │ ├── errors.html
│ │ ├── errors_formset.html
│ │ ├── display_form.html
│ │ ├── uni_form.html
│ │ ├── whole_uni_form.html
│ │ ├── widgets
│ │ └── date.html
│ │ ├── accordion-group.html
│ │ └── whole_uni_formset.html
│ ├── utils.py
│ └── __init__.py
├── .env.example
├── manage.py
├── .readthedocs.yml
├── .editorconfig
├── .gitignore
├── .github
└── workflows
│ ├── ci.yml
│ └── pypi.yml
├── .pre-commit-config.yaml
├── .envrc
└── LICENCE.txt
/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/unit/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/components/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/_templates/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/models.py:
--------------------------------------------------------------------------------
1 | # Nothing here
2 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore the file where the docs are built
2 | _build
3 |
--------------------------------------------------------------------------------
/demo/components/models.py:
--------------------------------------------------------------------------------
1 | # These aren't the models you're looking for.
2 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # The version of the Gov.uk Design System
2 | CRISPY_GDS_FRONTEND_VERSION=5.3.0
3 |
--------------------------------------------------------------------------------
/docs/start/form.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StuartMacKay/crispy-forms-gds/HEAD/docs/start/form.png
--------------------------------------------------------------------------------
/docs/reference/layout/div.rst:
--------------------------------------------------------------------------------
1 | ===
2 | Div
3 | ===
4 |
5 | .. autoclass:: crispy_forms_gds.layout.Div
6 |
--------------------------------------------------------------------------------
/docs/start/errors.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StuartMacKay/crispy-forms-gds/HEAD/docs/start/errors.png
--------------------------------------------------------------------------------
/tests/unit/templatetags/results/back_link.html:
--------------------------------------------------------------------------------
1 | Back
2 |
--------------------------------------------------------------------------------
/docs/reference/layout/html.rst:
--------------------------------------------------------------------------------
1 | ====
2 | HTML
3 | ====
4 |
5 | .. autoclass:: crispy_forms_gds.layout.HTML
6 | :members:
7 |
--------------------------------------------------------------------------------
/docs/screenshots/accordion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StuartMacKay/crispy-forms-gds/HEAD/docs/screenshots/accordion.png
--------------------------------------------------------------------------------
/docs/reference/layout/fieldset.rst:
--------------------------------------------------------------------------------
1 | ========
2 | Fieldset
3 | ========
4 |
5 | .. autoclass:: crispy_forms_gds.layout.Fieldset
6 |
--------------------------------------------------------------------------------
/docs/screenshots/radio-buttons.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StuartMacKay/crispy-forms-gds/HEAD/docs/screenshots/radio-buttons.png
--------------------------------------------------------------------------------
/docs/urls.py:
--------------------------------------------------------------------------------
1 | # A minimal Django setup requires urlpatterns to be defined but it
2 | # can happily be empty.
3 | urlpatterns = []
4 |
--------------------------------------------------------------------------------
/tests/unit/urls.py:
--------------------------------------------------------------------------------
1 | # A minimal Django setup requires urlpatterns to be defined but it
2 | # can happily be empty.
3 | urlpatterns = []
4 |
--------------------------------------------------------------------------------
/docs/screenshots/validation-errors.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StuartMacKay/crispy-forms-gds/HEAD/docs/screenshots/validation-errors.png
--------------------------------------------------------------------------------
/docs/reference/form/fields.rst:
--------------------------------------------------------------------------------
1 | ======
2 | Fields
3 | ======
4 |
5 | .. autoclass:: crispy_forms_gds.fields.DateInputField
6 | :members:
7 |
8 |
--------------------------------------------------------------------------------
/tests/unit/conftest.py:
--------------------------------------------------------------------------------
1 | from tests.unit.utils import configure_django
2 |
3 |
4 | def pytest_sessionstart(session):
5 | configure_django()
6 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/fieldset/css_id.html:
--------------------------------------------------------------------------------
1 |
/", ComponentView.as_view(), name="name"),
10 | ]
11 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings")
7 |
8 | from django.core.management import execute_from_command_line
9 |
10 | execute_from_command_line(sys.argv)
11 |
--------------------------------------------------------------------------------
/tests/demo/snapshots/test_radios_conditional/test_radios_conditional__fill_hidden_and_submit[chromium][linux]-gds-5.0.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StuartMacKay/crispy-forms-gds/HEAD/tests/demo/snapshots/test_radios_conditional/test_radios_conditional__fill_hidden_and_submit[chromium][linux]-gds-5.0.0.png
--------------------------------------------------------------------------------
/tests/demo/snapshots/test_radios_conditional/test_radios_conditional__fill_hidden_and_submit[chromium][linux]-gds-5.2.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StuartMacKay/crispy-forms-gds/HEAD/tests/demo/snapshots/test_radios_conditional/test_radios_conditional__fill_hidden_and_submit[chromium][linux]-gds-5.2.0.png
--------------------------------------------------------------------------------
/tests/demo/snapshots/test_radios_conditional/test_radios_conditional__fill_hidden_and_submit[chromium][linux]-gds-5.4.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StuartMacKay/crispy-forms-gds/HEAD/tests/demo/snapshots/test_radios_conditional/test_radios_conditional__fill_hidden_and_submit[chromium][linux]-gds-5.4.0.png
--------------------------------------------------------------------------------
/tests/demo/snapshots/test_radios_conditional/test_radios_conditional__fill_hidden_and_submit[chromium][linux]-gds-5.6.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StuartMacKay/crispy-forms-gds/HEAD/tests/demo/snapshots/test_radios_conditional/test_radios_conditional__fill_hidden_and_submit[chromium][linux]-gds-5.6.0.png
--------------------------------------------------------------------------------
/tests/demo/snapshots/test_radios_conditional/test_radios_conditional__fill_hidden_and_submit[chromium][linux]-gds-5.8.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StuartMacKay/crispy-forms-gds/HEAD/tests/demo/snapshots/test_radios_conditional/test_radios_conditional__fill_hidden_and_submit[chromium][linux]-gds-5.8.0.png
--------------------------------------------------------------------------------
/tests/demo/snapshots/test_radios_conditional/test_radios_conditional__show_hidden_and_submit[chromium][linux]-gds-5.0.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StuartMacKay/crispy-forms-gds/HEAD/tests/demo/snapshots/test_radios_conditional/test_radios_conditional__show_hidden_and_submit[chromium][linux]-gds-5.0.0.png
--------------------------------------------------------------------------------
/tests/demo/snapshots/test_radios_conditional/test_radios_conditional__show_hidden_and_submit[chromium][linux]-gds-5.2.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StuartMacKay/crispy-forms-gds/HEAD/tests/demo/snapshots/test_radios_conditional/test_radios_conditional__show_hidden_and_submit[chromium][linux]-gds-5.2.0.png
--------------------------------------------------------------------------------
/tests/demo/snapshots/test_radios_conditional/test_radios_conditional__show_hidden_and_submit[chromium][linux]-gds-5.4.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StuartMacKay/crispy-forms-gds/HEAD/tests/demo/snapshots/test_radios_conditional/test_radios_conditional__show_hidden_and_submit[chromium][linux]-gds-5.4.0.png
--------------------------------------------------------------------------------
/tests/demo/snapshots/test_radios_conditional/test_radios_conditional__show_hidden_and_submit[chromium][linux]-gds-5.6.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StuartMacKay/crispy-forms-gds/HEAD/tests/demo/snapshots/test_radios_conditional/test_radios_conditional__show_hidden_and_submit[chromium][linux]-gds-5.6.0.png
--------------------------------------------------------------------------------
/tests/demo/snapshots/test_radios_conditional/test_radios_conditional__show_hidden_and_submit[chromium][linux]-gds-5.8.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StuartMacKay/crispy-forms-gds/HEAD/tests/demo/snapshots/test_radios_conditional/test_radios_conditional__show_hidden_and_submit[chromium][linux]-gds-5.8.0.png
--------------------------------------------------------------------------------
/src/crispy_forms_gds/templates/gds/errors.html:
--------------------------------------------------------------------------------
1 | {% if form.non_field_errors %}
2 |
3 | {% if form_error_title %}
{{ form_error_title }} {% endif %}
4 |
5 | {{ form.non_field_errors|unordered_list }}
6 |
7 |
8 | {% endif %}
9 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/templates/gds/errors_formset.html:
--------------------------------------------------------------------------------
1 | {% if formset.non_form_errors %}
2 |
3 | {% if formset_error_title %}
{{ formset_error_title }} {% endif %}
4 |
5 | {{ formset.non_form_errors|unordered_list }}
6 |
7 |
8 | {% endif %}
9 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/text_input/no_help_text.html:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/templates/gds/layout/field_errors.html:
--------------------------------------------------------------------------------
1 | {% if form_show_errors and field.errors %}
2 | {% for error in field.errors %}
3 |
4 | Error: {{ error }}
5 |
6 | {% endfor %}
7 | {% endif %}
8 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/templates/gds/display_form.html:
--------------------------------------------------------------------------------
1 | {% if form.form_html %}
2 | {% if include_media %}{{ form.media }}{% endif %}
3 | {% if form_show_errors and form.helper.show_non_field_errors %}
4 | {% include "gds/errors.html" %}
5 | {% endif %}
6 | {{ form.form_html }}
7 | {% else %}
8 | {% include "gds/uni_form.html" %}
9 | {% endif %}
10 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/templates/gds/layout/help_text.html:
--------------------------------------------------------------------------------
1 | {% if field.help_text %}
2 | {% if help_text_inline %}
3 | {{ field.help_text|safe }}
4 | {% else %}
5 | {{ field.help_text|safe }}
6 | {% endif %}
7 | {% endif %}
8 |
--------------------------------------------------------------------------------
/.readthedocs.yml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 2
3 |
4 | build:
5 | os: ubuntu-20.04
6 | tools:
7 | python: '3.8'
8 | commands:
9 | - asdf plugin add uv
10 | - asdf install uv latest
11 | - asdf global uv latest
12 | - uv sync --extra docs --frozen
13 | - uv run -m sphinx -T -b html -d docs/_build/doctrees -D language=en docs $READTHEDOCS_OUTPUT/html
14 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | A Django application to add django-crispy-forms template pack and layout
3 | objects for the GOV.UK Design System.
4 |
5 | """
6 |
7 | __version__ = "2.0.1"
8 | __author__ = "Stuart MacKay"
9 | __email__ = "smackay@flagstonesoftware.com"
10 | __license__ = "MIT License"
11 | __copyright__ = "Copyright (C) 2020-2025 Stuart MacKay"
12 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/templates/gds/layout/button.html:
--------------------------------------------------------------------------------
1 | {{ input.value|safe }}
6 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/templates/gds/layout/field_errors_block.html:
--------------------------------------------------------------------------------
1 | {% if form_show_errors and field.errors %}
2 | {% for error in field.errors %}
3 |
4 | Error: {{ error }}
5 |
6 | {% endfor %}
7 | {% endif %}
8 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/templates/gds/uni_form.html:
--------------------------------------------------------------------------------
1 | {% load crispy_forms_utils %}
2 |
3 | {% specialspaceless %}
4 | {% if include_media %}{{ form.media }}{% endif %}
5 | {% if form_show_errors %}
6 | {% include "gds/errors.html" %}
7 | {% endif %}
8 | {% for field in form %}
9 | {% include field_template %}
10 | {% endfor %}
11 | {% endspecialspaceless %}
12 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/file_upload/no_help_text.html:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/text_input/no_label.html:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/docs/components/skip_link.rst:
--------------------------------------------------------------------------------
1 | .. _Skip link: https://design-system.service.gov.uk/components/skip-link/
2 |
3 | #########
4 | Skip link
5 | #########
6 | There is a `Skip link`_ component implemented in the header of the ``gds/base.html``
7 | template that is include in the template pack. It is wrapped with a ``{% block %}``
8 | tag so if you extend the base template you can override it if you need to.
9 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/templates/gds/layout/tabs.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Contents
4 |
5 |
8 | {{ content|safe }}
9 |
10 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/textarea/no_help_text.html:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/tests/unit/templatetags/results/button_start.html:
--------------------------------------------------------------------------------
1 |
2 | Start now
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/file_upload/no_label.html:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
--------------------------------------------------------------------------------
/demo/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.conf.urls.static import static
3 | from django.urls import include, path
4 | from django.views.generic.base import TemplateView
5 |
6 | urlpatterns = [
7 | path(r"", TemplateView.as_view(template_name="demo/index.html"), name="home"),
8 | path(r"components/", include("demo.components.urls", namespace="components")),
9 | ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
10 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/templates/gds/layout/help_text_and_errors.html:
--------------------------------------------------------------------------------
1 | {% if help_text_inline and not error_text_inline %}
2 | {% include 'gds/layout/help_text.html' %}
3 | {% endif %}
4 |
5 | {% if not help_text_inline %}
6 | {% include 'gds/layout/help_text.html' %}
7 | {% endif %}
8 |
9 | {% if error_text_inline %}
10 | {% include 'gds/layout/field_errors.html' %}
11 | {% else %}
12 | {% include 'gds/layout/field_errors_block.html' %}
13 | {% endif %}
14 |
--------------------------------------------------------------------------------
/docs/conf.rst:
--------------------------------------------------------------------------------
1 | .. Color profiles and font sizes for the Design System.
2 | .. Intended to be used with _static/css/gds.css
3 | .. role:: colour-blue
4 | .. role:: colour-green
5 | .. role:: colour-grey
6 | .. role:: colour-orange
7 | .. role:: colour-pink
8 | .. role:: colour-purple
9 | .. role:: colour-red
10 | .. role:: colour-turquoise
11 | .. role:: colour-yellow
12 | .. role:: font-small
13 | .. role:: font-medium
14 | .. role:: font-large
15 | .. role:: font-extra-large
16 |
--------------------------------------------------------------------------------
/tests/unit/helpers/results/label_size.html:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/unit/helpers/results/override_label_size.html:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/textarea/no_label.html:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/text_input/label_size.html:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/demo/test_tabs.py:
--------------------------------------------------------------------------------
1 | from playwright.sync_api import Page
2 |
3 |
4 | def test_tabs__layout(live_server, assert_snapshot, page: Page):
5 | page.goto(f"{live_server.url}/components/tabs/")
6 | assert_snapshot(page.screenshot(full_page=True))
7 |
8 |
9 | def test_tabs__click_tab(live_server, assert_snapshot, page: Page):
10 | page.goto(f"{live_server.url}/components/tabs/")
11 | page.locator("a[id=tab_past-week]").click()
12 | assert_snapshot(page.screenshot(full_page=True))
13 |
--------------------------------------------------------------------------------
/tests/unit/layout/test_conditional_radios.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tests.unit.forms import ConditionalRadiosForm
4 | from tests.unit.utils import TEST_DIR, parse_contents, parse_form
5 |
6 | RESULT_DIR = os.path.join(TEST_DIR, "layout", "results", "conditional_radios")
7 |
8 |
9 | def test_conditional_radios():
10 | """Verify the HTML for conditional radios"""
11 | form = ConditionalRadiosForm()
12 | assert parse_form(form) == parse_contents(RESULT_DIR, "conditional_radios.html")
13 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/text_input/initial.html:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/tabs/panel_css_id.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Contents
5 |
6 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/demo/components/templates/components/component.html:
--------------------------------------------------------------------------------
1 | {% extends "components/base.html" %}
2 | {% load i18n crispy_forms_tags crispy_forms_gds %}
3 |
4 | {% block right %}
5 |
6 | {% breadcrumbs breadcrumbs %}
7 |
8 | {% error_summary form %}
9 |
10 |
11 | {% trans 'Components' %}
12 |
13 |
14 | {{ title }}
15 |
16 |
17 |
18 | {% crispy form %}
19 |
20 |
21 | {% endblock %}
22 |
--------------------------------------------------------------------------------
/tests/unit/templatetags/results/breadcrumbs.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Home
5 |
6 |
7 | Previous
8 |
9 |
10 | Current
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | indent_style = space
7 | indent_size = 4
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 | charset = utf-8
11 | end_of_line = lf
12 |
13 | [*.{css,html,json,scss,yml}]
14 | indent_size = 2
15 |
16 | [*.bat]
17 | indent_style = tab
18 | end_of_line = crlf
19 |
20 | [LICENSE.txt]
21 | insert_final_newline = false
22 |
23 | [Makefile]
24 | indent_style = tab
25 |
26 | [*.{diff,patch}]
27 | trim_trailing_whitespace = false
28 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/templates/gds/layout/baseinput.html:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/tabs/panel_attributes.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Contents
5 |
6 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/tabs/panel_css_class.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Contents
5 |
6 |
13 |
14 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/file_upload/initial.html:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
--------------------------------------------------------------------------------
/demo/components/forms/warning.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 |
3 | from crispy_forms_gds.helper import FormHelper
4 | from crispy_forms_gds.layout import HTML, Layout
5 |
6 |
7 | class WarningForm(forms.Form):
8 | def __init__(self, *args, **kwargs):
9 | super().__init__(*args, **kwargs)
10 | self.helper = FormHelper()
11 | self.helper.layout = Layout(
12 | HTML.warning(
13 | "You can be fined up to £5,000 if you do not like this template pack."
14 | )
15 | )
16 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/text_input/label_heading.html:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/templates/gds/whole_uni_form.html:
--------------------------------------------------------------------------------
1 | {% load crispy_forms_utils %}
2 |
3 | {% specialspaceless %}
4 | {% if form_tag %}{% endif %}
5 | {% if form_method|lower == 'post' and not disable_csrf %}
6 | {% csrf_token %}
7 | {% endif %}
8 |
9 | {% include "gds/display_form.html" %}
10 |
11 | {% include "gds/inputs.html" %}
12 |
13 | {% if form_tag %} {% endif %}
14 | {% endspecialspaceless %}
15 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/templates/gds/layout/conditional_question.html:
--------------------------------------------------------------------------------
1 | {% extends 'gds/layout/radio_item.html' %}
2 |
3 | {% block additional_input_attributes %}
4 | aria-controls="conditional_{{ field.html_name }}_{{ position }}"
5 | {% endblock %}
6 |
7 | {% block additional_content %}
8 | {% if conditional_content %}
9 |
10 | {{ conditional_content|safe }}
11 |
12 | {% endif %}
13 | {% endblock %}
14 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/checkbox/no_help_text.html:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/file_upload/label_size.html:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
--------------------------------------------------------------------------------
/demo/components/forms/panel.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 |
3 | from crispy_forms_gds.helper import FormHelper
4 | from crispy_forms_gds.layout import HTML, Layout
5 |
6 |
7 | class PanelForm(forms.Form):
8 | def __init__(self, *args, **kwargs):
9 | super().__init__(*args, **kwargs)
10 | self.helper = FormHelper()
11 | self.helper.layout = Layout(
12 | HTML.panel(
13 | "Application complete",
14 | "Your reference number HDJ2123F ",
15 | )
16 | )
17 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/templates/gds/widgets/date.html:
--------------------------------------------------------------------------------
1 | {% load crispy_forms_gds %}
2 |
12 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/select/no_help_text.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | How would you like to be contacted?
5 |
6 |
7 |
8 | Choose
9 | Email
10 | Phone
11 | Text message
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/templates/gds/layout/breadcrumbs.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {% for title, url in crumbs %}
4 | {% if forloop.last %}
5 |
6 | {{ title }}
7 |
8 | {% else %}
9 |
10 | {{ title }}
11 |
12 | {% endif %}
13 | {% endfor %}
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/tests/unit/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | MVS (Minimalist Viable Settings) for running the tests.
3 |
4 | """
5 |
6 | INSTALLED_APPS = (
7 | "crispy_forms",
8 | "crispy_forms_gds",
9 | )
10 |
11 | ROOT_URLCONF = "tests.unit.urls"
12 |
13 | TEMPLATES = [
14 | {
15 | "BACKEND": "django.template.backends.django.DjangoTemplates",
16 | "DIRS": [],
17 | "APP_DIRS": True,
18 | "OPTIONS": {},
19 | },
20 | ]
21 |
22 | CRISPY_ALLOWED_TEMPLATE_PACKS = ("gds",)
23 |
24 | CRISPY_TEMPLATE_PACK = "gds"
25 |
26 | CRISPY_FAIL_SILENTLY = False
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Python
2 | __pycache__/
3 | *.py[co]
4 | *.egg*
5 |
6 | # Editor temp files
7 | *~
8 |
9 | # Ignore dot files except those required for the project
10 | .*
11 | !.editorconfig
12 | !.env.example
13 | !.envrc
14 | !.github
15 | !.gitignore
16 | !.gitkeep
17 | !.nvmrc
18 | !.pre-commit-config.yaml
19 | !.readthedocs.yml
20 |
21 | # Ignore the database used in the demo
22 | db.sqlite3
23 |
24 | # Ignore the gds asset files
25 | /assets/fonts/
26 | /assets/images/
27 | /assets/javascripts/
28 | /assets/stylesheets/
29 | /assets/manifest.json
30 | /assets/VERSION.txt
31 |
--------------------------------------------------------------------------------
/docs/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | MVS (Minimalist Viable Settings) for running the tests.
3 |
4 | """
5 |
6 | SECRET_KEY = "secret"
7 |
8 | INSTALLED_APPS = (
9 | "crispy_forms",
10 | "crispy_forms_gds",
11 | )
12 |
13 | ROOT_URLCONF = "tests.urls"
14 |
15 | TEMPLATES = [
16 | {
17 | "BACKEND": "django.template.backends.django.DjangoTemplates",
18 | "DIRS": [],
19 | "APP_DIRS": True,
20 | "OPTIONS": {},
21 | },
22 | ]
23 |
24 | CRISPY_ALLOWED_TEMPLATE_PACKS = ("gds",)
25 |
26 | CRISPY_TEMPLATE_PACK = "gds"
27 |
28 | CRISPY_FAIL_SILENTLY = False
29 |
--------------------------------------------------------------------------------
/demo/components/forms/inset.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 |
3 | from crispy_forms_gds.helper import FormHelper
4 | from crispy_forms_gds.layout import HTML, Layout
5 |
6 |
7 | class InsetForm(forms.Form):
8 | def __init__(self, *args, **kwargs):
9 | super().__init__(*args, **kwargs)
10 | self.helper = FormHelper()
11 | self.helper.layout = Layout(
12 | HTML.inset(
13 | "It can take up to 8 weeks to register a lasting power of "
14 | "attorney if there are no mistakes in the application. "
15 | )
16 | )
17 |
--------------------------------------------------------------------------------
/tests/demo/test_select.py:
--------------------------------------------------------------------------------
1 | from playwright.sync_api import Page
2 |
3 |
4 | def test_select__layout(live_server, assert_snapshot, page: Page):
5 | page.goto(f"{live_server.url}/components/select/")
6 | assert_snapshot(page.screenshot(full_page=True))
7 |
8 |
9 | def test_select__select_item_and_submit(live_server, assert_snapshot, page: Page):
10 | page.goto(f"{live_server.url}/components/select/")
11 | page.locator("select[id=id_sort_by]").select_option("updated")
12 | page.locator("button[id=id_submit]").click()
13 | assert_snapshot(page.screenshot(full_page=True))
14 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/textarea/label_size.html:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/file_upload/label_heading.html:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/select/no_label.html:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/text_input/no_help_text_errors.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Name
5 |
6 |
7 | Error: Required error message
8 |
9 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/textarea/initial.html:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/accordion/section_css_id.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 | Contents
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/accordion/section_attributes.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 | Contents
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/accordion/section_css_class.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/text_input/no_help_text_errors_aria_invalid.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Name
5 |
6 |
7 | Error: Required error message
8 |
9 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/textarea/label_heading.html:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
--------------------------------------------------------------------------------
/docs/components/phase_banner.rst:
--------------------------------------------------------------------------------
1 | .. _Phase banner: https://design-system.service.gov.uk/components/phase-banner/
2 |
3 | ############
4 | Phase banner
5 | ############
6 | There is a `Phase banner`_ component implemented in the ``gds/base.html`` template
7 | that is include in the template pack. It is wrapped with a ``{% block %}`` tag so if
8 | you extend the base template you can override it if you need to.
9 |
10 | Within the ``{% block phase_banner %}`` tag there are two daughter blocks:
11 | ``{% block phase_banner__tag %}`` and ``{% block phase_banner__text %}`` so you can
12 | selectively override these if you don't want to override the entire phase_banner
13 | block.
14 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/file_upload/no_help_text_errors.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Upload a file
5 |
6 |
7 | Error: Select the CSV file you exported from the spreadsheet
8 |
9 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/templates/gds/layout/fieldset.html:
--------------------------------------------------------------------------------
1 |
4 | {% if legend %}
5 |
6 | {% if legend_tag %}<{{ legend_tag }} class="govuk-fieldset__heading">{% endif %}
7 | {{ legend|safe }}
8 | {% if legend_tag %}{{ legend_tag }}>{% endif %}
9 |
10 | {% endif %}
11 | {{ fields|safe }}
12 |
13 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/text_input/validation_errors.html:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/textarea/no_help_text_errors.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Description
5 |
6 |
7 | Error: Required error message
8 |
9 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tests/demo/test_buttons.py:
--------------------------------------------------------------------------------
1 | from playwright.sync_api import Page
2 |
3 |
4 | def test_buttons__layout(live_server, assert_snapshot, page: Page):
5 | page.goto(f"{live_server.url}/components/buttons/")
6 | assert_snapshot(page.screenshot(full_page=True))
7 |
8 |
9 | def test_buttons__click_add_button(live_server, assert_snapshot, page: Page):
10 | page.goto(f"{live_server.url}/components/buttons/")
11 | page.locator("button[id=id_add]").click()
12 | assert_snapshot(page.screenshot(full_page=True))
13 |
14 |
15 | def test_buttons__disabled_button(live_server, page: Page):
16 | page.goto(f"{live_server.url}/components/buttons/")
17 | assert page.locator("button[id=id_win]").is_disabled()
18 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/select/initial.html:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/file_upload/no_help_text_errors_aria_invalid.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Upload a file
5 |
6 |
7 | Error: Select the CSV file you exported from the spreadsheet
8 |
9 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/select/label_size.html:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | # Test django-star-ratings on:
4 | # all supported Django and Python versions
5 |
6 | on:
7 | push:
8 | pull_request:
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | python-version:
16 | - "3.10"
17 | steps:
18 | - uses: actions/checkout@v1
19 | - name: Set up Python ${{ matrix.python-version }}
20 | uses: actions/setup-python@v2
21 | with:
22 | python-version: ${{ matrix.python-version }}
23 | - name: Install dependencies
24 | run: |
25 | python -m pip install --upgrade pip
26 | pip install tox tox-gh-actions
27 | - name: Test with tox
28 | run: tox
29 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/checkbox/initial.html:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/text_input/validation_errors_aria_invalid.html:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | # See https://pre-commit.com for more information
2 | # See https://pre-commit.com/hooks.html for more hooks
3 | repos:
4 | - repo: https://github.com/pre-commit/pre-commit-hooks
5 | rev: v3.2.0
6 | hooks:
7 | - id: trailing-whitespace
8 | - id: end-of-file-fixer
9 | - id: check-yaml
10 | - id: check-added-large-files
11 | - repo: https://github.com/astral-sh/ruff-pre-commit
12 | # Ruff version.
13 | rev: v0.8.4
14 | hooks:
15 | # Run the linter.
16 | - id: ruff
17 | types_or: [ python, pyi ]
18 | args: [ --fix ]
19 | # Run the formatter.
20 | - id: ruff-format
21 | types_or: [ python, pyi ]
22 | - repo: https://github.com/pycqa/isort
23 | rev: 5.13.2
24 | hooks:
25 | - id: isort
26 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/checkbox/checkbox_size.html:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/textarea/no_help_text_errors_aria_invalid.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Description
5 |
6 |
7 | Error: Required error message
8 |
9 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/demo/components/forms/details.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 |
3 | from crispy_forms_gds.helper import FormHelper
4 | from crispy_forms_gds.layout import HTML, Layout
5 |
6 |
7 | class DetailsForm(forms.Form):
8 | def __init__(self, *args, **kwargs):
9 | super().__init__(*args, **kwargs)
10 | self.helper = FormHelper()
11 | self.helper.layout = Layout(
12 | HTML.details(
13 | "Help with nationality",
14 | "We need to know your nationality so we can work out which "
15 | "elections you’re entitled to vote in. If you cannot provide "
16 | "your nationality, you’ll have to send copies of identity "
17 | "documents through the post.",
18 | )
19 | )
20 |
--------------------------------------------------------------------------------
/demo/templates/demo/index.html:
--------------------------------------------------------------------------------
1 | {% extends "demo/base.html" %}
2 | {% load i18n crispy_forms_gds %}
3 |
4 | {% block content %}
5 |
6 |
7 |
8 |
9 | {% trans "Demo" %}
10 |
11 |
12 |
13 | {% blocktrans %}
14 | Welcome to the Demo site for crispy-forms-gds . Here you will
15 | find fully working example of all the components in the GOV.UK Design System.
16 | {% endblocktrans %}
17 |
18 |
19 | {% url 'components:index' as start_url %}
20 | {% button_start start_url "Start now" %}
21 |
22 |
23 |
24 | {% endblock %}
25 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/templates/gds/accordion-group.html:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 | {{ fields|safe }}
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/select/label_heading.html:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
--------------------------------------------------------------------------------
/demo/static/css/demo.css:
--------------------------------------------------------------------------------
1 | nav a {
2 | text-decoration: none;
3 | }
4 |
5 | .demo-width-container {
6 | margin-right: 30px;
7 | margin-left: 30px;
8 | }
9 |
10 | .demo-navigation {
11 | font-size: 1.1875rem;
12 | line-height: 1.31579;
13 | background-color: #f8f8f8;
14 | }
15 |
16 | .demo-navigation__list {
17 | position: relative;
18 | left: -15px;
19 | list-style: none;
20 | margin: 0;
21 | padding: 0;
22 | }
23 |
24 | .demo-navigation__list-item {
25 | -webkit-box-sizing: border-box;
26 | box-sizing: border-box;
27 | display: block;
28 | position: relative;
29 | height: 50px;
30 | padding: 0 15px;
31 | float: left;
32 | line-height: 50px;
33 | }
34 |
35 | .demo-navigation__list-item--current {
36 | border-bottom: 4px solid #1d70b8;
37 | }
38 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/file_upload/validation_errors.html:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
--------------------------------------------------------------------------------
/demo/components/templates/components/index.html:
--------------------------------------------------------------------------------
1 | {% extends "components/base.html" %}
2 | {% load i18n crispy_forms_gds %}
3 |
4 | {% block right %}
5 |
6 |
7 |
8 | {% url 'home' as back_url %}
9 | {% back_link back_url %}
10 |
11 |
12 | {% trans "Components" %}
13 |
14 |
15 |
16 | {% blocktrans %}
17 | Here you will find examples showing how different types of field are displayed
18 | using the GOV.UK Design System. Select a link from the sidebar to see each field
19 | in more detail.
20 | {% endblocktrans %}
21 |
22 |
23 |
24 |
25 | {% endblock %}
26 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/layout/__init__.py:
--------------------------------------------------------------------------------
1 | from .base import Layout
2 | from .buttons import Button, Submit
3 | from .constants import Fixed, Fluid, Size
4 | from .containers import (
5 | Accordion,
6 | AccordionSection,
7 | ConditionalQuestion,
8 | ConditionalRadios,
9 | Div,
10 | Fieldset,
11 | TabPanel,
12 | Tabs,
13 | )
14 | from .content import HTML, Colour
15 | from .fields import Field, Hidden
16 |
17 | __all__ = [
18 | "Accordion",
19 | "AccordionSection",
20 | "Button",
21 | "Colour",
22 | "ConditionalQuestion",
23 | "ConditionalRadios",
24 | "Div",
25 | "Field",
26 | "Fixed",
27 | "Fieldset",
28 | "Fluid",
29 | "Hidden",
30 | "HTML",
31 | "Layout",
32 | "Size",
33 | "Submit",
34 | "TabPanel",
35 | "Tabs",
36 | ]
37 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/tabs/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Contents
5 |
6 |
7 |
19 |
20 |
23 |
24 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/templates/gds/layout/multifield.html:
--------------------------------------------------------------------------------
1 | {% load l10n crispy_forms_gds %}
2 |
3 |
4 | {% if field.label %}
5 |
6 | {% if legend_tag %}<{{ legend_tag }} class="govuk-fieldset__heading">{% endif %}
7 | {{ field.label|safe }}
8 | {% if legend_tag %}{{ legend_tag }}>{% endif %}
9 |
10 | {% endif %}
11 |
12 | {% include 'gds/layout/help_text_and_errors.html' %}
13 | {% crispy_gds_field field %}
14 |
15 |
16 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/fieldset/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Contact
6 |
7 |
8 |
9 |
10 | Name
11 |
12 |
15 |
16 |
17 |
18 |
19 | Email
20 |
21 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/select/no_help_text_errors.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | How would you like to be contacted?
5 |
6 |
7 |
8 | Error: Enter the best way to contact you
9 |
10 |
11 |
12 | Choose
13 | Email
14 | Phone
15 | Text message
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/textarea/validation_errors.html:
--------------------------------------------------------------------------------
1 |
2 |
20 |
21 |
--------------------------------------------------------------------------------
/.github/workflows/pypi.yml:
--------------------------------------------------------------------------------
1 | name: PyPI
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | publish:
10 | name: Build and publish to PyPI
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | - name: Set up Python 3.10
15 | uses: actions/setup-python@v2
16 | with:
17 | python-version: 3.10
18 | - name: Install dependencies
19 | run: |
20 | python -m pip install --upgrade pip
21 | pip install setuptools wheel
22 | - name: Build a binary wheel and a source tarball
23 | run: |
24 | python setup.py sdist bdist_wheel
25 | - name: Publish to PyPI
26 | if: startsWith(github.ref, 'refs/tags')
27 | uses: pypa/gh-action-pypi-publish@master
28 | with:
29 | password: ${{ secrets.pypi_password }}
30 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/checkbox/no_help_text_errors.html:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/file_upload/validation_errors_aria_invalid.html:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/fieldset/legend_size.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Contact
6 |
7 |
8 |
9 |
10 | Name
11 |
12 |
15 |
16 |
17 |
18 |
19 | Email
20 |
21 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/tests/demo/test_checkboxes.py:
--------------------------------------------------------------------------------
1 | from playwright.sync_api import Page
2 |
3 |
4 | def test_checkboxes__layout(live_server, assert_snapshot, page: Page):
5 | page.goto(f"{live_server.url}/components/checkboxes/")
6 | assert_snapshot(page.screenshot(full_page=True))
7 |
8 |
9 | def test_checkboxes__select_option_and_submit(live_server, assert_snapshot, page: Page):
10 | page.goto(f"{live_server.url}/components/checkboxes/")
11 | page.locator("input[id=id_method_1]").click()
12 | page.locator("button[id=id_submit]").click()
13 | assert_snapshot(page.screenshot(full_page=True))
14 |
15 |
16 | def test_checkboxes__leave_blank_and_submit(live_server, assert_snapshot, page: Page):
17 | page.goto(f"{live_server.url}/components/checkboxes/")
18 | page.locator("button[id=id_submit]").click()
19 | assert_snapshot(page.screenshot(full_page=True))
20 |
--------------------------------------------------------------------------------
/tests/demo/test_details.py:
--------------------------------------------------------------------------------
1 | from playwright.sync_api import Page
2 |
3 |
4 | def test_details__layout(live_server, assert_snapshot, page: Page):
5 | page.goto(f"{live_server.url}/components/details/")
6 | assert_snapshot(page.screenshot(full_page=True))
7 |
8 |
9 | def test_details__show_summary(live_server, assert_snapshot, page: Page):
10 | page.goto(f"{live_server.url}/components/details/")
11 | page.locator("summary[class=govuk-details__summary]").click()
12 | assert_snapshot(page.screenshot(full_page=True))
13 |
14 |
15 | def test_details__hide_summary(live_server, assert_snapshot, page: Page):
16 | page.goto(f"{live_server.url}/components/details/")
17 | page.locator("summary[class=govuk-details__summary]").click()
18 | page.locator("summary[class=govuk-details__summary]").click()
19 | assert_snapshot(page.screenshot(full_page=True))
20 |
--------------------------------------------------------------------------------
/tests/demo/test_radios.py:
--------------------------------------------------------------------------------
1 | from playwright.sync_api import Page
2 |
3 |
4 | def test_radios__layout(live_server, assert_snapshot, page: Page):
5 | page.goto(f"{live_server.url}/components/radios/")
6 | assert_snapshot(page.screenshot(full_page=True))
7 |
8 |
9 | def test_radios__click_and_submit(live_server, assert_snapshot, page: Page):
10 | page.goto(f"{live_server.url}/components/radios/")
11 | page.locator("input[id=id_name_1]").click()
12 | page.locator("input[id=id_method_2]").click()
13 | page.locator("button[id=id_submit]").click()
14 | assert_snapshot(page.screenshot(full_page=True))
15 |
16 |
17 | def test_radios__leave_blank_and_submit(live_server, assert_snapshot, page: Page):
18 | page.goto(f"{live_server.url}/components/radios/")
19 | page.locator("button[id=id_submit]").click()
20 | assert_snapshot(page.screenshot(full_page=True))
21 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/textarea/validation_errors_aria_invalid.html:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/checkbox/no_help_text_errors_aria_invalid.html:
--------------------------------------------------------------------------------
1 |
2 |
24 |
25 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/fieldset/legend_heading.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Contact
7 |
8 |
9 |
10 |
11 |
12 | Name
13 |
14 |
17 |
18 |
19 |
20 |
21 | Email
22 |
23 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/docs/reference/templates.rst:
--------------------------------------------------------------------------------
1 | =========
2 | Templates
3 | =========
4 |
5 | Crispy-forms-gds contains a base template, ``gds/base.html`` that is production ready
6 | and can be used in your site. The template has several ``{% block %}`` tags which allow
7 | various aspects of the template to be customised. The description of the ``Header``,
8 | ``Footer``, ``Skip link`` and ``Phase banner`` components in the ``Components`` section
9 | describe each of the available blocks.
10 |
11 | .. note::
12 | IMPORTANT: The template contains elements to display the crown and crown
13 | copyright logos. These are included as a convenience for projects and sites that
14 | are part of Her Majesty's Government. If your project is not directly part of
15 | the Government of the United Kingdom you expressly DO NOT have permission to display
16 | these logos and these sections must be overridden.
17 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/select/no_help_text_errors_aria_invalid.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | How would you like to be contacted?
5 |
6 |
7 |
8 | Error: Enter the best way to contact you
9 |
10 |
11 |
15 | Choose
16 | Email
17 | Phone
18 | Text message
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/docs/components/panel.rst:
--------------------------------------------------------------------------------
1 | =====
2 | Panel
3 | =====
4 |
5 | Use a `Panel`_ component for confirmation messages or to highlight important sections.
6 |
7 | .. _Panel: https://design-system.service.gov.uk/components/panel/
8 |
9 |
10 | .. code-block::
11 |
12 | from django import forms
13 |
14 | from crispy_forms_gds.helper import FormHelper
15 | from crispy_forms_gds.layout import HTML, Layout
16 |
17 |
18 | class InsetForm(forms.Form):
19 | def __init__(self, *args, **kwargs):
20 | super(InsetForm, self).__init__(*args, **kwargs)
21 | self.helper = FormHelper()
22 | self.helper.layout = Layout(
23 | HTML.panel(
24 | "Application complete",
25 | "Your reference number HDJ2123F "
26 | )
27 | )
28 |
29 |
30 | You can see this form live in the Demo site.
31 |
--------------------------------------------------------------------------------
/docs/components/index.rst:
--------------------------------------------------------------------------------
1 | =============
2 | Design System
3 | =============
4 |
5 | There is support for all the `Design System Components`_ that you might expect to
6 | encounter in a complex form.
7 |
8 | .. _Design System Components: https://design-system.service.gov.uk/components
9 |
10 |
11 | .. toctree::
12 | :titlesonly:
13 |
14 | accordion
15 | back_link
16 | breadcrumbs
17 | button
18 | checkboxes
19 | date
20 | details
21 | fieldset
22 | file
23 | footer
24 | header
25 | inset
26 | panel
27 | phase_banner
28 | radios
29 | select
30 | skip_link
31 | table
32 | tabs
33 | tag
34 | text
35 | textarea
36 | warning
37 |
38 | The only Design System component not in this list is `Summary list`. Right now we are
39 | just not sure how to implement it, given that the list contents and specifically
40 | actions can be laid out in various ways.
41 |
--------------------------------------------------------------------------------
/.envrc:
--------------------------------------------------------------------------------
1 | # direnv file
2 |
3 | # This is just a bash script.
4 | # https://direnv.net/man/direnv-stdlib.1.html
5 | # https://github.com/direnv/direnv/wiki
6 |
7 | # Activate the virtualenv
8 | # This simply replicates what venv/bin/activate does.
9 | # See, https://docs.python.org/3/library/venv.html#module-venv
10 |
11 | if [[ -d ".venv/bin" ]]; then
12 | # The path to the virtualenv should be absolute. If you use ipython
13 | # for the Django shell it will raise an error if the path is relative.
14 | # https://github.com/ipython/ipython/issues/13268
15 | # https://github.com/direnv/direnv/issues/304
16 | export VIRTUAL_ENV=`pwd`/.venv
17 | PATH_add ".venv/bin"
18 | fi
19 |
20 | # Set environment variables from .env. We're using a conditional
21 | # in case an older version of direnv is used. In recent releases
22 | # there is the stdlib function dotenv_if_exists
23 |
24 | if [[ -f ".env" ]]; then
25 | dotenv
26 | fi
27 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/templates/gds/whole_uni_formset.html:
--------------------------------------------------------------------------------
1 | {% load crispy_forms_tags %}
2 | {% load crispy_forms_utils %}
3 |
4 | {% specialspaceless %}
5 | {% if formset_tag %}
6 |
7 | {% endif %}
8 | {% if formset_method|lower == 'post' and not disable_csrf %}
9 | {% csrf_token %}
10 | {% endif %}
11 |
12 |
13 | {{ formset.management_form|crispy }}
14 |
15 |
16 | {% include "gds/errors_formset.html" %}
17 |
18 | {% for form in formset %}
19 | {% include "gds/display_form.html" %}
20 | {% endfor %}
21 |
22 | {% if inputs %}
23 |
24 | {% for input in inputs %}
25 | {% include "gds/layout/baseinput.html" %}
26 | {% endfor %}
27 |
28 | {% endif %}
29 | {% if formset_tag %} {% endif %}
30 | {% endspecialspaceless %}
31 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 |
13 | if "%1" == "" goto help
14 |
15 | %SPHINXBUILD% >NUL 2>NUL
16 | if errorlevel 9009 (
17 | echo.
18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19 | echo.installed, then set the SPHINXBUILD environment variable to point
20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
21 | echo.may add the Sphinx directory to PATH.
22 | echo.
23 | echo.If you don't have Sphinx installed, grab it from
24 | echo.http://sphinx-doc.org/
25 | exit /b 1
26 | )
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/textarea/word_count.html:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/select/validation_errors.html:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/textarea/character_count.html:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/checkbox/validation_errors.html:
--------------------------------------------------------------------------------
1 |
2 |
27 |
28 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/table/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Caption
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | David Francis
13 | 3
14 |
15 |
16 | Paul Farmer
17 | 1
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/tests/demo/test_date_input.py:
--------------------------------------------------------------------------------
1 | from playwright.sync_api import Page
2 |
3 |
4 | def test_date_input__layout(live_server, assert_snapshot, page: Page):
5 | page.goto(f"{live_server.url}/components/date-input/")
6 | assert_snapshot(page.screenshot(full_page=True))
7 |
8 |
9 | def test_date_input__enter_date_and_submit(live_server, assert_snapshot, page: Page):
10 | page.goto(f"{live_server.url}/components/date-input/")
11 | page.locator("input[id=id_date_0]").fill("25")
12 | page.locator("input[id=id_date_1]").fill("12")
13 | page.locator("input[id=id_date_2]").fill("2024")
14 | page.locator("button[id=id_submit]").click()
15 | assert_snapshot(page.screenshot(full_page=True))
16 |
17 |
18 | def test_date_input__leave_blank_and_submit(live_server, assert_snapshot, page: Page):
19 | page.goto(f"{live_server.url}/components/date-input/")
20 | page.locator("button[id=id_submit]").click()
21 | assert_snapshot(page.screenshot(full_page=True))
22 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/textarea/threshold.html:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
--------------------------------------------------------------------------------
/docs/components/inset.rst:
--------------------------------------------------------------------------------
1 | .. _Inset text: https://design-system.service.gov.uk/components/inset-text/
2 |
3 | ##########
4 | Inset text
5 | ##########
6 | An `Inset text`_ component is simply HTML. It's included as a form component
7 | so you can notes or side-comments to the body of a form. ::
8 |
9 | from django import forms
10 |
11 | from crispy_forms_gds.helper import FormHelper
12 | from crispy_forms_gds.layout import HTML, Layout
13 |
14 |
15 | class InsetForm(forms.Form):
16 | def __init__(self, *args, **kwargs):
17 | super(InsetForm, self).__init__(*args, **kwargs)
18 | self.helper = FormHelper()
19 | self.helper.layout = Layout(
20 | HTML.inset(
21 | "It can take up to 8 weeks to register a lasting power of "
22 | "attorney if there are no mistakes in the application."
23 | )
24 | )
25 |
26 | You can see this form live in the Demo site.
27 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/checkbox/validation_errors_aria_invalid.html:
--------------------------------------------------------------------------------
1 |
2 |
28 |
29 |
--------------------------------------------------------------------------------
/docs/components/footer.rst:
--------------------------------------------------------------------------------
1 | .. _Footer: https://design-system.service.gov.uk/components/footer/
2 |
3 | ######
4 | Footer
5 | ######
6 | There is a `Footer`_ component implemented in the ``gds/base.html`` template that
7 | is include in the template pack. It is wrapped with a block tag ``{% block footer %}``
8 | so if you extend the base template you can override it if you need to.
9 |
10 | Currently the footer only contains the meta information from the Design System site,
11 | which includes the licence terms and the crown copyright symbol. This is wrapped
12 | in a ``{% block footer__meta %}`` tag.
13 |
14 | .. note::
15 | The footer__meta block is specifically overridden in the Demo site as the
16 | author does not have permission to display the crown copyright symbol.
17 |
18 | There is also an empty block for site navigation, ``{% block footer__navigation %}``, but
19 | it is really only intended as a guide - the footer section has quite a bit of variation
20 | since the content is tied completely to the implementation of each site.
21 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/select/validation_errors_aria_invalid.html:
--------------------------------------------------------------------------------
1 |
2 |
27 |
28 |
--------------------------------------------------------------------------------
/tests/demo/test_text_input.py:
--------------------------------------------------------------------------------
1 | from playwright.sync_api import Page
2 |
3 |
4 | def test_text_input__layout(live_server, assert_snapshot, page: Page):
5 | page.goto(f"{live_server.url}/components/text-input/")
6 | assert_snapshot(page.screenshot(full_page=True))
7 |
8 |
9 | def test_text_input__fill_fields_and_submit(live_server, assert_snapshot, page: Page):
10 | page.goto(f"{live_server.url}/components/text-input/")
11 | page.locator("input[id=id_name]").fill("D. Veloper")
12 | page.locator("input[id=id_email]").fill("developer@crispy-forms-gds.org")
13 | page.locator("input[id=id_phone]").fill("555-2322")
14 | page.locator("input[id=id_password]").fill("password")
15 | page.locator("button[id=id_submit]").click()
16 | assert_snapshot(page.screenshot(full_page=True))
17 |
18 |
19 | def test_text_input__leave_blank_and_submit(live_server, assert_snapshot, page: Page):
20 | page.goto(f"{live_server.url}/components/text-input/")
21 | page.locator("button[id=id_submit]").click()
22 | assert_snapshot(page.screenshot(full_page=True))
23 |
--------------------------------------------------------------------------------
/tests/demo/test_radios_conditional.py:
--------------------------------------------------------------------------------
1 | from playwright.sync_api import Page
2 |
3 |
4 | def test_radios_conditional__layout(live_server, assert_snapshot, page: Page):
5 | page.goto(f"{live_server.url}/components/conditional_radios/")
6 | assert_snapshot(page.screenshot(full_page=True))
7 |
8 |
9 | def test_radios_conditional__fill_hidden_and_submit(
10 | live_server, assert_snapshot, page: Page
11 | ):
12 | page.goto(f"{live_server.url}/components/conditional_radios/")
13 | page.locator("input[id=id_method_1]").click()
14 | page.locator("input[id=id_email_address]").fill("developer@crispy-forms-gds.org")
15 | page.locator("button[id=id_submit]").click()
16 | assert_snapshot(page.screenshot(full_page=True))
17 |
18 |
19 | def test_radios_conditional__show_hidden_and_submit(
20 | live_server, assert_snapshot, page: Page
21 | ):
22 | page.goto(f"{live_server.url}/components/conditional_radios/")
23 | page.locator("input[id=id_method_1]").click()
24 | page.locator("button[id=id_submit]").click()
25 | assert_snapshot(page.screenshot(full_page=True))
26 |
--------------------------------------------------------------------------------
/demo/components/forms/__init__.py:
--------------------------------------------------------------------------------
1 | from .accordion import AccordionForm
2 | from .buttons import ButtonsForm
3 | from .checkboxes import CheckboxesForm
4 | from .conditional_radios import ConditionalRadiosForm
5 | from .date_input import DateInputForm
6 | from .details import DetailsForm
7 | from .fieldset import FieldsetForm
8 | from .file_upload import FileUploadForm
9 | from .inset import InsetForm
10 | from .panel import PanelForm
11 | from .radios import RadiosForm
12 | from .select import SelectForm
13 | from .tabs import TabsForm
14 | from .tag import TagForm
15 | from .text_input import TextInputForm
16 | from .textarea import TextareaForm
17 | from .warning import WarningForm
18 |
19 | __all__ = [
20 | "AccordionForm",
21 | "ButtonsForm",
22 | "CheckboxesForm",
23 | "ConditionalRadiosForm",
24 | "DateInputForm",
25 | "DetailsForm",
26 | "FieldsetForm",
27 | "FileUploadForm",
28 | "InsetForm",
29 | "PanelForm",
30 | "RadiosForm",
31 | "SelectForm",
32 | "TabsForm",
33 | "TagForm",
34 | "TextInputForm",
35 | "TextareaForm",
36 | "WarningForm",
37 | ]
38 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/templates/gds/layout/radios.html:
--------------------------------------------------------------------------------
1 | {% load l10n crispy_forms_gds %}
2 |
3 |
4 | {% if field.label %}
5 |
6 | {% if legend_tag %}<{{ legend_tag }} class="govuk-fieldset__heading">{% endif %}
7 | {{ field.label|safe }}
8 | {% if legend_tag %}{{ legend_tag }}>{% endif %}
9 |
10 | {% endif %}
11 |
12 | {% include 'gds/layout/help_text_and_errors.html' %}
13 |
14 | {% block radios %}
15 |
16 | {% for choice in field.field.choices %}
17 | {% include 'gds/layout/radio_item.html' with position=forloop.counter%}
18 | {% endfor %}
19 |
20 | {% endblock %}
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/templates/gds/layout/error_summary.html:
--------------------------------------------------------------------------------
1 | {% load i18n crispy_forms_gds %}
2 | {% if form.helper.form_show_errors and form.errors %}
3 |
4 |
5 | {% trans 'There is a problem' %}
6 |
7 |
8 |
9 | {% with first_id=form.visible_fields.0.auto_id %}
10 | {% for error in form.non_field_errors %}
11 | {{ error }}
12 | {% endfor %}
13 | {% endwith %}
14 | {% for field in form.visible_fields %}
15 | {% for field_id, errors in field|field_errors %}
16 | {% for error in errors %}
17 | {{ error }}
18 | {% endfor %}
19 | {% endfor %}
20 | {% endfor %}
21 |
22 |
23 |
24 | {% endif %}
25 |
--------------------------------------------------------------------------------
/docs/components/select.rst:
--------------------------------------------------------------------------------
1 | .. _Select: https://design-system.service.gov.uk/components/select/
2 |
3 | ######
4 | Select
5 | ######
6 | The Design System recommends using a `Select`_ component only as a last resort
7 | for public facing services as there is often a high rate of errors. ::
8 |
9 | from django import forms
10 |
11 | from crispy_forms_gds.helper import FormHelper
12 | from crispy_forms_gds.layout import Layout, Submit
13 |
14 |
15 | class SelectForm(forms.Form):
16 |
17 | sort_by = forms.ChoiceField(
18 | choices=(
19 | ("published", "Recently published"),
20 | ("updated", "Recently updated"),
21 | ("views", "Most views"),
22 | ("comments", "Most comments"),
23 | ),
24 | label="Sort by",
25 | )
26 |
27 | def __init__(self, *args, **kwargs):
28 | super(SelectForm, self).__init__(*args, **kwargs)
29 | self.helper = FormHelper()
30 | self.helper.layout = Layout("sort_by", Submit("submit", "Submit"),)
31 |
32 | You can see this form live in the Demo site.
33 |
--------------------------------------------------------------------------------
/src/crispy_forms_gds/templates/gds/layout/radio_item.html:
--------------------------------------------------------------------------------
1 | {% load l10n %}
2 |
3 |
17 |
18 |
19 | {{ choice.1|unlocalize }}
20 |
21 | {% if choice.hint %}
22 |
23 | {{ choice.hint }}
24 |
25 | {% endif %}
26 |
27 | {% if choice.divider %}
28 | {{ choice.divider }}
29 | {% endif %}
30 | {% block additional_content %}{% endblock %}
31 |
--------------------------------------------------------------------------------
/demo/components/forms/file_upload.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.urls import reverse
3 |
4 | from crispy_forms_gds.helper import FormHelper
5 | from crispy_forms_gds.layout import HTML, Button, Layout
6 |
7 |
8 | class FileUploadForm(forms.Form):
9 | file = forms.FileField(
10 | label="Upload a file",
11 | help_text="Select the CSV file to upload.",
12 | error_messages={
13 | "required": "Choose the CSV file you exported from the spreadsheet"
14 | },
15 | )
16 |
17 | def __init__(self, *args, **kwargs):
18 | super().__init__(*args, **kwargs)
19 | self.helper = FormHelper()
20 | self.helper.layout = Layout("file", Button("submit", "Submit"))
21 |
22 | def valid_layout(self):
23 | file = self.cleaned_data["file"]
24 | self.helper.layout = Layout(
25 | HTML.h2("You uploaded..."),
26 | HTML.p("File: %s" % file),
27 | HTML(
28 | 'Continue '
29 | % reverse("components:name", kwargs={"name": "file-upload"})
30 | ),
31 | )
32 |
--------------------------------------------------------------------------------
/docs/components/file.rst:
--------------------------------------------------------------------------------
1 | .. _File upload: https://design-system.service.gov.uk/components/file-upload/
2 |
3 | ###########
4 | File upload
5 | ###########
6 | A `File upload`_ component helps users select and upload a file. ::
7 |
8 | from django import forms
9 |
10 | from crispy_forms_gds.helper import FormHelper
11 | from crispy_forms_gds.layout import HTML, Layout, Submit
12 |
13 |
14 | class FileUploadForm(forms.Form):
15 |
16 | file = forms.FileField(
17 | label="Upload a file",
18 | help_text="Select the CSV file to upload.",
19 | error_messages={
20 | "required": "Choose the CSV file you exported from the spreadsheet"
21 | },
22 | )
23 |
24 | def __init__(self, *args, **kwargs):
25 | super(FileUploadForm, self).__init__(*args, **kwargs)
26 | self.helper = FormHelper()
27 | self.helper.layout = Layout("file", Submit("submit", "Submit"))
28 |
29 | You can see this form live in the Demo site.
30 |
31 | The Design System adds some styling but other than that file upload is a
32 | vanilla element.
33 |
--------------------------------------------------------------------------------
/docs/components/header.rst:
--------------------------------------------------------------------------------
1 | .. _Header: https://design-system.service.gov.uk/components/header/
2 |
3 | ######
4 | Header
5 | ######
6 | There is a `Header`_ component implemented in the ``gds/base.html`` template that
7 | is include in the template pack. It is wrapped with a block tag, ``{% block hjeader %}``
8 | so if you extend the base template you can override it if you need to.
9 |
10 | The block implements the "Service name with navigation" variation of the different headers shown
11 | on the Design System page. There are two main daughter blocks ``{% block header__logo %}``
12 | for displaying the crown logo and "GOV.UK" and ``{% block header__service %}``
13 |
14 | .. note::
15 | The header__logo block is specifically overridden in the Demo site as the
16 | author does not have permission to display the crown logo.
17 |
18 | Within ``{% block header__service %}`` there are three further blocks: ``{% block header__service__url %}``
19 | and ``{% block header__service__name %}`` where you can set the URL, and name for your site
20 | and ``{% block header__service__navigation %}`` where you can set navigation links for
21 | your site. This block is left empty for now.
22 |
--------------------------------------------------------------------------------
/tests/unit/choices/test_choice.py:
--------------------------------------------------------------------------------
1 | from crispy_forms_gds.choices import Choice
2 |
3 |
4 | def test_set_attribute():
5 | """Verify an attribute can be set on a Choice after creation."""
6 | item = Choice("email", "Email")
7 | item.any = "An attribute"
8 | assert item.any == "An attribute"
9 |
10 |
11 | def test_item_index():
12 | """Verify ChoiceItem supports indexing."""
13 | item = Choice("email", "Email", hint="Your email address")
14 | assert item[0] == "email"
15 | assert item[1] == "Email"
16 |
17 |
18 | def test_iterator_unpacking():
19 | """Verify iterating over and unpacking a Choice returns the key and value."""
20 | choices = (Choice("email", "Email", hint="Your email address"),)
21 | for k, v in choices:
22 | assert k == "email"
23 | assert v == "Email"
24 |
25 |
26 | def test_iterator_object():
27 | """Verify iterating over a Choice returns a ChoiceItem object."""
28 | choices = (
29 | Choice("email", "Email", hint="Your email address"),
30 | Choice("text", "Text", hint="Your mobile phone number"),
31 | )
32 | for item in choices:
33 | assert isinstance(item, Choice)
34 |
--------------------------------------------------------------------------------
/tests/unit/helpers/results/error_summary.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | There is a problem
4 |
5 |
11 |
12 |
13 |
26 |
--------------------------------------------------------------------------------
/LICENCE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2024-2025 Stuart MacKay
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 |
2 | Crispy Forms GDS
3 | ================
4 |
5 | This is a `Django`_ application to add a template pack to `django-crispy-forms`_
6 | for the `GOV.UK Design System`_.
7 |
8 | .. _Django: https://www.djangoproject.com/
9 | .. _django-crispy-forms: https://github.com/maraujop/django-crispy-forms
10 | .. _GOV.UK Design System: https://design-system.service.gov.uk/
11 |
12 |
13 | .. toctree::
14 | :caption: Get started
15 | :titlesonly:
16 |
17 | start/install
18 | start/tutorial
19 | start/demo
20 |
21 | .. toctree::
22 | :caption: Components
23 | :titlesonly:
24 |
25 | components/index
26 |
27 | .. toctree::
28 | :caption: Reference
29 | :titlesonly:
30 |
31 | reference/constants
32 | reference/form/index
33 | reference/layout/index
34 | reference/settings
35 | reference/templates
36 | reference/templatetags
37 | reference/filters
38 |
39 | .. toctree::
40 | :caption: Development
41 | :titlesonly:
42 |
43 | development
44 |
45 | You can find a detailed history of the project in the `CHANGELOG`_ on GitHub.
46 |
47 | .. _CHANGELOG: https://github.com/StuartMacKay/crispy-forms-gds/blob/master/CHANGELOG.rst
48 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/accordion/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
15 |
First section contents.
16 |
17 |
18 |
19 |
26 |
27 |
Second section contents.
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/demo/components/forms/fieldset.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 |
3 | from crispy_forms_gds.helper import FormHelper
4 | from crispy_forms_gds.layout import Field, Fieldset, Fluid, Layout, Size
5 |
6 |
7 | class FieldsetForm(forms.Form):
8 | street1 = forms.CharField(label="Building and street")
9 | street2 = forms.CharField(label="")
10 | city = forms.CharField(label="Town or city")
11 | county = forms.CharField(label="County")
12 | postcode = forms.CharField(label="Post code")
13 |
14 | def __init__(self, *args, **kwargs):
15 | super().__init__(*args, **kwargs)
16 | self.helper = FormHelper()
17 | self.helper.layout = Layout(
18 | Fieldset(
19 | Field.text("street1", field_width=Fluid.THREE_QUARTERS),
20 | Field.text("street2", field_width=Fluid.THREE_QUARTERS),
21 | Field.text("city", field_width=Fluid.ONE_HALF),
22 | Field.text("county", field_width=Fluid.ONE_HALF),
23 | Field.text("postcode", field_width=Fluid.ONE_QUARTER),
24 | legend="What is your address?",
25 | legend_size=Size.LARGE,
26 | legend_tag="h1",
27 | )
28 | )
29 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/radios/initial_overlapping.html:
--------------------------------------------------------------------------------
1 |
2 |
35 |
36 |
--------------------------------------------------------------------------------
/tests/demo/test_textarea.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from playwright.sync_api import Page
3 |
4 |
5 | def test_textarea__layout(live_server, assert_snapshot, page: Page):
6 | page.goto(f"{live_server.url}/components/textarea/")
7 | assert_snapshot(page.screenshot(full_page=True))
8 |
9 |
10 | @pytest.mark.skip(reason="This text 'pastes' the text so the word count is not updated")
11 | def test_textarea__word_count(live_server, assert_snapshot, page: Page):
12 | page.goto(f"{live_server.url}/components/textarea/")
13 | page.locator("textarea[id=id_description]").fill("This is the time for...")
14 | assert_snapshot(page.screenshot(full_page=True))
15 |
16 |
17 | def test_textarea__fill_and_submit(live_server, assert_snapshot, page: Page):
18 | page.goto(f"{live_server.url}/components/textarea/")
19 | page.locator("textarea[id=id_description]").fill("This is the time for...")
20 | page.locator("button[id=id_submit]").click()
21 | assert_snapshot(page.screenshot(full_page=True))
22 |
23 |
24 | def test_textarea__leave_blank_and_submit(live_server, assert_snapshot, page: Page):
25 | page.goto(f"{live_server.url}/components/textarea/")
26 | page.locator("button[id=id_submit]").click()
27 | assert_snapshot(page.screenshot(full_page=True))
28 |
--------------------------------------------------------------------------------
/docs/components/warning.rst:
--------------------------------------------------------------------------------
1 | .. _Warning text: https://design-system.service.gov.uk/components/warning-text/
2 |
3 | ############
4 | Warning text
5 | ############
6 | A `Warning text`_ component is simply HTML. It's included as a form component as
7 | warnings are useful for frightening your users into filling out the form
8 | correctly. ::
9 |
10 | from django import forms
11 |
12 | from crispy_forms_gds.helper import FormHelper
13 | from crispy_forms_gds.layout import HTML, Layout
14 |
15 |
16 | class WarningForm(forms.Form):
17 | def __init__(self, *args, **kwargs):
18 | super(WarningForm, self).__init__(*args, **kwargs)
19 | self.helper = FormHelper()
20 | self.helper.layout = Layout(
21 | HTML.warning(
22 | "You can be fined up to £5,000 if you do not like this template pack."
23 | )
24 | )
25 |
26 | You can see this form live in the Demo site.
27 |
28 | The styling and layout for a warning is not flexible. You could the symbol but
29 | the styling is fixed so the symbol's background is black. Since a Warning component
30 | is just a snippet of HTML it is much easier simply to clone the styling and generate
31 | your own HTML if you need something different.
32 |
--------------------------------------------------------------------------------
/demo/components/forms/select.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 |
3 | from crispy_forms_gds.helper import FormHelper
4 | from crispy_forms_gds.layout import HTML, Button, Hidden, Layout
5 |
6 |
7 | class SelectForm(forms.Form):
8 | sort_by = forms.ChoiceField(
9 | choices=(
10 | ("published", "Recently published"),
11 | ("updated", "Recently updated"),
12 | ("views", "Most views"),
13 | ("comments", "Most comments"),
14 | ),
15 | label="Sort by",
16 | )
17 |
18 | def __init__(self, *args, **kwargs):
19 | super().__init__(*args, **kwargs)
20 | self.helper = FormHelper()
21 | self.helper.layout = Layout(
22 | "sort_by",
23 | Button("submit", "Submit"),
24 | )
25 |
26 | def get_choice(self, field):
27 | value = self.cleaned_data[field]
28 | return dict(self.fields[field].choices).get(value)
29 |
30 | def valid_layout(self):
31 | value = self.cleaned_data["sort_by"]
32 | self.helper.layout = Layout(
33 | Hidden("sort_by", value),
34 | HTML.h2("You answered..."),
35 | HTML.table(None, [("Sort by:", self.get_choice("sort_by"))]),
36 | Button("continue", "Continue"),
37 | )
38 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/checkboxes/initial_overlapping.html:
--------------------------------------------------------------------------------
1 |
2 |
35 |
36 |
--------------------------------------------------------------------------------
/docs/start/install.rst:
--------------------------------------------------------------------------------
1 | .. _install-intro:
2 |
3 | ============
4 | Installation
5 | ============
6 |
7 | #. Install ``django-crispy-forms`` using their `installation guide`__.
8 |
9 | .. __: https://django-crispy-forms.readthedocs.io/en/latest/install.html
10 |
11 |
12 | #. Install ``crispy-forms-gds`` from PyPI::
13 |
14 | pip install crispy-forms-gds
15 |
16 |
17 | #. Add the app to your settings::
18 |
19 | INSTALLED_APPS = (
20 | ...
21 | 'crispy_forms_gds',
22 | ...
23 |
24 | )
25 |
26 | #. Override the crispy forms settings to set the template pack as the default::
27 |
28 | CRISPY_ALLOWED_TEMPLATE_PACKS = (
29 | "bootstrap", "bootstrap3", "bootstrap4", "uni_form", "gds"
30 | )
31 | CRISPY_TEMPLATE_PACK = "gds"
32 |
33 | #. Also in your settings.py, set the version of the govuk frontend you are using::
34 |
35 | CRISPY_GDS_FRONTEND_VERSION = "5.0.0"
36 |
37 | #. Install the GDS assets using their `Getting started guide`_ and `production
38 | installation instructions`_.
39 |
40 | .. _Getting started guide: https://design-system.service.gov.uk/get-started/
41 | .. _production installation instructions: https://design-system.service.gov.uk/get-started/production/
42 |
--------------------------------------------------------------------------------
/tests/unit/helpers/results/error_summary_aria_invalid.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | There is a problem
4 |
5 |
11 |
12 |
13 |
31 |
--------------------------------------------------------------------------------
/demo/components/forms/textarea.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 |
3 | from crispy_forms_gds.helper import FormHelper
4 | from crispy_forms_gds.layout import HTML, Button, Field, Hidden, Layout
5 |
6 |
7 | class TextareaForm(forms.Form):
8 | description = forms.CharField(
9 | label="Can you provide more detail?",
10 | widget=forms.Textarea,
11 | help_text="Do not include personal or financial information, like your "
12 | "National Insurance number or credit card details.",
13 | error_messages={"required": "Enter a short description of your application"},
14 | )
15 |
16 | def __init__(self, *args, **kwargs):
17 | super().__init__(*args, **kwargs)
18 | self.helper = FormHelper()
19 | self.helper.layout = Layout(
20 | Field.textarea(
21 | "description",
22 | rows=3,
23 | max_words=100,
24 | ),
25 | Button("submit", "Submit"),
26 | )
27 |
28 | def valid_layout(self):
29 | value = self.cleaned_data["description"]
30 | self.helper.layout = Layout(
31 | Hidden("description", value),
32 | HTML.h2("You answered..."),
33 | HTML.p(value),
34 | Button("continue", "Continue"),
35 | )
36 |
--------------------------------------------------------------------------------
/demo/components/forms/tag.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.utils.safestring import mark_safe
3 |
4 | from crispy_forms_gds.helper import FormHelper
5 | from crispy_forms_gds.layout import HTML, Colour, Layout
6 |
7 |
8 | class TagForm(forms.Form):
9 | def __init__(self, *args, **kwargs):
10 | super().__init__(*args, **kwargs)
11 |
12 | headings = ["Name of user", "status"]
13 | statuses = [
14 | ["Rachel Silver", mark_safe(HTML.tag("Pending", Colour.BLUE).html)],
15 | ["Jesse Smith", mark_safe(HTML.tag("Inactive", Colour.PURPLE).html)],
16 | ["Joshua Wessel ", mark_safe(HTML.tag("Active", Colour.GREEN).html)],
17 | ["Tim Harvey", mark_safe(HTML.tag("Blocked", Colour.RED).html)],
18 | ["Rachael Pepper", mark_safe(HTML.tag("Disabled", Colour.GREY).html)],
19 | ["Stuart Say", mark_safe(HTML.tag("Declined", Colour.ORANGE).html)],
20 | ["Laura Frith", mark_safe(HTML.tag("Waiting", Colour.PINK).html)],
21 | ["Emma Tennant", mark_safe(HTML.tag("New", Colour.TURQUOISE).html)],
22 | ["Nigel Starmer", mark_safe(HTML.tag("Delayed", Colour.YELLOW).html)],
23 | ]
24 |
25 | self.helper = FormHelper()
26 | self.helper.layout = Layout(HTML.table(headings, statuses))
27 |
--------------------------------------------------------------------------------
/demo/components/forms/accordion.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 |
3 | from crispy_forms_gds.helper import FormHelper
4 | from crispy_forms_gds.layout import HTML, Accordion, AccordionSection, Layout
5 |
6 |
7 | class AccordionForm(forms.Form):
8 | def __init__(self, *args, **kwargs):
9 | super().__init__(*args, **kwargs)
10 | self.helper = FormHelper()
11 | self.helper.layout = Layout(
12 | Accordion(
13 | AccordionSection(
14 | "Writing well for the web",
15 | HTML.p("This is the content for Writing well for the web."),
16 | summary="An introduction to clear and concise writing.",
17 | ),
18 | AccordionSection(
19 | "Writing well for specialists",
20 | HTML.p("This is the content for Writing well for specialists."),
21 | ),
22 | AccordionSection(
23 | "Know your audience",
24 | HTML.p("This is the content for Know your audience."),
25 | ),
26 | AccordionSection(
27 | "How people read",
28 | HTML.p("This is the content for How people read."),
29 | ),
30 | )
31 | )
32 |
--------------------------------------------------------------------------------
/docs/_static/css/gds.css:
--------------------------------------------------------------------------------
1 | .colour-blue { display: inline-block; font-weight: bold; color: #144e81; background-color: #d2e2f1; padding: 4px; }
2 | .colour-green { display: inline-block; font-weight: bold; color: #005a30; background-color: #cce2d8; padding: 4px; }
3 | .colour-grey { display: inline-block; font-weight: bold; color: #454a4d; background-color: #eff0f1; padding: 4px; }
4 | .colour-orange { display: inline-block; font-weight: bold; color: #6e3619; background-color: #fcd6c3; padding: 4px; }
5 | .colour-pink { display: inline-block; font-weight: bold; color: #80224d; background-color: #f7d7e6; padding: 4px; }
6 | .colour-purple { display: inline-block; font-weight: bold; color: #3d2375; background-color: #dbd5e9; padding: 4px; }
7 | .colour-red { display: inline-block; font-weight: bold; color: #942514; background-color: #f6d7d2; padding: 4px; }
8 | .colour-turquoise { display: inline-block; font-weight: bold; color: #10403c; background-color: #bfe3e0; padding: 4px; }
9 | .colour-yellow { display: inline-block; font-weight: bold; color: #594d00; background-color: #fff7bf; padding: 4px; }
10 | .font-small { font-weight: 700; font-size: 1rem; }
11 | .font-medium { font-weight: 700; font-size: 1.125rem; }
12 | .font-large { font-weight: 700; font-size: 1.5rem; }
13 | .font-extra-large { font-weight: 700; font-size: 2rem; }
14 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/radios/no_help_text.html:
--------------------------------------------------------------------------------
1 |
2 |
36 |
37 |
--------------------------------------------------------------------------------
/tests/unit/layout/results/radios/no_legend.html:
--------------------------------------------------------------------------------
1 |
2 |
36 |
37 |
--------------------------------------------------------------------------------