diff --git a/common/tests/test_models.py b/common/tests/test_models.py index 74a333b99..61d43f3ac 100644 --- a/common/tests/test_models.py +++ b/common/tests/test_models.py @@ -81,6 +81,25 @@ def test_as_at_today(model1_with_history): } +def test_as_at_today_and_beyond(date_ranges, validity_factory): + """Ensure only records active at the current date and future records are + fetched.""" + outdated_record = {validity_factory.create(valid_between=date_ranges.earlier).pk} + + active_and_future_records = { + validity_factory.create(valid_between=date_ranges.normal).pk, + validity_factory.create(valid_between=date_ranges.later).pk, + } + + test_model = validity_factory._meta.get_model_class() + queryset = set( + test_model.objects.as_at_today_and_beyond().values_list("pk", flat=True), + ) + + assert queryset != outdated_record + assert queryset == active_and_future_records + + def test_get_version_raises_error(): """Ensure that trying to get a specific version raises an error if no identifiers given.""" diff --git a/geo_areas/forms.py b/geo_areas/forms.py index c629c0442..f59241703 100644 --- a/geo_areas/forms.py +++ b/geo_areas/forms.py @@ -85,7 +85,7 @@ def __init__(self, *args, **kwargs): GeographicalArea.objects.filter(area_code=AreaCode.COUNTRY) .current() .with_latest_description() - .as_at_today() + .as_at_today_and_beyond() .order_by("description") ) self.fields[ @@ -107,7 +107,7 @@ def __init__(self, *args, **kwargs): GeographicalArea.objects.filter(area_code=AreaCode.REGION) .current() .with_latest_description() - .as_at_today() + .as_at_today_and_beyond() .order_by("description") ) self.fields[ @@ -196,7 +196,7 @@ def __init__(self, *args, **kwargs): GeographicalArea.objects.filter(area_code=AreaCode.GROUP) .current() .with_latest_description() - .as_at_today() + .as_at_today_and_beyond() .order_by("description") ) self.fields[ @@ -542,7 +542,7 @@ def __init__(self, *args, **kwargs): .exclude(pk__in=current_memberships) .current() .with_latest_description() - .as_at_today() + .as_at_today_and_beyond() .order_by("description") ) self.fields[ @@ -631,7 +631,7 @@ def __init__(self, *args, **kwargs): ) .current() .with_latest_description() - .as_at_today() + .as_at_today_and_beyond() .order_by("description") ) self.fields[ @@ -768,7 +768,7 @@ def __init__(self, *args, **kwargs): self.fields["erga_omnes_exclusion"].queryset = ( GeographicalArea.objects.current() .with_latest_description() - .as_at_today() + .as_at_today_and_beyond() .order_by("description") ) self.fields[ @@ -803,7 +803,7 @@ def __init__(self, *args, **kwargs): GeographicalArea.objects.current() .with_latest_description() .filter(area_code=AreaCode.GROUP) - .as_at_today() + .as_at_today_and_beyond() .order_by("description") ) # descriptions__description" should make this implicitly distinct() @@ -830,7 +830,7 @@ def __init__(self, *args, **kwargs): self.fields["geo_group_exclusion"].queryset = ( GeographicalArea.objects.current() .with_latest_description() - .as_at_today() + .as_at_today_and_beyond() .order_by("description") ) self.fields[ @@ -879,7 +879,7 @@ def __init__(self, *args, **kwargs): GeographicalArea.objects.current() .with_latest_description() .exclude(area_code=AreaCode.GROUP) - .as_at_today() + .as_at_today_and_beyond() .order_by("description") ) diff --git a/quotas/forms.py b/quotas/forms.py index 0a36cce69..3c5d3dbfb 100644 --- a/quotas/forms.py +++ b/quotas/forms.py @@ -90,7 +90,7 @@ def __init__(self, *args, **kwargs): self.fields["exclusion"].queryset = ( GeographicalArea.objects.current() .with_latest_description() - .as_at_today() + .as_at_today_and_beyond() .order_by("description") ) self.fields[ @@ -258,7 +258,7 @@ def init_fields(self): self.fields["geographical_area"].queryset = ( GeographicalArea.objects.current() .with_latest_description() - .as_at_today() + .as_at_today_and_beyond() .order_by("description") ) self.fields[ @@ -274,11 +274,58 @@ class Meta: model = models.QuotaDefinition fields = [ "valid_between", + "description", + "volume", + "initial_volume", + "measurement_unit", + "measurement_unit_qualifier", + "quota_critical_threshold", + "quota_critical", ] + description = forms.CharField(label="", widget=forms.Textarea(), required=False) + volume = forms.DecimalField( + label="Current volume", + widget=forms.TextInput(), + error_messages={"invalid": "Volume must be a number"}, + ) + initial_volume = forms.DecimalField( + widget=forms.TextInput(), + error_messages={"invalid": "Initial volume must be a number"}, + ) + quota_critical_threshold = forms.DecimalField( + label="Threshold", + help_text="The point at which this quota definition period becomes critical, as a percentage of the total volume.", + widget=forms.TextInput(), + error_messages={"invalid": "Critical threshold must be a number"}, + ) + quota_critical = forms.TypedChoiceField( + label="Is the quota definition period in a critical state?", + help_text="This determines if a trader needs to pay securities when utilising the quota.", + coerce=lambda value: value == "True", + choices=((True, "Yes"), (False, "No")), + widget=forms.RadioSelect(), + ) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.init_layout() + self.init_fields() + + def init_fields(self): + self.fields["measurement_unit"].queryset = self.fields[ + "measurement_unit" + ].queryset.order_by("code") + self.fields[ + "measurement_unit" + ].label_from_instance = lambda obj: f"{obj.code} - {obj.description}" + + self.fields["measurement_unit_qualifier"].queryset = self.fields[ + "measurement_unit_qualifier" + ].queryset.order_by("code") + self.fields[ + "measurement_unit_qualifier" + ].label_from_instance = lambda obj: f"{obj.code} - {obj.description}" def init_layout(self): self.helper = FormHelper(self) @@ -286,9 +333,31 @@ def init_layout(self): self.helper.legend_size = Size.SMALL self.helper.layout = Layout( - Div( - "start_date", - "end_date", + Accordion( + AccordionSection( + "Description", + "description", + ), + AccordionSection( + "Validity period", + "start_date", + "end_date", + ), + AccordionSection( + "Measurements", + Field("measurement_unit", css_class="govuk-!-width-full"), + Field("measurement_unit_qualifier", css_class="govuk-!-width-full"), + ), + AccordionSection( + "Volume", + "initial_volume", + "volume", + ), + AccordionSection( + "Criticality", + "quota_critical_threshold", + "quota_critical", + ), css_class="govuk-!-width-two-thirds", ), Submit( diff --git a/quotas/jinja2/quotas/tables/definitions.jinja b/quotas/jinja2/quotas/tables/definitions.jinja index 2b72bdf71..de7310835 100644 --- a/quotas/jinja2/quotas/tables/definitions.jinja +++ b/quotas/jinja2/quotas/tables/definitions.jinja @@ -10,7 +10,7 @@ "rows": [ { "key": { "text": "Description" }, - "value": { "text": object.description if object.description else "-" }, + "value": { "text": object.description if object.description else "—" }, "actions": {"items": []} }, { @@ -53,12 +53,12 @@ {{ table_rows.append([ {"text": object_details }, - {"text": quota_data[object.sid].status if quota_data[object.sid] else "-" }, + {"text": quota_data[object.sid].status if quota_data[object.sid] else "—" }, {"text": "{:%d %b %Y}".format(object.valid_between.lower) }, - {"text": "{:%d %b %Y}".format(object.valid_between.upper) if object.valid_between.upper else "-"}, + {"text": "{:%d %b %Y}".format(object.valid_between.upper) if object.valid_between.upper else "—"}, {"text": intcomma(object.initial_volume) }, {"text": intcomma(object.volume) }, - {"text": intcomma(quota_data[object.sid].balance) if quota_data[object.sid] else "-" }, + {"text": intcomma(quota_data[object.sid].balance) if quota_data[object.sid] else "—" }, {"text": object.measurement_unit.abbreviation|title}, {"text": actions_html }, ]) or "" }} diff --git a/quotas/tests/test_views.py b/quotas/tests/test_views.py index 45ea87bf4..21ab32a82 100644 --- a/quotas/tests/test_views.py +++ b/quotas/tests/test_views.py @@ -866,6 +866,7 @@ def test_update_quota_definition(valid_user_client, date_ranges): valid_between=date_ranges.big_no_end, ) url = reverse("quota_definition-ui-edit", kwargs={"sid": quota_definition.sid}) + measurement_unit = factories.MeasurementUnitFactory() data = { "start_date_0": date_ranges.normal.lower.day, @@ -874,6 +875,13 @@ def test_update_quota_definition(valid_user_client, date_ranges): "end_date_0": date_ranges.normal.upper.day, "end_date_1": date_ranges.normal.upper.month, "end_date_2": date_ranges.normal.upper.year, + "description": "Lorem ipsum.", + "volume": "80601000.000", + "initial_volume": "80601000.000", + "measurement_unit": measurement_unit.pk, + "measurement_unit_qualifier": "", + "quota_critical_threshold": "90", + "quota_critical": "False", } response = valid_user_client.post(url, data) @@ -892,6 +900,12 @@ def test_update_quota_definition(valid_user_client, date_ranges): ) assert updated_definition.valid_between == date_ranges.normal + assert updated_definition.description == "Lorem ipsum." + assert updated_definition.volume == 80601000.000 + assert updated_definition.initial_volume == 80601000.000 + assert updated_definition.measurement_unit == measurement_unit + assert updated_definition.quota_critical_threshold == 90 + assert updated_definition.quota_critical == False def test_delete_quota_definition_page_200(valid_user_client): diff --git a/quotas/urls.py b/quotas/urls.py index 7d128e3c1..c1a5138c6 100644 --- a/quotas/urls.py +++ b/quotas/urls.py @@ -74,6 +74,11 @@ views.QuotaDefinitionUpdate.as_view(), name="quota_definition-ui-edit", ), + path( + f"quota_definitions//edit-update/", + views.QuotaDefinitionUpdate.as_view(), + name="quota_definition-ui-edit-update", + ), path( f"quota_definitions//delete/", views.QuotaDefinitionDelete.as_view(), diff --git a/reports/migrations/0001_report.py b/reports/migrations/0001_report.py new file mode 100644 index 000000000..187809243 --- /dev/null +++ b/reports/migrations/0001_report.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.20 on 2023-09-11 14:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Report", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ], + options={ + "db_table": "report", + }, + ), + ] diff --git a/reports/migrations/0002_create_custom_permissions.py b/reports/migrations/0002_create_custom_permissions.py new file mode 100644 index 000000000..187b9fa12 --- /dev/null +++ b/reports/migrations/0002_create_custom_permissions.py @@ -0,0 +1,30 @@ +from django.contrib.auth.models import Permission, Group +from django.contrib.contenttypes.models import ContentType +from reports.models import Report +from django.db import migrations + + +def create_custom_permissions(apps, schema_editor): + content_type = ContentType.objects.get_for_model(Report) + + view_permission, _ = Permission.objects.get_or_create( + codename="view_report_index", + name="Can view report index", + content_type=content_type, + ) + + edit_permission, _ = Permission.objects.get_or_create( + codename="view_report", + name="Can view reports", + content_type=content_type, + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0001_report"), + ] + + operations = [ + migrations.RunPython(create_custom_permissions), + ] diff --git a/reports/models.py b/reports/models.py index 6b2021999..aace44019 100644 --- a/reports/models.py +++ b/reports/models.py @@ -1 +1,7 @@ -# Create your models here. +from django.db import models + + +class Report(models.Model): + class Meta: + # Define the name for the database table (optional) + db_table = "report" diff --git a/reports/views.py b/reports/views.py index 0053f9513..ddb59f46d 100644 --- a/reports/views.py +++ b/reports/views.py @@ -7,7 +7,7 @@ import reports.utils as utils -@permission_required("app.view_report_index") +@permission_required("reports.view_report_index") def index(request): context = { "report": index_model.IndexTable(), @@ -16,7 +16,7 @@ def index(request): return render(request, "reports/index.jinja", context) -@permission_required("app.view_report") +@permission_required("reports.view_report") def report(request): # find the report based on the request report_class = utils.get_report_by_slug(request.resolver_match.url_name) diff --git a/requirements.txt b/requirements.txt index 417ca30bf..cd109a217 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ aiohttp==3.8.5 +apsw==3.43.0.0 aioresponses==0.7.4 allure-pytest-bdd==2.8.40 api-client==1.3.0