Is there a way to make a non-form widget?

I’m just playing around and learning/exploring. I was thinking about how form fields render just by putting the field variable in the template. I’ve created some custom form widgets in the past and I think it’s pretty cool.

I’ve been working to create a derived ListView class that integrates bootstrap table functionality. I was lamenting how busy the template was, and I thought, what if I can add a widget to my BootstrapTableColumn class so that all I need to do is reference the object in the template to generate the th tag and possibly a default td tag?

For example, I have this currently in a SampleListView template that inherits from my BootstrapTableListView class:

                <th data-field="{{ name.name }}"
                    data-valign="top"
                    data-filter-control="{{ name.filter_control }}"
                    {{ name.filter_data_attr|safe }}
                    data-filter-default="{{ name.filter }}"
                    data-sortable="{{ name.sortable }}"
                    data-sorter="{{ name.sorter }}"
                    data-visible="{{ name.visible }}">Sample</th>
                <th data-field="{{ animal__name.name }}"
                    data-valign="top"
                    data-filter-control="{{ animal__name.filter_control }}"
                    {{ animal__name.filter_data_attr|safe }}
                    data-filter-default="{{ animal__name.filter }}"
                    data-sortable="{{ animal__name.sortable }}"
                    data-sorter="{{ animal__name.sorter }}"
                    data-visible="{{ animal__name.visible }}">Animal</th>
                <th data-field="{{ tissue__name.name }}"
                    data-valign="top"
                    data-filter-control="{{ tissue__name.filter_control }}"
                    {{ tissue__name.filter_data_attr|safe }}
                    data-filter-default="{{ tissue__name.filter }}"
                    data-sortable="{{ tissue__name.sortable }}"
                    data-sorter="{{ tissue__name.sorter }}"
                    data-visible="{{ tissue__name.visible }}">Tissue</th>

That’s just the first 3 of 22 columns. And I thought, what if it could look like this:

{{ name }}
{{ animal__name }}
{{ tissue__name }}

… like the way django form elements are rendered? It would be a lot cleaner.

I know I could use include tags, which would be a huge improvement:

{{ include "DataRepo/templates/DataRepo/widgets/bstlistview_th.html" with column=name }}
{{ include "DataRepo/templates/DataRepo/widgets/bstlistview_th.html" with column=animal__name }}
{{ include "DataRepo/templates/DataRepo/widgets/bstlistview_th.html" with column=tissue__name }}

but I like how with a widget, you don’t have to supply the template path or the object. They’re there without having to pass them.

I perused the source of forms and widgets and managed to work it out, but I’m hijacking django.forms.widgets to render these th tags. Here’s a peek at the components:

widgets.py

class BSTHeader(Widget):
    template_name = "DataRepo/widgets/bstlistview_th.html"

    def get_context(self, name, column, attrs=None):
        context = super().get_context(name, None, None)
        column_attrs = attrs or {}
        context["column"] = {
            "name": column.name,
            "filter_control": column.filter_control,
            "sortable": column.sortable,
            "sorter": column.sorter,
            "visible": column_attrs.get("visible") or column.visible,
            "filter": column_attrs.get("filter") or column.filter,
            "FILTER_CONTROL_CHOICES": column.FILTER_CONTROL_CHOICES,
            "many_related": column.many_related,
            "strict_select": column.strict_select,
            "header": column.header,
        }
        return context

bst_list_view.py

class BootstrapTableColumn:

    def __init__(self):
        self.th_widget = BSTHeader()

    def __str__(self):
        return self.as_th_widget()

    def as_th_widget(self, attrs=None):
        return self.th_widget.render(
            self.name,
            self,
            attrs=attrs,
        )

class BootstrapTableListView(ListView):

    def get_context_data(self, **kwargs):
        column: BootstrapTableColumn
        for column in self.columns:
            context[column.name] = column.as_th_widget(
                attrs={
                    "visible": self.get_column_cookie(column, "visible", column.visible),
                    "filter": filter_term,
                }
            )
        ...

It actually works and I spent less than a work day on it. I’ve already updated 2 derived ListView classes to inherit from this and I updated their templates and it’s pretty slick.

I’d like to know if there’s a better way to do this… something not quite so hacky/fragile (i.e. I wouldn’t have to have the inner workings of form stuff in the widget…)

I left out a lot, but included the pivotal stuff. Here’s a portion of one of the final 2 derived classes I’ve used this with…

from DataRepo.views.models.base import BootstrapTableColumn as BSTColumn
from DataRepo.views.models.base import BootstrapTableListView as BSTListView

class ArchiveFileListView(BSTListView):
    model = ArchiveFile
    context_object_name = "archive_file_list"
    template_name = "DataRepo/archive_file_list.html"

    DATETIME_FORMAT = "YYYY-MM-DD HH:MI a.m."  # Postgres date format
    DBSTRING_FUNCTION = "to_char"  # Postgres function

    def __init__(self, *args, **kwargs):
        """Only creating this method to keep database calls out of the class attribute area (for populating the
        select_options values).  Otherwise, columns can be defined as a class attribute."""

        self.columns = [
            # The first arg in each case is the template table's column name, which is expected to be the data-field
            # value in the bootstrap table's th tag.
            BSTColumn(
                "filename",
                sorter="htmlSorter",
                header="File Name",
            ),

            BSTColumn(
                # The column name in this case is an annotated field.  An annotated field is used in this case so that a
                # BST substring search can find matching values in the DB.
                "imported_timestamp_str",

                # We provide the actual model field name as a fallback in case the converter fails.  (Conversion
                # functions can be database-specific.  If the backend DB is different, the annotation will be created
                # directly from the field.  Searching will be disabled.)
                field="imported_timestamp",
                converter=Func(
                    F("imported_timestamp"),
                    Value(self.DATETIME_FORMAT),
                    output_field=CharField(),
                    function=self.DBSTRING_FUNCTION,
                ),
                header="Import Timestamp",
            ),

            # The name in the following 2 cases are a related field, but it's 1:1.  The field is automatically set to
            # the name's value.
            BSTColumn(
                "data_format__name",
                select_options=DataFormat.objects.order_by("name").distinct("name").values_list("name", flat=True),
                header="File Type",
            ),
            BSTColumn(
                "data_type__name",
                visible=False,  # Initial visibility
                select_options=DataType.objects.order_by("name").distinct("name").values_list("name", flat=True),
                header="File Format",
            ),

If you’re looking for a more general solution, there’s the basic issue that the variable does need to know what template to use for being rendered. This is fundamentally different from the forms where the form field uses predefined or preidentified templates. It’s the Form field that is being rendered, not a Model field.

So handling this does require some method of associating a template with the Model field.

You have a couple different options that I can think of right off-hand.

You could:

  • Create a custom __str__ method assigned to the fields. (Unfortunately, this would end up always rendering it the same way in every template, and would likely prevent the admin from working properly as well.)

  • Create a custom method in the fields, that can cause it to be rendered by:
    {{ name.render }}

  • Create a custom filter, something that could look like:
    {{ name|render:bstlistview_th.html }}

  • Create a custom tag to render a template with a variable:
    {% template_name variable_name %}

But in all cases the idea is the same - somehow you need to associate a template with that model field.

Doesn’t the fact that I’m setting template_name in the derived class (BootstrapTableColumn) satisfy that need? What am I missing?

Incidentally, I would like the class containing the column objects (BootstrapTableListView) to be able to render the table tag. I haven’t gotten that far yet, but thinking about it, I’m not sure how to incorporate it in a holistic way (e.g. also render the closing tag) and still be true to the way ListView works (where you loop on a queryset in the template to render a whole table).

Sorry, I should have made it more clear that I was going off on a tangent - more thinking aloud about a more general case.

Yes, your view with the BootstrapTableColumn does this, but if I’m understanding it correctly, it’s limited to doing this with fields.

I was going beyond that, thinking about making this a more global solution, and was floating some thoughts of what those other approaches might look like or what they need to address. The sticking point I keep hitting is that what you are rendering with the {{ }} notation isn’t necessarily an object. It can also be a callable or a primitive, which means you can’t necessarily rely upon the ability to add attributes to the element being rendered.

Anyway, I’m sorry if I was just confusing the issue.

Oh, I see. Yes, you’re right. It would be nice if it could stay an object and render as HTML when evaluated in string context.

Maybe that’s possible? The values I am passing to the method that renders the HTML from the template, are all controlled by cookies that are changed in JavaScript written for bootstrap table controls. And all of those values are to Support bootstrap table features.

There are two of them. One is the value for “visible” that is controlled by bootstrap tables column switch feature. The only reason I need it to be changeable Is because I wanted to display correctly Initially whenever ListView sends another page of results from the server. The other is “filter” To Support bootstrap table’s column filter. And again, I want the initial filter value filled in in the column filter input box when a new page of results is loaded.

Maybe I can just add calls to “get cookie” in the __str__ override in BootstapTableColumn?

I still have a lot to learn. I have not tested what I was thinking about how an object behaves in a template. I was wondering if it actually is an object when it’s referenced in the template. I had started to think (just based on my usage experience and without looking it up to learn how precisely it works), that the context variables are only dicts whose keys are accessed using dot notation. Is that right?

Maybe that’s technically correct (and I’m just imagining how things work here, because I don’t know), but perhaps a bunch of objects’ attributes and things are added to the dicts automatically by django when the context is returned from a derived class method? But if that was even the case, and say it stored a dict key for __str__ with a value set when the context was initialized, does the template have the ability to interpret string context to decide what to render? In other words, if it is a dict, and you’re not resolving to an actual string value stored in the dict, will it automatically look up a saved string value?

Not necessarily. The docs at The Django template language | Django documentation | Django cover this in a lot more detail, but briefly, a context variable can be either an object, a dict, a list, or a method.
(You can also find the code where these lookups are done in django.template.base.Variable._resolve_lookup)
For example, if:

def some_func():
   return 42

a_list = ['a', 'b', 'c', 'd']

a_dict = {'x': 3, 'y': 4}

ctx = {'a': some_func, 'b': a_list, 'c': a_dict}

Then, the template fragments below produce these results:
{{ a }} → 42
{{ b.2 }} → ‘c’
{{ c.y }} → 4

However, each “component” (segment separated by periods) of the name is evaluated in the context of the previous component, allowing these structures to be “stacked” or “chained”.

For example, if instead of the above, we define:

a_list = ['a', 'b', 'c', 'd']

a_dict = {'x': 3, 'y': a_list}

def some_func():
   return a_dict

ctx = {'a': some_func}

Then {{ a.y.1 }} → ‘b’

This is just the “variable resolution” process. After the variable is retrieved, it’s then passed through any filters that may be applied.
Filters can accept any data type (See the first bullet point at How to create custom template tags and filters | Django documentation | Django) This implies to me that internally, the resolved variable maintains its “Type” at that point - it has not yet been reduced to a string.

(This is about as far as my understanding goes - I’m not knowledgeable about the internals of the template engine.)

My conjecture beyond this is that a custom engine could change how those variables get rendered at that point, but I don’t think it can be done much easier than that. That’s why I was thinking that a custom filter might achieve pretty much what you’re looking to do without a whole lot of internals work.

I’m not sure I’m understanding what you’re asking here, but if your question is:

  • Can I render {{ x.a_var }}, where a_var is a variable, such that if a_var = 'y' then I would actually retrieve {{ x.y }}?

The answer is “no”. That sort of variable substitution is not directly possible.

1 Like

Sorry, I think my question was unclear. My question about string context is like this… Take my BSTHeader widget class as an example. In BootstrapTableListView.get_context, I create the rendered string for a BSTHeader like this:

# NOTE: column: BSTHeader
# NOTE: column.name = "sample__name"
context[column.name] = column.as_th_widget(
    attrs={
        "visible": self.get_column_cookie(column, "visible", column.visible),
        "filter": filter_term,
    }
)

…because I assumed that if I were to do this instead:

context[column.name] = column

…the HTML wouldn’t render using BSTHeader.__str__() when I referenced the object in the template:

{{ sample__name }}

So when I asked if string context would work in the template, that’s what I meant.

The values that change inside column (and are governed by cookies) could either be updated there, like:

column.visible = self.get_column_cookie(column, "visible", column.visible)
context[column.name] = column

…or maybe, I could do that somehow inside BSTHeader.get_context instead of using attrs?

Well, let’s try a couple things and find out…

In [1]: from django.template import Template, Context

In [2]: (Error edited out)

In [3]: class BTSHeader:
   ...:     def __str__(self):
   ...:         if hasattr(self, 'value'):
   ...:             return f'Value {self.value}'
   ...:         else: return super().__str__()
   ...: 

In [4]: a_header = BTSHeader()

In [5]: a_header.value = 42

In [6]: print(a_header)
Value 42

In [7]: t = Template('{{ a }}')

In [8]: t.render(Context({'a': a_header}))
Out[8]: 'Value 42'

Looks like it would to me…

1 Like

That’s excellent! I was working on other little issues this morning, but was intending to conduct the same experiment. I’m stoked to make this improvement.

One thing that still nags at me is that BSTHeader is executing code from the Widget superclass that I’m not using. It just seems like a waste? For example, it’s creating a widget context variable in the super() call:

class BSTHeader(Widget):
    template_name = "DataRepo/widgets/bstlistview_th.html"
    def get_context(self, name, column, attrs=None):
        context = super().get_context(name, None, None)  # <-- HERE
        ...
        return context

I had tried removing that call to super, but then I got something like a TemplateNotFound error.

The constructor may be doing some things I’m not using as well… Actually, I just looked at the code and init and get_context are not doing much… Maybe I’ll experiment a little bit and see what I can do…

The definition of the Widget class is in django.forms.widgets. The get_context method returns a dict with the single key of widget and the various attributes needed to render that widget. (It’s used by the render method in that same class.)

If nothing else, it is what is using the template_name attribute to set the widget context entry to the proper template.