<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>David Smith's Blog</title><link href="http://smithdc.uk/blog/" rel="alternate"/><link href="http://smithdc.uk/blog/feed" rel="self"/><id>http://smithdc.uk/blog/</id><updated>2026-08-16T20:44:19.817047+00:00</updated><entry><title>Customising your Python REPL</title><link href="http://smithdc.uk/blog/2024/customising_your_python_repl/" rel="alternate"/><id>http://smithdc.uk/blog/2024/customising_your_python_repl/</id><summary type="html">This blog post will show you how to customise your Python REPL by running a
Python file before the interactive prompt is loaded. This is achieved using the 
`PYTHONSTARTUP` environment variable.
&lt;p&gt;This blog post will show you how to customise your Python REPL by running a
Python file before the interactive prompt is loaded. This is achieved using the 
&lt;a href="https://docs.python.org/3/using/cmdline.html#envvar-PYTHONSTARTUP"&gt;&lt;code&gt;PYTHONSTARTUP&lt;/code&gt;&lt;/a&gt; environment variable.&lt;/p&gt;
&lt;h2&gt;Initial set up.&lt;/h2&gt;
&lt;p&gt;By way of an initial example, you may wish to always have the &lt;code&gt;pprint&lt;/code&gt; function available
when the REPL loads.&lt;/p&gt;
&lt;p&gt;To achieve this we must first have a python file that will be run before the REPL loads.&lt;/p&gt;
&lt;p&gt;Create a file:&lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;touch&lt;span class="w"&gt; &lt;/span&gt;.pythonstartup.py
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Populate file with code to import &lt;code&gt;pprint&lt;/code&gt;:&lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# import pprint&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pprint&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pprint&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And finally, set the &lt;code&gt;PYTHONSTARTUP&lt;/code&gt; environment variable to point at our Python file:&lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;PYTHONSTARTUP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/.pythonstartup.py
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To test if this is working, launch a Python REPL and check the function is available. &lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;pprint&lt;/span&gt;
&lt;span class="go"&gt;&amp;lt;function pprint at 0x7f4c0e9ebb00&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Alternative shell&lt;/h2&gt;
&lt;p&gt;Alternative shells are available such as &lt;a href="https://ipython.org"&gt;IPython&lt;/a&gt; or &lt;a href="https://github.com/prompt-toolkit/ptpython"&gt;Ptpython&lt;/a&gt;. While these
can be launched via their own commands you could also use your &lt;code&gt;PYTHONSTARTUP&lt;/code&gt; 
script to try to launch them as your default shell. &lt;/p&gt;
&lt;p&gt;We'll use Ptpython as an example. The &lt;code&gt;.pythonstartup.py&lt;/code&gt; file becomes:&lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pprint&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pprint&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;ptpython.repl&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;embed&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;ImportError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ptpython is not available: falling back to standard prompt&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;globals&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;locals&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;By passing &lt;code&gt;globals()&lt;/code&gt; and &lt;code&gt;locals()&lt;/code&gt; to &lt;code&gt;embed()&lt;/code&gt; this allows the imported &lt;code&gt;pprint()&lt;/code&gt; 
function to be available in the new shell.&lt;/p&gt;
&lt;p&gt;If you were using IPython this could be achieved by using &lt;a href="https://ipython.org/ipython-doc/3/api/generated/IPython.html#IPython.start_ipython"&gt;&lt;code&gt;start_ipython()&lt;/code&gt;&lt;/a&gt; 
and using the &lt;code&gt;user_ns&lt;/code&gt; parameter to initalise the IPython namespace. &lt;/p&gt;
&lt;h2&gt;Django shell&lt;/h2&gt;
&lt;p&gt;The django-admin CLI has a &lt;a href="https://docs.djangoproject.com/en/5.0/ref/django-admin/#shell"&gt;&lt;code&gt;shell&lt;/code&gt;&lt;/a&gt; command. &lt;/p&gt;
&lt;p&gt;This has an &lt;code&gt;--interface&lt;/code&gt; option which can be used to launch the shell with different shells (ipython, bpython, python).&lt;/p&gt;
&lt;p&gt;Using the script above the &lt;code&gt;PYTHONSTARTUP&lt;/code&gt; the Ptpython shell will be launched by default. 
If you wished to instead use the default Python shell this could be achieved by specifying the Python shell and using the &lt;code&gt;--no-startup&lt;/code&gt; option:&lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;django-admin&lt;span class="w"&gt; &lt;/span&gt;shell&lt;span class="w"&gt; &lt;/span&gt;--interface&lt;span class="w"&gt; &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;--no-startup
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Using a custom &lt;code&gt;PYTHONSTARTUP&lt;/code&gt; script can be used to import frequently used 
functions so they are always available and allow use of your favourite 
Python shell with Django even if it doesn't have a built-in option.&lt;/p&gt;</summary></entry><entry><title>Generational Sustainability</title><link href="http://smithdc.uk/blog/2024/generational_sustainability/" rel="alternate"/><id>http://smithdc.uk/blog/2024/generational_sustainability/</id><summary type="html">In community-led organizations, I believe that for long-term sustainability, we need to do more to encourage younger folks to become involved and take on leadership positions. Younger individuals won’t break a community, but not encouraging them may well do so.
&lt;p&gt;In community-led organizations, I believe that for long-term sustainability, we need to do more to encourage younger folks to become involved and take on leadership positions. Younger individuals won’t break a community, but not encouraging them may well do so.&lt;/p&gt;
&lt;p&gt;A story:&lt;/p&gt;
&lt;p&gt;As a young person, I was a member of a youth group. I thoroughly enjoyed my time there, meeting new friends, learning new skills and participating in exciting activities. Looking back now, I realize that I learned a lot through these experiences.&lt;/p&gt;
&lt;p&gt;As the years passed, I started volunteering as a young leader, giving back to the community by working alongside the adults who were leaders when I was a youth member. During this time, I completed the formal training for the organisation. Having been involved with the organization for about 15 years, most of it came naturally. However, it provided me with the opportunity to learn new things. I encountered ideas and concepts that were new to me, and I realised that young people can bring fresh perspectives and enthusiasm.&lt;/p&gt;
&lt;p&gt;Around this time, the organization recognized they had a problem. Many of the leaders were aging; many had been in their roles for 20+ years, some 40+. The organisation needs new young leaders to ensure long-term sustainability, they said. Everyone nodded in agreement.&lt;/p&gt;
&lt;p&gt;I continued volunteering in the same role for the next decade or so. Life went on — education, finding employment, and so on. The enthusiasm for volunteering became more challenging as I had less time. Those in senior positions had accumulated many years of experience. While they were always grateful for the help enacting change within the organisation is challenging (committee meetings were particularly tiresome). Around the same time, I had my first child and used this as my excuse to step back from volunteering.&lt;/p&gt;
&lt;p&gt;A decade passed.&lt;/p&gt;
&lt;p&gt;My children are now members of youth organisations. The folk in senior positions are the same people that were leaders when I was a youth member (Yes, I'm old). This is not an example of a single group within this organisation or even specific to a single organisation. I see it across groups and organisations in my local area. &lt;/p&gt;
&lt;p&gt;It's evident that transition needs to happen to a younger generation. Preferably sooner rather than later. There's already consequences being seen of this not happening. &lt;/p&gt;
&lt;p&gt;The pandemic resulted in groups closing, some did not reopen. A youth movement needs to engage people at a young age, so they keep on being members for years to come. Attracting members at an older age is challenging. Therefore, due to this one section closing the sections aimed at older ages have also closed as they saw member numbers fall away.&lt;/p&gt;
&lt;p&gt;I wonder if younger folk had been encouraged and given the opportunity to take on senior leadership positions this would not have happened. Those with experience can still be there to support and coach, but not everything needs to be on their shoulders. &lt;/p&gt;
&lt;p&gt;For long term sustainability we need to encourage and support the next generation to take on senior positions. Older folk, I'm not saying that younger folk don't want you about any more, they value and need your experience and guidance. But everything you have built and contributed to may fade away if the next generation doesn't, at some point, step up and take the lead.&lt;/p&gt;</summary></entry><entry><title>How to find a ticket on Django's trac</title><link href="http://smithdc.uk/blog/2023/how_to_find_a_ticket_on_django_trac/" rel="alternate"/><id>http://smithdc.uk/blog/2023/how_to_find_a_ticket_on_django_trac/</id><summary type="html">This is my guide on how I go about finding a ticket on Django's trac to work on.
&lt;h2&gt;Django's issue tracker&lt;/h2&gt;
&lt;p&gt;Django's issue tracker is hosted at &lt;a href="https://code.djangoproject.com/"&gt;code.djangoproject.com&lt;/a&gt;. This contains the entire history of ticket's opened for Django. At time of writing this is some 18 years ago! This is a valuable resource when working on Django as you are able to research previous decisions. &lt;/p&gt;
&lt;p&gt;From the summary page clicking &lt;a href="https://code.djangoproject.com/query"&gt;"View Tickets"&lt;/a&gt; will show a listing of Django's tickets which are not closed. As of November 2023 Django currently has 996 open tickets. This is alot! Luckily, Trac (the software which Django's issue tracker uses) has a number of filtering options to help this down.&lt;/p&gt;
&lt;h2&gt;Filter to accepted tickets&lt;/h2&gt;
&lt;p&gt;Django's triaging process means that not all open tickets are currently accepted. Some may be marked to be reviewed in the future, for example a ticket may be proposed to use a feature in a new release of Python. However, this will only be possible to achieve in the future once that feature is in all versions of Python which Django currently supports. &lt;/p&gt;
&lt;p&gt;To do this click on the filter category box and select triage stage:&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://s3.us-east-005.backblazeb2.com/BlogBucket/markdownx/93613e30-e8b3-4f4e-bfac-e0c9733522aa.png"&gt;&lt;/p&gt;
&lt;p&gt;This option will then appear in the filter box. Then select "Accepted" from the choice of options. You'll need to click the "Update" button to apply this filter selection. &lt;/p&gt;
&lt;h2&gt;Filter by component&lt;/h2&gt;
&lt;p&gt;This'll bring the number of tickets down to ~930 tickets. Better, but we're not there yet. &lt;/p&gt;
&lt;p&gt;The next filter I reach for is component. This is used to categorise tickets into which area of Django they relate to. Here's a chart which shows the number of tickets by component.&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://s3.us-east-005.backblazeb2.com/BlogBucket/markdownx/b3e7ab5f-d6bc-4d01-bfc2-5a9d25bc19f0.png"&gt;&lt;/p&gt;
&lt;p&gt;This chart shows around 40% of tickets are for Django's ORM or Admin. Choosing any other component than these means the tickets will be reduced to a more manageable number. At this point I suggest reviewing the list of components and trying to find one that you're interested in. &lt;/p&gt;
&lt;h2&gt;Ticket Investigation&lt;/h2&gt;
&lt;p&gt;Say you chose the "Template System" as the component. This reduces the number of tickets down to just 24. This is a more manageable number to have a more detailed review of. At this point I would suggest starting to look through tickets. In particular:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ignore tickets which are actively being worked on. I'd judge actively as meaning anything in the last ~3 months. &lt;/li&gt;
&lt;li&gt;Review the tickets for descriptions of things I have any knowledge on.&lt;/li&gt;
&lt;li&gt;Bear in mind that tickets with higher numbers are more recent. It's likely that ticket #34981 (Opened Nov 2023), is likely easier to solve than #373 (Opened 18 Years ago). &lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Standing on the shoulders of giants&lt;/h2&gt;
&lt;p&gt;At this point you'll start to get a feeling of the types of tickets there are to be tackled. Clicking on the description will take you to the ticket page giving a summary of the issue and the previous discussion that's taken place. &lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://s3.us-east-005.backblazeb2.com/BlogBucket/markdownx/a4a54513-9859-4c9a-a446-a5773137a942.png"&gt;&lt;/p&gt;
&lt;p&gt;My tip here is to look for tickets that have entries against "Pull Requests". There are many tickets which have had previous Pull Requests opened and reviewed. Some of these may just need updating for review comments, additional documentation or adding additional test coverage.&lt;/p&gt;
&lt;p&gt;Finding something where previous work has been conducted can ease your contribution as you can build upon previous work. The task may be less daunting than a ticket where it may be slightly unclear what the required bug or fix is. &lt;/p&gt;
&lt;h2&gt;Assign a ticket to yourself&lt;/h2&gt;
&lt;p&gt;At this point you are very knowledgeable about a certain ticket or category of tickets. Once you have selected a ticket, do assign it to yourself. To do this you'll need to register / log in and complete the "Action" form at the bottom of the page. &lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://s3.us-east-005.backblazeb2.com/BlogBucket/markdownx/6cf4b831-f2ca-4a6c-bc5f-0a82c08712b6.png"&gt;&lt;/p&gt;
&lt;h2&gt;Next Steps&lt;/h2&gt;
&lt;p&gt;I'd start with referring to Django's contribution guide in the &lt;a href="https://docs.djangoproject.com/en/4.2/internals/contributing/"&gt;documentation&lt;/a&gt;. In addition Django's &lt;a href="https://forum.djangoproject.com/c/internals/5"&gt;forum&lt;/a&gt; and Discord (see invite link on the &lt;a href="https://www.djangoproject.com/community/"&gt;community page&lt;/a&gt;) are channels where there are folk who will be willing to help you. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How to ask for help&lt;/strong&gt; &lt;/p&gt;
&lt;p&gt;Something that took me a long time to learn is that other folk won't know the answer, or even be familiar with an issue. You will be the expert.  What is helpful is if you can summarise the issue and your thoughts on what is required. Specific questions are more likely to get a response than general ones. General questions about tickets are tricky to answer as it's highly likely no one yet knows the answer, otherwise it would have been fixed all ready!&lt;/p&gt;
&lt;p&gt;Good luck in your contribution to Django 💚&lt;/p&gt;</summary></entry><entry><title>Building a Bootstrap styled form in vanilla Django.</title><link href="http://smithdc.uk/blog/2023/bootstrap_form_in_vanilla_django/" rel="alternate"/><id>http://smithdc.uk/blog/2023/bootstrap_form_in_vanilla_django/</id><summary type="html">A walk through of building a bootstrap 5 styled form using only Vanilla Django. Exploring per-project, per-form and per-field customisations.
&lt;p&gt;I &lt;a href="https://fosstodon.org/@davidsmith/111358822195170895"&gt;tooted&lt;/a&gt; a little while back about how using 
&lt;code&gt;django-crispy-forms&lt;/code&gt; and it's &lt;code&gt;Layout&lt;/code&gt; class could ease writing forms. It showed how a minimal &lt;code&gt;Layout&lt;/code&gt;
could result in a nice looking form with some complex layouts such as two input boxes on the same row.&lt;/p&gt;
&lt;p&gt;Here's the content of that toot. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;django-crispy-forms layout:&lt;/strong&gt;
&lt;img alt="" src="https://s3.us-east-005.backblazeb2.com/BlogBucket/markdownx/8a5c115d-c642-4d94-9001-747fa3bd43a8.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Resulting form:&lt;/strong&gt;
&lt;img alt="" src="https://s3.us-east-005.backblazeb2.com/BlogBucket/markdownx/6f142df5-c3f3-4a28-b63f-0154e43386b7.png"&gt;&lt;/p&gt;
&lt;p&gt;I followed this up with a bit of a personal challenge. What would it take to recreate this form using
vanilla Django. No 3rd party packages allowed. &lt;/p&gt;
&lt;p&gt;This post follows my thought process as I worked through this challenge and highlights the challenges I 
faced. I'd love to hear feedback on these, what am I doing wrong and if there anything here that Django
itself could improve.&lt;/p&gt;
&lt;p&gt;Let's go!&lt;/p&gt;
&lt;h2&gt;Create the base form&lt;/h2&gt;
&lt;p&gt;The first step was to create a test project, I used a
&lt;a href="https://noumenal.es/notes/django/single-folder-layout/"&gt;single project folder layout&lt;/a&gt; putting everything in the &lt;code&gt;urls.py&lt;/code&gt;
file. Here's the relevant section that shows the base form. I'm also using 
&lt;a href="https://docs.djangoproject.com/en/dev/releases/5.0/"&gt;Django 5.0&lt;/a&gt; (a few weeks away from release at time of writing) as it includes a number of new features that I'll be
making use of. &lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;forms&lt;/span&gt;


&lt;span class="c1"&gt;# 🎉 Django 5.0 allows options to be defined as a mapping. 🎉&lt;/span&gt;
&lt;span class="n"&gt;days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Monday&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Tuesday&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Wednesday&amp;quot;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BootstrapForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Form&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;forms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CharField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;forms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CharField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;appointment_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;forms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChoiceField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;forms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RadioSelect&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;help_text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Select which day you would like your appointment. We&amp;#39;re only open Monday-Wednesday.&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;attachment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;forms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FileField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;help_text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Please provide any files you wish us to review prior to your appointment.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I added a view that returns a &lt;code&gt;TemplateResponse&lt;/code&gt;. The template used contains the Bootstrap CSS and JavaScript and renders the form using &lt;code&gt;{{ form }}&lt;/code&gt; along with some input buttons. Rendering the whole form using &lt;code&gt;{{ form }}&lt;/code&gt; is a key aim here. Here's the part of the template showing the form.&lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;post&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;csrf_token&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;form&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;button&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Submit&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;btn btn-primary&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;button&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Cancel&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;btn btn-danger&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;By default this will use Django's &lt;code&gt;as_div&lt;/code&gt; template, rendering each field in a series of &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; elements. We can now begin to customise the form.&lt;/p&gt;
&lt;h2&gt;Add css classes to a field's &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;.&lt;/h2&gt;
&lt;p&gt;Let's begin styling our form by adding classes to the &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; elements. &lt;/p&gt;
&lt;p&gt;Form widgets are Django's representation of &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;s. Classes can be added to these by using the &lt;code&gt;Widget.attrs&lt;/code&gt; argument. Bootstrap has requires &lt;code&gt;form-control&lt;/code&gt; for most widgets but &lt;code&gt;form-check-inputs&lt;/code&gt; for radios and checkboxes. Here's an example of how the form can be updated to add these classes.&lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gd"&gt;-    name = forms.CharField(max_length=50)&lt;/span&gt;
&lt;span class="gi"&gt;+    name = forms.CharField(max_length=50, widget=forms.TextInput(attrs={&amp;quot;class&amp;quot;: &amp;quot;form-control&amp;quot;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Create a custom field template&lt;/h2&gt;
&lt;p&gt;A &lt;code&gt;BoundField&lt;/code&gt; represents the whole field including its label, input, help text and errors. Django 5.0 added the ability to render a &lt;code&gt;BoundField&lt;/code&gt; using its &lt;code&gt;as_field_group ()&lt;/code&gt; method. The template used can be set project-wide, per-field or per-instance. &lt;/p&gt;
&lt;p&gt;I began by creating a copy of Django's &lt;a href="https://github.com/django/django/blob/main/django/forms/templates/django/forms/field.html"&gt;&lt;code&gt;field.html&lt;/code&gt;&lt;/a&gt; 
template in my project. To use this template project-wide create a custom &lt;code&gt;FORM_RENDERER&lt;/code&gt;:&lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# urls.py&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django.forms.renderers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TemplatesSetting&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomFormRenderer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TemplatesSetting&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;field_template_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;forms/field.html&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and in your &lt;code&gt;settings.py&lt;/code&gt; file define the &lt;code&gt;FORM_RENDERER&lt;/code&gt; setting. &lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# settings.py&lt;/span&gt;
&lt;span class="n"&gt;FORM_RENDERER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;bootstrap_form.urls.CustomFormRenderer&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Styling &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt; tags.&lt;/h2&gt;
&lt;p&gt;A custom field template eases adding additional styles and changing the HTML layout. Let's look at this by adding Bootstrap's &lt;code&gt;.form-label&lt;/code&gt; to &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;legend&amp;gt;&lt;/code&gt; elements.&lt;/p&gt;
&lt;p&gt;Django allows css classes to be added to labels if the field is required or has errors with 
&lt;a href="https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.error_css_class"&gt;&lt;code&gt;Form.error_css_class&lt;/code&gt;&lt;/a&gt;
and &lt;a href="https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.required_css_class"&gt;&lt;code&gt;Form.required_css_class&lt;/code&gt;&lt;/a&gt;. 
However, the requirement here is to always add a class to a label irresepective of form state. An 
alternative approach is required. &lt;/p&gt;
&lt;p&gt;The default template renders the label with &lt;code&gt;{{ field.label_tag }}&lt;/code&gt;, and although the &lt;a href="https://github.com/django/django/blob/f7389c4b07ceeb036436e065898e411b247bca78/django/forms/boundfield.py#L165"&gt;&lt;code&gt;label_tag&lt;/code&gt;&lt;/a&gt;
function allows &lt;code&gt;attrs&lt;/code&gt; to be added you can't pass arguments to the function with the 
Django Template Language. We could override the default &lt;code&gt;label.html&lt;/code&gt; template, but as it is a small change I included the customisation in the &lt;code&gt;field.html&lt;/code&gt; template. Here's the customisation I made to the &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt; tag, a similar change was made for &lt;code&gt;&amp;lt;legend&amp;gt;&lt;/code&gt;. &lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gd"&gt;- {{ field.label_tag }}&lt;/span&gt;
&lt;span class="gi"&gt;+ &amp;lt;label class=&amp;quot;form-label&amp;quot;&amp;gt;{{ field.label }}&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Styling help text.&lt;/h2&gt;
&lt;p&gt;Changing the style of help text is straight forward. The field template is updated to change the default &lt;code&gt;.helptext&lt;/code&gt; to bootstrap's  &lt;code&gt;.form-text&lt;/code&gt;.&lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gd"&gt;- {% if field.help_text %}&amp;lt;div class=&amp;quot;helptext&amp;quot;{% if field.id_for_label %} id=&amp;quot;{{ field.id_for_label}}_helptext&amp;quot;{% endif %}&amp;gt;{{ field.help_text|safe }}&amp;lt;/div&amp;gt;{% endif %}&lt;/span&gt;
&lt;span class="gi"&gt;+ {% if field.help_text %}&amp;lt;div class=&amp;quot;form-text&amp;quot;{% if field.id_for_label %} id=&amp;quot;{{ field.id_for_label}}_helptext&amp;quot;{% endif %}&amp;gt;{{ field.help_text|safe }}&amp;lt;/div&amp;gt;{% endif %}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Using a per-field template.&lt;/h2&gt;
&lt;p&gt;Django's default template for &lt;code&gt;radio&lt;/code&gt; inputs is to render it as an unordered list using &lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt;
elements. Bootstrap 5 requires the input to be rendered alongside the &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;, in addition the &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; wrapping the &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; also needs &lt;code&gt;.form-check&lt;/code&gt;. See &lt;a href="https://getbootstrap.com/docs/5.2/forms/checks-radios/#radios"&gt;docs&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Changes are needed both to the widget and field templates. I began by creating a per-field template and defined this in the form:&lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt; &lt;/span&gt;    appointment_date = forms.ChoiceField(
&lt;span class="w"&gt; &lt;/span&gt;        choices=days,
&lt;span class="w"&gt; &lt;/span&gt;        widget=forms.RadioSelect(),
&lt;span class="gi"&gt;+        template_name=&amp;quot;forms/radio_field.html&amp;quot;,&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;        help_text=&amp;quot;Select which day you would like your appointment. We&amp;#39;re only open Monday-Wednesday.&amp;quot;
&lt;span class="w"&gt; &lt;/span&gt;   )
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I used the field template to customise the layout of both the field and the widget by using the more &lt;a href="https://docs.djangoproject.com/en/4.2/ref/forms/widgets/#radioselect"&gt;granular control available&lt;/a&gt; in Django for widgets with choices. This allows wrapping of each &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; pair in a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; with the required css classes. Here's the full template:&lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;fieldset&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;legend&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;fs-6&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;field.label&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;legend&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;field.help_text&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;form-text&amp;quot;&lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;field.id_for_label&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;field.id_for_label&lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;_helptext&amp;quot;&lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endif&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;field.help_text&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nf"&gt;safe&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endif&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;field.errors&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;choice&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;field&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;form-check&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;choice.id_for_label&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;form-check-label&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;choice.choice_label&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;choice.tag&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endfor&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;fieldset&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Customising the form template.&lt;/h2&gt;
&lt;p&gt;The fields are looking much better, but we need to customise the layout of the field groups to add some space between them and to have the name and email fields to be on the same row. I copied the 
&lt;a href="https://github.com/django/django/blob/main/django/forms/templates/django/forms/div.html"&gt;&lt;code&gt;div.html&lt;/code&gt;&lt;/a&gt; template to my project and added it to my form renderer. The template can also be set per-form by defining &lt;a href="https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.template_name"&gt;&lt;code&gt;template_name&lt;/code&gt;&lt;/a&gt; on the form class. &lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt; &lt;/span&gt;class CustomFormRenderer(TemplatesSetting):
&lt;span class="w"&gt; &lt;/span&gt;    field_template_name = &amp;quot;forms/field.html&amp;quot;
&lt;span class="gi"&gt;+    form_template_name = &amp;quot;forms/form.html&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Adding classes to each field group.&lt;/h2&gt;
&lt;p&gt;The fields are currently too close together. To give them some space &lt;code&gt;.mb-3&lt;/code&gt; can be added to &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; wrapping the field group. The relevant line from the template is:&lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nv"&gt;classes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;field.css_classes&lt;/span&gt; &lt;span class="cp"&gt;%}{%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;classes&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;classes&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endif&lt;/span&gt; &lt;span class="cp"&gt;%}{%&lt;/span&gt; &lt;span class="k"&gt;endwith&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;field.css_classes&lt;/code&gt; returns the classes for fields which are required of have errors mentioned above. In addition it takes an optional argument to add additional classes. Accessing this method from a Django template is not straight forward. However, given this is already a template &lt;code&gt;.mb-3&lt;/code&gt; can be added directly to the template. &lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nv"&gt;classes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;field.css_classes&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;mb-3&lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;classes&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;classes&lt;/span&gt; &lt;span class="cp"&gt;}}{%&lt;/span&gt; &lt;span class="k"&gt;endif&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endwith&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Customising the form layout&lt;/h2&gt;
&lt;p&gt;The form we're working towards has the name and email fields on the same row. The custom per-form
template can be edited to achieve this. The new &lt;code&gt;as_field_group()&lt;/code&gt; method introduced in Django 5.0
eases this as the form can be laid out by accessing each field on the form rendering it as a 
field group. This avoids needing to write out each element for each field of your form. &lt;/p&gt;
&lt;p&gt;The core part of the form template now becomes:&lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;row&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;mb-3 col&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;form.name.as_field_group&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;mb-3 col&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;form.email.as_field_group&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;mb-3&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;form.appointment_date.as_field_group&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;mb-3&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;form.attachment.as_field_group&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Customise form errors&lt;/h2&gt;
&lt;p&gt;The form is now looking pretty similar to our target (radio's are not on the same row -- you'll have to take inspiration from the crispy-form template pack for that 😊). However, there's no errors on this form. If the submitted form returned validation errors these would be styled using Django's defaults. We'll need to customise these to work with Bootstrap. &lt;/p&gt;
&lt;p&gt;Django uses a class to hold information about form errors themselves and how to render them. A custom class can be used to change the template used to render errors. &lt;/p&gt;
&lt;p&gt;To create a custom error class:&lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django.forms.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ErrorList&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomErrorList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ErrorList&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;template_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;forms/errors.html&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The template renders each error in a span with the &lt;code&gt;.invalid-feedback&lt;/code&gt; class. &lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;error&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;errors&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;invalid-feedback&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;strong&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;error&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;strong&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endfor&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To use it in your form you need to define it when the field is created. I overrode the forms
&lt;code&gt;__init__&lt;/code&gt; method to ensure that this custom error list is always used. &lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BootstrapForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Form&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;error_class&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CustomErrorList&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;While the form will render HTML for Bootstrap 5 style errors they currently don't appear!&lt;/p&gt;
&lt;p&gt;For errors to be shown two things are required:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The field must have the &lt;code&gt;.is-invalid&lt;/code&gt; class&lt;/li&gt;
&lt;li&gt;The error must come &lt;strong&gt;after&lt;/strong&gt; the field and is must be a sibling. &lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Adding error styles to widgets&lt;/h2&gt;
&lt;p&gt;Error styles were discussed above and can easily be added to surrounding &lt;code&gt;&amp;lt;divs&amp;gt;&lt;/code&gt; or a &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt;. 
For Bootstrap the error class needs to be added to the &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Django allows additional classes to be added to a widget at render time when using the &lt;code&gt;attrs&lt;/code&gt; 
argument of &lt;a href="https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.BoundField.as_widget"&gt;&lt;code&gt;BoundField.as_widget()&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We need to pass in additional error classes to this method when the field has errors. While it's 
not possible to do this directly from Django a &lt;a href="https://docs.djangoproject.com/en/4.2/howto/custom-template-tags/"&gt;custom template tag&lt;/a&gt; 
can be created to achieve this.&lt;/p&gt;
&lt;p&gt;Here's my template filter. It takes in a field and some css classes. It adds the classes provided
to any classes already on the field (such as &lt;code&gt;.form-control&lt;/code&gt;) and renders the field with the 
combined classes. Note, this is a minimal example and is not well tested like some solutions in 
3rd party packages. &lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@register&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_class&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;css_class&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;widget_class&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;class&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;css_class&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot; &amp;quot;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;widget_class&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;as_widget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;class&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;css_class&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In the field template we can use this to add the &lt;code&gt;.is-invalid&lt;/code&gt; class when the field has errors.&lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;field.errors&lt;/span&gt; &lt;span class="cp"&gt;%}{{&lt;/span&gt; &lt;span class="nv"&gt;field&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nf"&gt;add_class&lt;/span&gt;&lt;span class="s2"&gt;:&amp;quot;is-invalid&amp;quot;&lt;/span&gt; &lt;span class="cp"&gt;}}{%&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="cp"&gt;%}{{&lt;/span&gt; &lt;span class="nv"&gt;field&lt;/span&gt; &lt;span class="cp"&gt;}}{%&lt;/span&gt; &lt;span class="k"&gt;endif&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This works great for our &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;email&lt;/code&gt; fields. However, it doesn't work for the &lt;code&gt;RadioSelect&lt;/code&gt;
widget!&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; for the radio is rendered using &lt;code&gt;{{ choice.tag }}&lt;/code&gt; in the template. Adding the 
above filter here will result in an error. This is because &lt;code&gt;choice.tag&lt;/code&gt; is already a string, with
&lt;code&gt;choice&lt;/code&gt; being a &lt;code&gt;BoundWidget&lt;/code&gt; (designed for use with widgets like radios) not a &lt;code&gt;BoundField&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We can update the template filter to also work for &lt;code&gt;BoundWidget&lt;/code&gt;.&lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt; &lt;/span&gt;@register.filter
&lt;span class="w"&gt; &lt;/span&gt;def add_class(field, css_class):
&lt;span class="gi"&gt;+    if isinstance(field, BoundWidget):&lt;/span&gt;
&lt;span class="gi"&gt;+        # copy data to avoid changing it, this method can be called multiple times&lt;/span&gt;
&lt;span class="gi"&gt;+        data = copy.copy(field.data)&lt;/span&gt;
&lt;span class="gi"&gt;+        if widget_class := data[&amp;quot;attrs&amp;quot;].get(&amp;quot;class&amp;quot;):&lt;/span&gt;
&lt;span class="gi"&gt;+            data[&amp;quot;attrs&amp;quot;][&amp;quot;class&amp;quot;] = css_class + &amp;quot; &amp;quot; + widget_class&lt;/span&gt;
&lt;span class="gi"&gt;+        else:&lt;/span&gt;
&lt;span class="gi"&gt;+            data[&amp;quot;attrs&amp;quot;][&amp;quot;class&amp;quot;] = css_class&lt;/span&gt;
&lt;span class="gi"&gt;+        context = {&amp;quot;widget&amp;quot;: {**data, &amp;quot;wrap_label&amp;quot;: False}}&lt;/span&gt;
&lt;span class="gi"&gt;+        return field.parent_widget._render(field.template_name, context, field.renderer)&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;    if widget_class := field.field.widget.attrs.get(&amp;quot;class&amp;quot;):
&lt;span class="w"&gt; &lt;/span&gt;        css_class += &amp;quot; &amp;quot; + widget_class
&lt;span class="w"&gt; &lt;/span&gt;    return field.as_widget(attrs={&amp;quot;class&amp;quot;: css_class})```
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;As per the warning above. This isn't well tested! The key takeaway is that some kind of template
filter/tag is how I'd go about solving this. This can be used in our template like this. &lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;field.errors&lt;/span&gt; &lt;span class="cp"&gt;%}{{&lt;/span&gt; &lt;span class="nv"&gt;choice&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nf"&gt;add_class&lt;/span&gt;&lt;span class="s2"&gt;:&amp;quot;is-invalid&amp;quot;&lt;/span&gt; &lt;span class="cp"&gt;}}{%&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="cp"&gt;%}{{&lt;/span&gt; &lt;span class="nv"&gt;choice.tag&lt;/span&gt; &lt;span class="cp"&gt;}}{%&lt;/span&gt; &lt;span class="k"&gt;endif&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Changing where errors are rendered in the form&lt;/h2&gt;
&lt;p&gt;The above section solved the need to add &lt;code&gt;.is-invalid&lt;/code&gt; to &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; with errors. Step two is to 
address where the errors are rendered, they need to be directly after the &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For our standard fields this is straight forward. &lt;code&gt;{{ field.errors }}&lt;/code&gt; can be moved from above the
input to below. &lt;/p&gt;
&lt;p&gt;It's more complex for radios, the error has to follow the last &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; in the group. This is inside
a &lt;code&gt;for&lt;/code&gt; loop. The Django Template Language allows us to use &lt;code&gt;forloop.last&lt;/code&gt; to check if it's the last item in the collection. &lt;/p&gt;
&lt;p&gt;Labels and inputs also need switching round for bootstrap errors to display correctly. The core part of
the radio field template becomes:&lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt; &lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;choice&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;field&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
     &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;form-check&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
     &lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;field.errors&lt;/span&gt; &lt;span class="cp"&gt;%}{{&lt;/span&gt; &lt;span class="nv"&gt;choice&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nf"&gt;add_class&lt;/span&gt;&lt;span class="s2"&gt;:&amp;quot;is-invalid&amp;quot;&lt;/span&gt; &lt;span class="cp"&gt;}}{%&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="cp"&gt;%}{{&lt;/span&gt; &lt;span class="nv"&gt;choice.tag&lt;/span&gt; &lt;span class="cp"&gt;}}{%&lt;/span&gt; &lt;span class="k"&gt;endif&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
     &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;choice.id_for_label&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;form-check-label&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;choice.choice_label&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;forloop&lt;/span&gt;&lt;span class="nv"&gt;.last&lt;/span&gt; &lt;span class="cp"&gt;%}{{&lt;/span&gt; &lt;span class="nv"&gt;field.errors&lt;/span&gt; &lt;span class="cp"&gt;}}{%&lt;/span&gt; &lt;span class="k"&gt;endif&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
     &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
 &lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endfor&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Make use of template filter for all widget classes.&lt;/h2&gt;
&lt;p&gt;The introduction of a template filter to add classes to widgets in templates means that the styles 
previously added in the form (which can get quite repetative) can be moved to the template. &lt;/p&gt;
&lt;p&gt;For example, the need to define a widget and &lt;code&gt;attrs&lt;/code&gt; dict for each field can be removed:&lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;class BootstrapForm(forms.Form):
&lt;span class="w"&gt; &lt;/span&gt;    ...
&lt;span class="gd"&gt;-    name = forms.CharField(max_length=50, widget=forms.TextInput(attrs={&amp;quot;class&amp;quot;: &amp;quot;form-control&amp;quot;}))&lt;/span&gt;
&lt;span class="gi"&gt;+    name = forms.CharField(max_length=50)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;... and can be added for all widgets in the template:&lt;/p&gt;
&lt;div class="codehilite not-prose"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gd"&gt;- {% if field.errors %}{{ field|add_class:&amp;quot;is-invalid&amp;quot; }}{% else %}{{ field }}{% endif %}&lt;/span&gt;
&lt;span class="gi"&gt;+ {% if field.errors %}{{ field|add_class:&amp;quot;form-control is-invalid&amp;quot; }}{% else %}{{ field|add_class:&amp;quot;form-control&amp;quot; }}{% endif %}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;What about using a custom BoundField?&lt;/h2&gt;
&lt;p&gt;The key class in rendering of form fields is the &lt;code&gt;BoundField&lt;/code&gt;. A custom &lt;code&gt;BoundField&lt;/code&gt; could be created which 
could add custom css classes to widgets or labels. However, to use a custom &lt;code&gt;BoundField&lt;/code&gt; every field's 
&lt;a href="https://github.com/django/django/blob/main/django/forms/fields.py#L248"&gt;get_bound_field()&lt;/a&gt; method would need
to be overriden. I think if this could be done at the form or project level what would ease this approach. &lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Reproducing the form using Django's core features proved to be mostly template work making use of per-site, per-form and per-field customisations. Some areas that were more challenging were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Customising errors where a per-form error class needs to be provided. &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here I'd like to see the ability to set a project-wide template on the form renderer, if that's possible. I think most folk would want to customise the ErrorLists template, and not it's other features. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Customising classes for &lt;code&gt;&amp;lt;inputs&amp;gt;&lt;/code&gt; which can be somewhat repetitive to add to a form and are more complex to add when they are dependant upon the form state, for example when the field has errors.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This seems harder, I'm not sure if there's a change that I'd like to see in Django itself for this. Maybe my solution to this problem isn't the correct one. Let me know your thoughts!&lt;/p&gt;</summary></entry></feed>