Skip to content

Commit

Permalink
feat: add soil metadata table to support soil selection feature
Browse files Browse the repository at this point in the history
  • Loading branch information
tm-ruxandra committed Nov 19, 2024
1 parent fe73599 commit fac222b
Show file tree
Hide file tree
Showing 11 changed files with 340 additions and 1 deletion.
19 changes: 19 additions & 0 deletions terraso_backend/apps/graphql/schema/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,7 @@ type SiteNode implements Node {
id: ID!
seen: Boolean!
soilData: SoilDataNode!
soilMetadata: SoilMetadataNode!
}

"""An enumeration."""
Expand Down Expand Up @@ -1541,6 +1542,11 @@ enum SoilIdDepthDependentSoilDataCarbonatesChoices {
VIOLENTLY_EFFERVESCENT
}

type SoilMetadataNode {
site: SiteNode!
selectedSoilId: String
}

type ProjectSoilSettingsNode {
project: ProjectNode!
depthIntervalPreset: SoilIdProjectSoilSettingsDepthIntervalPresetChoices!
Expand Down Expand Up @@ -1647,6 +1653,7 @@ type Mutations {
pushSoilData(input: SoilDataPushInput!): SoilDataPushPayload!
updateSoilDataDepthInterval(input: SoilDataUpdateDepthIntervalMutationInput!): SoilDataUpdateDepthIntervalMutationPayload! @deprecated(reason: "Use push_soil_data instead.")
deleteSoilDataDepthInterval(input: SoilDataDeleteDepthIntervalMutationInput!): SoilDataDeleteDepthIntervalMutationPayload! @deprecated(reason: "Use push_soil_data instead.")
updateSoilMetadata(input: SoilMetadataUpdateMutationInput!): SoilMetadataUpdateMutationPayload!
updateProjectSoilSettings(input: ProjectSoilSettingsUpdateMutationInput!): ProjectSoilSettingsUpdateMutationPayload!
updateProjectSoilSettingsDepthInterval(input: ProjectSoilSettingsUpdateDepthIntervalMutationInput!): ProjectSoilSettingsUpdateDepthIntervalMutationPayload!
deleteProjectSoilSettingsDepthInterval(input: ProjectSoilSettingsDeleteDepthIntervalMutationInput!): ProjectSoilSettingsDeleteDepthIntervalMutationPayload!
Expand Down Expand Up @@ -2411,6 +2418,18 @@ input SoilDataDeleteDepthIntervalMutationInput {
clientMutationId: String
}

type SoilMetadataUpdateMutationPayload {
errors: GenericScalar
soilMetadata: SoilMetadataNode
clientMutationId: String
}

input SoilMetadataUpdateMutationInput {
selectedSoilId: String
siteId: ID!
clientMutationId: String
}

type ProjectSoilSettingsUpdateMutationPayload {
errors: GenericScalar
projectSoilSettings: ProjectSoilSettingsNode
Expand Down
2 changes: 2 additions & 0 deletions terraso_backend/apps/graphql/schema/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
)
from apps.soil_id.graphql.soil_data.push_mutation import SoilDataPush
from apps.soil_id.graphql.soil_id.queries import soil_id
from apps.soil_id.graphql.soil_metadata.mutations import SoilMetadataUpdateMutation
from apps.soil_id.graphql.soil_project.mutations import (
ProjectSoilSettingsDeleteDepthIntervalMutation,
ProjectSoilSettingsUpdateDepthIntervalMutation,
Expand Down Expand Up @@ -203,6 +204,7 @@ class Mutations(graphene.ObjectType):
delete_soil_data_depth_interval = SoilDataDeleteDepthIntervalMutation.Field(
deprecation_reason="Use push_soil_data instead."
)
update_soil_metadata = SoilMetadataUpdateMutation.Field()
update_project_soil_settings = ProjectSoilSettingsUpdateMutation.Field()
update_project_soil_settings_depth_interval = (
ProjectSoilSettingsUpdateDepthIntervalMutation.Field()
Expand Down
8 changes: 7 additions & 1 deletion terraso_backend/apps/graphql/schema/sites.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
check_project_permission,
check_site_permission,
)
from apps.soil_id.models import SoilData
from apps.soil_id.models import SoilData, SoilMetadata

from .commons import (
BaseAuthenticatedMutation,
Expand Down Expand Up @@ -67,6 +67,11 @@ class SiteNode(DjangoObjectType):
required=True,
default_value=SoilData(),
)
soil_metadata = graphene.Field(
"apps.soil_id.graphql.soil_metadata.queries.SoilMetadataNode",
required=True,
default_value=SoilMetadata(),
)

class Meta:
model = Site
Expand Down Expand Up @@ -148,6 +153,7 @@ def mutate_and_get_payload(cls, root, info, create_soil_data=True, **kwargs):

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

site = result.site
site.mark_seen_by(user)
Expand Down
51 changes: 51 additions & 0 deletions terraso_backend/apps/soil_id/graphql/soil_metadata/mutations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright © 2024 Technology Matters
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/.

import graphene
from django.db import transaction

from apps.graphql.schema.commons import BaseWriteMutation
from apps.graphql.schema.constants import MutationTypes
from apps.project_management.models.sites import Site
from apps.project_management.permission_rules import Context
from apps.project_management.permission_table import SiteAction, check_site_permission
from apps.soil_id.graphql.soil_metadata.queries import SoilMetadataNode
from apps.soil_id.graphql.soil_metadata.types import SoilMetadataInputs
from apps.soil_id.models.soil_metadata import SoilMetadata


class SoilMetadataUpdateMutation(BaseWriteMutation):
soil_metadata = graphene.Field(SoilMetadataNode)
model_class = SoilMetadata

class Input(SoilMetadataInputs):
site_id = graphene.ID(required=True)

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

user = info.context.user
if not check_site_permission(user, SiteAction.ENTER_DATA, Context(site=site)):
raise cls.not_allowed(MutationTypes.UPDATE)

if not hasattr(site, "soil_metadata"):
site.soil_metadata = SoilMetadata()

kwargs["model_instance"] = site.soil_metadata

with transaction.atomic():
result = super().mutate_and_get_payload(root, info, **kwargs)
return result
30 changes: 30 additions & 0 deletions terraso_backend/apps/soil_id/graphql/soil_metadata/queries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Copyright © 2024 Technology Matters
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/.


import graphene
from graphene_django import DjangoObjectType

from apps.graphql.schema.commons import data_model_excluded_fields
from apps.graphql.schema.sites import SiteNode
from apps.soil_id.models.soil_metadata import SoilMetadata


class SoilMetadataNode(DjangoObjectType):
site = graphene.Field(SiteNode, source="soil_metadata__site", required=True)

class Meta:
model = SoilMetadata
exclude = data_model_excluded_fields()
20 changes: 20 additions & 0 deletions terraso_backend/apps/soil_id/graphql/soil_metadata/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright © 2024 Technology Matters
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/.

import graphene


class SoilMetadataInputs:
selected_soil_id = graphene.String()
48 changes: 48 additions & 0 deletions terraso_backend/apps/soil_id/migrations/0021_soilmetadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Generated by Django 5.1.3 on 2024-11-18 20:52

import uuid

import django.db.models.deletion
import rules.contrib.models
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("project_management", "0028_site_elevation"),
("soil_id", "0020_alter_projectsoilsettings_slope_required_and_more"),
]

operations = [
migrations.CreateModel(
name="SoilMetadata",
fields=[
("deleted_at", models.DateTimeField(db_index=True, editable=False, null=True)),
("deleted_by_cascade", models.BooleanField(default=False, editable=False)),
(
"id",
models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("selected_soil_id", models.CharField(blank=True, null=True)),
(
"site",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="soil_metadata",
to="project_management.site",
),
),
],
options={
"verbose_name_plural": "soil metadata",
"ordering": ["created_at"],
"get_latest_by": "-created_at",
"abstract": False,
},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
]
2 changes: 2 additions & 0 deletions terraso_backend/apps/soil_id/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@
from .soil_data import SoilData, SoilDataDepthInterval
from .soil_data_history import SoilDataHistory
from .soil_id_cache import SoilIdCache
from .soil_metadata import SoilMetadata

__all__ = [
"SoilData",
"SoilMetadata",
"DepthDependentSoilData",
"ProjectSoilSettings",
"ProjectDepthInterval",
Expand Down
29 changes: 29 additions & 0 deletions terraso_backend/apps/soil_id/models/soil_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Copyright © 2024 Technology Matters
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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 django.db import models

from apps.core.models.commons import BaseModel
from apps.project_management.models.sites import Site


class SoilMetadata(BaseModel):
site = models.OneToOneField(Site, on_delete=models.CASCADE, related_name="soil_metadata")

# upcoming work will support multiple soil ID ratings, but for now there's only one value
selected_soil_id = models.CharField(blank=True, null=True)

class Meta(BaseModel.Meta):
verbose_name_plural = "soil metadata"
85 changes: 85 additions & 0 deletions terraso_backend/tests/graphql/mutations/test_soil_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Copyright © 2023 Technology Matters
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/.

import json

import pytest
import structlog
from graphene_django.utils.testing import graphql_query
from mixer.backend.django import mixer

from apps.core.models import User
from apps.project_management.models.sites import Site

pytestmark = pytest.mark.django_db

logger = structlog.get_logger(__name__)

UPDATE_SOIL_METADATA_QUERY = """
mutation SoilMetadataUpdateMutation($input: SoilMetadataUpdateMutationInput!) {
updateSoilMetadata(input: $input) {
soilMetadata {
selectedSoilId
}
errors
}
}
"""


def test_update_soil_metadata(client, user, site):
client.force_login(user)
new_data = {
"siteId": str(site.id),
"selectedSoilId": "test",
}
response = graphql_query(
UPDATE_SOIL_METADATA_QUERY, variables={"input": new_data}, client=client
)
assert response.json()["data"]["updateSoilMetadata"]["errors"] is None
payload = response.json()["data"]["updateSoilMetadata"]["soilMetadata"]

assert payload["selectedSoilId"] == "test"


def test_update_soil_metadata_clear(client, user, site):
client.force_login(user)
cleared_data = {
"siteId": str(site.id),
"selectedSoilId": None,
}
response = graphql_query(
UPDATE_SOIL_METADATA_QUERY, variables={"input": cleared_data}, client=client
)
payload = response.json()["data"]["updateSoilMetadata"]["soilMetadata"]
assert response.json()["data"]["updateSoilMetadata"]["errors"] is None

assert payload["selectedSoilId"] is None


def test_update_soil_metadata_not_allowed(client, site):
user = mixer.blend(User)
client.force_login(user)
new_data = {
"siteId": str(site.id),
"selectedSoilId": None,
}
response = graphql_query(
UPDATE_SOIL_METADATA_QUERY, variables={"input": new_data}, client=client
)
error_msg = response.json()["data"]["updateSoilMetadata"]["errors"][0]["message"]
assert json.loads(error_msg)[0]["code"] == "update_not_allowed"

assert not hasattr(Site.objects.get(id=site.id), "soil_metadata")
Loading

0 comments on commit fac222b

Please sign in to comment.