diff --git a/templates/taxbrain/input_form.html b/templates/taxbrain/input_form.html
index 85b0f56a..15a33163 100644
--- a/templates/taxbrain/input_form.html
+++ b/templates/taxbrain/input_form.html
@@ -192,6 +192,7 @@
User tips
DO NOT use commas as thousands separators.
Express rates as decimals.
You can represent a value that would otherwise manifest itself in a year with the * operator. So, for example, if you enter 0.1, *, 0.2 in a tax rate field, the rate will be 10 percent in the Start Year, 10 percent a year after the Start Year, and 20 percent two years after the Start Year and forward. If you enter 100, *, 200 in a CPI-indexed field for a credit amount, the credit will be $100 in the Start Year, $100*(1+CPI) the next year, and $200 the following year.
+ You may specify a parameter change in the year before the Start Year with the < operator. For example, if you set the Start Year as 2018 and a Parameter Indexing Offset parameter to <,-0.0025,-0.001 then this sets the Offset to -0.0025 in 2017 and -0.001 in 2018.
Read the Docs
Leave Feedback
diff --git a/webapp/apps/taxbrain/forms.py b/webapp/apps/taxbrain/forms.py
index e60b9d90..f6c09ed6 100644
--- a/webapp/apps/taxbrain/forms.py
+++ b/webapp/apps/taxbrain/forms.py
@@ -275,11 +275,12 @@ def do_taxcalc_validations(self):
continue
submitted_col_values_raw = self.cleaned_data[col_field.id]
-
if len(submitted_col_values_raw) > 0 and submitted_col_values_raw not in BOOLEAN_FLAGS:
try:
INPUT.parseString(submitted_col_values_raw)
- except ParseException as pe:
+ # reverse character is not at the beginning
+ assert submitted_col_values_raw.find('<') <= 0
+ except (ParseException, AssertionError):
# Parse Error - we don't recognize what they gave us
self.add_error(col_field.id, "Unrecognized value: {}".format(submitted_col_values_raw))
diff --git a/webapp/apps/taxbrain/helpers.py b/webapp/apps/taxbrain/helpers.py
index e2169975..d77aa858 100644
--- a/webapp/apps/taxbrain/helpers.py
+++ b/webapp/apps/taxbrain/helpers.py
@@ -37,13 +37,14 @@
DEC_POINT = pp.Word('.', exact=1)
FLOAT_LIT_FULL = pp.Word(pp.nums + '.' + pp.nums)
COMMON = pp.Word(",", exact=1)
+REVERSE = pp.Word("<") + COMMON
VALUE = WILDCARD | NEG_DASH | FLOAT_LIT_FULL | FLOAT_LIT | INT_LIT
MORE_VALUES = COMMON + VALUE
BOOL = WILDCARD | TRUE | FALSE
MORE_BOOLS = COMMON + BOOL
-INPUT = BOOL + pp.ZeroOrMore(MORE_BOOLS) | VALUE + pp.ZeroOrMore(MORE_VALUES)
+INPUT = pp.Optional(REVERSE) + BOOL + pp.ZeroOrMore(MORE_BOOLS) | pp.Optional(REVERSE) + VALUE + pp.ZeroOrMore(MORE_VALUES)
TRUE_REGEX = re.compile('(?i)true')
FALSE_REGEX = re.compile('(?i)false')
@@ -54,6 +55,14 @@ def is_wildcard(x):
else:
return False
+
+def is_reverse(x):
+ if isinstance(x, six.string_types):
+ return x in ['<', u'<'] or x.strip() in ['<', u'<']
+ else:
+ return False
+
+
def check_wildcards(x):
if isinstance(x, list):
return any([check_wildcards(i) for i in x])
@@ -88,6 +97,8 @@ def make_bool(x):
def convert_val(x):
if is_wildcard(x):
return x
+ if is_reverse(x):
+ return x
try:
return float(x)
except ValueError:
@@ -399,6 +410,7 @@ def to_json_reform(fields, start_year, cls=taxcalc.Policy):
returns json style reform
"""
map_back_to_tb = {}
+ number_reverse_operators = 1
default_params = cls.default_data(start_year=start_year,
metadata=True)
ignore = (u'has_errors', u'csrfmiddlewaretoken', u'start_year',
@@ -417,14 +429,32 @@ def to_json_reform(fields, start_year, cls=taxcalc.Policy):
assert isinstance(fields[param], bool) and param.endswith('_cpi')
reform[param_name][str(start_year)] = fields[param]
continue
- for i in range(len(fields[param])):
+ i = 0
+ while i < len(fields[param]):
if is_wildcard(fields[param][i]):
# may need to do something here
pass
+ elif is_reverse(fields[param][i]):
+ # only the first character can be a reverse char
+ # and there must be a following character
+ assert len(fields[param]) > 1
+ # set value for parameter in start_year - 1
+ assert (isinstance(fields[param][i + 1], (int, float)) or
+ isinstance(fields[param][i + 1], bool))
+ reform[param_name][str(start_year - 1)] = \
+ [fields[param][i + 1]]
+
+ # realign year and parameter indices
+ for op in (0, number_reverse_operators + 1):
+ fields[param].pop(0)
+ continue
else:
assert (isinstance(fields[param][i], (int, float)) or
isinstance(fields[param][i], bool))
- reform[param_name][str(start_year + i)] = [fields[param][i]]
+ reform[param_name][str(start_year + i)] = \
+ [fields[param][i]]
+
+ i += 1
return reform, map_back_to_tb
diff --git a/webapp/apps/taxbrain/models.py b/webapp/apps/taxbrain/models.py
index 12e2c68a..c87f455e 100644
--- a/webapp/apps/taxbrain/models.py
+++ b/webapp/apps/taxbrain/models.py
@@ -37,7 +37,7 @@ def numberfy(x):
COMMASEP_REGEX = "(\\d*\\.\\d+|\\d+)|((?i)(true|false))"
class CommaSeparatedField(models.CharField):
- default_validators = [validators.RegexValidator(regex="(\\d*\\.\\d+|\\d+)|((?i)(true|false))")]
+ default_validators = [validators.RegexValidator(regex="(<,)|(\\d*\\.\\d+|\\d+)|((?i)(true|false))")]
description = "A comma separated field that allows multiple floats."
def __init__(self, verbose_name=None, name=None, **kwargs):
diff --git a/webapp/apps/taxbrain/tests/test_helpers.py b/webapp/apps/taxbrain/tests/test_helpers.py
index b0cd566f..0c98fbb9 100644
--- a/webapp/apps/taxbrain/tests/test_helpers.py
+++ b/webapp/apps/taxbrain/tests/test_helpers.py
@@ -4,7 +4,8 @@
import taxcalc
import pyparsing as pp
from ..helpers import (nested_form_parameters, rename_keys, INPUT, make_bool,
- TaxCalcParam)
+ is_reverse, TaxCalcParam)
+
CURRENT_LAW_POLICY = """
{
@@ -139,12 +140,14 @@ def test_rename_keys(monkeypatch):
'False', 'false', 'FALSE','faLSe',
'true,*', '*, true', '*,*,false',
'true,*,false,*,*,true',
- '1,*,False', '0.0,True', '1.0,False']
+ '1,*,False', '0.0,True', '1.0,False',
+ '<,True', '<,1']
)
def test_parsing_pass(item):
INPUT.parseString(item)
-def test_parsing_fail():
+@pytest.mark.parametrize('item', ['abc', '<,', '<', '1,<', '0,<,1', 'True,<', '-0.002,<,-0.001'])
+def test_parsing_fail(item):
with pytest.raises(pp.ParseException):
INPUT.parseString('abc')
@@ -166,6 +169,12 @@ def test_make_bool_fail(item):
with pytest.raises((ValueError, TypeError)):
make_bool(item)
+@pytest.mark.parametrize(
+ 'item,exp',
+ [('<', True), ('a', False), ('1', False), (1, False), (False, False)])
+def test_is_reverse(item, exp):
+ assert is_reverse(item) is exp
+
def test_make_taxcalc_param():
"""
diff --git a/webapp/apps/taxbrain/tests/test_views.py b/webapp/apps/taxbrain/tests/test_views.py
index a82d2ec6..0953622e 100644
--- a/webapp/apps/taxbrain/tests/test_views.py
+++ b/webapp/apps/taxbrain/tests/test_views.py
@@ -361,7 +361,7 @@ def test_taxbrain_wildcard_params_with_validation_gives_error(self):
assert response.context['has_errors'] is True
- def test_taxbrain_wildcard_in_validation_params_OK(self):
+ def test_taxbrain_spec_operators_in_validation_params_OK(self):
"""
Set upper threshold for income tax bracket 1 to *, 38000
Set upper threshold for income tax bracket 2 to *, *, 39500
@@ -369,9 +369,27 @@ def test_taxbrain_wildcard_in_validation_params_OK(self):
"""
data = get_post_data(START_YEAR, _ID_BenefitSurtax_Switches=False)
mod = {u'II_brk1_0': [u'*, *, 38000'],
- u'II_brk2_0': [u'*, *, 39500']}
+ u'II_brk2_0': [u'*, *, 39500'],
+ u'cpi_offset': [u'<,-0.0025'],
+ u'FICA_ss_trt': [u'< ,0.1,*,0.15,0.2']}
data.update(mod)
- do_micro_sim(self.client, data)
+ result = do_micro_sim(self.client, data)
+
+ truth_mods = {
+ START_YEAR - 1: {
+ '_cpi_offset': [-0.0025],
+ '_FICA_ss_trt': [0.1]
+ },
+ START_YEAR + 1: {
+ '_FICA_ss_trt': [0.15]
+ },
+ START_YEAR + 2: {
+ '_FICA_ss_trt': [0.2]
+ }
+ }
+
+ check_posted_params(result['tb_dropq_compute'], truth_mods, START_YEAR)
+
def test_taxbrain_wildcard_in_validation_params_gives_error(self):
@@ -395,6 +413,39 @@ def test_taxbrain_wildcard_in_validation_params_gives_error(self):
self.assertEqual(response.status_code, 200)
assert response.context['has_errors'] is True
+
+ def test_taxbrain_improper_reverse_gives_error1(self):
+ """
+ Check reverse operator post without other numbers throws error
+ """
+ #Monkey patch to mock out running of compute jobs
+ get_dropq_compute_from_module('webapp.apps.taxbrain.views')
+
+ data = get_post_data(START_YEAR, _ID_BenefitSurtax_Switches=False)
+ mod = {u'cpi_offset': [u'<,']}
+ data.update(mod)
+
+ response = self.client.post('/taxbrain/', data)
+ # Check that redirect happens
+ self.assertEqual(response.status_code, 200)
+ assert response.context['has_errors'] is True
+
+ def test_taxbrain_improper_reverse_gives_error2(self):
+ """
+ Check reverse operator not in first position throws error
+ """
+ #Monkey patch to mock out running of compute jobs
+ get_dropq_compute_from_module('webapp.apps.taxbrain.views')
+
+ data = get_post_data(START_YEAR, _ID_BenefitSurtax_Switches=False)
+ mod = {u'cpi_offset': [u'-0.002,<,-0.001']}
+ data.update(mod)
+
+ response = self.client.post('/taxbrain/', data)
+ # Check that redirect happens
+ self.assertEqual(response.status_code, 200)
+ assert response.context['has_errors'] is True
+
def test_taxbrain_bool_separated_values(self):
"""
Test _DependentCredit_before_CTC can be posted as comma separated
diff --git a/webapp/apps/taxbrain/views.py b/webapp/apps/taxbrain/views.py
index 4998a236..0e0c9339 100644
--- a/webapp/apps/taxbrain/views.py
+++ b/webapp/apps/taxbrain/views.py
@@ -798,7 +798,8 @@ def edit_personal_results(request, pk):
'webapp_version': webapp_vers_disp,
'start_years': START_YEARS,
'start_year': str(start_year),
- 'is_edit_page': True
+ 'is_edit_page': True,
+ 'has_errors': False
}
return render(request, 'taxbrain/input_form.html', init_context)
diff --git a/webapp/apps/test_assets/test_reform.py b/webapp/apps/test_assets/test_reform.py
index fa1c8973..31d89200 100644
--- a/webapp/apps/test_assets/test_reform.py
+++ b/webapp/apps/test_assets/test_reform.py
@@ -204,8 +204,10 @@
}
test_coverage_fields = dict(
+ cpi_offset = ['<', -0.0025],
CG_nodiff = [False],
- FICA_ss_trt = [u'*', 0.1, u'*', 0.2],
+ FICA_ss_trt = ['<',0.1,'*',0.15,0.2],
+ FICA_mc_trt = ['<',0.1,0.15],
STD_0 = [8000.0, '*', 10000.0],
ID_BenefitSurtax_Switch_0 = [True],
ID_Charity_c_cpi = True,
@@ -214,8 +216,10 @@
)
test_coverage_reform = {
+ '_cpi_offset': {'2016': [-0.0025]},
'_CG_nodiff': {'2017': [False]},
- '_FICA_ss_trt': {'2020': [0.2], '2018': [0.1]},
+ '_FICA_ss_trt': {'2016': [0.1], '2018': [0.15], '2019': [0.2]},
+ '_FICA_mc_trt': {'2016': [0.1], '2017': [0.15]},
'_STD_single': {'2017': [8000.0], '2019': [10000.0]},
'_ID_Charity_c_cpi': {'2017': True},
'_ID_BenefitSurtax_Switch_medical': {'2017': [True]},
@@ -267,7 +271,8 @@
u'_ID_BenefitCap_Switch_realestate': 'ID_BenefitCap_Switch_2',
u'_STD_single': 'STD_0',
u'_STD_headhousehold': 'STD_3',
- u'_II_brk4_single': 'II_brk4_0'
+ u'_II_brk4_single': 'II_brk4_0',
+ u'_cpi_offset': 'cpi_offset'
}
test_coverage_json_reform = """