Files
Ai/location_data/test_cluster_block_live_api.py
T

255 lines
9.5 KiB
Python
Raw Normal View History

2026-05-11 04:38:44 +03:30
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,
2026-05-13 22:28:56 +03:30
AnalysisGridObservation,
2026-05-11 04:38:44 +03:30
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)
2026-05-13 22:28:56 +03:30
@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()