Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[WIP] Fixed #34077 -- Made BoundField renderable. #16247

Closed
wants to merge 2 commits into from

Conversation

smithdc1
Copy link
Member

@smithdc1 smithdc1 commented Nov 2, 2022

Copy link
Member

@carltongibson carltongibson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick initial comment: this should have a major section at the top of the release notes, with a full usage example, I think. (We wanna tell folks about it! 😜)

Nice work picking this up David 🦄

Comment on lines 85 to 92
def as_field(self, template_name=None, context=None, renderer=None):
"""
Render the field including labels, help text and errors.
"""
renderer = renderer or self.form.renderer
template = template_name or self.field.template_name
context = context or {"field": self}
return mark_safe(renderer.render(template, context))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very close to django.forms.utils.RenderableMixin.render(), so wondering if we could define:

# django/form/utils.py

class RenderableFieldMixin(RenderableMixin):
    def as_field(self):
        return self.render(self.template_name_field)

Or something like that. And then have the following here:

class BoundField(RenderableFieldMixin):
    ...

    def get_context(self):
        return {"field": self}

We'd also need to set self.renderer in BoundField.__init__(). Then we could potentially ditch the @html_safe and __str__(), but we'd have to see how we can make that behave still as currently it relies on .as_widget() which looks a bit different. I think we could overcome that though with some thought... It might be that we then later want to revisit what widgets are doing and see if we need something like a RenderableWidgetMixin too. It'd be really good to support a consistent API for all of this, especially for long term maintenance 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This highlighted a nice bug I'd likely introduced. Hopefully we'll have one test failure showing this.

We have a circular series of renderables as the form contains its boundfield and the boundfield has a reference back to the form.

While I need to spend more time looking at this I'd also appreciate ideas.

@@ -1243,6 +1243,14 @@ Attributes of ``BoundField``
Methods of ``BoundField``
-------------------------

.. method:: BoundField.as_field(template_name=None, context=None, renderer=None)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, looking at this, this is the signature of RenderableMixin.render(), not the various .as_*() methods.
Would definitely be nice to align this better.

@carltongibson
Copy link
Member

Hey @smithdc1 — Just looking at the discussion here. Can I ask you to ping me when you're ready for a review. Thanks. 🎁

@smithdc1
Copy link
Member Author

@carltongibson -- Can I ask you to put this on your list please?

On the circular rendering issue, my current thinking is that it's actually ok for now. The original issue was recurively calling __str__ gave rise to a circular context. This patch doesn't change BoundField.__str__ and therefore I don't belive this patch as is should be causing that regession. I think the error is that the test incorrectly calls render().

I've changed the test to call __str__() instead. I believe this to still be a regression test for https://code.djangoproject.com/ticket/33134 but also allows the tests to pass with this patch. 🤞

The for now reference is that if we ever did want to change BoundField.__str__() to render the field then we'd encounter that regression test.

Copy link
Member

@ngnpope ngnpope left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks David. This is a good next step, but I think we need to think about extracting the field rendering for more than just the div.html. Really .as_field() should be .as_div() and we should add the equivalent for the other tags.

I'll try and have a think through the mess with .as_widget() and .as_hidden() in .__str__(). It feels pretty clunky and looks sort of outdated w.r.t. moving stuff to template rendering. Biggest pain is that they're documented API.

"Subclasses of RenderableFieldMixin must provide an as_widget() method."
)

def __str__(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably worth a comment to state that we override this here for backward compatibility, i.e. we can't rely on the default implementation from the parent that uses self.render().

Comment on lines +85 to +66
# TODO -- I'm not sure how best to structure this as __str__ on boundfield does
# something different to RenderableMixin.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this comment is really related to __str__() below. I think we can start out with the comment I've mentioned.

I'll try to have a think if there is some backward-compatible way we can improve this. But either way, even if it's not an entirely clean fit we're at least coercing this into a more clear pattern.

@@ -0,0 +1,10 @@
{% if field.use_fieldset %}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I notice that we're extracting this from the django/forms/div.html template.. But what about the others?

Maybe this should be django/forms/fields/div.html and we should also extract the equivalent from p.html, table.html and ul.html. Shouldn't we have .as_div(), .as_table(), .as_p(), and .as_ul() like at the form level instead of .as_field()?

It certainly feels like this should mirror what happens at the form level as the field should follow the same templating structure as the form to avoid incorrect output.

tests/forms_tests/templatetags/tags.py Outdated Show resolved Hide resolved
Comment on lines +99 to +81
if self.field.show_hidden_initial:
return self.as_widget() + self.as_hidden(only_initial=True)
return self.as_widget()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. The more I look into the these and some of the existing helpers, the more I feel that some stuff should be cleaned up and ripped out. I'll need some time to have a play though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mornings thought was I wonder if the first step here would be to see if we can make Widget inherit from RenderableMixin or similar.

I don't think we can currently call str() on a widget to get it to render. That could be nice in a template. 🤔

{{ field.label }}
{{ field.help_text }}
{{ field.widget }}
...

Thinking out loud here!

@smithdc1
Copy link
Member Author

Thank you for you review and comments as always.

Just a couple of initial comments.

we should add the equivalent for the other tags.

We don't recommend the other methods. We stopped short of removing them (that was the suggestion by the accessibility team) due to backward compatibility concerns. So I wasn't sure on adding additional support for them.

Maybe more importantly I'm not sure there's that much benefit. How many folk are using exactly the standard layout for those methods and would benefit (e.g im not sure how altering the table layout would help)? As soon as you have something slightly different the advise would be to create a custom template and use that instead? Assign it in the renderer or field or ...

Biggest pain is that they're documented API.

It's also been like this for some time.
https://github.com/django/django/blame/4d596a1f6443eaf5d18d70a18aaac25030c7fc81/django/newforms/__init__.py#L829

I had assumed that was too much to be changing at this stage.

@carltongibson
Copy link
Member

We don't recommend the other methods.

Yes... — if we were starting from scratch now, we wouldn't have the as_*() methods on form: rather you'd just set the template and output as a string (str(form))

So here… — if we were starting from scratch now, we'd do the same for BoundField, except we can't because __str__() is already taken. Thus, we need to add the new as_field() (still thinking about the name there) in order to allow folks to output the errors/help-text/widget group all in one go. (The docs should be clear that str(bf) just output the widget, where bf.as_field() outputs the collected parts.)

I don't think we need (or, for the a11y concerns, want) to add as_p() &co like templates. Let's just provide one that. The div templates are becoming the default, let's go with that. (Any issues there? 🤔)

On the circular rendering issue...

👀

@ngnpope
Copy link
Member

ngnpope commented Nov 24, 2022

Yes... — if we were starting from scratch now, we wouldn't have the as_*() methods on form: rather you'd just set the template and output as a string (str(form))

Ah, if only, eh? 🙂

I don't think we need (or, for the a11y concerns, want) to add as_p() &co like templates. Let's just provide one that. The div templates are becoming the default, let's go with that. (Any issues there? thinking)

If we're to do this - only focus on improving the .as_div() case - I would like to see a stronger admonition under the docs for .as_p(), .as_table(), and .as_ul(). It should state that we're no longer developing improvements around those helpers, that they're poor for accessibility reasons, and that we recommend migrating over to using .as_div() instead. We should possibly even say that they're soft-deprecated. Currently we only mention this under .as_div(), but it doesn't feel strong enough (to me) to warn people away from them.

Not splitting out those cases and advertising this split-out field template as a new feature may be confusing to users of those non-recommended methods.

@carltongibson
Copy link
Member

carltongibson commented Nov 24, 2022

Hey @ngnpope. 👍

We already moved the as_* methods to the bottom of Outputting forms as HTML, in the subsection here: https://docs.djangoproject.com/en/4.1/ref/forms/api/#output-styles, that you linked. This was an attempt to de-emphasise them. We could move the note in the as_div() bit to the top of that subsection, and add another sentence saying, ≈_"In general the {{ form }} usage shown above is preferred. These helpers largely remain for backwards compatibility reasons."_

@smithdc1
Copy link
Member Author

I'll come back to this after the feature freeze. 🎅🎄

I've spent a bit of time looking at the comments and it seems to unravel and get more complex the more I think through it. I'm not sure unpicking the current str behaviour is that straight forward. 🤔

@carltongibson
Copy link
Member

I'll come back to this after the feature freeze. 🎅🎄

Hey @smithdc1 — OK. Always wise 💝 — Overall, I think this is a (really) good feature to have, but there's no urgency. 5.0 will be more than perfect. 🎁

@smithdc1
Copy link
Member Author

smithdc1 commented Feb 8, 2023

Hey @carltongibson 👋

Just to say, I'm planning on coming back to this next. I've got one eye on trying to get this in before you move onto new adventures. 😀

@carltongibson
Copy link
Member

Cool. Yes. Let's get it in. It's the icing on the cake for the forms changes over the last few releases.

(I didn't get to think about it but, did we consider the template tag approach? Do we favour the method on BoundField?)

@smithdc1
Copy link
Member Author

(I didn't get to think about it but, did we consider the template tag approach? Do we favour the method on BoundField?)

I think I originally suggested this as an option on the ticket. I've come round to the idea that it is not the best option here. All of the other ways of rendering a form/field/widget are a method, usually one of __str__(), render() or one of the as_*() methods.

To suddenly have a template tag to render a field seems like an odd choice.

I think the most difficult question we have to answer is what do we do about calling __str__ on a BoundField, and the fact that it returns the fields widget. As Nick pointed out it's not just __str__ there's also .as_widget() and .as_hidden() so it's quite a lot to try and unpick. It's also been that way ~forever. Do we try and deprecate that so we can clean up the rest of the code?

We could move the note in the as_div() bit to the top of that subsection ...

I'll think I'll start here, likely as another PR so we can get that in.

@@ -37,6 +37,70 @@ compatible with Django 5.0.
What's new in Django 5.0
========================

BoundField becomes renderable
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a better title.

@carltongibson
Copy link
Member

Closing in favour of #16574

@smithdc1 smithdc1 deleted the boundfield branch November 27, 2023 10:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants