-
Notifications
You must be signed in to change notification settings - Fork 18
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
Add class specialisations for using WTForms with the frontend toolkit #400
Conversation
9ba47fc
to
cb61614
Compare
Needs a version bump in I'd probably prefer doing it as a major. 🤔 Even though it's not technically breaking backwards compatibility, we should try to get this standard applied to the frontend apps (as long as it can go in fairly easily). Open to being convinced it should just be a minor though. |
@samuelhwilliams I'm happy for it to be a major version if you are. I'm happy to start changing frontend apps myself, as far as I'm aware WTForms currently isn't used in many apps. |
I think it's used in 3 apps, so it shouldn't be awful to pull in. Hopefully easier once I get the existing validation masthead consolidation stuff in properly (for supplier and buyer...) |
Tests have been added, which resulted in some fixes, as tests do |
dmutils/forms/dm_fields.py
Outdated
|
||
|
||
class DMBooleanField(DMFieldMixin, wtforms.fields.BooleanField): | ||
pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we include all of these classes that don't do anything?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I included specialisations for all classes that I saw being used in the in the app codebase. Although the class body is empty, the fact that it inherits from the mixin means that the new class gets all the features of https://github.com/alphagov/digitalmarketplace-utils/blob/ldeb-wtforms-dmfields/dmutils/forms/dm_fields.py.
I'll add a docstring to the module that explains this.
dmutils/forms/dm_fields.py
Outdated
option['description'] = choice[2] | ||
|
||
# construct a choices argument suitable for wtforms.fields.RadioField | ||
choices = [(option['value'], option['label']) for option in self.options] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
might be more efficient to do the following:
self.options = []
wtforms_choices = []
for choice in choices:
option = {}
option['value'] = choice[0]
option['label'] = choice[1]
if len(choice) > 2 and choice[2]:
option['description'] = choice[2]
wtforms_choices.append( (option['value'], option['label']) )
super().__init__(label, validators, choices=wtforms_choices, **kwargs)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer to keep the two separate as I think it's clearer to read. Is that okay with you?
I'm going to do a rebase and force push now to clean up the commit history |
7202ed2
to
5815b65
Compare
Would like to have a general discussion about this, but am vaguely thinking I'd be in favour of just having these replacing/augmenting |
dmutils/forms/mixins.py
Outdated
''' | ||
def __init__(self, label=None, validators=None, hint=None, **kwargs): | ||
self.hint = hint | ||
self.question = label |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels like this could get out of sync with the WTForm native label
attribute. I think there are at least a few forms that update the label based on some dynamic context when the form is initialized.
Can we do a proxy on self.label.text
using the @property
decorator rather than copy the value across?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
dmutils/forms/mixins.py
Outdated
|
||
super().__init__(label=label, validators=validators, **kwargs) | ||
|
||
@property | ||
def question(self): | ||
return self.label |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be a proxy on self.label.text
specifically. Using self.label
will emit the text inside HTML <label>
tags, which we probably wouldn't want in most cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes that makes sense
dmutils/forms/fields.py
Outdated
@@ -17,7 +17,7 @@ | |||
class StripWhitespaceStringField(StringField): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the suggestion now is that all of the implementation here should be moved into dm_fields
classes; these classes would inherit from the dm_fields
ones and would be deprecated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to stick a DeprecationWarning
on them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, reckon a warning emitted by the constructor is the right thing here? And move any tests over from the old thing to the new thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
dmutils/forms/dm_fields.py
Outdated
option['value'] = choice[0] | ||
option['label'] = choice[1] | ||
if len(choice) > 2 and choice[2]: | ||
option['description'] = choice[2] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels like we probably need to append the constructed option
dict to self.options
, unless I'm missing something magical. 🤔 I can't see where it would otherwise get persisted here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
tests/forms/test_fields.py
Outdated
(StripWhitespaceStringFieldForm(), StripWhitespaceStringField), | ||
)) | ||
def test_form_with_field(form, field_class): | ||
assert isinstance(form.field, field_class) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand what this is testing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be honest I don't remember.
I will take the test out.
996632c
to
3f75626
Compare
dmutils/forms/dm_fields.py
Outdated
|
||
|
||
class DMBooleanField(DMFieldMixin, wtforms.fields.BooleanField): | ||
pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From my experience in Crown-Commercial-Service/digitalmarketplace-buyer-frontend#798 we are missing some detail here required to make this work, specifically we need options
I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, will add 👍
3f75626
to
a42e3af
Compare
I've just rebased the changes on master, and also added |
b2d0cf8
to
22ebeb9
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems good to me.
I'd still have been tempted to keep both versions of the classes for a little while rather than have a breaking change, but doing it this way is fine, as long as you've got a clear idea of how much work will be needed across all the apps, and have gotten that agreed with @benvand.
6360b10
to
0b1b296
Compare
We create 'DM' versions of common WTForms fields that have the DMFieldMixin in their MRO. We also remove our non-DM versions of custom fields, so this is a breaking change
This commit adds widgets for WTForms that render field html based on Jinja templates. By default the templates used are those in digitalmarketplace-frontend-toolkit.
0b1b296
to
a8ce893
Compare
For more examples of these mixins in action, see `dmutils/forms/dm_fields.py`. | ||
|
||
For more about mixins in general, see this `online article`_, | ||
or the very excellent `Python Cookbook`_ by David Beazley. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
insert amazon affiliate link
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also available at all good bookstores 😛
Would there be a neat way of adding a method to these field instances which will do all of the template-calling for us? So we could just do Or even just override wtforms standard way of doing this - |
@risicle that functionality is already there, I'll update the documentation to reflect that |
a8ce893
to
84902c5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neat
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome stuff.
Do we know (roughly) the impact of the breaking changes - that is, how many apps currently use StripWhitespaceStringField, EmailField etc and how extensively?
@katstevens 6 apps in total:
I've already done the work for buyer-frontend here |
The idea is to make it so that when using WTForms there is an unambiguous and obvious way of linking it up with the frontend toolkit macro.
To be clear that these are specialisations, the class names all have the 'DM' prefix.
Example
Say we want a set of radio buttons. In WTForms you would use
RadioField
, but now there is aDMRadioField
that you can use. You create your form in the same way:The improvement comes when it is time to use it in the Jinja template. Behind the scenes
DMRadioField
uses a new widget,DMSelectionButtons
, that is able to use the frontend toolkit macros. So, to use a DMRadioField object with the selection-buttons macro, the template code would be:which (hopefully 🤞) is self-explanatory and easy to write.
You can also use the macros in full yourself, like this:
which is still a bit of an improvement I think.