255 lines
9.5 KiB
Python
255 lines
9.5 KiB
Python
from datetime import date, timedelta
|
|
import uuid
|
|
from unittest.mock import patch
|
|
|
|
from django.test import TestCase, override_settings
|
|
from django.utils import timezone
|
|
from rest_framework.test import APIClient
|
|
|
|
from location_data.models import (
|
|
AnalysisGridCell,
|
|
AnalysisGridObservation,
|
|
BlockSubdivision,
|
|
RemoteSensingClusterBlock,
|
|
RemoteSensingRun,
|
|
RemoteSensingSubdivisionResult,
|
|
SoilLocation,
|
|
)
|
|
|
|
|
|
@override_settings(ROOT_URLCONF="location_data.urls")
|
|
class RemoteSensingClusterBlockLiveApiTests(TestCase):
|
|
def setUp(self):
|
|
self.client = APIClient()
|
|
self.boundary = {
|
|
"type": "Polygon",
|
|
"coordinates": [
|
|
[
|
|
[51.3890, 35.6890],
|
|
[51.3900, 35.6890],
|
|
[51.3900, 35.6900],
|
|
[51.3890, 35.6900],
|
|
[51.3890, 35.6890],
|
|
]
|
|
],
|
|
}
|
|
self.location = SoilLocation.objects.create(
|
|
latitude="35.689200",
|
|
longitude="51.389000",
|
|
farm_boundary=self.boundary,
|
|
)
|
|
self.subdivision = BlockSubdivision.objects.create(
|
|
soil_location=self.location,
|
|
block_code="block-1",
|
|
source_boundary=self.boundary,
|
|
chunk_size_sqm=900,
|
|
status="subdivided",
|
|
)
|
|
self.run = RemoteSensingRun.objects.create(
|
|
soil_location=self.location,
|
|
block_subdivision=self.subdivision,
|
|
block_code="block-1",
|
|
chunk_size_sqm=900,
|
|
temporal_start=date(2025, 1, 1),
|
|
temporal_end=date(2025, 1, 31),
|
|
status=RemoteSensingRun.STATUS_SUCCESS,
|
|
metadata={"stage": "completed"},
|
|
)
|
|
self.result = RemoteSensingSubdivisionResult.objects.create(
|
|
soil_location=self.location,
|
|
run=self.run,
|
|
block_subdivision=self.subdivision,
|
|
block_code="block-1",
|
|
chunk_size_sqm=900,
|
|
temporal_start=date(2025, 1, 1),
|
|
temporal_end=date(2025, 1, 31),
|
|
cluster_count=1,
|
|
selected_features=["ndvi", "ndwi", "soil_vv_db"],
|
|
metadata={"used_cell_count": 2, "skipped_cell_count": 0},
|
|
)
|
|
self.cluster_block = RemoteSensingClusterBlock.objects.create(
|
|
result=self.result,
|
|
soil_location=self.location,
|
|
block_subdivision=self.subdivision,
|
|
block_code="block-1",
|
|
sub_block_code="cluster-0",
|
|
cluster_label=0,
|
|
chunk_size_sqm=900,
|
|
centroid_lat="35.689500",
|
|
centroid_lon="51.389500",
|
|
cell_count=2,
|
|
cell_codes=["cell-1", "cell-2"],
|
|
geometry=self.boundary,
|
|
metadata={"source": "analysis_grid_cells"},
|
|
)
|
|
|
|
def test_get_cluster_block_live_returns_404_when_uuid_missing(self):
|
|
response = self.client.get(
|
|
f"/remote-sensing/cluster-blocks/{uuid.uuid4()}/live/"
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 404)
|
|
self.assertEqual(response.json()["msg"], "زیربلاک KMeans پیدا نشد.")
|
|
|
|
@patch("location_data.views.compute_remote_sensing_metrics")
|
|
def test_get_cluster_block_live_reads_fresh_metrics_from_openeo(self, compute_mock):
|
|
compute_mock.return_value = {
|
|
"results": {
|
|
f"cluster-{self.cluster_block.uuid}": {
|
|
"ndvi": 0.63,
|
|
"ndwi": 0.21,
|
|
"soil_vv": 0.13,
|
|
"soil_vv_db": -8.860566,
|
|
}
|
|
},
|
|
"metadata": {
|
|
"backend": "openeo",
|
|
"backend_url": "https://openeofed.dataspace.copernicus.eu",
|
|
"collections_used": ["SENTINEL2_L2A", "SENTINEL1_GRD"],
|
|
"job_refs": {"ndvi": "job-1", "ndwi": "job-2", "soil_vv": "job-3"},
|
|
"failed_metrics": [],
|
|
"payload_diagnostics": {},
|
|
},
|
|
}
|
|
|
|
response = self.client.get(
|
|
f"/remote-sensing/cluster-blocks/{self.cluster_block.uuid}/live/",
|
|
data={"temporal_start": "2025-02-01", "temporal_end": "2025-02-15"},
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
payload = response.json()["data"]
|
|
self.assertEqual(payload["status"], "success")
|
|
self.assertEqual(payload["source"], "openeo")
|
|
self.assertEqual(payload["cluster_block"]["uuid"], str(self.cluster_block.uuid))
|
|
self.assertEqual(payload["summary"]["cell_count"], 2)
|
|
self.assertEqual(payload["summary"]["ndvi_mean"], 0.63)
|
|
self.assertEqual(payload["metrics"]["soil_vv_db"], -8.860566)
|
|
self.assertEqual(payload["temporal_extent"]["start_date"], "2025-02-01")
|
|
self.assertEqual(payload["temporal_extent"]["end_date"], "2025-02-15")
|
|
self.assertEqual(
|
|
payload["metadata"]["requested_cluster_uuid"],
|
|
str(self.cluster_block.uuid),
|
|
)
|
|
|
|
compute_mock.assert_called_once()
|
|
args, kwargs = compute_mock.call_args
|
|
self.assertEqual(len(args[0]), 1)
|
|
self.assertEqual(args[0][0].geometry, self.boundary)
|
|
self.assertEqual(kwargs["temporal_start"].isoformat(), "2025-02-01")
|
|
self.assertEqual(kwargs["temporal_end"].isoformat(), "2025-02-15")
|
|
|
|
@patch("location_data.views.compute_remote_sensing_metrics")
|
|
def test_get_cluster_block_live_rebuilds_geometry_from_member_cells_when_missing(self, compute_mock):
|
|
cell_geometry = {
|
|
"type": "Polygon",
|
|
"coordinates": [
|
|
[
|
|
[51.3890, 35.6890],
|
|
[51.3895, 35.6890],
|
|
[51.3895, 35.6895],
|
|
[51.3890, 35.6895],
|
|
[51.3890, 35.6890],
|
|
]
|
|
],
|
|
}
|
|
AnalysisGridCell.objects.create(
|
|
soil_location=self.location,
|
|
block_subdivision=self.subdivision,
|
|
block_code="block-1",
|
|
cell_code="cell-1",
|
|
chunk_size_sqm=900,
|
|
geometry=cell_geometry,
|
|
centroid_lat="35.689250",
|
|
centroid_lon="51.389250",
|
|
)
|
|
self.cluster_block.geometry = {}
|
|
self.cluster_block.save(update_fields=["geometry", "updated_at"])
|
|
compute_mock.return_value = {
|
|
"results": {
|
|
f"cluster-{self.cluster_block.uuid}": {
|
|
"ndvi": 0.55,
|
|
"ndwi": 0.18,
|
|
"soil_vv": 0.10,
|
|
"soil_vv_db": -10.0,
|
|
}
|
|
},
|
|
"metadata": {"job_refs": {}, "failed_metrics": [], "payload_diagnostics": {}},
|
|
}
|
|
|
|
response = self.client.get(
|
|
f"/remote-sensing/cluster-blocks/{self.cluster_block.uuid}/live/",
|
|
data={"days": 7},
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
payload = response.json()["data"]
|
|
self.assertEqual(payload["metrics"]["ndvi"], 0.55)
|
|
|
|
compute_mock.assert_called_once()
|
|
args, kwargs = compute_mock.call_args
|
|
self.assertEqual(args[0][0].geometry["type"], "Polygon")
|
|
self.assertEqual(args[0][0].geometry, cell_geometry)
|
|
expected_end = timezone.localdate() - timedelta(days=1)
|
|
expected_start = expected_end - timedelta(days=6)
|
|
self.assertEqual(kwargs["temporal_start"], expected_start)
|
|
self.assertEqual(kwargs["temporal_end"], expected_end)
|
|
|
|
@patch("location_data.views.compute_remote_sensing_metrics")
|
|
def test_get_cluster_block_live_uses_database_cache_for_matching_window(self, compute_mock):
|
|
cell_1 = AnalysisGridCell.objects.create(
|
|
soil_location=self.location,
|
|
block_subdivision=self.subdivision,
|
|
block_code="block-1",
|
|
cell_code="cell-1",
|
|
chunk_size_sqm=900,
|
|
geometry=self.boundary,
|
|
centroid_lat="35.689250",
|
|
centroid_lon="51.389250",
|
|
)
|
|
cell_2 = AnalysisGridCell.objects.create(
|
|
soil_location=self.location,
|
|
block_subdivision=self.subdivision,
|
|
block_code="block-1",
|
|
cell_code="cell-2",
|
|
chunk_size_sqm=900,
|
|
geometry=self.boundary,
|
|
centroid_lat="35.689750",
|
|
centroid_lon="51.389750",
|
|
)
|
|
AnalysisGridObservation.objects.create(
|
|
cell=cell_1,
|
|
run=self.run,
|
|
temporal_start=date(2025, 1, 1),
|
|
temporal_end=date(2025, 1, 31),
|
|
ndvi=0.44,
|
|
ndwi=0.12,
|
|
soil_vv=0.09,
|
|
soil_vv_db=-11.0,
|
|
metadata={"backend_name": "openeo"},
|
|
)
|
|
AnalysisGridObservation.objects.create(
|
|
cell=cell_2,
|
|
run=self.run,
|
|
temporal_start=date(2025, 1, 1),
|
|
temporal_end=date(2025, 1, 31),
|
|
ndvi=0.64,
|
|
ndwi=0.22,
|
|
soil_vv=0.19,
|
|
soil_vv_db=-7.0,
|
|
metadata={"backend_name": "openeo"},
|
|
)
|
|
|
|
response = self.client.get(
|
|
f"/remote-sensing/cluster-blocks/{self.cluster_block.uuid}/live/",
|
|
data={"temporal_start": "2025-01-01", "temporal_end": "2025-01-31"},
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
payload = response.json()["data"]
|
|
self.assertEqual(payload["source"], "database")
|
|
self.assertTrue(payload["metadata"]["cache_hit"])
|
|
self.assertEqual(payload["summary"]["ndvi_mean"], 0.54)
|
|
self.assertEqual(payload["metrics"]["soil_vv_db"], -9.0)
|
|
compute_mock.assert_not_called()
|