Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Manage soil intervals on backend #1100

Merged
merged 18 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions terraso_backend/apps/graphql/schema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
ProjectSoilSettingsUpdateMutation,
SoilDataDeleteDepthIntervalMutation,
SoilDataUpdateDepthIntervalMutation,
SoilDataUpdateDepthPresetMutation,
SoilDataUpdateMutation,
)

Expand Down Expand Up @@ -189,7 +188,6 @@ class Mutations(graphene.ObjectType):
update_depth_dependent_soil_data = DepthDependentSoilDataUpdateMutation.Field()
update_soil_data_depth_interval = SoilDataUpdateDepthIntervalMutation.Field()
delete_soil_data_depth_interval = SoilDataDeleteDepthIntervalMutation.Field()
update_soil_data_depth_preset = SoilDataUpdateDepthPresetMutation.Field()
update_project_soil_settings = ProjectSoilSettingsUpdateMutation.Field()
update_project_soil_settings_depth_interval = (
ProjectSoilSettingsUpdateDepthIntervalMutation.Field()
Expand Down
18 changes: 3 additions & 15 deletions terraso_backend/apps/graphql/schema/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1646,7 +1646,6 @@ type Mutations {
updateDepthDependentSoilData(input: DepthDependentSoilDataUpdateMutationInput!): DepthDependentSoilDataUpdateMutationPayload!
updateSoilDataDepthInterval(input: SoilDataUpdateDepthIntervalMutationInput!): SoilDataUpdateDepthIntervalMutationPayload!
deleteSoilDataDepthInterval(input: SoilDataDeleteDepthIntervalMutationInput!): SoilDataDeleteDepthIntervalMutationPayload!
updateSoilDataDepthPreset(input: SoilDataUpdateDepthPresetMutationInput!): SoilDataUpdateDepthPresetMutationPayload!
updateProjectSoilSettings(input: ProjectSoilSettingsUpdateMutationInput!): ProjectSoilSettingsUpdateMutationPayload!
updateProjectSoilSettingsDepthInterval(input: ProjectSoilSettingsUpdateDepthIntervalMutationInput!): ProjectSoilSettingsUpdateDepthIntervalMutationPayload!
deleteProjectSoilSettingsDepthInterval(input: ProjectSoilSettingsDeleteDepthIntervalMutationInput!): ProjectSoilSettingsDeleteDepthIntervalMutationPayload!
Expand Down Expand Up @@ -2034,6 +2033,7 @@ input SiteAddMutationInput {
longitude: Float!
privacy: ProjectManagementSitePrivacyChoices
projectId: ID
createSoilData: Boolean
clientMutationId: String
}

Expand Down Expand Up @@ -2238,6 +2238,7 @@ input SoilDataUpdateMutationInput {
soilDepthSelect: SoilIdSoilDataSoilDepthSelectChoices
landCoverSelect: SoilIdSoilDataLandCoverSelectChoices
grazingSelect: SoilIdSoilDataGrazingSelectChoices
depthIntervalPreset: SoilIdSoilDataDepthIntervalPresetChoices
clientMutationId: String
}

Expand Down Expand Up @@ -2296,7 +2297,7 @@ input SoilDataUpdateDepthIntervalMutationInput {
electricalConductivityEnabled: Boolean
sodiumAdsorptionRatioEnabled: Boolean
soilStructureEnabled: Boolean
applyToAll: Boolean
applyToIntervals: [DepthIntervalInput!] = null
clientMutationId: String
}

Expand All @@ -2312,19 +2313,6 @@ input SoilDataDeleteDepthIntervalMutationInput {
clientMutationId: String
}

type SoilDataUpdateDepthPresetMutationPayload {
errors: GenericScalar
site: SiteNode!
intervals: [SoilDataDepthIntervalNode!]!
clientMutationId: String
}

input SoilDataUpdateDepthPresetMutationInput {
siteId: ID!
preset: SoilIdSoilDataDepthIntervalPresetChoices
clientMutationId: String
}

type ProjectSoilSettingsUpdateMutationPayload {
errors: GenericScalar
projectSoilSettings: ProjectSoilSettingsNode
Expand Down
17 changes: 14 additions & 3 deletions terraso_backend/apps/graphql/schema/sites.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from apps.audit_logs import api as audit_log_api
from apps.project_management.graphql.projects import ProjectNode
from apps.project_management.models import Project, Site, sites
from apps.soil_id.models.soil_data import SoilData
from apps.soil_id.models import SoilData

from .commons import (
BaseAuthenticatedMutation,
Expand Down Expand Up @@ -108,9 +108,10 @@ class Input:
longitude = graphene.Float(required=True)
privacy = SiteNode.privacy_enum()
project_id = graphene.ID()
create_soil_data = graphene.Boolean()

@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
def mutate_and_get_payload(cls, root, info, create_soil_data=True, **kwargs):
log = cls.get_logger()
user = info.context.user

Expand All @@ -134,6 +135,9 @@ def mutate_and_get_payload(cls, root, info, **kwargs):
if result.errors:
return result

if create_soil_data:
SoilData.objects.create(site=result.site)

site = result.site
site.mark_seen_by(user)
metadata = {
Expand Down Expand Up @@ -181,6 +185,7 @@ class Input:
project_id = graphene.ID()

@classmethod
@transaction.atomic
def mutate_and_get_payload(cls, root, info, **kwargs):
log = cls.get_logger()
user = info.context.user
Expand Down Expand Up @@ -215,7 +220,13 @@ def mutate_and_get_payload(cls, root, info, **kwargs):
continue
metadata[key] = value
if project_id:
metadata["project_id"] = str(project.id)
if hasattr(project, "soil_settings") and hasattr(site, "soil_data"):
if project_id is not None:
metadata["project_id"] = str(project.id)
else:
if hasattr(site, "soil_data"):
# Delete existing intervals if removed from project
site.soil_data.remove_from_project()

log.log(
user=user,
Expand Down
111 changes: 47 additions & 64 deletions terraso_backend/apps/soil_id/graphql/soil_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
from apps.project_management.models.sites import Site
from apps.soil_id.models.depth_dependent_soil_data import DepthDependentSoilData
from apps.soil_id.models.project_soil_settings import (
LandPKSIntervalDefaults,
NRCSIntervalDefaults,
ProjectDepthInterval,
ProjectSoilSettings,
)
Expand Down Expand Up @@ -234,12 +232,17 @@ class Input:
electrical_conductivity_enabled = graphene.Boolean()
sodium_adsorption_ratio_enabled = graphene.Boolean()
soil_structure_enabled = graphene.Boolean()

apply_to_all = graphene.Boolean()
apply_to_intervals = graphene.Field(graphene.List(graphene.NonNull(DepthIntervalInput)))

@classmethod
def mutate_and_get_payload(
cls, root, info, site_id, depth_interval, apply_to_all=False, **kwargs
cls,
root,
info,
site_id,
depth_interval,
apply_to_intervals=None,
**kwargs,
):
site = cls.get_or_throw(Site, "id", site_id)

Expand All @@ -260,19 +263,25 @@ def mutate_and_get_payload(
result = super().mutate_and_get_payload(
root, info, result_instance=site.soil_data, **kwargs
)

if apply_to_all:
intervals = site.soil_data.depth_intervals.exclude(
id=kwargs["model_instance"].id
).all()
kwargs.pop("model_instance")
kwargs.pop("label") # label shouldn't be applied to other intervals
for interval in intervals:
if apply_to_intervals:
for key in ("label", "model_instance"):
kwargs.pop(key, "")
# TODO: Would be better to do bulk create, but can't get that to work:
# "there is no unique or exclusion constraint matching the ON CONFLICT
# specification"
for interval in apply_to_intervals:
soil_interval, _ = SoilDataDepthInterval.objects.get_or_create(
soil_data=site.soil_data,
depth_interval_start=interval.start,
depth_interval_end=interval.end,
)
for key, value in kwargs.items():
setattr(interval, key, value)
SoilDataDepthInterval.objects.bulk_update(intervals, kwargs.keys())
setattr(soil_interval, key, value)
soil_interval.save()

return result
result.soil_data.refresh_from_db()

return result


class SoilDataDeleteDepthIntervalMutation(BaseAuthenticatedMutation):
Expand Down Expand Up @@ -329,6 +338,7 @@ class Input:
soil_depth_select = SoilDataNode.soil_depth_enum()
land_cover_select = SoilDataNode.land_cover_enum()
grazing_select = SoilDataNode.grazing_enum()
depth_interval_preset = SoilDataNode.depth_interval_preset_enum()

@classmethod
def mutate_and_get_payload(cls, root, info, site_id, **kwargs):
Expand All @@ -343,7 +353,14 @@ def mutate_and_get_payload(cls, root, info, site_id, **kwargs):

kwargs["model_instance"] = site.soil_data

return super().mutate_and_get_payload(root, info, **kwargs)
with transaction.atomic():
if (
"depth_interval_preset" in kwargs
and kwargs["depth_interval_preset"] != site.soil_data.depth_interval_preset
):
site.soil_data.depth_intervals.all().delete()
result = super().mutate_and_get_payload(root, info, **kwargs)
return result


class DepthDependentSoilDataUpdateMutation(BaseWriteMutation):
Expand Down Expand Up @@ -435,7 +452,13 @@ def mutate_and_get_payload(cls, root, info, project_id, **kwargs):

kwargs["model_instance"] = project.soil_settings

return super().mutate_and_get_payload(root, info, **kwargs)
with transaction.atomic():
if (
"depth_interval_preset" in kwargs
and kwargs["depth_interval_preset"] != project.soil_settings.depth_interval_preset
):
SoilDataDepthInterval.objects.filter(soil_data__site__project=project).delete()
return super().mutate_and_get_payload(root, info, **kwargs)


class ProjectSoilSettingsUpdateDepthIntervalMutation(BaseWriteMutation):
Expand Down Expand Up @@ -468,10 +491,12 @@ def mutate_and_get_payload(cls, root, info, project_id, depth_interval, **kwargs
depth_interval_end=depth_interval["end"],
)

return super().mutate_and_get_payload(
result = super().mutate_and_get_payload(
root, info, result_instance=project.soil_settings, **kwargs
)

return result


class ProjectSoilSettingsDeleteDepthIntervalMutation(BaseAuthenticatedMutation):
project_soil_settings = graphene.Field(ProjectSoilSettingsNode)
Expand All @@ -492,57 +517,15 @@ def mutate_and_get_payload(cls, root, info, project_id, depth_interval, **kwargs
cls.not_found()

try:
depth_interval = project.soil_settings.depth_intervals.get(
project_depth_interval = project.soil_settings.depth_intervals.get(
depth_interval_start=depth_interval["start"],
depth_interval_end=depth_interval["end"],
)
except ProjectDepthInterval.DoesNotExist:
cls.not_found()

depth_interval.delete()
project_depth_interval.delete()

return ProjectSoilSettingsDeleteDepthIntervalMutation(
project_soil_settings=project.soil_settings
)


class SoilDataUpdateDepthPresetMutation(BaseAuthenticatedMutation):
site = graphene.Field(SiteNode, required=True)
intervals = graphene.List(graphene.NonNull(SoilDataDepthIntervalNode), required=True)

class Input:
site_id = graphene.ID(required=True)
preset = SoilDataNode.depth_interval_preset_enum()

@classmethod
@transaction.atomic
def mutate_and_get_payload(cls, root, info, site_id, preset, **kwargs):
site = cls.get_or_throw(Site, "site_id", site_id)
user = info.context.user
if not user.has_perm(Site.get_perm("update_depth_interval"), site):
raise cls.not_allowed(MutationTypes.UPDATE)
if not site.soil_data:
site.soil_data = SoilData()

existing_intervals = SoilDataDepthInterval.objects.filter(soil_data=site.soil_data)

if site.soil_data.depth_interval_preset == preset.value:
return cls(site=site, intervals=existing_intervals)

# remove existing intervals
existing_intervals.delete()

# insert new intervals
match preset.value:
case "LANDPKS":
preset_values = LandPKSIntervalDefaults
case "NRCS":
preset_values = NRCSIntervalDefaults
case "CUSTOM" | "NONE":
preset_values = []

new_intervals = [
SoilDataDepthInterval(soil_data=site.soil_data, **kwargs) for kwargs in preset_values
]
SoilDataDepthInterval.objects.bulk_create(new_intervals)

return cls(site=site, intervals=new_intervals)
34 changes: 15 additions & 19 deletions terraso_backend/apps/soil_id/models/project_soil_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,13 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see https://www.gnu.org/licenses/.

from dirtyfields import DirtyFieldsMixin
from django.db import models, transaction
from django.db import models

from apps.core.models.commons import BaseModel
from apps.project_management.models.projects import Project
from apps.project_management.models import Project
from apps.soil_id import permission_rules
from apps.soil_id.models.depth_dependent_soil_data import DepthDependentSoilData
from apps.soil_id.models.depth_interval import BaseDepthInterval
from apps.soil_id.models.soil_data import SoilDataDepthInterval


class DepthIntervalPreset(models.TextChoices):
Expand Down Expand Up @@ -49,7 +47,7 @@ class DepthIntervalPreset(models.TextChoices):
]


class ProjectSoilSettings(BaseModel, DirtyFieldsMixin):
class ProjectSoilSettings(BaseModel):
class Meta(BaseModel.Meta):
abstract = False
rules_permissions = {
Expand Down Expand Up @@ -90,19 +88,17 @@ class MeasurementUnit(models.TextChoices):
def is_custom_preset(self):
return self.depth_interval_preset == DepthIntervalPreset.CUSTOM

def save(self, *args, **kwargs):
dirty_fields = self.get_dirty_fields()
with transaction.atomic():
result = super().save(*args, **kwargs)
if (
"depth_interval_preset" in dirty_fields
and dirty_fields.get("depth_interval_preset") != self.depth_interval_preset
):
# delete project intervals
ProjectDepthInterval.objects.filter(project=self).delete()
# delete related soil data
DepthDependentSoilData.delete_in_project(self.project.id)
return result
@property
def methods(self):
field_names = [
field.name.removesuffix("_enabled")
for field in SoilDataDepthInterval._meta.fields
if field.name.endswith("_enabled")
]
return {
f"{field_name}_enabled": getattr(self, f"{field_name}_required")
for field_name in field_names
}


class ProjectDepthInterval(BaseModel, BaseDepthInterval):
Expand Down
11 changes: 11 additions & 0 deletions terraso_backend/apps/soil_id/models/soil_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,13 @@ class SoilDataDepthIntervalPreset(models.TextChoices):
choices=SoilDataDepthIntervalPreset.choices, blank=True, null=True
)

def remove_from_project(self):
SoilDataDepthInterval.objects.filter(soil_data=self).delete()
# Default is to set the site interval to custom for now
# TODO: At some point, user will be able to set a default preset
self.depth_interval_preset = self.SoilDataDepthIntervalPreset.CUSTOM
self.save()


class SoilDataDepthInterval(BaseModel, BaseDepthInterval):
soil_data = models.ForeignKey(
Expand All @@ -225,6 +232,10 @@ class Meta(BaseModel.Meta):
ordering = ["depth_interval_start"]
constraints = BaseDepthInterval.constraints("soil_data")

@classmethod
def soil_inputs(cls):
return [field.name for field in cls._meta.fields if field.name.endswith("_enabled")]

def clean(self):
super().clean()
BaseDepthInterval.validate_intervals(list(self.soil_data.depth_intervals.all()))
Expand Down
Loading
Loading