From d3b181bb7058738f4d7f0df07e0109dabc23abaf Mon Sep 17 00:00:00 2001 From: christopherpettinga Date: Wed, 28 Feb 2024 13:28:17 +0000 Subject: [PATCH] ICMSLST-1349 - SupplementaryReportForm Restrict year range for Date Received to 6 years --- pii-ner-exclude.txt | 2 + web/domains/case/_import/fa_dfl/forms.py | 6 +- web/domains/case/_import/fa_oil/forms.py | 6 +- web/domains/case/_import/fa_sil/forms.py | 6 +- web/forms/fields.py | 52 ++++++++++++++ web/static/web/js/fox/core-footer.js | 34 +++++++-- .../domains/case/test_jquery_date_field.py | 70 +++++++++++++++++++ 7 files changed, 163 insertions(+), 13 deletions(-) create mode 100644 web/tests/domains/case/test_jquery_date_field.py diff --git a/pii-ner-exclude.txt b/pii-ner-exclude.txt index d8b6d0630..26404c10c 100644 --- a/pii-ner-exclude.txt +++ b/pii-ner-exclude.txt @@ -3857,3 +3857,5 @@ Create Reports Data the Supplementary Report {{ application_field(report.transport | Add New Supplementary Report +jQuery +"Check if the date diff --git a/web/domains/case/_import/fa_dfl/forms.py b/web/domains/case/_import/fa_dfl/forms.py index afb8fc8fd..15c7fecd5 100644 --- a/web/domains/case/_import/fa_dfl/forms.py +++ b/web/domains/case/_import/fa_dfl/forms.py @@ -5,7 +5,7 @@ from web.domains.case._import.forms import ChecklistBaseForm from web.domains.case.forms import application_contacts from web.domains.file.utils import ICMSFileField -from web.forms.fields import JqueryDateField +from web.forms.fields import PastOnlyJqueryDateField from web.forms.mixins import OptionalFormMixin from web.models import Country @@ -178,7 +178,9 @@ def clean(self) -> dict[str, Any]: class DFLSupplementaryReportForm(forms.ModelForm): - date_received = JqueryDateField(required=True, label="Date Received") + date_received = PastOnlyJqueryDateField( + required=True, label="Date Received", year_select_range=6 + ) class Meta: model = models.DFLSupplementaryReport diff --git a/web/domains/case/_import/fa_oil/forms.py b/web/domains/case/_import/fa_oil/forms.py index 3bb0c33ce..80863ad55 100644 --- a/web/domains/case/_import/fa_oil/forms.py +++ b/web/domains/case/_import/fa_oil/forms.py @@ -5,7 +5,7 @@ from web.domains.case._import.forms import ChecklistBaseForm from web.domains.case.forms import application_contacts from web.domains.file.utils import ICMSFileField -from web.forms.fields import JqueryDateField +from web.forms.fields import PastOnlyJqueryDateField from web.forms.mixins import OptionalFormMixin from web.models import Country @@ -123,7 +123,9 @@ def clean(self) -> dict[str, Any]: class OILSupplementaryReportForm(forms.ModelForm): - date_received = JqueryDateField(required=True, label="Date Received") + date_received = PastOnlyJqueryDateField( + required=True, label="Date Received", year_select_range=6 + ) class Meta: model = models.OILSupplementaryReport diff --git a/web/domains/case/_import/fa_sil/forms.py b/web/domains/case/_import/fa_sil/forms.py index b53a79c1c..c03d15402 100644 --- a/web/domains/case/_import/fa_sil/forms.py +++ b/web/domains/case/_import/fa_sil/forms.py @@ -7,7 +7,7 @@ from web.domains.case._import.forms import ChecklistBaseForm from web.domains.case.forms import application_contacts -from web.forms.fields import JqueryDateField +from web.forms.fields import PastOnlyJqueryDateField from web.forms.mixins import OptionalFormMixin from web.forms.widgets import YesNoRadioSelectInline from web.models import Country, ObsoleteCalibre, Template @@ -574,7 +574,9 @@ def clean(self) -> dict[str, Any]: class SILSupplementaryReportForm(forms.ModelForm): - date_received = JqueryDateField(required=True, label="Date Received") + date_received = PastOnlyJqueryDateField( + required=True, label="Date Received", year_select_range=6 + ) class Meta: model = models.SILSupplementaryReport diff --git a/web/forms/fields.py b/web/forms/fields.py index dc601dae2..4982f8b2b 100644 --- a/web/forms/fields.py +++ b/web/forms/fields.py @@ -1,8 +1,10 @@ +import datetime as dt import re from typing import Any import phonenumber_field.formfields from django import forms +from django.forms import Widget from phonenumber_field.widgets import PhoneNumberInternationalFallbackWidget from .utils import clean_postcode @@ -69,3 +71,53 @@ def to_python(self, value: str) -> str: class JqueryDateField(forms.DateField): widget = DateInput(format=JQUERY_DATE_FORMAT) input_formats = [JQUERY_DATE_FORMAT] + + def __init__(self, *args, year_select_range: int = 100, **kwargs) -> None: + self.year_select_range = year_select_range + super().__init__(*args, **kwargs) + + def widget_attrs(self, widget: Widget) -> dict[str, Any]: + attrs = super().widget_attrs(widget) + # we want to pass this attribute to the widget so jQuery can use it + attrs["data-year-select-range"] = self.year_select_range + return attrs + + def validate(self, value: dt.datetime) -> None: + """Check if the date is within the year_select_range (if it has been provided).""" + if self.year_select_range and value: + current_year = dt.date.today().year + if abs(value.year - current_year) > self.year_select_range: + raise forms.ValidationError( + f"Date cannot be more than {self.year_select_range} years in the past/future." + ) + return super().validate(value) + + +class PastOnlyJqueryDateField(JqueryDateField): + """A jQuery datepicker field that only allows past dates.""" + + def widget_attrs(self, widget: Widget) -> dict[str, Any]: + attrs = super().widget_attrs(widget) + attrs["data-past-only"] = "yes" + return attrs + + def validate(self, value: dt.datetime) -> None: + current_year = dt.date.today().year + if value.year > current_year: + raise forms.ValidationError("Date cannot be in the future.") + return super().validate(value) + + +class FutureOnlyJqueryDateField(JqueryDateField): + """A jQuery datepicker field that only allows future dates.""" + + def widget_attrs(self, widget: Widget) -> dict[str, Any]: + attrs = super().widget_attrs(widget) + attrs["data-future-only"] = "yes" + return attrs + + def validate(self, value: dt.datetime) -> None: + current_year = dt.date.today().year + if value.year < current_year: + raise forms.ValidationError("Date cannot be in the past.") + return super().validate(value) diff --git a/web/static/web/js/fox/core-footer.js b/web/static/web/js/fox/core-footer.js index 5a7b54dab..efeb3b037 100644 --- a/web/static/web/js/fox/core-footer.js +++ b/web/static/web/js/fox/core-footer.js @@ -97,13 +97,33 @@ var FOXjs = { } // Initialise the date pickers for fields that need them - $( ".date-input").not("[readonly='readonly']").datepicker({ - changeMonth: true, - changeYear: true, - dateFormat: "dd'-'M'-'yy", - showButtonPanel: true, - yearRange: "c-100:c+100" - }); + $( ".date-input").not("[readonly='readonly']").each(function(){ + let el = $(this); + // Set the year range for the date picker, this can be customised by adding a data-year-select-range attribute + let yearSelectRange = el.attr("data-year-select-range") || 100; + + // The default year range is 100 years either side of the current year + let yearRange = `c-${yearSelectRange}:c+${yearSelectRange}`; + + // If the date picker should only allow past or future dates, set the year range accordingly + let pastOnly = el.attr("data-past-only") || false; + let futureOnly = el.attr("data-future-only") || false; + if (pastOnly) { + yearRange = `c-${yearSelectRange}:c`; + } + if (futureOnly) { + yearRange = `c:c+${yearSelectRange}`; + } + + // Finally, initialise the date picker + el.datepicker({ + changeMonth: true, + changeYear: true, + dateFormat: "dd'-'M'-'yy", + showButtonPanel: true, + yearRange: yearRange + }) + }) $( ".date-icon").click(function(){ var inputId = '#' + $(this).attr("id").replace("icon",""); diff --git a/web/tests/domains/case/test_jquery_date_field.py b/web/tests/domains/case/test_jquery_date_field.py new file mode 100644 index 000000000..66b3276eb --- /dev/null +++ b/web/tests/domains/case/test_jquery_date_field.py @@ -0,0 +1,70 @@ +import datetime as dt + +from django.forms import forms + +from web.forms.fields import ( + FutureOnlyJqueryDateField, + JqueryDateField, + PastOnlyJqueryDateField, +) + + +def test_year_select_render(): + field = JqueryDateField(year_select_range=10) + assert "data-year-select-range" in field.widget_attrs(field.widget) + assert field.widget_attrs(field.widget)["data-year-select-range"] == 10 + + # test the rendering of the widget + html = field.widget.render("name", None) + assert 'data-year-select-range="10"' in html + + +def test_year_select_incorrect_validation(): + field = JqueryDateField(year_select_range=5) + entered_value = dt.date(2000, 1, 1) + try: + field.validate(entered_value) + except forms.ValidationError as e: + assert "Date cannot be more than 5 years in the past/future." in str(e) + + +def test_year_select_correct_validation(): + field = JqueryDateField(year_select_range=10) + entered_value = dt.date(2021, 1, 1) + assert field.clean(entered_value) == entered_value + + +def test_past_only_date_field_render(): + field = PastOnlyJqueryDateField() + assert "data-past-only" in field.widget_attrs(field.widget) + assert field.widget_attrs(field.widget)["data-past-only"] == "yes" + + # test the rendering of the widget + html = field.widget.render("name", None) + assert 'data-past-only="yes"' in html + + +def test_past_only_date_field(): + field = PastOnlyJqueryDateField(year_select_range=10) + try: + field.validate(dt.datetime.now() + dt.timedelta(days=400)) + except forms.ValidationError as e: + assert "Date cannot be in the future." in str(e) + + +def test_future_only_date_field_render(): + field = FutureOnlyJqueryDateField() + assert "data-future-only" in field.widget_attrs(field.widget) + assert field.widget_attrs(field.widget)["data-future-only"] == "yes" + + # test the rendering of the widget + html = field.widget.render("name", None) + assert 'data-future-only="yes"' in html + + +def test_future_only_date_field(): + field = FutureOnlyJqueryDateField(year_select_range=10) + try: + field.validate(dt.datetime.now() - dt.timedelta(days=400)) + except forms.ValidationError as e: + assert "Date cannot be in the past." in str(e)