From 83e8ec6541b74781124bc34f29130815ceca1dbb Mon Sep 17 00:00:00 2001 From: Henry Doupe Date: Wed, 28 Feb 2018 22:49:29 -0500 Subject: [PATCH 1/8] Generate new form data for each page load instead manipulating previous data --- webapp/apps/taxbrain/forms.py | 126 ++++++++++++++++++---------------- 1 file changed, 68 insertions(+), 58 deletions(-) diff --git a/webapp/apps/taxbrain/forms.py b/webapp/apps/taxbrain/forms.py index 61c352b6..6e97406b 100644 --- a/webapp/apps/taxbrain/forms.py +++ b/webapp/apps/taxbrain/forms.py @@ -76,6 +76,61 @@ def expand_unless_empty(param_values, param_name, param_column_name, form, new_l return param_values +def set_form(): + """ + Setup all of the form fields and widgets with the the 2016 default data + """ + widgets = {} + labels = {} + update_fields = {} + boolean_fields = [] + + for param in TAXCALC_DEFAULTS_2016.values(): + for field in param.col_fields: + attrs = { + 'class': 'form-control', + 'placeholder': field.default_value, + } + + if param.coming_soon: + attrs['disabled'] = True + + if param.tc_id in boolean_fields: + checkbox = forms.CheckboxInput(attrs=attrs, check_test=bool_like) + widgets[field.id] = checkbox + update_fields[field.id] = forms.BooleanField( + label='', + widget=widgets[field.id], + required=False + ) + else: + widgets[field.id] = forms.TextInput(attrs=attrs) + update_fields[field.id] = forms.fields.CharField( + label='', + widget=widgets[field.id], + required=False + ) + + labels[field.id] = field.label + + if param.inflatable: + field = param.cpi_field + attrs = { + 'class': 'form-control sr-only', + 'placeholder': bool(field.default_value), + } + + if param.coming_soon: + attrs['disabled'] = True + + widgets[field.id] = forms.NullBooleanSelect(attrs=attrs) + update_fields[field.id] = forms.NullBooleanField( + label='', + widget=widgets[field.id], + required=False + ) + return widgets, labels, update_fields + class PolicyBrainForm: @@ -138,6 +193,7 @@ def do_taxcalc_validations(self): class TaxBrainForm(PolicyBrainForm, ModelForm): def __init__(self, first_year, *args, **kwargs): + self.set_form_data() # move parameter fields into `raw_fields` JSON object args = self.add_fields(args) # Override `initial` with `instance`. The only relevant field @@ -160,12 +216,15 @@ def __init__(self, first_year, *args, **kwargs): # '3': False # value_from_datadict unpacks this data: # https://github.com/django/django/blob/1.9/django/forms/widgets.py#L582-L589 - django_val = self._meta.widgets[k].value_from_datadict( + if v == '1': + continue + django_val = self.widgets[k].value_from_datadict( kwargs["initial"], None, k ) - self._meta.widgets[k].attrs["placeholder"] = django_val + self.widgets[k].attrs["placeholder"] = django_val + if first_year is None: first_year = START_YEAR self._first_year = int(first_year) @@ -180,16 +239,13 @@ def __init__(self, first_year, *args, **kwargs): all_defaults.append((field.id, field.default_value)) for _id, default in all_defaults: - self._meta.widgets[_id].attrs['placeholder'] = default + self.widgets[_id].attrs['placeholder'] = default super(TaxBrainForm, self).__init__(*args, **kwargs) + # update fields in a similar way as # https://www.pydanny.com/overloading-form-fields.html - self.fields.update(self.Meta.update_fields) - - print('SS_Earnings_c_cpi field', self.fields['SS_Earnings_c_cpi'].__dict__) - print('SS_Earnings_c_cpi widget', self.fields['SS_Earnings_c_cpi'].widget.__dict__) - print('SS_Earnings_c_cpi meta widget', self._meta.widgets['SS_Earnings_c_cpi'].__dict__) + self.fields.update(self.update_fields.copy()) def clean(self): """ @@ -215,61 +271,15 @@ def add_error(self, field, error): self.cleaned_data = {} ModelForm.add_error(self, field, error) - class Meta: + def set_form_data(self): + self.widgets, self.labels, self.update_fields = set_form() + class Meta: model = TaxSaveInputs # we are only updating the "first_year", "raw_fields", and "fields" # fields fields = ['first_year', 'raw_input_fields', 'input_fields'] - widgets = {} - labels = {} - update_fields = {} - boolean_fields = [] - - for param in TAXCALC_DEFAULTS_2016.values(): - for field in param.col_fields: - attrs = { - 'class': 'form-control', - 'placeholder': field.default_value, - } - - if param.coming_soon: - attrs['disabled'] = True - - if param.tc_id in boolean_fields: - checkbox = forms.CheckboxInput(attrs=attrs, check_test=bool_like) - widgets[field.id] = checkbox - update_fields[field.id] = forms.BooleanField( - label='', - widget=widgets[field.id], - required=False - ) - else: - widgets[field.id] = forms.TextInput(attrs=attrs) - update_fields[field.id] = forms.fields.CharField( - label='', - widget=widgets[field.id], - required=False - ) - - labels[field.id] = field.label - - if param.inflatable: - field = param.cpi_field - attrs = { - 'class': 'form-control sr-only', - 'placeholder': bool(field.default_value), - } - - if param.coming_soon: - attrs['disabled'] = True - - widgets[field.id] = forms.NullBooleanSelect(attrs=attrs) - update_fields[field.id] = forms.NullBooleanField( - label='', - widget=widgets[field.id], - required=False - ) + widgets, labels, update_fields = set_form() def has_field_errors(form, include_parse_errors=False): """ From 1f3098080abffdb8a090e11ee0608262bd761722 Mon Sep 17 00:00:00 2001 From: Henry Doupe Date: Thu, 1 Mar 2018 10:38:28 -0500 Subject: [PATCH 2/8] Update data all times where 'initial' arg is set --- webapp/apps/taxbrain/forms.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/webapp/apps/taxbrain/forms.py b/webapp/apps/taxbrain/forms.py index 6e97406b..e1cb9be2 100644 --- a/webapp/apps/taxbrain/forms.py +++ b/webapp/apps/taxbrain/forms.py @@ -182,7 +182,6 @@ def do_taxcalc_validations(self): ("Operator '<' can only be used " "at the beginning") ) - else: assert isinstance(value, bool) or len(value) == 0 @@ -207,6 +206,12 @@ def __init__(self, first_year, *args, **kwargs): # https://github.com/django/django/blob/1.9/django/forms/models.py#L284-L285 if "instance" in kwargs: kwargs["initial"] = kwargs["instance"].raw_input_fields + + # Update CPI flags if either + # 1. initial is specified in `kwargs` (reform has warning/error msgs) + # 2. if `instance` is specified and `initial` is added above + # (edit parameters page) + if kwargs.get("initial", False): for k, v in kwargs["initial"].iteritems(): if k.endswith("cpi") and v: # raw data is stored as choices 1, 2, 3 with the following From 2fd0e82e99d74bbe703165d9b487da606fd2a395 Mon Sep 17 00:00:00 2001 From: Henry Doupe Date: Thu, 1 Mar 2018 10:45:57 -0500 Subject: [PATCH 3/8] Update TaxbrainForm comments --- webapp/apps/taxbrain/forms.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/webapp/apps/taxbrain/forms.py b/webapp/apps/taxbrain/forms.py index e1cb9be2..444ec3e6 100644 --- a/webapp/apps/taxbrain/forms.py +++ b/webapp/apps/taxbrain/forms.py @@ -192,6 +192,8 @@ def do_taxcalc_validations(self): class TaxBrainForm(PolicyBrainForm, ModelForm): def __init__(self, first_year, *args, **kwargs): + # reset form data; form data from the `Meta` class is not updated each + # time a new `TaxBrainForm` instance is created self.set_form_data() # move parameter fields into `raw_fields` JSON object args = self.add_fields(args) From 37e3922a66e0ae085f88c9e8497745e101b4c741 Mon Sep 17 00:00:00 2001 From: Henry Doupe Date: Thu, 1 Mar 2018 11:12:57 -0500 Subject: [PATCH 4/8] Update RELEASES for pr 829 --- RELEASES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASES.md b/RELEASES.md index 47f3849b..d25bda91 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -14,6 +14,7 @@ Release 1.4.2 on 2018-02-28 **Bug Fixes** - [#827](https://github.com/OpenSourcePolicyCenter/PolicyBrain/pull/827) - Remove errors for un-displayed parameters and save input data for warning/error message page - Hank Doupe +- [#829](https://github.com/OpenSourcePolicyCenter/PolicyBrain/pull/829) - Generate new form data for each page load - Hank Doupe Release 1.4.1 on 2018-02-27 From 83ea5efc1a55cf65e0214bd949fbbb8f0b4a8767 Mon Sep 17 00:00:00 2001 From: Henry Doupe Date: Thu, 1 Mar 2018 11:15:42 -0500 Subject: [PATCH 5/8] Move set_form into PolicyBrainForm mixin class --- webapp/apps/taxbrain/forms.py | 116 ++++++++++++++++++---------------- 1 file changed, 60 insertions(+), 56 deletions(-) diff --git a/webapp/apps/taxbrain/forms.py b/webapp/apps/taxbrain/forms.py index 444ec3e6..80bc8caa 100644 --- a/webapp/apps/taxbrain/forms.py +++ b/webapp/apps/taxbrain/forms.py @@ -76,60 +76,6 @@ def expand_unless_empty(param_values, param_name, param_column_name, form, new_l return param_values -def set_form(): - """ - Setup all of the form fields and widgets with the the 2016 default data - """ - widgets = {} - labels = {} - update_fields = {} - boolean_fields = [] - - for param in TAXCALC_DEFAULTS_2016.values(): - for field in param.col_fields: - attrs = { - 'class': 'form-control', - 'placeholder': field.default_value, - } - - if param.coming_soon: - attrs['disabled'] = True - - if param.tc_id in boolean_fields: - checkbox = forms.CheckboxInput(attrs=attrs, check_test=bool_like) - widgets[field.id] = checkbox - update_fields[field.id] = forms.BooleanField( - label='', - widget=widgets[field.id], - required=False - ) - else: - widgets[field.id] = forms.TextInput(attrs=attrs) - update_fields[field.id] = forms.fields.CharField( - label='', - widget=widgets[field.id], - required=False - ) - - labels[field.id] = field.label - - if param.inflatable: - field = param.cpi_field - attrs = { - 'class': 'form-control sr-only', - 'placeholder': bool(field.default_value), - } - - if param.coming_soon: - attrs['disabled'] = True - - widgets[field.id] = forms.NullBooleanSelect(attrs=attrs) - update_fields[field.id] = forms.NullBooleanField( - label='', - widget=widgets[field.id], - required=False - ) - return widgets, labels, update_fields class PolicyBrainForm: @@ -185,6 +131,62 @@ def do_taxcalc_validations(self): else: assert isinstance(value, bool) or len(value) == 0 + @staticmethod + def set_form(defaults): + """ + Setup all of the form fields and widgets with the the 2016 default data + """ + widgets = {} + labels = {} + update_fields = {} + boolean_fields = [] + + for param in defaults.values(): + for field in param.col_fields: + attrs = { + 'class': 'form-control', + 'placeholder': field.default_value, + } + + if param.coming_soon: + attrs['disabled'] = True + + if param.tc_id in boolean_fields: + checkbox = forms.CheckboxInput(attrs=attrs, check_test=bool_like) + widgets[field.id] = checkbox + update_fields[field.id] = forms.BooleanField( + label='', + widget=widgets[field.id], + required=False + ) + else: + widgets[field.id] = forms.TextInput(attrs=attrs) + update_fields[field.id] = forms.fields.CharField( + label='', + widget=widgets[field.id], + required=False + ) + + labels[field.id] = field.label + + if getattr(param, "inflatable", False): + field = param.cpi_field + attrs = { + 'class': 'form-control sr-only', + 'placeholder': bool(field.default_value), + } + + if param.coming_soon: + attrs['disabled'] = True + + widgets[field.id] = forms.NullBooleanSelect(attrs=attrs) + update_fields[field.id] = forms.NullBooleanField( + label='', + widget=widgets[field.id], + required=False + ) + return widgets, labels, update_fields + TAXCALC_DEFAULTS_2016 = default_policy(2016) @@ -279,14 +281,16 @@ def add_error(self, field, error): ModelForm.add_error(self, field, error) def set_form_data(self): - self.widgets, self.labels, self.update_fields = set_form() + (self.widgets, self.labels, + self.update_fields) = PolicyBrainForm.set_form(TAXCALC_DEFAULTS_2016) class Meta: model = TaxSaveInputs # we are only updating the "first_year", "raw_fields", and "fields" # fields fields = ['first_year', 'raw_input_fields', 'input_fields'] - widgets, labels, update_fields = set_form() + (widgets, labels, + update_fields) = PolicyBrainForm.set_form(TAXCALC_DEFAULTS_2016) def has_field_errors(form, include_parse_errors=False): """ From 98f69c32b385427a28edc5bdc5a792ae572d0059 Mon Sep 17 00:00:00 2001 From: Henry Doupe Date: Thu, 1 Mar 2018 11:16:14 -0500 Subject: [PATCH 6/8] Port TaxBrain static changes to dynamic-behavorial changes --- webapp/apps/dynamic/forms.py | 45 +++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/webapp/apps/dynamic/forms.py b/webapp/apps/dynamic/forms.py index 13b10a6c..dd327122 100644 --- a/webapp/apps/dynamic/forms.py +++ b/webapp/apps/dynamic/forms.py @@ -217,15 +217,29 @@ class Meta: class DynamicBehavioralInputsModelForm(PolicyBrainForm, ModelForm): def __init__(self, first_year, *args, **kwargs): + # reset form data; form data from the `Meta` class is not updated each + # time a new `TaxBrainForm` instance is created + self.set_form_data() + # move parameter fields into `raw_fields` JSON object args = self.add_fields(args) - # this seems to update the saved data in the appropriate way + # Override `initial` with `instance`. The only relevant field + # in `instance` is `raw_input_fields` which contains all of the user + # input data from the stored run. By overriding the `initial` kw + # argument we are making all of the user input from the previous run + # as stored in the `raw_input_fields` field of `instance` available + # to the fields attribute in django forms. This front-end data is + # derived from this fields attribute. + # Take a look at the source code for more info: + # https://github.com/django/django/blob/1.9/django/forms/models.py#L284-L285 if "instance" in kwargs: kwargs["initial"] = kwargs["instance"].raw_input_fields + if first_year is None: + first_year = START_YEAR self._first_year = int(first_year) self._default_params = default_behavior_parameters(self._first_year) # Defaults are set in the Meta, but we need to swap - # those outs here in the init because the user may + # those out here in the init because the user may # have chosen a different start year all_defaults = [] for param in self._default_params.values(): @@ -233,9 +247,10 @@ def __init__(self, first_year, *args, **kwargs): all_defaults.append((field.id, field.default_value)) for _id, default in all_defaults: - self._meta.widgets[_id].attrs['placeholder'] = default + self.widgets[_id].attrs['placeholder'] = default super(DynamicBehavioralInputsModelForm, self).__init__(*args, **kwargs) + # update fields in a similar way as # https://www.pydanny.com/overloading-form-fields.html self.fields.update(self.Meta.update_fields) @@ -254,30 +269,18 @@ def clean(self): self.do_taxcalc_validations() self.add_errors_on_extra_inputs() + def set_form_data(self): + (self.widgets, self.labels, + self.update_fields) = PolicyBrainForm.set_form(BEHAVIOR_DEFAULT_PARAMS) + class Meta: model = DynamicBehaviorSaveInputs # we are only updating the "first_year", "raw_fields", and "fields" # fields fields = ['first_year', 'raw_input_fields', 'input_fields'] - widgets = {} - labels = {} - update_fields = {} - for param in BEHAVIOR_DEFAULT_PARAMS.values(): - for field in param.col_fields: - attrs = { - 'class': 'form-control', - 'placeholder': field.default_value, - } - - widgets[field.id] = forms.TextInput(attrs=attrs) - update_fields[field.id] = forms.BooleanField( - label='', - widget=widgets[field.id], - required=False - ) - - labels[field.id] = field.label + (widgets, labels, + update_fields) = PolicyBrainForm.set_form(BEHAVIOR_DEFAULT_PARAMS) class DynamicInputsModelForm(ModelForm): From f6e8d0e62be6ad1598400c6657282ff98d412353 Mon Sep 17 00:00:00 2001 From: Henry Doupe Date: Thu, 1 Mar 2018 11:44:54 -0500 Subject: [PATCH 7/8] Use START_YEAR to load data by default and cached static default data --- webapp/apps/dynamic/forms.py | 32 ++++++++++------------------- webapp/apps/taxbrain/forms.py | 38 ++++++++++++++++------------------- 2 files changed, 28 insertions(+), 42 deletions(-) diff --git a/webapp/apps/dynamic/forms.py b/webapp/apps/dynamic/forms.py index dd327122..981917e8 100644 --- a/webapp/apps/dynamic/forms.py +++ b/webapp/apps/dynamic/forms.py @@ -2,6 +2,7 @@ from django.forms import ModelForm from django.utils.translation import ugettext_lazy as _ +from ..constants import START_YEAR from .models import (DynamicSaveInputs, DynamicBehaviorSaveInputs, DynamicElasticitySaveInputs) from ..taxbrain.helpers import (TaxCalcField, TaxCalcParam, @@ -15,9 +16,9 @@ def bool_like(x): b = True if x == 'True' or x == True else False return b -OGUSA_DEFAULT_PARAMS = default_parameters(2015) -BEHAVIOR_DEFAULT_PARAMS = default_behavior_parameters(2015) -ELASTICITY_DEFAULT_PARAMS = default_elasticity_parameters(2015) +OGUSA_DEFAULT_PARAMS = default_parameters(int(START_YEAR)) +BEHAVIOR_DEFAULT_PARAMS = default_behavior_parameters(int(START_YEAR)) +ELASTICITY_DEFAULT_PARAMS = default_elasticity_parameters(int(START_YEAR)) class DynamicElasticityInputsModelForm(ModelForm): @@ -217,9 +218,12 @@ class Meta: class DynamicBehavioralInputsModelForm(PolicyBrainForm, ModelForm): def __init__(self, first_year, *args, **kwargs): + if first_year is None: + first_year = START_YEAR + self._first_year = int(first_year) # reset form data; form data from the `Meta` class is not updated each # time a new `TaxBrainForm` instance is created - self.set_form_data() + self.set_form_data(self._first_year) # move parameter fields into `raw_fields` JSON object args = self.add_fields(args) # Override `initial` with `instance`. The only relevant field @@ -234,21 +238,6 @@ def __init__(self, first_year, *args, **kwargs): if "instance" in kwargs: kwargs["initial"] = kwargs["instance"].raw_input_fields - if first_year is None: - first_year = START_YEAR - self._first_year = int(first_year) - self._default_params = default_behavior_parameters(self._first_year) - # Defaults are set in the Meta, but we need to swap - # those out here in the init because the user may - # have chosen a different start year - all_defaults = [] - for param in self._default_params.values(): - for field in param.col_fields: - all_defaults.append((field.id, field.default_value)) - - for _id, default in all_defaults: - self.widgets[_id].attrs['placeholder'] = default - super(DynamicBehavioralInputsModelForm, self).__init__(*args, **kwargs) # update fields in a similar way as @@ -269,9 +258,10 @@ def clean(self): self.do_taxcalc_validations() self.add_errors_on_extra_inputs() - def set_form_data(self): + def set_form_data(self, start_year): + defaults = default_behavior_parameters(start_year) (self.widgets, self.labels, - self.update_fields) = PolicyBrainForm.set_form(BEHAVIOR_DEFAULT_PARAMS) + self.update_fields) = PolicyBrainForm.set_form(defaults) class Meta: diff --git a/webapp/apps/taxbrain/forms.py b/webapp/apps/taxbrain/forms.py index 80bc8caa..08678234 100644 --- a/webapp/apps/taxbrain/forms.py +++ b/webapp/apps/taxbrain/forms.py @@ -188,15 +188,19 @@ def set_form(defaults): return widgets, labels, update_fields -TAXCALC_DEFAULTS_2016 = default_policy(2016) +TAXCALC_DEFAULTS = {int(START_YEAR): default_policy(int(START_YEAR))} class TaxBrainForm(PolicyBrainForm, ModelForm): def __init__(self, first_year, *args, **kwargs): + if first_year is None: + first_year = START_YEAR + self._first_year = int(first_year) + # reset form data; form data from the `Meta` class is not updated each # time a new `TaxBrainForm` instance is created - self.set_form_data() + self.set_form_data(self._first_year) # move parameter fields into `raw_fields` JSON object args = self.add_fields(args) # Override `initial` with `instance`. The only relevant field @@ -234,22 +238,6 @@ def __init__(self, first_year, *args, **kwargs): ) self.widgets[k].attrs["placeholder"] = django_val - if first_year is None: - first_year = START_YEAR - self._first_year = int(first_year) - self._default_params = default_policy(self._first_year) - - # Defaults are set in the Meta, but we need to swap - # those out here in the init because the user may - # have chosen a different start year - all_defaults = [] - for param in self._default_params.values(): - for field in param.col_fields: - all_defaults.append((field.id, field.default_value)) - - for _id, default in all_defaults: - self.widgets[_id].attrs['placeholder'] = default - super(TaxBrainForm, self).__init__(*args, **kwargs) # update fields in a similar way as @@ -280,17 +268,25 @@ def add_error(self, field, error): self.cleaned_data = {} ModelForm.add_error(self, field, error) - def set_form_data(self): + def set_form_data(self, start_year): + if start_year not in TAXCALC_DEFAULTS: + TAXCALC_DEFAULTS[start_year] = default_policy(start_year) + defaults = TAXCALC_DEFAULTS[start_year] (self.widgets, self.labels, - self.update_fields) = PolicyBrainForm.set_form(TAXCALC_DEFAULTS_2016) + self.update_fields) = PolicyBrainForm.set_form(defaults) class Meta: model = TaxSaveInputs # we are only updating the "first_year", "raw_fields", and "fields" # fields fields = ['first_year', 'raw_input_fields', 'input_fields'] + start_year = int(START_YEAR) + if start_year not in TAXCALC_DEFAULTS: + TAXCALC_DEFAULTS[start_year] = default_policy(start_year) (widgets, labels, - update_fields) = PolicyBrainForm.set_form(TAXCALC_DEFAULTS_2016) + update_fields) = PolicyBrainForm.set_form( + TAXCALC_DEFAULTS[start_year] + ) def has_field_errors(form, include_parse_errors=False): """ From 8278eeed93a18f006090b37a5d5cb5270101cceb Mon Sep 17 00:00:00 2001 From: Henry Doupe Date: Thu, 1 Mar 2018 13:26:20 -0500 Subject: [PATCH 8/8] Whoops 2/28 isn't a thing in 2017 --- RELEASES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASES.md b/RELEASES.md index d25bda91..a8eff2de 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -4,7 +4,7 @@ Go [here](https://github.com/OpenSourcePolicyCenter/PolicyBrain/pulls?q=is%3Apr+is%3Aclosed) for a complete commit history. -Release 1.4.2 on 2018-02-28 +Release 1.4.2 on 2018-03-01 ---------------------------- **Major Changes** - None