diff --git a/RELEASES.md b/RELEASES.md index 47f3849b..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 @@ -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 diff --git a/webapp/apps/dynamic/forms.py b/webapp/apps/dynamic/forms.py index 13b10a6c..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,25 +218,28 @@ 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._first_year) + # 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 - 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 - # 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._meta.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 +258,19 @@ def clean(self): self.do_taxcalc_validations() self.add_errors_on_extra_inputs() + def set_form_data(self, start_year): + defaults = default_behavior_parameters(start_year) + (self.widgets, self.labels, + self.update_fields) = PolicyBrainForm.set_form(defaults) + 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): diff --git a/webapp/apps/taxbrain/forms.py b/webapp/apps/taxbrain/forms.py index 61c352b6..08678234 100644 --- a/webapp/apps/taxbrain/forms.py +++ b/webapp/apps/taxbrain/forms.py @@ -77,6 +77,7 @@ def expand_unless_empty(param_values, param_name, param_column_name, form, new_l return param_values + class PolicyBrainForm: def add_fields(self, args): @@ -127,17 +128,79 @@ def do_taxcalc_validations(self): ("Operator '<' can only be used " "at the beginning") ) - 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 = [] -TAXCALC_DEFAULTS_2016 = default_policy(2016) + 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 = {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._first_year) # move parameter fields into `raw_fields` JSON object args = self.add_fields(args) # Override `initial` with `instance`. The only relevant field @@ -151,6 +214,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 @@ -160,36 +229,20 @@ 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 - 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._meta.widgets[_id].attrs['placeholder'] = default + self.widgets[k].attrs["placeholder"] = django_val 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 +268,25 @@ def add_error(self, field, error): self.cleaned_data = {} ModelForm.add_error(self, field, error) - class Meta: + 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(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'] - 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 - ) + 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[start_year] + ) def has_field_errors(form, include_parse_errors=False): """