This commit is contained in:
2026-04-24 02:50:27 +03:30
parent 302124aa87
commit a76af4e766
20 changed files with 430 additions and 147 deletions
@@ -0,0 +1,26 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("irrigation", "0001_initial"),
("sensor_data", "0010_rename_tables_to_farm_data"),
]
operations = [
migrations.AddField(
model_name="sensordata",
name="irrigation_method",
field=models.ForeignKey(
blank=True,
db_column="irrigation_method_id",
help_text="روش آبیاری انتخاب‌شده برای این farm",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="farm_data",
to="irrigation.irrigationmethod",
),
),
]
+9
View File
@@ -115,6 +115,15 @@ class SensorData(SensorPayloadMixin, models.Model):
related_name="farm_data",
help_text="گیاهان مرتبط با این farm",
)
irrigation_method = models.ForeignKey(
"irrigation.IrrigationMethod",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="farm_data",
db_column="irrigation_method_id",
help_text="روش آبیاری انتخاب‌شده برای این farm",
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
+29 -3
View File
@@ -1,6 +1,8 @@
from rest_framework import serializers
from location_data.serializers import SoilDepthDataSerializer
from irrigation.models import IrrigationMethod
from irrigation.serializers import IrrigationMethodSerializer
from plant.serializers import PlantSerializer
from weather.models import WeatherForecast
@@ -19,6 +21,11 @@ class SensorDataUpdateSerializer(serializers.Serializer):
required=False,
help_text="لیست شناسه گیاهان مرتبط",
)
irrigation_method_id = serializers.IntegerField(
required=False,
allow_null=True,
help_text="شناسه روش آبیاری مرتبط",
)
def to_internal_value(self, data):
if not isinstance(data, dict):
@@ -31,6 +38,7 @@ class SensorDataUpdateSerializer(serializers.Serializer):
"sensor_key",
"sensor_payload",
"plant_ids",
"irrigation_method_id",
}
flat_metrics = {
key: value
@@ -71,10 +79,21 @@ class SensorDataUpdateSerializer(serializers.Serializer):
)
return value
def validate_irrigation_method_id(self, value):
if value is None:
return value
if not IrrigationMethod.objects.filter(pk=value).exists():
raise serializers.ValidationError("روش آبیاری معتبر نیست.")
return value
def validate(self, attrs):
if "sensor_payload" not in attrs and "plant_ids" not in attrs:
if (
"sensor_payload" not in attrs
and "plant_ids" not in attrs
and "irrigation_method_id" not in attrs
):
raise serializers.ValidationError(
"حداقل یکی از sensor_payload یا plant_ids باید ارسال شود."
"حداقل یکی از sensor_payload یا plant_ids یا irrigation_method_id باید ارسال شود."
)
return attrs
@@ -87,6 +106,11 @@ class SensorDataResponseSerializer(serializers.ModelSerializer):
many=True,
read_only=True,
)
irrigation_method_id = serializers.IntegerField(
source="irrigation_method.id",
read_only=True,
allow_null=True,
)
class Meta:
model = SensorData
@@ -96,6 +120,7 @@ class SensorDataResponseSerializer(serializers.ModelSerializer):
"weather_forecast_id",
"sensor_payload",
"plant_ids",
"irrigation_method_id",
"created_at",
"updated_at",
]
@@ -148,12 +173,13 @@ class FarmSoilPayloadSerializer(serializers.Serializer):
class FarmDetailSerializer(serializers.Serializer):
farm_uuid = serializers.UUIDField()
center_location = FarmCenterLocationSerializer()
weather = WeatherForecastDetailSerializer(allow_null=True)
sensor_payload = serializers.JSONField()
soil = FarmSoilPayloadSerializer()
plant_ids = serializers.ListField(child=serializers.IntegerField())
plants = PlantSerializer(many=True)
irrigation_method_id = serializers.IntegerField(allow_null=True)
irrigation_method = IrrigationMethodSerializer(allow_null=True)
created_at = serializers.DateTimeField()
updated_at = serializers.DateTimeField()
+12 -2
View File
@@ -7,6 +7,7 @@ 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 irrigation.serializers import IrrigationMethodSerializer
from plant.serializers import PlantSerializer
from weather.services import update_weather_for_location
from weather.models import WeatherForecast
@@ -25,7 +26,11 @@ class ExternalDataSyncError(Exception):
def get_farm_details(farm_uuid: str):
farm = (
SensorData.objects.select_related("center_location", "weather_forecast")
SensorData.objects.select_related(
"center_location",
"weather_forecast",
"irrigation_method",
)
.prefetch_related("plants", "center_location__depths")
.filter(farm_uuid=farm_uuid)
.first()
@@ -53,7 +58,6 @@ def get_farm_details(farm_uuid: str):
metric_sources[key] = "sensor"
return {
"farm_uuid": farm.farm_uuid,
"center_location": {
"id": center_location.id,
"lat": center_location.latitude,
@@ -69,6 +73,12 @@ def get_farm_details(farm_uuid: str):
},
"plant_ids": list(farm.plants.values_list("id", flat=True)),
"plants": PlantSerializer(farm.plants.all(), many=True).data,
"irrigation_method_id": farm.irrigation_method_id,
"irrigation_method": (
IrrigationMethodSerializer(farm.irrigation_method).data
if farm.irrigation_method
else None
),
"created_at": farm.created_at,
"updated_at": farm.updated_at,
}
+9 -1
View File
@@ -7,6 +7,7 @@ from rest_framework.test import APIClient
from location_data.models import SoilDepthData, SoilLocation
from farm_data.models import SensorData
from irrigation.models import IrrigationMethod
from plant.models import Plant
from weather.models import WeatherForecast
@@ -58,11 +59,13 @@ class FarmDetailApiTests(TestCase):
)
self.plant1 = Plant.objects.create(name="گوجه‌فرنگی")
self.plant2 = Plant.objects.create(name="خیار")
self.irrigation_method = IrrigationMethod.objects.create(name="آبیاری قطره‌ای")
self.farm_uuid = uuid.uuid4()
self.farm = SensorData.objects.create(
farm_uuid=self.farm_uuid,
center_location=self.location,
weather_forecast=self.weather,
irrigation_method=self.irrigation_method,
sensor_payload={
"sensor-7-1": {
"soil_moisture": 33.5,
@@ -78,7 +81,7 @@ class FarmDetailApiTests(TestCase):
self.assertEqual(response.status_code, 200)
payload = response.json()["data"]
self.assertEqual(payload["farm_uuid"], str(self.farm_uuid))
self.assertNotIn("farm_uuid", payload)
self.assertEqual(payload["center_location"]["id"], self.location.id)
self.assertEqual(payload["weather"]["id"], self.weather.id)
self.assertEqual(
@@ -100,6 +103,8 @@ class FarmDetailApiTests(TestCase):
self.assertEqual(returned_plants[self.plant1.id]["name"], self.plant1.name)
self.assertEqual(returned_plants[self.plant2.id]["name"], self.plant2.name)
self.assertIn("light", returned_plants[self.plant1.id])
self.assertEqual(payload["irrigation_method_id"], self.irrigation_method.id)
self.assertEqual(payload["irrigation_method"]["name"], self.irrigation_method.name)
def test_returns_404_when_farm_is_missing(self):
response = self.client.get(f"/api/farm-data/{uuid.uuid4()}/detail/")
@@ -123,6 +128,7 @@ class FarmDataUpsertApiTests(TestCase):
temperature_max=24.0,
temperature_mean=17.5,
)
self.irrigation_method = IrrigationMethod.objects.create(name="بارانی")
def test_post_creates_farm_data_with_explicit_farm_uuid(self):
farm_uuid = uuid.uuid4()
@@ -138,6 +144,7 @@ class FarmDataUpsertApiTests(TestCase):
"nitrogen": 18.0,
}
},
"irrigation_method_id": self.irrigation_method.id,
},
format="json",
)
@@ -150,6 +157,7 @@ class FarmDataUpsertApiTests(TestCase):
farm = SensorData.objects.get(farm_uuid=farm_uuid)
self.assertEqual(farm.center_location_id, self.location.id)
self.assertEqual(farm.weather_forecast_id, self.weather.id)
self.assertEqual(farm.irrigation_method_id, self.irrigation_method.id)
self.assertEqual(
farm.sensor_payload["sensor-7-1"]["soil_moisture"],
31.2,
+4
View File
@@ -168,6 +168,7 @@ class FarmDataUpsertView(APIView):
farm_uuid = serializer.validated_data["farm_uuid"]
farm_boundary = serializer.validated_data["farm_boundary"]
plant_ids = serializer.validated_data.get("plant_ids")
irrigation_method_id = serializer.validated_data.get("irrigation_method_id")
sensor_payload = serializer.validated_data.get("sensor_payload", {})
try:
center_location = resolve_center_location_from_boundary(farm_boundary)
@@ -210,12 +211,15 @@ class FarmDataUpsertView(APIView):
farm_data.center_location = center_location
farm_data.weather_forecast = weather_forecast
if "irrigation_method_id" in serializer.validated_data:
farm_data.irrigation_method_id = irrigation_method_id
if not created:
farm_data.save(
update_fields=[
"center_location",
"weather_forecast",
"sensor_payload",
"irrigation_method",
"updated_at",
]
)