from datetime import date from unittest.mock import patch from django.test import TestCase, override_settings from rest_framework.test import APIClient from location_data.models import ( AnalysisGridCell, BlockSubdivision, RemoteSensingSubdivisionOption, RemoteSensingSubdivisionOptionAssignment, RemoteSensingSubdivisionOptionBlock, RemoteSensingRun, RemoteSensingSubdivisionResult, SoilLocation, ) @override_settings(ROOT_URLCONF="location_data.urls") class RemoteSensingSubdivisionOptionApiTests(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={"recommended_requested_k": 2, "active_requested_k": 1}, ) self.cells = [ AnalysisGridCell.objects.create( soil_location=self.location, block_subdivision=self.subdivision, block_code="block-1", cell_code=f"cell-{index}", chunk_size_sqm=900, geometry={ "type": "Polygon", "coordinates": [[ [51.3890 + (index * 0.0001), 35.6890], [51.3891 + (index * 0.0001), 35.6890], [51.3891 + (index * 0.0001), 35.6891], [51.3890 + (index * 0.0001), 35.6891], [51.3890 + (index * 0.0001), 35.6890], ]], }, centroid_lat=f"{35.68905 + (index * 0.0001):.6f}", centroid_lon=f"{51.38905 + (index * 0.0001):.6f}", ) for index in range(2) ] self.option_k1 = RemoteSensingSubdivisionOption.objects.create( result=self.result, requested_k=1, effective_cluster_count=1, is_active=True, is_recommended=False, selection_source="system", metadata={"cluster_summaries": []}, ) self.option_k2 = RemoteSensingSubdivisionOption.objects.create( result=self.result, requested_k=2, effective_cluster_count=2, is_active=False, is_recommended=True, selection_source="system", metadata={"cluster_summaries": []}, ) for cell in self.cells: RemoteSensingSubdivisionOptionAssignment.objects.create( option=self.option_k1, cell=cell, cluster_label=0, raw_feature_values={"ndvi": 0.4}, scaled_feature_values={"ndvi": 0.0}, ) RemoteSensingSubdivisionOptionBlock.objects.create( option=self.option_k1, cluster_label=0, sub_block_code="cluster-0", chunk_size_sqm=900, centroid_lat="35.689100", centroid_lon="51.389100", center_cell_code="cell-0", center_cell_lat="35.689050", center_cell_lon="51.389050", cell_count=2, cell_codes=[cell.cell_code for cell in self.cells], geometry=self.boundary, metadata={ "source": "analysis_grid_cells", "center_selection": {"strategy": "coordinate_1_center", "center_cell_code": "cell-0"}, }, ) for index, cell in enumerate(self.cells): RemoteSensingSubdivisionOptionAssignment.objects.create( option=self.option_k2, cell=cell, cluster_label=index, raw_feature_values={"ndvi": 0.4 + index}, scaled_feature_values={"ndvi": float(index)}, ) RemoteSensingSubdivisionOptionBlock.objects.create( option=self.option_k2, cluster_label=index, sub_block_code=f"cluster-{index}", chunk_size_sqm=900, centroid_lat=f"{35.68905 + (index * 0.0001):.6f}", centroid_lon=f"{51.38905 + (index * 0.0001):.6f}", center_cell_code=cell.cell_code, center_cell_lat=f"{35.68905 + (index * 0.0001):.6f}", center_cell_lon=f"{51.38905 + (index * 0.0001):.6f}", cell_count=1, cell_codes=[cell.cell_code], geometry=cell.geometry, metadata={ "source": "analysis_grid_cells", "center_selection": {"strategy": "coordinate_1_center", "center_cell_code": cell.cell_code}, }, ) def test_get_k_options_returns_all_persisted_options(self): response = self.client.get( f"/remote-sensing/results/{self.result.id}/k-options/" ) self.assertEqual(response.status_code, 200) payload = response.json()["data"] self.assertEqual(payload["result_id"], self.result.id) self.assertEqual(payload["active_requested_k"], 1) self.assertEqual(payload["recommended_requested_k"], 2) self.assertEqual([item["requested_k"] for item in payload["options"]], [1, 2]) self.assertEqual(payload["options"][0]["cluster_blocks"][0]["center_cell_code"], "cell-0") @patch("location_data.data_driven_subdivision.render_elbow_plot", return_value=None) def test_post_activate_k_marks_selected_option_active_and_syncs_result(self, _mock_plot): response = self.client.post( f"/remote-sensing/results/{self.result.id}/k-options/activate/", data={"requested_k": 2}, format="json", ) self.assertEqual(response.status_code, 200) payload = response.json()["data"] self.assertEqual(payload["activated_requested_k"], 2) self.assertEqual(payload["subdivision_result"]["cluster_count"], 2) self.assertEqual( payload["subdivision_result"]["metadata"]["active_requested_k"], 2, ) self.assertEqual(len(payload["subdivision_result"]["cluster_blocks"]), 2) self.assertEqual( payload["subdivision_result"]["cluster_blocks"][0]["center_cell_code"], "cell-0", ) self.option_k1.refresh_from_db() self.option_k2.refresh_from_db() self.assertFalse(self.option_k1.is_active) self.assertTrue(self.option_k2.is_active) self.assertEqual(self.option_k2.selection_source, "user") self.result.refresh_from_db() self.assertEqual(self.result.cluster_count, 2) self.assertEqual(self.result.assignments.count(), 2) self.assertEqual(self.result.cluster_blocks.count(), 2) self.assertEqual(self.result.cluster_blocks.order_by("cluster_label").first().center_cell_code, "cell-0")