UPDATE
This commit is contained in:
@@ -6,7 +6,9 @@ from django.db import transaction
|
||||
|
||||
from location_data.models import SoilLocation
|
||||
from location_data.serializers import SoilDepthDataSerializer
|
||||
from location_data.tasks import fetch_soil_data_for_coordinates
|
||||
from plant.serializers import PlantSerializer
|
||||
from weather.services import update_weather_for_location
|
||||
from weather.models import WeatherForecast
|
||||
|
||||
from .models import SensorData
|
||||
@@ -17,6 +19,10 @@ DEPTH_PRIORITY = ["0-5cm", "5-15cm", "15-30cm"]
|
||||
DECIMAL_PRECISION = Decimal("0.000001")
|
||||
|
||||
|
||||
class ExternalDataSyncError(Exception):
|
||||
"""خطا در همگامسازی داده از سرویسهای بیرونی."""
|
||||
|
||||
|
||||
def get_farm_details(farm_uuid: str):
|
||||
farm = (
|
||||
SensorData.objects.select_related("center_location", "weather_forecast")
|
||||
@@ -104,6 +110,38 @@ def resolve_weather_for_location(location: SoilLocation) -> WeatherForecast | No
|
||||
)
|
||||
|
||||
|
||||
def ensure_location_and_weather_data(location: SoilLocation) -> tuple[SoilLocation, WeatherForecast | None]:
|
||||
"""
|
||||
اگر داده خاک یا آبوهوا برای location موجود نباشد، از سرویس مربوطه
|
||||
واکشی و در دیتابیس ذخیره میشود.
|
||||
"""
|
||||
if not location.is_complete:
|
||||
try:
|
||||
soil_result = fetch_soil_data_for_coordinates(
|
||||
latitude=float(location.latitude),
|
||||
longitude=float(location.longitude),
|
||||
)
|
||||
except Exception as exc:
|
||||
raise ExternalDataSyncError(f"خطا در واکشی داده خاک: {exc}") from exc
|
||||
|
||||
if soil_result.get("status") != "completed":
|
||||
raise ExternalDataSyncError(
|
||||
soil_result.get("error") or "واکشی داده خاک کامل نشد."
|
||||
)
|
||||
location.refresh_from_db()
|
||||
|
||||
weather_forecast = resolve_weather_for_location(location)
|
||||
if weather_forecast is None:
|
||||
weather_result = update_weather_for_location(location)
|
||||
if weather_result.get("status") not in {"success", "no_data"}:
|
||||
raise ExternalDataSyncError(
|
||||
weather_result.get("error") or "واکشی داده آبوهوا کامل نشد."
|
||||
)
|
||||
weather_forecast = resolve_weather_for_location(location)
|
||||
|
||||
return location, weather_forecast
|
||||
|
||||
|
||||
def _flatten_sensor_metrics(sensor_payload: dict | None) -> dict:
|
||||
if not isinstance(sensor_payload, dict):
|
||||
return {}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from datetime import date
|
||||
from unittest.mock import patch
|
||||
import uuid
|
||||
|
||||
from django.test import TestCase
|
||||
@@ -193,3 +194,68 @@ class FarmDataUpsertApiTests(TestCase):
|
||||
self.assertEqual(str(farm.center_location.latitude), "50.010000")
|
||||
self.assertEqual(str(farm.center_location.longitude), "50.010000")
|
||||
self.assertIsNone(farm.weather_forecast_id)
|
||||
|
||||
@patch("farm_data.services.update_weather_for_location")
|
||||
@patch("farm_data.services.fetch_soil_data_for_coordinates")
|
||||
def test_post_fetches_missing_location_and_weather_data(
|
||||
self,
|
||||
mock_fetch_soil_data_for_coordinates,
|
||||
mock_update_weather_for_location,
|
||||
):
|
||||
missing_boundary = square_boundary_for_center(36.0, 52.0)
|
||||
farm_uuid = uuid.uuid4()
|
||||
|
||||
def soil_side_effect(latitude, longitude, task_id="", progress_callback=None):
|
||||
location = SoilLocation.objects.get(
|
||||
latitude="36.000000",
|
||||
longitude="52.000000",
|
||||
)
|
||||
SoilDepthData.objects.update_or_create(
|
||||
soil_location=location,
|
||||
depth_label="0-5cm",
|
||||
defaults={"clay": 20.0},
|
||||
)
|
||||
SoilDepthData.objects.update_or_create(
|
||||
soil_location=location,
|
||||
depth_label="5-15cm",
|
||||
defaults={"clay": 18.0},
|
||||
)
|
||||
SoilDepthData.objects.update_or_create(
|
||||
soil_location=location,
|
||||
depth_label="15-30cm",
|
||||
defaults={"clay": 16.0},
|
||||
)
|
||||
return {"status": "completed", "location_id": location.id, "depths": ["0-5cm", "5-15cm", "15-30cm"]}
|
||||
|
||||
def weather_side_effect(location):
|
||||
WeatherForecast.objects.update_or_create(
|
||||
location=location,
|
||||
forecast_date=date(2026, 4, 12),
|
||||
defaults={
|
||||
"temperature_min": 10.0,
|
||||
"temperature_max": 20.0,
|
||||
"temperature_mean": 15.0,
|
||||
},
|
||||
)
|
||||
return {"status": "success", "location_id": location.id, "days_updated": 1}
|
||||
|
||||
mock_fetch_soil_data_for_coordinates.side_effect = soil_side_effect
|
||||
mock_update_weather_for_location.side_effect = weather_side_effect
|
||||
|
||||
response = self.client.post(
|
||||
"/api/farm-data/",
|
||||
data={
|
||||
"farm_uuid": str(farm_uuid),
|
||||
"farm_boundary": missing_boundary,
|
||||
"sensor_payload": {"sensor-7-1": {"soil_moisture": 44.0}},
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 201)
|
||||
mock_fetch_soil_data_for_coordinates.assert_called_once()
|
||||
mock_update_weather_for_location.assert_called_once()
|
||||
|
||||
farm = SensorData.objects.get(farm_uuid=farm_uuid)
|
||||
self.assertEqual(farm.center_location.depths.count(), 3)
|
||||
self.assertIsNotNone(farm.weather_forecast_id)
|
||||
|
||||
+15
-2
@@ -21,9 +21,10 @@ from .serializers import (
|
||||
SensorParameterSerializer,
|
||||
)
|
||||
from .services import (
|
||||
ExternalDataSyncError,
|
||||
ensure_location_and_weather_data,
|
||||
get_farm_details,
|
||||
resolve_center_location_from_boundary,
|
||||
resolve_weather_for_location,
|
||||
)
|
||||
|
||||
|
||||
@@ -93,6 +94,10 @@ class FarmDataUpsertView(APIView):
|
||||
SensorDataValidationErrorSerializer,
|
||||
"داده ورودی نامعتبر است.",
|
||||
),
|
||||
502: build_response(
|
||||
SensorDataNotFoundSerializer,
|
||||
"واکشی داده خاک یا آبوهوا از سرویس بیرونی ناموفق بود.",
|
||||
),
|
||||
},
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
@@ -171,7 +176,15 @@ class FarmDataUpsertView(APIView):
|
||||
{"code": 400, "msg": "داده نامعتبر.", "data": {"farm_boundary": [str(exc)]}},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
weather_forecast = resolve_weather_for_location(center_location)
|
||||
try:
|
||||
center_location, weather_forecast = ensure_location_and_weather_data(
|
||||
center_location
|
||||
)
|
||||
except ExternalDataSyncError as exc:
|
||||
return Response(
|
||||
{"code": 502, "msg": str(exc), "data": None},
|
||||
status=status.HTTP_502_BAD_GATEWAY,
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
farm_data, created = SensorData.objects.get_or_create(
|
||||
|
||||
Reference in New Issue
Block a user