diff --git a/bc_obps/registration/tests/models/test_reporting_activity.py b/bc_obps/registration/tests/models/test_reporting_activity.py index 89787997f3..b72507f9ab 100644 --- a/bc_obps/registration/tests/models/test_reporting_activity.py +++ b/bc_obps/registration/tests/models/test_reporting_activity.py @@ -11,7 +11,6 @@ def setUpTestData(cls): ("applicable_to", "applicable to", None, None), ("operations", "operation", None, None), ("configuration_elements", "configuration element", None, None), - ("activity_source_type_base_schemas", "activity source type base schema", None, None), ] cls.test_object = ReportingActivity.objects.create( name="test activity", diff --git a/bc_obps/reporting/json_schemas/2024/general_stationary_combustion/activity.json b/bc_obps/reporting/json_schemas/2024/general_stationary_combustion/activity.json new file mode 100644 index 0000000000..4626eee701 --- /dev/null +++ b/bc_obps/reporting/json_schemas/2024/general_stationary_combustion/activity.json @@ -0,0 +1,5 @@ +{ + "title": "General stationary combustion", + "type": "object", + "properties": {} +} diff --git a/bc_obps/reporting/json_schemas/2024/general_stationary_combustion/with_useful_energy.json b/bc_obps/reporting/json_schemas/2024/general_stationary_combustion/with_useful_energy.json new file mode 100644 index 0000000000..d9e6325e59 --- /dev/null +++ b/bc_obps/reporting/json_schemas/2024/general_stationary_combustion/with_useful_energy.json @@ -0,0 +1,80 @@ +{ + "type": "object", + "title": "General stationary combustion of fuel or waste with production of useful energy", + "properties": { + "units": { + "type": "array", + "title": "Units", + "items": { + "type": "object", + "properties": { + "gscUnitName": { + "title": "GSC Unit Name", + "maxLength": 200, + "type": "string" + }, + "gscUnitType": { + "title": "GSC Unit Type", + "maxLength": 200, + "type": "string" + }, + "description": { + "title": "Description", + "default": "", + "type": "string" + }, + "fuels": { + "title": "Fuel Data", + "type": "array", + "items": { + "type": "object", + "properties": { + "fuelName": { + "title": "Fuel Name", + "maxLength": 200, + "type": "string" + }, + "fuelUnit": { + "title": "Fuel Unit", + "maxLength": 200, + "type": "string" + }, + "annualFuelAmount": { + "title": "Annual Fuel Amount", + "type": "number" + }, + "emissions": { + "title": "Emission Data", + "type": "array", + "items": { + "type": "object", + "properties": { + "gasType": { + "title": "Gas Type", + "type": "string", + "enum": [] + }, + "emission": { + "title": "Emission", + "type": "number" + }, + "equivalentEmission": { + "title": "Equivalent Emission", + "type": "number" + }, + "methodology": { + "title": "Methodology", + "type": "string", + "enum": [] + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/bc_obps/reporting/json_schemas/2024/general_stationary_combustion/without_useful_energy.json b/bc_obps/reporting/json_schemas/2024/general_stationary_combustion/without_useful_energy.json new file mode 100644 index 0000000000..fd3853131b --- /dev/null +++ b/bc_obps/reporting/json_schemas/2024/general_stationary_combustion/without_useful_energy.json @@ -0,0 +1,80 @@ +{ + "type": "object", + "title": "General stationary combustion of fuel or waste without production of useful energy", + "properties": { + "units": { + "type": "array", + "title": "Units", + "items": { + "type": "object", + "properties": { + "gscUnitName": { + "title": "GSC Unit Name", + "maxLength": 200, + "type": "string" + }, + "gscUnitType": { + "title": "GSC Unit Type", + "maxLength": 200, + "type": "string" + }, + "description": { + "title": "Description", + "default": "", + "type": "string" + }, + "fuels": { + "title": "Fuel Data", + "type": "array", + "items": { + "type": "object", + "properties": { + "fuelName": { + "title": "Fuel Name", + "maxLength": 200, + "type": "string" + }, + "fuelUnit": { + "title": "Fuel Unit", + "maxLength": 200, + "type": "string" + }, + "annualFuelAmount": { + "title": "Annual Fuel Amount", + "type": "number" + }, + "emissions": { + "title": "Emission Data", + "type": "array", + "items": { + "type": "object", + "properties": { + "gasType": { + "title": "Gas Type", + "type": "string", + "enum": [] + }, + "emission": { + "title": "Emission", + "type": "number" + }, + "equivalentEmission": { + "title": "Equivalent Emission", + "type": "number" + }, + "methodology": { + "title": "Methodology", + "type": "string", + "enum": [] + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/bc_obps/reporting/migrations/0006_reinit.py b/bc_obps/reporting/migrations/0006_reinit.py index f6f8743429..a95d95148d 100644 --- a/bc_obps/reporting/migrations/0006_reinit.py +++ b/bc_obps/reporting/migrations/0006_reinit.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.6 on 2024-06-28 23:04 +# Generated by Django 5.0.6 on 2024-07-02 21:15 import django.contrib.postgres.constraints import django.contrib.postgres.fields.ranges @@ -15,29 +15,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.CreateModel( - name='BaseSchema', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ( - 'slug', - models.CharField( - db_comment='Name of the base schema. Should describe what form it is used to generate and when the base schema took effect. For example: general_stationary_combustion_2024', - max_length=1000, - ), - ), - ( - 'schema', - models.JSONField( - db_comment='The base json schema for a form. This schema defines the static set of fields that should be shown on a form, static meaning that they do not dynamically change based on user input.' - ), - ), - ], - options={ - 'db_table': 'erc"."base_schema', - 'db_table_comment': 'This table contains the base json schema data for displaying emission forms. The base schema can be defined by activity and source type and does not change based on user input so it can be stored statically', - }, - ), migrations.CreateModel( name='Configuration', fields=[ @@ -243,29 +220,65 @@ class Migration(migrations.Migration): ), ), migrations.CreateModel( - name='ActivitySourceTypeBaseSchema', + name='ActivityJsonSchema', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'json_schema', + models.JSONField( + db_comment='The json schema for a specific activity. This defines the shape of the data collected for the related activity' + ), + ), ( 'reporting_activity', models.ForeignKey( on_delete=django.db.models.deletion.DO_NOTHING, - related_name='activity_source_type_base_schemas', + related_name='+', to='registration.reportingactivity', ), ), + ], + options={ + 'db_table': 'erc"."activity_json_schema', + 'db_table_comment': 'Intersection table that assigns a json_schema as valid for a period of time given an activity', + }, + ), + migrations.CreateModel( + name='ActivitySourceTypeJsonSchema', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'json_schema', + models.JSONField( + db_comment='The json schema for a specific activity-source type pair. This defines the shape of the data collected for the source type' + ), + ), + ( + 'has_unit', + models.BooleanField( + db_comment='Whether or not this source type should collect unit data. If true, add a unit schema when buidling the form object', + default=True, + ), + ), ( - 'base_schema', + 'has_fuel', + models.BooleanField( + db_comment='Whether or not this source type should collect fuel data. If true, add a fuel schema when buidling the form object', + default=True, + ), + ), + ( + 'reporting_activity', models.ForeignKey( on_delete=django.db.models.deletion.DO_NOTHING, - related_name='activity_source_type_base_schemas', - to='reporting.baseschema', + related_name='+', + to='registration.reportingactivity', ), ), ], options={ - 'db_table': 'erc"."activity_source_type_base_schema', - 'db_table_comment': 'Intersection table that assigns a base_schema as valid for a period of time given an activity-sourceType pair', + 'db_table': 'erc"."activity_source_type_json_schema', + 'db_table_comment': 'Intersection table that assigns a json_schema as valid for a period of time given an activity-sourceType pair', }, ), migrations.AddConstraint( @@ -283,14 +296,28 @@ class Migration(migrations.Migration): ), ), migrations.AddField( - model_name='activitysourcetypebaseschema', + model_name='activitysourcetypejsonschema', name='valid_from', field=models.ForeignKey( on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='reporting.configuration' ), ), migrations.AddField( - model_name='activitysourcetypebaseschema', + model_name='activitysourcetypejsonschema', + name='valid_to', + field=models.ForeignKey( + on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='reporting.configuration' + ), + ), + migrations.AddField( + model_name='activityjsonschema', + name='valid_from', + field=models.ForeignKey( + on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='reporting.configuration' + ), + ), + migrations.AddField( + model_name='activityjsonschema', name='valid_to', field=models.ForeignKey( on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='reporting.configuration' @@ -349,7 +376,7 @@ class Migration(migrations.Migration): name='reporting_year', field=models.ForeignKey( db_comment='The reporting year, for which this report is filled', - default=1, + default=1900, on_delete=django.db.models.deletion.DO_NOTHING, to='reporting.reportingyear', ), @@ -365,12 +392,10 @@ class Migration(migrations.Migration): ), ), migrations.AddField( - model_name='activitysourcetypebaseschema', + model_name='activitysourcetypejsonschema', name='source_type', field=models.ForeignKey( - on_delete=django.db.models.deletion.DO_NOTHING, - related_name='activity_source_type_base_schemas', - to='reporting.sourcetype', + on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='reporting.sourcetype' ), ), ] diff --git a/bc_obps/reporting/migrations/0007_prod_data.py b/bc_obps/reporting/migrations/0007_prod_data.py index a1ae8f923f..202b2f7c6b 100644 --- a/bc_obps/reporting/migrations/0007_prod_data.py +++ b/bc_obps/reporting/migrations/0007_prod_data.py @@ -1,8 +1,25 @@ -# Generated by Django 5.0.6 on 2024-06-21 21:03 +# Generated by Django 5.0.6 on 2024-07-02 21:15 from django.db import migrations +def init_configuration_data(apps, schema_monitor): + ''' + Add initial data to erc.configuration + ''' + + Configuration = apps.get_model('reporting', 'Configuration') + Configuration.objects.bulk_create([Configuration(slug='2024', valid_from='2024-01-01', valid_to='2099-12-31')]) + + +def reverse_init_configuration_data(apps, schema_monitor): + ''' + Remove initial data from erc.configuration + ''' + Configuration = apps.get_model('reporting', 'Configuration') + Configuration.objects.filter(slug__in=['2024']).delete() + + def init_source_type_data(apps, schema_monitor): ''' Add initial data to erc.sourcetype @@ -343,31 +360,6 @@ def reverse_init_methodology_data(apps, schema_monitor): ).delete() -def init_reporting_years_data(apps, schema_monitor): - ''' - Add initial data to the reporting years table - ''' - ReportingYear = apps.get_model('reporting', 'ReportingYear') - ReportingYear.objects.bulk_create( - [ - ReportingYear( - reporting_year=2024, - reporting_window_start="2025-01-01T00:00:00Z", - reporting_window_end="2025-12-31T23:59:59Z", - description="2024 reporting year", - ) - ] - ) - - -def reverse_init_reporting_years_data(apps, schema_monitor): - ''' - Remove initial data to the reporting years table - ''' - ReportingYear = apps.get_model('reporting', 'ReportingYear') - ReportingYear.objects.filter(reporting_year__in=[2024]).delete() - - class Migration(migrations.Migration): dependencies = [ @@ -378,5 +370,5 @@ class Migration(migrations.Migration): migrations.RunPython(init_source_type_data, reverse_init_source_type_data), migrations.RunPython(init_gas_type_data, reverse_init_gas_type_data), migrations.RunPython(init_methodology_data, reverse_init_methodology_data), - migrations.RunPython(init_reporting_years_data, reverse_init_reporting_years_data), + migrations.RunPython(init_configuration_data, reverse_init_configuration_data), ] diff --git a/bc_obps/reporting/migrations/0008_schema_data.py b/bc_obps/reporting/migrations/0008_schema_data.py new file mode 100644 index 0000000000..8a5fa80591 --- /dev/null +++ b/bc_obps/reporting/migrations/0008_schema_data.py @@ -0,0 +1,98 @@ +# Generated by Django 5.0.6 on 2024-07-02 21:16 + +from django.db import migrations + +import json +from registration.models import ReportingActivity +from reporting.models import SourceType, Configuration + +# ACTIVITY +def init_activity_schema_data(apps, schema_monitor): + ''' + Add initial schema data to erc.activity_schema + ''' + ## Import JSON data + import os + + cwd = os.getcwd() + with open(f'{cwd}/reporting/json_schemas/2024/general_stationary_combustion/activity.json') as gsc_st1: + schema = json.load(gsc_st1) + + ActivitySchema = apps.get_model('reporting', 'ActivityJsonSchema') + ActivitySchema.objects.create( + reporting_activity_id=ReportingActivity.objects.get(name='General stationary combustion').id, + json_schema=schema, + valid_from_id=Configuration.objects.get(valid_from='2024-01-01').id, + valid_to_id=Configuration.objects.get(valid_to='2099-12-31').id, + ) + + +def reverse_init_activity_schema_data(apps, schema_monitor): + ''' + Remove initial data from erc.base_schema + ''' + ActivitySchema = apps.get_model('reporting', 'ActivityJsonSchema') + ActivitySchema.objects.filter( + reporting_activity_id=ReportingActivity.objects.get(name='General stationary combustion').id + ).delete() + + +# SOURCE TYPE +def init_activity_source_type_schema_data(apps, schema_monitor): + ''' + Add initial schema data to erc.activity_source_type_schema + ''' + ## Import JSON data + import os + + cwd = os.getcwd() + with open(f'{cwd}/reporting/json_schemas/2024/general_stationary_combustion/with_useful_energy.json') as gsc_st1: + schema1 = json.load(gsc_st1) + with open(f'{cwd}/reporting/json_schemas/2024/general_stationary_combustion/without_useful_energy.json') as gsc_st2: + schema2 = json.load(gsc_st2) + + ActivitySourceTypeSchema = apps.get_model('reporting', 'ActivitySourceTypeJsonSchema') + ActivitySourceTypeSchema.objects.bulk_create( + [ + ActivitySourceTypeSchema( + reporting_activity_id=ReportingActivity.objects.get(name='General stationary combustion').id, + source_type_id=SourceType.objects.get( + name='General stationary combustion of fuel or waste with production of useful energy' + ).id, + json_schema=schema1, + valid_from_id=Configuration.objects.get(valid_from='2024-01-01').id, + valid_to_id=Configuration.objects.get(valid_to='2099-12-31').id, + ), + ActivitySourceTypeSchema( + reporting_activity_id=ReportingActivity.objects.get(name='General stationary combustion').id, + source_type_id=SourceType.objects.get( + name='General stationary combustion of waste without production of useful energy' + ).id, + json_schema=schema2, + valid_from_id=Configuration.objects.get(valid_from='2024-01-01').id, + valid_to_id=Configuration.objects.get(valid_to='2099-12-31').id, + ), + ] + ) + + +def reverse_init_activity_source_type_schema_data(apps, schema_monitor): + ''' + Remove initial data from erc.base_schema + ''' + ActivitySourceTypeJsonSchema = apps.get_model('reporting', 'ActivitySourceTypeJsonSchema') + ActivitySourceTypeJsonSchema.objects.filter( + reporting_activity_id=ActivitySourceTypeJsonSchema.objects.get(name='General stationary combustion').id + ).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('reporting', '0007_prod_data'), + ] + + operations = [ + migrations.RunPython(init_activity_schema_data, reverse_init_activity_schema_data), + migrations.RunPython(init_activity_source_type_schema_data, reverse_init_activity_source_type_schema_data), + ] diff --git a/bc_obps/reporting/models/__init__.py b/bc_obps/reporting/models/__init__.py index 3cb41c1eee..ae305d6d68 100644 --- a/bc_obps/reporting/models/__init__.py +++ b/bc_obps/reporting/models/__init__.py @@ -9,8 +9,8 @@ from .reporting_field import ReportingField from .configuration import Configuration from .configuration_element import ConfigurationElement -from .base_schema import BaseSchema -from .activity_source_type_base_schema import ActivitySourceTypeBaseSchema +from .activity_json_schema import ActivityJsonSchema +from .activity_source_type_json_schema import ActivitySourceTypeJsonSchema __all__ = [ @@ -24,6 +24,6 @@ "ReportingField", "Configuration", "ConfigurationElement", - "BaseSchema", - "ActivitySourceTypeBaseSchema", + "ActivityJsonSchema", + "ActivitySourceTypeJsonSchema", ] diff --git a/bc_obps/reporting/models/activity_json_schema.py b/bc_obps/reporting/models/activity_json_schema.py new file mode 100644 index 0000000000..7f2f604ef3 --- /dev/null +++ b/bc_obps/reporting/models/activity_json_schema.py @@ -0,0 +1,34 @@ +from common.models import BaseModel +from django.db import models +from registration.models import ReportingActivity +from reporting.models import Configuration +import typing +from reporting.utils import validate_overlapping_records + + +class ActivityJsonSchema(BaseModel): + """Intersection table for Activity-JsonSchema""" + + # No history needed, these elements are immutable + reporting_activity = models.ForeignKey(ReportingActivity, on_delete=models.DO_NOTHING, related_name="+") + json_schema = models.JSONField( + db_comment="The json schema for a specific activity. This defines the shape of the data collected for the related activity", + ) + valid_from = models.ForeignKey(Configuration, on_delete=models.DO_NOTHING, related_name="+") + valid_to = models.ForeignKey(Configuration, on_delete=models.DO_NOTHING, related_name="+") + + class Meta: + db_table_comment = ( + "Intersection table that assigns a json_schema as valid for a period of time given an activity" + ) + db_table = 'erc"."activity_json_schema' + + @typing.no_type_check + def save(self, *args, **kwargs) -> None: + """ + Override the save method to validate if there are overlapping records. + """ + exception_message = f'This record will result in duplicate json schemas being returned for the date range {self.valid_from.valid_from} - {self.valid_to.valid_to} as it overlaps with a current record or records' + + validate_overlapping_records(ActivityJsonSchema, self, exception_message) + super().save(*args, **kwargs) diff --git a/bc_obps/reporting/models/activity_source_type_base_schema.py b/bc_obps/reporting/models/activity_source_type_base_schema.py deleted file mode 100644 index 3ba4dfd1d8..0000000000 --- a/bc_obps/reporting/models/activity_source_type_base_schema.py +++ /dev/null @@ -1,46 +0,0 @@ -from common.models import BaseModel -from django.db import models -from registration.models import ReportingActivity -from reporting.models import SourceType, BaseSchema, Configuration -import typing - - -class ActivitySourceTypeBaseSchema(BaseModel): - """Intersection table for Activity-SourceType-BaseSchema""" - - # No history needed, these elements are immutable - reporting_activity = models.ForeignKey( - ReportingActivity, on_delete=models.DO_NOTHING, related_name="activity_source_type_base_schemas" - ) - source_type = models.ForeignKey( - SourceType, on_delete=models.DO_NOTHING, related_name="activity_source_type_base_schemas" - ) - base_schema = models.ForeignKey( - BaseSchema, on_delete=models.DO_NOTHING, related_name="activity_source_type_base_schemas" - ) - valid_from = models.ForeignKey(Configuration, on_delete=models.DO_NOTHING, related_name="+") - valid_to = models.ForeignKey(Configuration, on_delete=models.DO_NOTHING, related_name="+") - - class Meta: - db_table_comment = "Intersection table that assigns a base_schema as valid for a period of time given an activity-sourceType pair" - db_table = 'erc"."activity_source_type_base_schema' - - @typing.no_type_check - def save(self, *args, **kwargs) -> None: - """ - Override the save method to validate if there are overlapping records. - """ - all_ranges = ActivitySourceTypeBaseSchema.objects.select_related('valid_from', 'valid_to').filter( - reporting_activity=self.reporting_activity, source_type=self.source_type - ) - for y in all_ranges: - if ( - (self.valid_from.valid_from >= y.valid_from.valid_from) - and (self.valid_from.valid_from <= y.valid_to.valid_to) - or (self.valid_to.valid_to <= y.valid_to.valid_to) - and (self.valid_to.valid_to >= y.valid_from.valid_from) - ): - raise Exception( - f'This record will result in duplicate base schemas being returned for the date range {self.valid_from.valid_from} - {self.valid_to.valid_to} as it overlaps with a current record or records' - ) - super().save(*args, **kwargs) diff --git a/bc_obps/reporting/models/activity_source_type_json_schema.py b/bc_obps/reporting/models/activity_source_type_json_schema.py new file mode 100644 index 0000000000..8eb10cc27d --- /dev/null +++ b/bc_obps/reporting/models/activity_source_type_json_schema.py @@ -0,0 +1,41 @@ +from common.models import BaseModel +from django.db import models +from registration.models import ReportingActivity +from reporting.models import SourceType, Configuration +import typing +from reporting.utils import validate_overlapping_records + + +class ActivitySourceTypeJsonSchema(BaseModel): + """Intersection table for Activity-SourceType-JsonSchema""" + + # No history needed, these elements are immutable + reporting_activity = models.ForeignKey(ReportingActivity, on_delete=models.DO_NOTHING, related_name="+") + source_type = models.ForeignKey(SourceType, on_delete=models.DO_NOTHING, related_name="+") + json_schema = models.JSONField( + db_comment="The json schema for a specific activity-source type pair. This defines the shape of the data collected for the source type", + ) + has_unit = models.BooleanField( + db_comment="Whether or not this source type should collect unit data. If true, add a unit schema when buidling the form object", + default=True, + ) + has_fuel = models.BooleanField( + db_comment="Whether or not this source type should collect fuel data. If true, add a fuel schema when buidling the form object", + default=True, + ) + valid_from = models.ForeignKey(Configuration, on_delete=models.DO_NOTHING, related_name="+") + valid_to = models.ForeignKey(Configuration, on_delete=models.DO_NOTHING, related_name="+") + + class Meta: + db_table_comment = "Intersection table that assigns a json_schema as valid for a period of time given an activity-sourceType pair" + db_table = 'erc"."activity_source_type_json_schema' + + @typing.no_type_check + def save(self, *args, **kwargs) -> None: + """ + Override the save method to validate if there are overlapping records. + """ + exception_message = f'This record will result in duplicate json schemas being returned for the date range {self.valid_from.valid_from} - {self.valid_to.valid_to} as it overlaps with a current record or records' + + validate_overlapping_records(ActivitySourceTypeJsonSchema, self, exception_message) + super().save(*args, **kwargs) diff --git a/bc_obps/reporting/models/base_schema.py b/bc_obps/reporting/models/base_schema.py deleted file mode 100644 index 77ea7fe5cf..0000000000 --- a/bc_obps/reporting/models/base_schema.py +++ /dev/null @@ -1,19 +0,0 @@ -from common.models import BaseModel -from django.db import models - - -class BaseSchema(BaseModel): - """Static base json schema for emissions reporting forms""" - - # No history needed, these elements are immutable - slug = models.CharField( - max_length=1000, - db_comment="Name of the base schema. Should describe what form it is used to generate and when the base schema took effect. For example: general_stationary_combustion_2024", - ) - schema = models.JSONField( - db_comment="The base json schema for a form. This schema defines the static set of fields that should be shown on a form, static meaning that they do not dynamically change based on user input.", - ) - - class Meta: - db_table_comment = "This table contains the base json schema data for displaying emission forms. The base schema can be defined by activity and source type and does not change based on user input so it can be stored statically" - db_table = 'erc"."base_schema' diff --git a/bc_obps/reporting/models/configuration_element.py b/bc_obps/reporting/models/configuration_element.py index b6f907fb0d..eb04fe2c7a 100644 --- a/bc_obps/reporting/models/configuration_element.py +++ b/bc_obps/reporting/models/configuration_element.py @@ -3,6 +3,7 @@ from registration.models import ReportingActivity from reporting.models import SourceType, GasType, Methodology, Configuration, ReportingField import typing +from reporting.utils import validate_overlapping_records class ConfigurationElement(BaseModel): @@ -32,20 +33,7 @@ def save(self, *args, **kwargs) -> None: """ Override the save method to validate if there are overlapping records. """ - all_ranges = ConfigurationElement.objects.select_related('valid_from', 'valid_to').filter( - reporting_activity=self.reporting_activity, - source_type=self.source_type, - gas_type=self.gas_type, - methodology=self.methodology, - ) - for y in all_ranges: - if ( - (self.valid_from.valid_from >= y.valid_from.valid_from) - and (self.valid_from.valid_from <= y.valid_to.valid_to) - or (self.valid_to.valid_to <= y.valid_to.valid_to) - and (self.valid_to.valid_to >= y.valid_from.valid_from) - ): - raise Exception( - f'This record will result in duplicate configurations being returned for the date range {self.valid_from.valid_from} - {self.valid_to.valid_to} as it overlaps with a current record or records' - ) + exception_message = f'This record will result in duplicate configuration elements being returned for the date range {self.valid_from.valid_from} - {self.valid_to.valid_to} as it overlaps with a current record or records' + + validate_overlapping_records(ConfigurationElement, self, exception_message) super().save(*args, **kwargs) diff --git a/bc_obps/reporting/tests/models/test_activity_json_schema.py b/bc_obps/reporting/tests/models/test_activity_json_schema.py new file mode 100644 index 0000000000..749a77b820 --- /dev/null +++ b/bc_obps/reporting/tests/models/test_activity_json_schema.py @@ -0,0 +1,47 @@ +from common.tests.utils.helpers import BaseTestCase +from registration.models import ReportingActivity +from reporting.models import ActivityJsonSchema +from reporting.tests.utils.bakers import configuration_baker +import pytest + + +class ActivityJsonSchemaTest(BaseTestCase): + @classmethod + def setUpTestData(cls): + config = configuration_baker({'slug': '5025', 'valid_from': '5025-01-01', 'valid_to': '5025-12-31'}) + cls.test_object = ActivityJsonSchema.objects.create( + reporting_activity=ReportingActivity.objects.get(pk=1), + json_schema='{}', + valid_from=config, + valid_to=config, + ) + cls.field_data = [ + ("id", "ID", None, None), + ("reporting_activity", "reporting activity", None, None), + ("json_schema", "json schema", None, None), + ("valid_from", "valid from", None, None), + ("valid_to", "valid to", None, None), + ] + + # Throws when a matching activity, source_type, json_schema has an overlapping date range + def testDuplicateJsonSchemaForDateRange(self): + invalid_record = ActivityJsonSchema( + reporting_activity=self.test_object.reporting_activity, + json_schema='{}', + valid_from=self.test_object.valid_from, + valid_to=self.test_object.valid_from, + ) + + with pytest.raises(Exception) as exc: + invalid_record.save() + assert exc.match(r"^This record will result in duplicate json schemas") + + def testValidInsert(self): + config = configuration_baker({'slug': '5026', 'valid_from': '5026-01-01', 'valid_to': '5026-12-31'}) + valid_record = ActivityJsonSchema( + reporting_activity=self.test_object.reporting_activity, + json_schema='{}', + valid_from=config, + valid_to=config, + ) + valid_record.save() diff --git a/bc_obps/reporting/tests/models/test_activity_source_type_base_schema.py b/bc_obps/reporting/tests/models/test_activity_source_type_json_schema.py similarity index 54% rename from bc_obps/reporting/tests/models/test_activity_source_type_base_schema.py rename to bc_obps/reporting/tests/models/test_activity_source_type_json_schema.py index 31839f1446..bd0b30be67 100644 --- a/bc_obps/reporting/tests/models/test_activity_source_type_base_schema.py +++ b/bc_obps/reporting/tests/models/test_activity_source_type_json_schema.py @@ -1,21 +1,20 @@ from common.tests.utils.helpers import BaseTestCase from registration.models import ReportingActivity -from reporting.models import ActivitySourceTypeBaseSchema, SourceType -from reporting.tests.utils.bakers import ( - configuration_baker, - base_schema_baker, -) +from reporting.models import ActivitySourceTypeJsonSchema, SourceType +from reporting.tests.utils.bakers import configuration_baker import pytest -class ActivitySourceTypeBaseSchemaTest(BaseTestCase): +class ActivitySourceTypeJsonSchemaTest(BaseTestCase): @classmethod def setUpTestData(cls): - config = configuration_baker({'slug': '2024', 'valid_from': '2024-01-01', 'valid_to': '2024-12-31'}) - cls.test_object = ActivitySourceTypeBaseSchema.objects.create( + config = configuration_baker({'slug': '5025', 'valid_from': '5025-01-01', 'valid_to': '5025-12-31'}) + cls.test_object = ActivitySourceTypeJsonSchema.objects.create( reporting_activity=ReportingActivity.objects.get(pk=1), source_type=SourceType.objects.get(pk=1), - base_schema=base_schema_baker(), + json_schema='{}', + has_unit=True, + has_fuel=True, valid_from=config, valid_to=config, ) @@ -23,31 +22,37 @@ def setUpTestData(cls): ("id", "ID", None, None), ("reporting_activity", "reporting activity", None, None), ("source_type", "source type", None, None), - ("base_schema", "base schema", None, None), + ("json_schema", "json schema", None, None), + ("has_unit", "has unit", None, None), + ("has_fuel", "has fuel", None, None), ("valid_from", "valid from", None, None), ("valid_to", "valid to", None, None), ] - # Throws when a matching activity, source_type, base_schema has an overlapping date range - def testDuplicateBaseSchemaForDateRange(self): - invalid_record = ActivitySourceTypeBaseSchema( + # Throws when a matching activity, source_type, json_schema has an overlapping date range + def testDuplicateJsonSchemaForDateRange(self): + invalid_record = ActivitySourceTypeJsonSchema( reporting_activity=self.test_object.reporting_activity, source_type=self.test_object.source_type, - base_schema=base_schema_baker(), + json_schema='{}', + has_unit=True, + has_fuel=True, valid_from=self.test_object.valid_from, valid_to=self.test_object.valid_from, ) with pytest.raises(Exception) as exc: invalid_record.save() - assert exc.match(r"^This record will result in duplicate base schemas") + assert exc.match(r"^This record will result in duplicate json schemas") def testValidInsert(self): - config = configuration_baker({'slug': '2026', 'valid_from': '2026-01-01', 'valid_to': '2026-12-31'}) - valid_record = ActivitySourceTypeBaseSchema( + config = configuration_baker({'slug': '5026', 'valid_from': '5026-01-01', 'valid_to': '5026-12-31'}) + valid_record = ActivitySourceTypeJsonSchema( reporting_activity=self.test_object.reporting_activity, source_type=self.test_object.source_type, - base_schema=base_schema_baker(), + json_schema='{}', + has_unit=True, + has_fuel=True, valid_from=config, valid_to=config, ) diff --git a/bc_obps/reporting/tests/models/test_base_schema.py b/bc_obps/reporting/tests/models/test_base_schema.py deleted file mode 100644 index dccdf340e2..0000000000 --- a/bc_obps/reporting/tests/models/test_base_schema.py +++ /dev/null @@ -1,14 +0,0 @@ -from common.tests.utils.helpers import BaseTestCase -from reporting.models import BaseSchema - - -class BaseSchemaTest(BaseTestCase): - @classmethod - def setUpTestData(cls): - cls.test_object = BaseSchema.objects.create(slug="testSlug", schema="{'testkey': 'testValue'}") - cls.field_data = [ - ("id", "ID", None, None), - ("slug", "slug", 1000, None), - ("schema", "schema", None, None), - ("activity_source_type_base_schemas", "activity source type base schema", None, None), - ] diff --git a/bc_obps/reporting/tests/models/test_configuration.py b/bc_obps/reporting/tests/models/test_configuration.py index 450887f191..cdf76af14c 100644 --- a/bc_obps/reporting/tests/models/test_configuration.py +++ b/bc_obps/reporting/tests/models/test_configuration.py @@ -1,16 +1,16 @@ -from common.tests.utils.helpers import BaseTestCase from reporting.models import Configuration from django.core.exceptions import ValidationError +from django.test import TestCase -class ConfigurationTest(BaseTestCase): +class ConfigurationTest(TestCase): @classmethod def setUpTestData(cls): cls.test_object = Configuration.objects.create( slug="testConfig", - valid_from="2024-01-01", - valid_to="2024-03-31", + valid_from="5025-01-01", + valid_to="5025-03-31", ) cls.field_data = [ ("id", "ID", None, None), @@ -21,10 +21,10 @@ def setUpTestData(cls): # Throws when a record being inserted has an overlapping date range def testExclusionConstraintOverlaps(self): - invalid_record = Configuration(slug='invalidRecord', valid_from='2024-02-01', valid_to='2024-12-31') - with self.assertRaises(ValidationError, msg="ActivitySourceTypeBaseSchema already exists."): + invalid_record = Configuration(slug='invalidRecord', valid_from='5025-02-01', valid_to='5025-12-31') + with self.assertRaises(ValidationError, msg="Configuration record already exists."): invalid_record.save() def testValidRecordInsert(self): - valid_record = Configuration(slug='validRecord', valid_from='2025-01-01', valid_to='2025-12-31') + valid_record = Configuration(slug='validRecord', valid_from='5026-01-01', valid_to='5026-12-31') valid_record.save() diff --git a/bc_obps/reporting/tests/models/test_configuration_element.py b/bc_obps/reporting/tests/models/test_configuration_element.py index 6039ac1b5f..32166fb3af 100644 --- a/bc_obps/reporting/tests/models/test_configuration_element.py +++ b/bc_obps/reporting/tests/models/test_configuration_element.py @@ -10,7 +10,7 @@ class ConfigurationElementTest(BaseTestCase): @classmethod def setUpTestData(cls): - config = configuration_baker({'slug': '2024', 'valid_from': '2024-01-01', 'valid_to': '2024-12-31'}) + config = configuration_baker({'slug': '5025', 'valid_from': '5025-01-01', 'valid_to': '5025-12-31'}) cls.test_object = ConfigurationElement.objects.create( reporting_activity=ReportingActivity.objects.get(pk=1), source_type=SourceType.objects.get(pk=1), @@ -42,10 +42,10 @@ def testDuplicateConfigElementForDateRange(self): with pytest.raises(Exception) as exc: invalid_record.save() - assert exc.match(r"^This record will result in duplicate configurations") + assert exc.match(r"^This record will result in duplicate configuration elements") def testValidInsert(self): - config = configuration_baker({'slug': '2026', 'valid_from': '2026-01-01', 'valid_to': '2026-12-31'}) + config = configuration_baker({'slug': '5026', 'valid_from': '5026-01-01', 'valid_to': '5026-12-31'}) valid_record = ConfigurationElement( reporting_activity=self.test_object.reporting_activity, source_type=self.test_object.source_type, diff --git a/bc_obps/reporting/tests/models/test_source_type.py b/bc_obps/reporting/tests/models/test_source_type.py index e76fe34fbf..bb335f3674 100644 --- a/bc_obps/reporting/tests/models/test_source_type.py +++ b/bc_obps/reporting/tests/models/test_source_type.py @@ -133,5 +133,4 @@ def setUpTestData(cls): ("id", "ID", None, None), ("name", "name", 1000, None), ("configuration_elements", "configuration element", None, None), - ("activity_source_type_base_schemas", "activity source type base schema", None, None), ] diff --git a/bc_obps/reporting/tests/utils/bakers.py b/bc_obps/reporting/tests/utils/bakers.py index 40295f551a..aff237dc8b 100644 --- a/bc_obps/reporting/tests/utils/bakers.py +++ b/bc_obps/reporting/tests/utils/bakers.py @@ -1,6 +1,5 @@ from registration.tests.utils.bakers import operation_baker from reporting.models import configuration_element -from reporting.models.base_schema import BaseSchema from reporting.models.gas_type import GasType from registration.models import ReportingActivity from reporting.models.reporting_year import ReportingYear @@ -40,10 +39,6 @@ def methodology_baker() -> Methodology: return baker.make(Methodology) -def base_schema_baker() -> BaseSchema: - return baker.make(BaseSchema, slug='testSlug', schema="{'testkey': 'testValue'}") - - def configuration_element_baker() -> ConfigurationElement: return baker.make(ConfigurationElement) diff --git a/bc_obps/reporting/utils.py b/bc_obps/reporting/utils.py new file mode 100644 index 0000000000..f9f48d8fa5 --- /dev/null +++ b/bc_obps/reporting/utils.py @@ -0,0 +1,25 @@ +from typing import Any + + +def validate_overlapping_records( + object_class: Any, + save_self: Any, + exception_message: str, +) -> None: + if hasattr(object_class, "source_type"): + all_ranges = object_class.objects.select_related('valid_from', 'valid_to').filter( + reporting_activity=save_self.reporting_activity, source_type=save_self.source_type + ) + else: + all_ranges = object_class.objects.select_related('valid_from', 'valid_to').filter( + reporting_activity=save_self.reporting_activity + ) + for y in all_ranges: + if ( + (save_self.valid_from.valid_from >= y.valid_from.valid_from) + and (save_self.valid_from.valid_from <= y.valid_to.valid_to) + ) or ( + (save_self.valid_to.valid_to <= y.valid_to.valid_to) + and (save_self.valid_to.valid_to >= y.valid_from.valid_from) + ): + raise Exception(exception_message) diff --git a/erd_diagrams/erd_reporting.md b/erd_diagrams/erd_reporting.md index 553198567d..85875dbcd6 100644 --- a/erd_diagrams/erd_reporting.md +++ b/erd_diagrams/erd_reporting.md @@ -68,16 +68,20 @@ ConfigurationElement { ForeignKey valid_to ManyToManyField reporting_fields } -BaseSchema { +ActivityJsonSchema { BigAutoField id - CharField slug - JSONField schema + ForeignKey reporting_activity + JSONField json_schema + ForeignKey valid_from + ForeignKey valid_to } -ActivitySourceTypeBaseSchema { +ActivitySourceTypeJsonSchema { BigAutoField id ForeignKey reporting_activity ForeignKey source_type - ForeignKey base_schema + JSONField json_schema + BooleanField has_unit + BooleanField has_fuel ForeignKey valid_from ForeignKey valid_to } @@ -95,8 +99,10 @@ ConfigurationElement }|--|| Methodology : methodology ConfigurationElement }|--|| Configuration : valid_from ConfigurationElement }|--|| Configuration : valid_to ConfigurationElement }|--|{ ReportingField : reporting_fields -ActivitySourceTypeBaseSchema }|--|| ReportingActivity : reporting_activity -ActivitySourceTypeBaseSchema }|--|| SourceType : source_type -ActivitySourceTypeBaseSchema }|--|| BaseSchema : base_schema -ActivitySourceTypeBaseSchema }|--|| Configuration : valid_from -ActivitySourceTypeBaseSchema }|--|| Configuration : valid_to \ No newline at end of file +ActivityJsonSchema }|--|| ReportingActivity : reporting_activity +ActivityJsonSchema }|--|| Configuration : valid_from +ActivityJsonSchema }|--|| Configuration : valid_to +ActivitySourceTypeJsonSchema }|--|| ReportingActivity : reporting_activity +ActivitySourceTypeJsonSchema }|--|| SourceType : source_type +ActivitySourceTypeJsonSchema }|--|| Configuration : valid_from +ActivitySourceTypeJsonSchema }|--|| Configuration : valid_to \ No newline at end of file