From 8d7b9d0d8fc475fe264f1bed82d8414e11c6d152 Mon Sep 17 00:00:00 2001 From: George Silva Date: Fri, 24 May 2024 15:04:55 -0300 Subject: [PATCH 1/4] creates impacts app and some models --- src/planscape/impacts/__init__.py | 0 src/planscape/impacts/models.py | 178 ++++++++++++++++++++++++++++ src/planscape/planning/models.py | 8 +- src/planscape/planscape/settings.py | 1 + 4 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 src/planscape/impacts/__init__.py create mode 100644 src/planscape/impacts/models.py diff --git a/src/planscape/impacts/__init__.py b/src/planscape/impacts/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/planscape/impacts/models.py b/src/planscape/impacts/models.py new file mode 100644 index 000000000..109fa81f6 --- /dev/null +++ b/src/planscape/impacts/models.py @@ -0,0 +1,178 @@ +from django.contrib.gis.db import models +from django.conf import settings +from django.contrib.auth import get_user_model +from planning.models import ProjectArea, Scenario +from stands.models import Stand +from core.models import CreatedAtMixin, UpdatedAtMixin, DeletedAtMixin, UUIDMixin + + +User = get_user_model() + + +class TreatmentPlanStatus(models.TextChoices): + PENDING = "PENDING", "Pending" + RUNNING = "RUNNING", "Running" + SUCCESS = "SUCCESS", "Suceess" + FAILURE = "FAILURE", "Failure" + + +class TreatmentPlan( + UUIDMixin, + CreatedAtMixin, + UpdatedAtMixin, + DeletedAtMixin, + models.Model, +): + created_by = models.ForeignKey( + User, + related_name="tx_plans", + on_delete=models.RESTRICT, + ) + scenario = models.ForeignKey( + Scenario, + related_name="tx_plans", + on_delete=models.RESTRICT, + ) + status = models.CharField( + choices=TreatmentPlanStatus.choices, + default=TreatmentPlanStatus.PENDING, + ) + + class Meta: + verbose_name = "Treatment Plan" + verbose_name_plural = "Treatment Plans" + + +class TreatmentPrescriptionType(models.TextChoices): + SINGLE = "SINGLE", "Single" + SEQUENCE = "SEQUENCE", "Sequence" + + +class TreatmentPrescriptionAction(models.TextChoices): + MODERATE_THINNING_BIOMASS = ( + "MODERATE_THINNING_BIOMASS", + "Moderate Thinning & Biomass Removal", + ) + HEAVY_THINNING_BIOMASS = ( + "HEAVY_THINNING_BIOMASS", + "Heavy Thinning & Biomass Removal", + ) + MODERATE_THINNING_BURN = ( + "MODERATE_THINNING_BURN", + "Moderate Thinning & Pile Burn", + ) + HEAVY_THINNING_BURN = "HEAVY_THINNING_BURN", "Heavy Thinning & Pile Burn" + MODERATE_MASTICATION = "MODERATE_MASTICATION", "Moderate Mastication" + HEAVY_MASTICATION = "HEAVY_MASTICATION", "Heavy Mastication" + RX_FIRE = "RX_FIRE", "Prescribed Fire" + HEAVY_THINNING_RX_FIRE = ( + "HEAVY_THINNING_RX_FIRE", + "Heavy Thinning & Prescribed Fire", + ) + MASTICATION_RX_FIRE = ( + "MASTICATION_RX_FIRE", + "Mastication & Prescribed Fire", + ) + MODERATE_THINNING_BURN_PLUS_RX_FIRE = ( + "MODERATE_THINNING_BURN_PLUS_RX_FIRE", + "Moderate Thinning & Pile Burn (year 0), Prescribed Burn (year 10)", + ) + MODERATE_THINNING_BURN_PLUS_MODERATE_THINNING_BURN = ( + "MODERATE_THINNING_BURN_PLUS_MODERATE_THINNING_BURN", + "Moderate Thinning & Pile Burn (year 0), Moderate Thinning & Pile Burn (year 10)", + ) + HEAVY_THINNING_BURN_PLUS_RX_FIRE = ( + "HEAVY_THINNING_BURN_PLUS_RX_FIRE", + "Heavy Thinning & Pile Burn (year 0), Prescribed Burn (year 10)", + ) + HEAVY_THINNING_BURN_PLUS_HEAVY_THINNING_BURN = ( + "HEAVY_THINNING_BURN_PLUS_HEAVY_THINNING_BURN", + "Heavy Thinning & Pile Burn (year 0), Heavy Thinning & Pile Burn (year 10)", + ) + RX_FIRE_PLUS_RX_FIRE = ( + "RX_FIRE_PLUS_RX_FIRE", + "Prescribed Fire (year 0), Prescribed Fire (year 10)", + ) + MODERATE_MASTICATION_PLUS_MODERATE_MASTICATION = ( + "MODERATE_MASTICATION_PLUS_MODERATE_MASTICATION", + "Moderate Mastication (year 0), Moderate Mastication (year 10)", + ) + HEAVY_THINNING_BIOMASS_PLUS_RX_FIRE = ( + "HEAVY_THINNING_BIOMASS_PLUS_RX_FIRE", + "Heavy Thinning & Biomass Removal (year 0), Prescribed Fire (year 10)", + ) + MODERATE_MASTICATION_PLUS_RX_FIRE = ( + "MODERATE_MASTICATION_PLUS_RX_FIRE", + "Moderate Mastication (year 0), Prescribed Fire (year 10)", + ) + + +def get_prescription_type( + action: TreatmentPrescriptionAction, +) -> TreatmentPrescriptionType: + """returns the prescription type based on the action""" + + is_sequence = action in [ + TreatmentPrescriptionAction.MODERATE_THINNING_BURN_PLUS_RX_FIRE, + TreatmentPrescriptionAction.MODERATE_THINNING_BURN_PLUS_MODERATE_THINNING_BURN, + TreatmentPrescriptionAction.HEAVY_THINNING_BURN_PLUS_RX_FIRE, + TreatmentPrescriptionAction.HEAVY_THINNING_BURN_PLUS_HEAVY_THINNING_BURN, + TreatmentPrescriptionAction.RX_FIRE_PLUS_RX_FIRE, + TreatmentPrescriptionAction.MODERATE_MASTICATION_PLUS_MODERATE_MASTICATION, + TreatmentPrescriptionAction.HEAVY_THINNING_BIOMASS_PLUS_RX_FIRE, + TreatmentPrescriptionAction.MODERATE_MASTICATION_PLUS_RX_FIRE, + ] + return ( + TreatmentPrescriptionType.SEQUENCE + if is_sequence + else TreatmentPrescriptionType.SINGLE + ) + + +class TreatmentPrescription( + UUIDMixin, + CreatedAtMixin, + UpdatedAtMixin, + DeletedAtMixin, + models.Model, +): + created_by = models.ForeignKey( + User, + related_name="created_tx_prescriptions", + on_delete=models.RESTRICT, + ) + + updated_by = models.ForeignKey( + User, + related_name="updated_tx_prescriptions", + on_delete=models.RESTRICT, + ) + + project_area = models.ForeignKey( + ProjectArea, + related_name="tx_prescriptions", + on_delete=models.RESTRICT, + ) + + type = models.CharField( + choices=TreatmentPrescriptionType.choices, + default=TreatmentPrescriptionType.SINGLE, + ) + + action = models.CharField(choices=TreatmentPrescriptionAction.choices) + + # I still can't decide if it's best for us to clone the stand geometry + # or to have a direct reference to it. I think cloning makes sense because + # it actually frees us from a lot of FK handling + stand = models.ForeignKey( + Stand, + related_name="tx_prescriptions", + on_delete=models.SET_NULL, + null=True, + ) + + geometry = models.PolygonField(srid=settings.CRS_INTERNAL_REPRESENTATION) + + class Meta: + verbose_name = "Treatment Prescription" + verbose_name_plural = "Treatment Prescriptions" diff --git a/src/planscape/planning/models.py b/src/planscape/planning/models.py index 122556402..ae287d740 100644 --- a/src/planscape/planning/models.py +++ b/src/planscape/planning/models.py @@ -239,7 +239,11 @@ class ProjectAreaOrigin(models.TextChoices): class ProjectArea( - UUIDMixin, CreatedAtMixin, UpdatedAtMixin, DeletedAtMixin, models.Model + UUIDMixin, + CreatedAtMixin, + UpdatedAtMixin, + DeletedAtMixin, + models.Model, ): created_by = models.ForeignKey( User, @@ -261,6 +265,8 @@ class ProjectArea( help_text="Determines where this project area came from.", ) + name = models.CharField(max_length=128, null=False) + data = models.JSONField(null=True) geometry = models.MultiPolygonField( diff --git a/src/planscape/planscape/settings.py b/src/planscape/planscape/settings.py index 001af0d77..b77d9a4bc 100644 --- a/src/planscape/planscape/settings.py +++ b/src/planscape/planscape/settings.py @@ -37,6 +37,7 @@ "core", "datasets", "goals", + "impacts", "metrics", "organizations", "planning", From 19d6b45f6a73792fa952859fd52737b19625bce6 Mon Sep 17 00:00:00 2001 From: George Silva Date: Fri, 24 May 2024 16:28:52 -0300 Subject: [PATCH 2/4] remove double name field --- src/planscape/planning/models.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/planscape/planning/models.py b/src/planscape/planning/models.py index ae287d740..1cb185a01 100644 --- a/src/planscape/planning/models.py +++ b/src/planscape/planning/models.py @@ -264,9 +264,6 @@ class ProjectArea( default=ProjectAreaOrigin.OPTIMIZATION, help_text="Determines where this project area came from.", ) - - name = models.CharField(max_length=128, null=False) - data = models.JSONField(null=True) geometry = models.MultiPolygonField( From e77c48ab1188525ce317f22cee8cf42e00b40fe5 Mon Sep 17 00:00:00 2001 From: George Silva Date: Mon, 27 May 2024 09:32:45 -0300 Subject: [PATCH 3/4] migrations --- .../impacts/migrations/0001_initial.py | 205 ++++++++++++++++++ src/planscape/impacts/migrations/__init__.py | 0 2 files changed, 205 insertions(+) create mode 100644 src/planscape/impacts/migrations/0001_initial.py create mode 100644 src/planscape/impacts/migrations/__init__.py diff --git a/src/planscape/impacts/migrations/0001_initial.py b/src/planscape/impacts/migrations/0001_initial.py new file mode 100644 index 000000000..79401a186 --- /dev/null +++ b/src/planscape/impacts/migrations/0001_initial.py @@ -0,0 +1,205 @@ +from django.conf import settings +import django.contrib.gis.db.models.fields +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("stands", "0007_alter_stand_created_at_alter_standmetric_created_at"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("planning", "0018_projectarea"), + ] + + operations = [ + migrations.CreateModel( + name="TreatmentPrescription", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True, null=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "deleted_at", + models.DateTimeField( + help_text="Define if the entity has been deleted or not and when", + null=True, + verbose_name="Deleted at", + ), + ), + ("uuid", models.UUIDField(db_index=True, default=uuid.uuid4)), + ( + "type", + models.CharField( + choices=[("SINGLE", "Single"), ("SEQUENCE", "Sequence")], + default="SINGLE", + ), + ), + ( + "action", + models.CharField( + choices=[ + ( + "MODERATE_THINNING_BIOMASS", + "Moderate Thinning & Biomass Removal", + ), + ( + "HEAVY_THINNING_BIOMASS", + "Heavy Thinning & Biomass Removal", + ), + ("MODERATE_THINNING_BURN", "Moderate Thinning & Pile Burn"), + ("HEAVY_THINNING_BURN", "Heavy Thinning & Pile Burn"), + ("MODERATE_MASTICATION", "Moderate Mastication"), + ("HEAVY_MASTICATION", "Heavy Mastication"), + ("RX_FIRE", "Prescribed Fire"), + ( + "HEAVY_THINNING_RX_FIRE", + "Heavy Thinning & Prescribed Fire", + ), + ("MASTICATION_RX_FIRE", "Mastication & Prescribed Fire"), + ( + "MODERATE_THINNING_BURN_PLUS_RX_FIRE", + "Moderate Thinning & Pile Burn (year 0), Prescribed Burn (year 10)", + ), + ( + "MODERATE_THINNING_BURN_PLUS_MODERATE_THINNING_BURN", + "Moderate Thinning & Pile Burn (year 0), Moderate Thinning & Pile Burn (year 10)", + ), + ( + "HEAVY_THINNING_BURN_PLUS_RX_FIRE", + "Heavy Thinning & Pile Burn (year 0), Prescribed Burn (year 10)", + ), + ( + "HEAVY_THINNING_BURN_PLUS_HEAVY_THINNING_BURN", + "Heavy Thinning & Pile Burn (year 0), Heavy Thinning & Pile Burn (year 10)", + ), + ( + "RX_FIRE_PLUS_RX_FIRE", + "Prescribed Fire (year 0), Prescribed Fire (year 10)", + ), + ( + "MODERATE_MASTICATION_PLUS_MODERATE_MASTICATION", + "Moderate Mastication (year 0), Moderate Mastication (year 10)", + ), + ( + "HEAVY_THINNING_BIOMASS_PLUS_RX_FIRE", + "Heavy Thinning & Biomass Removal (year 0), Prescribed Fire (year 10)", + ), + ( + "MODERATE_MASTICATION_PLUS_RX_FIRE", + "Moderate Mastication (year 0), Prescribed Fire (year 10)", + ), + ] + ), + ), + ( + "geometry", + django.contrib.gis.db.models.fields.PolygonField(srid=4269), + ), + ( + "created_by", + models.ForeignKey( + on_delete=django.db.models.deletion.RESTRICT, + related_name="created_tx_prescriptions", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "project_area", + models.ForeignKey( + on_delete=django.db.models.deletion.RESTRICT, + related_name="tx_prescriptions", + to="planning.projectarea", + ), + ), + ( + "stand", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="tx_prescriptions", + to="stands.stand", + ), + ), + ( + "updated_by", + models.ForeignKey( + on_delete=django.db.models.deletion.RESTRICT, + related_name="updated_tx_prescriptions", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "Treatment Prescription", + "verbose_name_plural": "Treatment Prescriptions", + }, + ), + migrations.CreateModel( + name="TreatmentPlan", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True, null=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "deleted_at", + models.DateTimeField( + help_text="Define if the entity has been deleted or not and when", + null=True, + verbose_name="Deleted at", + ), + ), + ("uuid", models.UUIDField(db_index=True, default=uuid.uuid4)), + ( + "status", + models.CharField( + choices=[ + ("PENDING", "Pending"), + ("RUNNING", "Running"), + ("SUCCESS", "Suceess"), + ("FAILURE", "Failure"), + ], + default="PENDING", + ), + ), + ( + "created_by", + models.ForeignKey( + on_delete=django.db.models.deletion.RESTRICT, + related_name="tx_plans", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "scenario", + models.ForeignKey( + on_delete=django.db.models.deletion.RESTRICT, + related_name="tx_plans", + to="planning.scenario", + ), + ), + ], + options={ + "verbose_name": "Treatment Plan", + "verbose_name_plural": "Treatment Plans", + }, + ), + ] diff --git a/src/planscape/impacts/migrations/__init__.py b/src/planscape/impacts/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb From 3581b5faf1496a11390f4f7bb3fd3418fc116bfe Mon Sep 17 00:00:00 2001 From: George Silva Date: Tue, 28 May 2024 11:25:28 -0300 Subject: [PATCH 4/4] migrations v2 --- .../0003_alter_metricusage_attribute.py | 28 +++++++++++++++++++ .../impacts/migrations/0001_initial.py | 3 +- src/planscape/impacts/models.py | 1 + 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 src/planscape/goals/migrations/0003_alter_metricusage_attribute.py diff --git a/src/planscape/goals/migrations/0003_alter_metricusage_attribute.py b/src/planscape/goals/migrations/0003_alter_metricusage_attribute.py new file mode 100644 index 000000000..68b82e9b0 --- /dev/null +++ b/src/planscape/goals/migrations/0003_alter_metricusage_attribute.py @@ -0,0 +1,28 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("goals", "0002_metricusage_output_units"), + ] + + operations = [ + migrations.AlterField( + model_name="metricusage", + name="attribute", + field=models.CharField( + choices=[ + ("min", "Min"), + ("max", "Max"), + ("mean", "Mean"), + ("sum", "Sum"), + ("majority", "Majority"), + ("minority", "Minority"), + ("COUNT", "Count"), + ("FIELD", "Field"), + ], + default="mean", + max_length=64, + ), + ), + ] diff --git a/src/planscape/impacts/migrations/0001_initial.py b/src/planscape/impacts/migrations/0001_initial.py index 79401a186..fa2f2ee9a 100644 --- a/src/planscape/impacts/migrations/0001_initial.py +++ b/src/planscape/impacts/migrations/0001_initial.py @@ -9,8 +9,8 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("stands", "0007_alter_stand_created_at_alter_standmetric_created_at"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("stands", "0007_alter_stand_created_at_alter_standmetric_created_at"), ("planning", "0018_projectarea"), ] @@ -180,6 +180,7 @@ class Migration(migrations.Migration): default="PENDING", ), ), + ("name", models.CharField(max_length=256)), ( "created_by", models.ForeignKey( diff --git a/src/planscape/impacts/models.py b/src/planscape/impacts/models.py index 109fa81f6..2479daa94 100644 --- a/src/planscape/impacts/models.py +++ b/src/planscape/impacts/models.py @@ -37,6 +37,7 @@ class TreatmentPlan( choices=TreatmentPlanStatus.choices, default=TreatmentPlanStatus.PENDING, ) + name = models.CharField(max_length=256) class Meta: verbose_name = "Treatment Plan"