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

Inherit directly from field & django 1.10 compatibility #19

Closed
wants to merge 21 commits into from
Closed

Inherit directly from field & django 1.10 compatibility #19

wants to merge 21 commits into from

Conversation

lucaswiman
Copy link

@lucaswiman lucaswiman commented Dec 29, 2016

@gregmuellegger This PR significantly simplifies django-superform by having FormSetField and FormField inherit directly from Field, using the builtin Widget.value_from_datadict API. This has a number of advantages:

I also added support for Django 1.10 by updating the test environment and removing a few deprecated kwargs in template rendering. That broke compatibility with Django <1.8. Since all versions before 1.8 have hit end-of-life (and do not even receive security updates), I dropped support for them in the build matrix, along with Python 2.6.

Thanks again for writing this library! It's proven quite useful.

@gregmuellegger
Copy link
Collaborator

On the first glance, this is a super sweet change, removing the nastier parts of the implementation! Awesome 🎆

I will try to dive deeper into it soonish. What might be worth adding before merging might be some kind of update-guide, or a description of the public-facing APIs that changed in the CHANGES.rst file.



class FormWidget(TemplateWidget):
template_name = 'superform/formfield.html'
value_context_name = 'form'

@property
def media(self):
return self.field.form_class().media
Copy link
Author

Choose a reason for hiding this comment

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

Instantiating the form_class / formset_class to get the media can cause some problems. I'll try to see if there's a way around it.

Copy link
Author

@lucaswiman lucaswiman Dec 29, 2016

Choose a reason for hiding this comment

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

Addressed / partially reverted in 1beb808.

Lucas Wiman added 5 commits December 29, 2016 15:48
I'd hoped to allow using the widget's `media` property to eventually allow use of
FormSetField and FormField on classes that do not inherit from `SuperFormMixin`.
Unfortunately, since `media` is a _property_ of the `Form`/`FormSet`, we need an
instantiated instance of those.

Due to the way field/widget scoping takes place, there's no way to access the
"already instantiated" form/formset without hacky stack introspection. Instantiating
a formset or form isn't too bad, but can lead to unexpected behavior (like duplicate
database queries), so probably best avoided.
@lucaswiman
Copy link
Author

@gregmuellegger After attempting to integrate this into a real-world application, I think this needs some more work before it's ready for review. I'll comment when I'd like you to take another look. Apologies for the delay.

@gregmuellegger
Copy link
Collaborator

No hurries on that. The internals would be much nicer that way. Just ping me, I'll try to give it a look timely after notify me. Thanks for all the effort you put into this.

@lucaswiman
Copy link
Author

I've updated the changelog, and I think this is ready for review. For the moment, this PR solves my immediate need of using superform with another forms library that has a conflicting custom metaclass, so it's good enough for now.

To summarize the issues I was considering, using value_from_datadict allows the form/formset to show up in cleaned_data, but makes it hard to ensure that the values in .forms and .formsets are pointing to the same instances. I think this branch is about as far as I can get on that front without removing the forms/formsets attributes, which would break backwards compatibility.

In my opinion, it is worth eventually breaking backwards compatibility (and possibly using other standard django mechanisms like Widget.subwidgets()) in the hopes of upstreaming this functionality to Django core. However, it might make sense to do that in a fork of this library, or as a prototype for inclusion inside Django.

@auvipy
Copy link

auvipy commented Feb 24, 2017

awesome changes. did you tried django 1.11b1 with this ?

@lucaswiman
Copy link
Author

awesome changes. did you tried django 1.11b1 with this ?

No. That's unrelated to this change and probably better addressed in a different PR.

@auvipy
Copy link

auvipy commented Feb 25, 2017

ok. but will be great to see this pr merged soon

Copy link
Collaborator

@gregmuellegger gregmuellegger left a comment

Choose a reason for hiding this comment

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

Thanks for all your work and patient wait for my response. I think it would be super nice to get rid of the metaclasses.

However I have concerns about returning the actual form instance by value_from_datadict. The problem is that the form itself is not really value that the field operates on. It's more a wrapper element around it.

One case where this surfaces is the use of changed_data property on a form, for example:

>>> from django_superform import *
>>> from django import forms
>>> class OtherForm(forms.Form):
...     field1 = forms.CharField()
...     
... 
>>> class MyForm(SuperForm):
...     field1 = forms.CharField()
...     form1 = FormField(OtherForm)
...     
... 
>>> superform = MyForm()
>>> superform
<MyForm bound=False, valid=Unknown, fields=(field1;form1)>
>>> superform.changed_data
['form1']

So the form indicates that the form1 value changed, even though I haven't bound any initial or new data to the superform instance.

This is due to the way Django implements the changed_data property: https://github.com/django/django/blob/master/django/forms/forms.py#L433
It uses the value_from_datadict.

Maybe there is a solution to make the changed_data property work, but I'm unsure if we would break the expectations of other uses for this function. What's your thought on this? Could we make it somehow behave more as Django expects it to behave? Could this be done by breaking the backwards compatibility you mentioned?

I would be happy to not be compatible with django-superform 0.3 if the implementation would benefit from it. That's why it's not yet 1.0 :)

``get_form``. You can override this method in subclasses to change
the behaviour of the given form class.
"""
return self.form_class
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is the reasoning to remove this method? I liked to have this behaviour separate so you could create a custom subclass for the form if really necessary. Such things have turned out very valueable for other libraries I use (however I'm not sure if I used get_form_class yet in superform to do anything fancy :))

And removing it seems inconsistent with the get_formset_class method of the FormSetField class.

Copy link
Author

Choose a reason for hiding this comment

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

Because I need the form_class in .widget_attrs() below, where the name is not available. It is inconsistent with get_formset_class, though in that case the calling context is controlled by this library, as opposed to Django, so form is available.

@@ -295,7 +249,7 @@ def get_kwargs(self, form, name):
kwargs['empty_permitted'] = True
return kwargs

def get_field_name(self, form, name):
def get_field_name(self, name):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is the form parameter removed here? It's not used in the implementation, but during writing this it made sense to me since this way most methods have the same signature which is easy to remember and contains all information relevant for the field that cannot be accessed via self.

Copy link
Author

Choose a reason for hiding this comment

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

This was some refactoring I did trying to understand the code. I found it difficult to figure out what a lot of parameters were for, which made it harder to understand what information I actually needed for which methods. In particular, get_form_class had a superfluous form parameter, which was inconsistent with the changes I wanted to make, since widget_attrs needed access to the form_class, but didn't have direct access to the form. I may be able to fix that using the "monkeypatching" you mentioned.

It sounds like we have differing ways of thinking about interfaces, but it's your project. 😃 I'll update the code to only remove the superfluous form parameter in cases where it's necessary to remove it.

@@ -207,26 +153,22 @@ def get_composite_field_value(self, name):
def _init_composite_field(self, name, field):
if hasattr(field, 'get_form'):
form = field.get_form(self, name)
field.widget.form = form
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there a way to avoid the monkeypatching of the form in here? I'm not sure if there is, but I would like to here your thoughts on this.

Copy link
Author

Choose a reason for hiding this comment

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

No, I didn't find a way to do so. The widget is constructed by Django, but we need the form to be accessible from the widget.

@gregmuellegger
Copy link
Collaborator

Finally got around to review the PR thoroughly. I'm keen to know what you think on the topics brought up.

Copy link
Author

@lucaswiman lucaswiman left a comment

Choose a reason for hiding this comment

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

Sorry for the delay replying to the code review comments. My son was born shortly after you made them, but I'm working on this PR again this week.

I have concerns about returning the actual form instance by value_from_datadict. The problem is that the form itself is not really value that the field operates on. It's more a wrapper element around it.

To me, it makes sense to me that something called FormField operates on a Form instance. I think this is the sort of thing that value_from_datadict was designed for: a single field corresponding to several HTML form elements. At the level of generality of FormField, there's no "single object" that corresponds to what the form is doing.

Returning the form was an idea I took from django-formsetfield, though you may be right that there's a less hacky solution. One option I'll investigate is returning a dict of data relevant to the form (possibly with the prefix stripped?). I'll report back how that goes.

Maybe there is a solution to make the changed_data property work, but I'm unsure if we would break the expectations of other uses for this function. What's your thought on this? Could we make it somehow behave more as Django expects it to behave? Could this be done by breaking the backwards compatibility you mentioned?

This is a good catch! I think this is a general problem with value_from_datadict, which is presumably why Django provided a solution in Field.has_changed. In the Django docs for value_from_datadict, their example is a date widget with multiple "subwidgets" on the year, month and day. DateField understand the output of a date, and can tell when it has changed, but I think I need to implement that behavior myself in FormField.has_changed regardless of what value_from_datadict returns. I'll add a test case reproducing your example of `has_changed as well.

``get_form``. You can override this method in subclasses to change
the behaviour of the given form class.
"""
return self.form_class
Copy link
Author

Choose a reason for hiding this comment

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

Because I need the form_class in .widget_attrs() below, where the name is not available. It is inconsistent with get_formset_class, though in that case the calling context is controlled by this library, as opposed to Django, so form is available.

@@ -295,7 +249,7 @@ def get_kwargs(self, form, name):
kwargs['empty_permitted'] = True
return kwargs

def get_field_name(self, form, name):
def get_field_name(self, name):
Copy link
Author

Choose a reason for hiding this comment

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

This was some refactoring I did trying to understand the code. I found it difficult to figure out what a lot of parameters were for, which made it harder to understand what information I actually needed for which methods. In particular, get_form_class had a superfluous form parameter, which was inconsistent with the changes I wanted to make, since widget_attrs needed access to the form_class, but didn't have direct access to the form. I may be able to fix that using the "monkeypatching" you mentioned.

It sounds like we have differing ways of thinking about interfaces, but it's your project. 😃 I'll update the code to only remove the superfluous form parameter in cases where it's necessary to remove it.

@@ -207,26 +153,22 @@ def get_composite_field_value(self, name):
def _init_composite_field(self, name, field):
if hasattr(field, 'get_form'):
form = field.get_form(self, name)
field.widget.form = form
Copy link
Author

Choose a reason for hiding this comment

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

No, I didn't find a way to do so. The widget is constructed by Django, but we need the form to be accessible from the widget.

@lucaswiman lucaswiman closed this Nov 13, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

There is no way to loop over all superform bound fields
3 participants