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

Add class specialisations for using WTForms with the frontend toolkit #400

Merged
merged 8 commits into from
Aug 6, 2018

Conversation

lfdebrux
Copy link
Contributor

@lfdebrux lfdebrux commented Jun 4, 2018

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 a DMRadioField that you can use. You create your form in the same way:

class DMRadioForm(FlaskForm):
    radio = DMRadioField(choices=[...])

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:

{{ form.radio }}

which (hopefully 🤞) is self-explanatory and easy to write.

You can also use the macros in full yourself, like this:

{%
  with
  name = form.radio.name,
  type = "radio",
  value = form.radio.value,
  error = form.radio.error,
  options = form.radio.options
%}
  {% include "toolkit/forms/selection-buttons.html" %}
{% endwith %}

which is still a bit of an improvement I think.

@lfdebrux lfdebrux force-pushed the ldeb-wtforms-dmfields branch 2 times, most recently from 9ba47fc to cb61614 Compare June 4, 2018 14:43
@samuelhwilliams
Copy link
Contributor

Needs a version bump in dmutils/__init__. Either a minor, or a major if we want to say that forms must be updated to this standard when this is pulled in. In the latter case, we'll also need to update the changelog.

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.

@lfdebrux
Copy link
Contributor Author

lfdebrux commented Jun 5, 2018

@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.

@samuelhwilliams
Copy link
Contributor

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...)

@lfdebrux
Copy link
Contributor Author

lfdebrux commented Jun 5, 2018

Tests have been added, which resulted in some fixes, as tests do



class DMBooleanField(DMFieldMixin, wtforms.fields.BooleanField):
pass
Copy link
Contributor

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?

Copy link
Contributor Author

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.

option['description'] = choice[2]

# construct a choices argument suitable for wtforms.fields.RadioField
choices = [(option['value'], option['label']) for option in self.options]
Copy link
Contributor

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)

Copy link
Contributor Author

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?

@lfdebrux
Copy link
Contributor Author

lfdebrux commented Jun 5, 2018

I'm going to do a rebase and force push now to clean up the commit history

@galund
Copy link
Contributor

galund commented Jun 8, 2018

Would like to have a general discussion about this, but am vaguely thinking I'd be in favour of just having these replacing/augmenting forms.fields rather than in their own dm_fields thing. Appreciate others have expressed different views so we should definitely discuss before deciding.

'''
def __init__(self, label=None, validators=None, hint=None, **kwargs):
self.hint = hint
self.question = label
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done


super().__init__(label=label, validators=validators, **kwargs)

@property
def question(self):
return self.label
Copy link
Contributor

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.

Copy link
Contributor Author

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

@@ -17,7 +17,7 @@
class StripWhitespaceStringField(StringField):
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

option['value'] = choice[0]
option['label'] = choice[1]
if len(choice) > 2 and choice[2]:
option['description'] = choice[2]
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

(StripWhitespaceStringFieldForm(), StripWhitespaceStringField),
))
def test_form_with_field(form, field_class):
assert isinstance(form.field, field_class)
Copy link
Contributor

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.

Copy link
Contributor Author

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.



class DMBooleanField(DMFieldMixin, wtforms.fields.BooleanField):
pass
Copy link
Contributor

@galund galund Jun 27, 2018

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Got it, will add 👍

@lfdebrux lfdebrux dismissed DilwoarH’s stale review July 2, 2018 13:45

Changes have been made

@lfdebrux lfdebrux force-pushed the ldeb-wtforms-dmfields branch from 3f75626 to a42e3af Compare July 2, 2018 14:51
@lfdebrux
Copy link
Contributor Author

lfdebrux commented Jul 2, 2018

I've just rebased the changes on master, and also added options to DMBooleanField as @galund suggested, and made this PR into a true breaking change by completely removing the old field code.

@lfdebrux lfdebrux force-pushed the ldeb-wtforms-dmfields branch 2 times, most recently from b2d0cf8 to 22ebeb9 Compare July 2, 2018 15:58
Copy link
Contributor

@galund galund left a 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.

@lfdebrux lfdebrux force-pushed the ldeb-wtforms-dmfields branch 4 times, most recently from 6360b10 to 0b1b296 Compare July 13, 2018 14:15
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.
@lfdebrux lfdebrux force-pushed the ldeb-wtforms-dmfields branch from 0b1b296 to a8ce893 Compare August 6, 2018 09:28
@lfdebrux lfdebrux requested review from risicle, katstevens, Wynndow and benvand and removed request for brenetic August 6, 2018 10:47
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.
Copy link
Contributor

Choose a reason for hiding this comment

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

insert amazon affiliate link

Copy link
Contributor Author

@lfdebrux lfdebrux Aug 6, 2018

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 😛

@risicle
Copy link
Contributor

risicle commented Aug 6, 2018

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 {{ someform.somefield.get_dmfrontend_widget() }}?

Or even just override wtforms standard way of doing this - __html__() or something?

@lfdebrux
Copy link
Contributor Author

lfdebrux commented Aug 6, 2018

@risicle that functionality is already there, I'll update the documentation to reflect that

@lfdebrux lfdebrux force-pushed the ldeb-wtforms-dmfields branch from a8ce893 to 84902c5 Compare August 6, 2018 11:31
Copy link
Contributor

@risicle risicle left a comment

Choose a reason for hiding this comment

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

Neat

Copy link
Contributor

@katstevens katstevens left a 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?

@lfdebrux
Copy link
Contributor Author

lfdebrux commented Aug 6, 2018

@katstevens 6 apps in total:

1. admin-frontend
2. brief-responses-frontend
3. supplier-frontend
4. buyer-frontend
5. briefs-frontend
6. user-frontend

I've already done the work for buyer-frontend here

@lfdebrux lfdebrux merged commit ff95fbc into master Aug 6, 2018
@lfdebrux lfdebrux deleted the ldeb-wtforms-dmfields branch August 6, 2018 15:21
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.

6 participants