Using a model property (list of dictionaries) as an input to django's format_html_join() yields KeyError
I am attempting to use Django's format_html_join()
util to return an html formatted version history for one of my models. But I cannot get format_html_join()
to accept my list of dictionaries.
Here is what the documentation suggests:
format_html_join(
"\n",
'<li data-id="{id}">{id} {title}</li>',
({"id": b.id, "title": b.title} for b in books),
)
That third argument is intended to be: args_generator should be an iterator that yields arguments to pass to format_html(), either sequences of positional arguments or mappings of keyword arguments.
I have tried different ways to get this to work and I'm not getting it, so I'm asking for help. I thought a list of dictionaries is iterable. I'm also thinking there has to be a way to use a list of dictionaries in a util that is expecting a list of dictionaries without having to re-create the list of dictionaries. Here is the model method I have to get the version history:
@property # I have tried this as a property and not as a property, neither works
def get_version_history(self):
versions = Version.objects.get_for_object(self)
version_history = []
for version in versions:
history_fields = version.field_dict
hdict = {"question": history_fields['question'],
"answer": history_fields['answer'],
"user": version.revision.user.username,
"timestamp": version.revision.date_created.strftime("%Y-%m-%d %H:%M"),
}
version_history.append(hdict)
return version_history
That method returns something like this:
[{'question': "I'm out of questions",
'answer': 'bye',
'user': 'test.supervisor',
'timestamp': '2025-07-31 20:19'},
{'question': "I'm out of questions",
'answer': 'me too',
'user': 'test.supervisor',
'timestamp': '2025-07-31 20:18'},
{'question': "I'm out of questions",
'answer': '',
'user': 'test.supervisor',
'timestamp': '2025-07-31 20:18'}]
Now I am trying to return an html formatted version of that list of dictionaries:
def version_html(self):
html = format_html_join(
"\n",
"""<tr>
<td>{question}</td>
<td>{answer}</td>
<td>{user}</td>
<td>{timestamp}</td>
</tr>""",
self.get_version_history
)
return html
The above code returns this error:
File ~/.virtualenvs/cep/lib/python3.12/site-packages/django/utils/html.py:112, in format_html(format_string, *args, **kwargs)
110 args_safe = map(conditional_escape, args)
111 kwargs_safe = {k: conditional_escape(v) for (k, v) in kwargs.items()}
--> 112 return mark_safe(format_string.format(*args_safe, **kwargs_safe))
KeyError: 'question'
I have tried various things for the third argument, all with various errors:
self.get_version_history
self.get_version_history()
**self.get_version_history
**self.get_version_history()
{"question": v.question, "answer": v.answer, "user": v.user, "timestamp": v.timestamp,} for v in self.get_version_history())
({"question": v['question'], "answer": v['answer'], "user": v['user'], "timestamp": v['timestamp']} for v in self.get_version_history())
{"question": v.question, "answer": v.answer, "user": v.user, "timestamp": v.timestamp,} for v in self.get_version_history)
({"question": v['question'], "answer": v['answer'], "user": v['user'], "timestamp": v['timestamp']} for v in self.get_version_history)
(d for d in self.get_version_history)
(d for d in self.get_version_history())
[d for d in self.get_version_history]
[d for d in self.get_version_history()]
Now I'm just thrashing.
This feature has been added recently. Indeed, this is implemented in pull request 18469 [GitHub], and thus was introduced in django-5.2. Indeed, it is mentioned in the release notes [Django-doc]:
format_html_join()
now supports taking an iterable of mappings, passing their contents as keyword arguments toformat_html()
.
Until django-5.2, one could only use iterables, like tuples or lists, making it harder to format it properly. We can see this in the source code [GitHub]:
return mark_safe( conditional_escape(sep).join( format_html(format_string, *args) for args in args_generator ) )
So you there are essentially three things you can do:
upgrade to django-5.2;
roll your own
format_html_join
, which is not very hard:from collections.abc import Mapping from django.utils.html import conditional_escape, format_html, mark_safe def format_html_join(sep, format_string, args_generator): return mark_safe( conditional_escape(sep).join( ( format_html(format_string, **args) if isinstance(args, Mapping) else format_html(format_string, *args) ) for args in args_generator ) )
or use tuples instead:
from operator import itemgetter def version_html(self): return format_html_join( '\n', '<tr>' + '<tr>{}</td>'*4 + '</tr>', map(itemgetter('question', 'answer', 'user', 'timestamp'), self.get_version_history) )