UPDATE
This commit is contained in:
@@ -42,7 +42,7 @@
|
||||
|
||||
### کار این view
|
||||
|
||||
- `sensor_uuid` را از query params میگیرد.
|
||||
- `farm_uuid` را از query params میگیرد.
|
||||
- `page` و `page_size` را هم از query params میگیرد.
|
||||
- از service میخواهد مطمئن شود برای این sensor یک area معتبر و zoneهای آن وجود دارند.
|
||||
- اگر zoneها وجود نداشته باشند، ساخته میشوند.
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
### ورودیهای `AreaView`
|
||||
|
||||
- `sensor_uuid`: اجباری
|
||||
- `farm_uuid`: اجباری
|
||||
- `page`: اختیاری، پیشفرض `1`
|
||||
- `page_size`: اختیاری، پیشفرض `10`
|
||||
|
||||
@@ -68,8 +68,8 @@
|
||||
|
||||
اگر هر کدام از این موارد رخ بدهد، خطای `400` داده میشود:
|
||||
|
||||
- `sensor_uuid` ارسال نشده باشد
|
||||
- `sensor_uuid` معتبر نباشد یا sensor پیدا نشود
|
||||
- `farm_uuid` ارسال نشده باشد
|
||||
- `farm_uuid` معتبر نباشد یا farm پیدا نشود
|
||||
- `page` نامعتبر باشد
|
||||
- `page_size` نامعتبر باشد
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
|
||||
### تفاوت با `AreaView`
|
||||
|
||||
- `AreaView` بر اساس `sensor_uuid` کار میکند و وضعیت taskها را هم برمیگرداند.
|
||||
- `AreaView` بر اساس `farm_uuid` کار میکند و وضعیت taskها را هم برمیگرداند.
|
||||
- `ZonesInitialView` بیشتر برای ساخت اولیه zoneها از روی polygon مناسب است.
|
||||
|
||||
---
|
||||
@@ -532,14 +532,14 @@ metrics را داخل مدلهای مختلف ذخیره میکند:
|
||||
|
||||
اگر area در دیتابیس وجود داشته باشد ولی zoneهایش از بین رفته باشند یا ساخته نشده باشند، دوباره از روی geometry آن zoneها را میسازد.
|
||||
|
||||
### `get_sensor_for_uuid(sensor_uuid)`
|
||||
### `get_farm_for_uuid(farm_uuid)`
|
||||
|
||||
اعتبارسنجی میکند که:
|
||||
|
||||
- `sensor_uuid` ارسال شده باشد
|
||||
- sensor واقعا در دیتابیس وجود داشته باشد
|
||||
- `farm_uuid` ارسال شده باشد
|
||||
- farm واقعا در دیتابیس وجود داشته باشد
|
||||
|
||||
### `ensure_latest_area_ready_for_processing(sensor_uuid, area_feature=None)`
|
||||
### `ensure_latest_area_ready_for_processing(farm_uuid, area_feature=None)`
|
||||
|
||||
این یکی از مهمترین توابع کل فایل است.
|
||||
|
||||
@@ -648,7 +648,7 @@ payload سادهتر برای endpoint اولیه zoneها میسازد.
|
||||
|
||||
اگر بخواهیم کل flow را از ابتدا تا انتها خیلی ساده توضیح بدهیم:
|
||||
|
||||
1. فرانت `sensor_uuid` و احتمالا `page` و `page_size` را میفرستد.
|
||||
1. فرانت `farm_uuid` و احتمالا `page` و `page_size` را میفرستد.
|
||||
2. `AreaView` پارامترها را میخواند.
|
||||
3. `ensure_latest_area_ready_for_processing` اجرا میشود.
|
||||
4. اگر area وجود نداشته باشد، area و zoneها ساخته میشوند.
|
||||
@@ -714,7 +714,7 @@ payload سادهتر برای endpoint اولیه zoneها میسازد.
|
||||
|
||||
### `_request()`
|
||||
|
||||
یک request استاندارد برای `AreaView` با `sensor_uuid` معتبر میسازد.
|
||||
یک request استاندارد برای `AreaView` با `farm_uuid` معتبر میسازد.
|
||||
|
||||
### `_request_with_pagination(page, page_size)`
|
||||
|
||||
@@ -724,9 +724,9 @@ payload سادهتر برای endpoint اولیه zoneها میسازد.
|
||||
|
||||
### تستهای اصلی `AreaView`
|
||||
|
||||
#### `test_get_requires_sensor_uuid`
|
||||
#### `test_get_requires_farm_uuid`
|
||||
|
||||
بررسی میکند اگر `sensor_uuid` ارسال نشود، پاسخ `400` برگردد.
|
||||
بررسی میکند اگر `farm_uuid` ارسال نشود، پاسخ `400` برگردد.
|
||||
|
||||
#### `test_get_returns_pending_task_status_until_all_zones_complete`
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ Content-Type: application/json
|
||||
|
||||
## Flow پیشنهادی فرانت
|
||||
|
||||
1. ابتدا `GET /area/` را با `sensor_uuid` صدا بزنید.
|
||||
1. ابتدا `GET /area/` را با `farm_uuid` صدا بزنید.
|
||||
2. اگر `task.status` برابر `PENDING` یا `PROCESSING` بود، polling انجام دهید.
|
||||
3. وقتی `task.status` برابر `SUCCESS` شد:
|
||||
- `area` را برای polygon اصلی زمین استفاده کنید.
|
||||
@@ -29,7 +29,7 @@ Content-Type: application/json
|
||||
|
||||
## وضعیتهای Task
|
||||
|
||||
- `IDLE`: هنوز area/taskی برای سنسور وجود ندارد.
|
||||
- `IDLE`: هنوز area/taskی برای مزرعه وجود ندارد.
|
||||
- `PENDING`: تسک ساخته شده ولی پردازش هنوز شروع نشده یا در صف است.
|
||||
- `PROCESSING`: بخشی از زونها در حال پردازش هستند یا برخی کامل شدهاند.
|
||||
- `SUCCESS`: همه زونها کامل پردازش شدهاند.
|
||||
@@ -51,18 +51,18 @@ Content-Type: application/json
|
||||
## 1) Get Area
|
||||
|
||||
```http
|
||||
GET /api/crop-zoning/area/?sensor_uuid=<sensor_uuid>&page=1&page_size=10
|
||||
GET /api/crop-zoning/area/?farm_uuid=<farm_uuid>&page=1&page_size=10
|
||||
```
|
||||
|
||||
### Query Params
|
||||
|
||||
- `sensor_uuid`: اجباری، UUID سنسور
|
||||
- `farm_uuid`: اجباری، UUID مزرعه
|
||||
- `page`: اختیاری، شماره صفحه زونها. پیشفرض `1`
|
||||
- `page_size`: اختیاری، تعداد زون در هر صفحه. پیشفرض `10`
|
||||
|
||||
### کاربرد
|
||||
|
||||
- گرفتن آخرین area مربوط به سنسور
|
||||
- گرفتن آخرین area مربوط به مزرعه
|
||||
- ساخت area و zoneها در صورت نبود داده
|
||||
- دریافت وضعیت task
|
||||
- دریافت لیست `zones` به صورت صفحهبندیشده برای نمایش روی نقشه
|
||||
@@ -175,13 +175,13 @@ GET /api/crop-zoning/area/?sensor_uuid=<sensor_uuid>&page=1&page_size=10
|
||||
#### صفحه اول با 10 زون در هر صفحه
|
||||
|
||||
```http
|
||||
GET /api/crop-zoning/area/?sensor_uuid=<sensor_uuid>&page=1&page_size=10
|
||||
GET /api/crop-zoning/area/?farm_uuid=<farm_uuid>&page=1&page_size=10
|
||||
```
|
||||
|
||||
#### صفحه سوم با 25 زون در هر صفحه
|
||||
|
||||
```http
|
||||
GET /api/crop-zoning/area/?sensor_uuid=<sensor_uuid>&page=3&page_size=25
|
||||
GET /api/crop-zoning/area/?farm_uuid=<farm_uuid>&page=3&page_size=25
|
||||
```
|
||||
|
||||
### فیلدهای مهم `zones`
|
||||
@@ -215,21 +215,21 @@ GET /api/crop-zoning/area/?sensor_uuid=<sensor_uuid>&page=3&page_size=25
|
||||
|
||||
### خطاها
|
||||
|
||||
#### وقتی `sensor_uuid` ارسال نشود
|
||||
#### وقتی `farm_uuid` ارسال نشود
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "sensor_uuid is required."
|
||||
"message": "farm_uuid is required."
|
||||
}
|
||||
```
|
||||
|
||||
#### وقتی سنسور پیدا نشود
|
||||
#### وقتی مزرعه پیدا نشود
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Sensor not found."
|
||||
"message": "Farm not found."
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+3
-3
@@ -4,20 +4,20 @@ import django.db.models.deletion
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("sensor_hub", "0001_initial"),
|
||||
("farm_hub", "0002_seed_default_catalog"),
|
||||
("crop_zoning", "0003_zone_processing_and_analysis"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="croparea",
|
||||
name="sensor",
|
||||
name="farm",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="crop_areas",
|
||||
to="sensor_hub.sensor",
|
||||
to="farm_hub.farmhub",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,13 +1,13 @@
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
from sensor_hub.models import Sensor
|
||||
from farm_hub.models import FarmHub
|
||||
|
||||
|
||||
class CropArea(models.Model):
|
||||
uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False, db_index=True)
|
||||
sensor = models.ForeignKey(
|
||||
Sensor,
|
||||
farm = models.ForeignKey(
|
||||
FarmHub,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="crop_areas",
|
||||
null=True,
|
||||
@@ -74,7 +74,6 @@ class CropZone(models.Model):
|
||||
return self.zone_id
|
||||
|
||||
|
||||
|
||||
class CropProduct(models.Model):
|
||||
product_id = models.CharField(max_length=64, unique=True)
|
||||
label = models.CharField(max_length=255)
|
||||
@@ -205,7 +204,6 @@ class CropZoneCultivationRiskLayer(models.Model):
|
||||
ordering = ["crop_zone_id"]
|
||||
|
||||
|
||||
|
||||
class CropZoneAnalysis(models.Model):
|
||||
source = models.CharField(max_length=64, blank=True, default="")
|
||||
external_record_id = models.CharField(max_length=64, blank=True, default="")
|
||||
@@ -224,4 +222,3 @@ class CropZoneAnalysis(models.Model):
|
||||
class Meta:
|
||||
db_table = "crop_zone_analyses"
|
||||
ordering = ["crop_zone_id"]
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
+18
-13
@@ -9,7 +9,7 @@ from kombu.exceptions import OperationalError
|
||||
from django.db import transaction
|
||||
from django.db.models import Prefetch
|
||||
from django.utils import timezone
|
||||
from sensor_hub.models import Sensor
|
||||
from farm_hub.models import FarmHub
|
||||
|
||||
from external_api_adapter.adapter import request as external_request
|
||||
|
||||
@@ -852,20 +852,25 @@ def create_missing_zones_for_area(crop_area):
|
||||
return list(crop_area.zones.order_by("sequence", "id"))
|
||||
|
||||
|
||||
def get_sensor_for_uuid(sensor_uuid):
|
||||
if not sensor_uuid:
|
||||
raise ValueError("sensor_uuid is required.")
|
||||
def get_farm_for_uuid(farm_uuid, owner=None):
|
||||
if not farm_uuid:
|
||||
raise ValueError("farm_uuid is required.")
|
||||
|
||||
filters = {"farm_uuid": farm_uuid}
|
||||
if owner is not None:
|
||||
filters["owner"] = owner
|
||||
|
||||
try:
|
||||
return Sensor.objects.get(uuid_sensor=sensor_uuid)
|
||||
except Sensor.DoesNotExist as exc:
|
||||
raise ValueError("Sensor not found.") from exc
|
||||
return FarmHub.objects.get(**filters)
|
||||
except FarmHub.DoesNotExist as exc:
|
||||
raise ValueError("Farm not found.") from exc
|
||||
|
||||
|
||||
def ensure_latest_area_ready_for_processing(sensor_uuid, area_feature=None):
|
||||
sensor = get_sensor_for_uuid(sensor_uuid)
|
||||
latest_area = CropArea.objects.filter(sensor=sensor).order_by("-created_at", "-id").first()
|
||||
def ensure_latest_area_ready_for_processing(farm_uuid, area_feature=None, owner=None):
|
||||
farm = get_farm_for_uuid(farm_uuid, owner=owner)
|
||||
latest_area = CropArea.objects.filter(farm=farm).order_by("-created_at", "-id").first()
|
||||
if latest_area is None:
|
||||
latest_area, _ = create_zones_and_dispatch(area_feature or get_default_area_feature(), sensor=sensor)
|
||||
latest_area, _ = create_zones_and_dispatch(area_feature or get_default_area_feature(), farm=farm)
|
||||
return latest_area
|
||||
|
||||
zones = create_missing_zones_for_area(latest_area)
|
||||
@@ -889,7 +894,7 @@ def ensure_latest_area_ready_for_processing(sensor_uuid, area_feature=None):
|
||||
return CropArea.objects.get(id=latest_area.id)
|
||||
|
||||
|
||||
def create_zones_and_dispatch(area_feature, cell_side_km=None, sensor=None):
|
||||
def create_zones_and_dispatch(area_feature, cell_side_km=None, farm=None):
|
||||
ensure_products_exist()
|
||||
area_feature = normalize_area_feature(area_feature)
|
||||
zoning_result = split_area_into_zones(area_feature, cell_side_km=cell_side_km)
|
||||
@@ -897,7 +902,7 @@ def create_zones_and_dispatch(area_feature, cell_side_km=None, sensor=None):
|
||||
|
||||
with transaction.atomic():
|
||||
crop_area = CropArea.objects.create(
|
||||
sensor=sensor,
|
||||
farm=farm,
|
||||
geometry=area_data["geometry"],
|
||||
points=area_data["points"],
|
||||
center=area_data["center"],
|
||||
|
||||
+54
-117
@@ -1,16 +1,15 @@
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from kombu.exceptions import OperationalError
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase, override_settings
|
||||
from django.utils import timezone
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from datetime import timedelta
|
||||
from kombu.exceptions import OperationalError
|
||||
from rest_framework.test import APIRequestFactory, force_authenticate
|
||||
|
||||
from crop_zoning.models import CropArea, CropZone
|
||||
from crop_zoning.views import AreaView, ZonesInitialView
|
||||
from sensor_hub.models import Sensor
|
||||
from farm_hub.models import FarmHub, FarmType
|
||||
|
||||
|
||||
AREA_GEOJSON = {
|
||||
@@ -69,11 +68,19 @@ class AreaViewTests(TestCase):
|
||||
email="farmer@example.com",
|
||||
phone_number="09120000000",
|
||||
)
|
||||
self.sensor = Sensor.objects.create(owner=self.user, name="sensor-1")
|
||||
self.other_user = get_user_model().objects.create_user(
|
||||
username="other-farmer",
|
||||
password="secret123",
|
||||
email="other@example.com",
|
||||
phone_number="09120000001",
|
||||
)
|
||||
self.farm_type = FarmType.objects.create(name="زراعی")
|
||||
self.farm = FarmHub.objects.create(owner=self.user, name="farm-1", farm_type=self.farm_type)
|
||||
self.other_farm = FarmHub.objects.create(owner=self.other_user, name="farm-2", farm_type=self.farm_type)
|
||||
|
||||
def _create_area(self, **kwargs):
|
||||
defaults = {
|
||||
"sensor": self.sensor,
|
||||
"farm": self.farm,
|
||||
"geometry": AREA_GEOJSON,
|
||||
"points": AREA_GEOJSON["geometry"]["coordinates"][0][:-1],
|
||||
"center": {"longitude": 51.40874867, "latitude": 35.69575533},
|
||||
@@ -86,18 +93,32 @@ class AreaViewTests(TestCase):
|
||||
return CropArea.objects.create(**defaults)
|
||||
|
||||
def _request(self):
|
||||
return self.factory.get(f"/api/crop-zoning/area/?sensor_uuid={self.sensor.uuid_sensor}")
|
||||
request = self.factory.get(f"/api/crop-zoning/area/?farm_uuid={self.farm.farm_uuid}")
|
||||
force_authenticate(request, user=self.user)
|
||||
return request
|
||||
|
||||
def _request_with_pagination(self, page=1, page_size=10):
|
||||
return self.factory.get(
|
||||
f"/api/crop-zoning/area/?sensor_uuid={self.sensor.uuid_sensor}&page={page}&page_size={page_size}"
|
||||
request = self.factory.get(
|
||||
f"/api/crop-zoning/area/?farm_uuid={self.farm.farm_uuid}&page={page}&page_size={page_size}"
|
||||
)
|
||||
force_authenticate(request, user=self.user)
|
||||
return request
|
||||
|
||||
def test_get_requires_sensor_uuid(self):
|
||||
def test_get_requires_farm_uuid(self):
|
||||
request = self.factory.get("/api/crop-zoning/area/")
|
||||
force_authenticate(request, user=self.user)
|
||||
response = AreaView.as_view()(request)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.data["message"], "sensor_uuid is required.")
|
||||
self.assertEqual(response.data["message"], "farm_uuid is required.")
|
||||
|
||||
def test_get_rejects_foreign_farm_uuid(self):
|
||||
request = self.factory.get(f"/api/crop-zoning/area/?farm_uuid={self.other_farm.farm_uuid}")
|
||||
force_authenticate(request, user=self.user)
|
||||
|
||||
response = AreaView.as_view()(request)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.data["message"], "Farm not found.")
|
||||
|
||||
def test_get_returns_pending_task_status_until_all_zones_complete(self):
|
||||
crop_area = self._create_area()
|
||||
@@ -219,7 +240,7 @@ class AreaViewTests(TestCase):
|
||||
mock_dispatch.assert_called_once()
|
||||
|
||||
@patch("crop_zoning.services.create_zones_and_dispatch")
|
||||
def test_get_creates_area_when_sensor_has_no_data(self, mock_create):
|
||||
def test_get_creates_area_when_farm_has_no_data(self, mock_create):
|
||||
created_area = self._create_area(zone_count=0)
|
||||
mock_create.return_value = (created_area, [])
|
||||
|
||||
@@ -227,7 +248,7 @@ class AreaViewTests(TestCase):
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
mock_create.assert_called_once()
|
||||
self.assertEqual(mock_create.call_args.kwargs["sensor"], self.sensor)
|
||||
self.assertEqual(mock_create.call_args.kwargs["farm"], self.farm)
|
||||
|
||||
@patch("crop_zoning.tasks.process_zone_soil_data.delay")
|
||||
def test_each_zone_gets_its_own_task(self, mock_delay):
|
||||
@@ -238,8 +259,8 @@ class AreaViewTests(TestCase):
|
||||
geometry=AREA_GEOJSON["geometry"],
|
||||
points=AREA_GEOJSON["geometry"]["coordinates"][0][:-1],
|
||||
center={"longitude": 51.4087, "latitude": 35.6957},
|
||||
area_sqm=150000,
|
||||
area_hectares=15,
|
||||
area_sqm=200000,
|
||||
area_hectares=20,
|
||||
sequence=0,
|
||||
processing_status=CropZone.STATUS_PENDING,
|
||||
task_id="",
|
||||
@@ -250,129 +271,45 @@ class AreaViewTests(TestCase):
|
||||
geometry=AREA_GEOJSON["geometry"],
|
||||
points=AREA_GEOJSON["geometry"]["coordinates"][0][:-1],
|
||||
center={"longitude": 51.4088, "latitude": 35.6958},
|
||||
area_sqm=150000,
|
||||
area_hectares=15,
|
||||
area_sqm=100000,
|
||||
area_hectares=10,
|
||||
sequence=1,
|
||||
processing_status=CropZone.STATUS_PENDING,
|
||||
task_id="",
|
||||
)
|
||||
|
||||
class Result:
|
||||
def __init__(self, task_id):
|
||||
self.id = task_id
|
||||
|
||||
mock_delay.side_effect = [Result("task-zone-0"), Result("task-zone-1")]
|
||||
|
||||
response = AreaView.as_view()(self._request())
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(mock_delay.call_count, 2)
|
||||
zone0.refresh_from_db()
|
||||
zone1.refresh_from_db()
|
||||
self.assertEqual(zone0.task_id, "task-zone-0")
|
||||
self.assertEqual(zone1.task_id, "task-zone-1")
|
||||
|
||||
@patch("crop_zoning.tasks.process_zone_soil_data.delay", side_effect=OperationalError("redis down"))
|
||||
def test_get_generates_local_task_id_when_broker_is_unavailable(self, mock_delay):
|
||||
crop_area = self._create_area(zone_count=1, area_sqm=200000, area_hectares=20)
|
||||
zone = CropZone.objects.create(
|
||||
crop_area=crop_area,
|
||||
zone_id="zone-0",
|
||||
geometry=AREA_GEOJSON["geometry"],
|
||||
points=AREA_GEOJSON["geometry"]["coordinates"][0][:-1],
|
||||
center={"longitude": 51.4087, "latitude": 35.6957},
|
||||
area_sqm=200000,
|
||||
area_hectares=20,
|
||||
sequence=0,
|
||||
processing_status=CropZone.STATUS_PENDING,
|
||||
task_id="",
|
||||
)
|
||||
|
||||
response = AreaView.as_view()(self._request())
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
zone.refresh_from_db()
|
||||
self.assertTrue(zone.task_id)
|
||||
self.assertEqual(response.data["data"]["task"]["summary"]["remaining"], 1)
|
||||
self.assertEqual(response.data["data"]["task"]["remaining_zones"], 1)
|
||||
self.assertEqual(response.data["data"]["task"]["status"], "PENDING")
|
||||
self.assertIn("Celery broker unavailable", zone.processing_error)
|
||||
|
||||
@patch("crop_zoning.tasks.process_zone_soil_data.delay")
|
||||
def test_get_stores_task_id_and_reuses_it_on_next_request(self, mock_delay):
|
||||
crop_area = self._create_area(zone_count=1, area_sqm=200000, area_hectares=20)
|
||||
zone = CropZone.objects.create(
|
||||
crop_area=crop_area,
|
||||
zone_id="zone-0",
|
||||
geometry=AREA_GEOJSON["geometry"],
|
||||
points=AREA_GEOJSON["geometry"]["coordinates"][0][:-1],
|
||||
center={"longitude": 51.4087, "latitude": 35.6957},
|
||||
area_sqm=200000,
|
||||
area_hectares=20,
|
||||
sequence=0,
|
||||
processing_status=CropZone.STATUS_PENDING,
|
||||
task_id="",
|
||||
)
|
||||
|
||||
class Result:
|
||||
id = "persisted-task-id"
|
||||
|
||||
mock_delay.return_value = Result()
|
||||
|
||||
first_response = AreaView.as_view()(self._request())
|
||||
self.assertEqual(first_response.status_code, 200)
|
||||
zone.refresh_from_db()
|
||||
self.assertEqual(zone.task_id, "persisted-task-id")
|
||||
self.assertEqual(first_response.data["data"]["task"]["summary"]["done"], 0)
|
||||
self.assertEqual(first_response.data["data"]["task"]["summary"]["remaining"], 1)
|
||||
self.assertEqual(mock_delay.call_count, 1)
|
||||
|
||||
second_response = AreaView.as_view()(self._request())
|
||||
self.assertEqual(second_response.status_code, 200)
|
||||
self.assertEqual(second_response.data["data"]["task"]["summary"]["remaining"], 1)
|
||||
self.assertEqual(second_response.data["data"]["task"]["status"], "PENDING")
|
||||
self.assertEqual(mock_delay.call_count, 1)
|
||||
self.assertTrue(zone0.task_id)
|
||||
self.assertTrue(zone1.task_id)
|
||||
self.assertNotEqual(zone0.task_id, zone1.task_id)
|
||||
|
||||
@patch("crop_zoning.services.AsyncResult")
|
||||
@patch("crop_zoning.tasks.process_zone_soil_data.delay")
|
||||
def test_get_redispatches_pending_zone_when_shared_task_already_completed(self, mock_delay, mock_async_result):
|
||||
def test_stale_tasks_are_redispatched(self, mock_async_result):
|
||||
crop_area = self._create_area()
|
||||
CropZone.objects.create(
|
||||
stale_time = timezone.now() - timedelta(minutes=10)
|
||||
stale_zone = CropZone.objects.create(
|
||||
crop_area=crop_area,
|
||||
zone_id="zone-0",
|
||||
geometry=AREA_GEOJSON["geometry"],
|
||||
points=AREA_GEOJSON["geometry"]["coordinates"][0][:-1],
|
||||
center={"longitude": 51.4087, "latitude": 35.6957},
|
||||
area_sqm=150000,
|
||||
area_hectares=15,
|
||||
area_sqm=200000,
|
||||
area_hectares=20,
|
||||
sequence=0,
|
||||
processing_status=CropZone.STATUS_COMPLETED,
|
||||
task_id="legacy-shared-task-id",
|
||||
processing_status=CropZone.STATUS_PROCESSING,
|
||||
task_id="stale-task",
|
||||
)
|
||||
stale_zone = CropZone.objects.create(
|
||||
crop_area=crop_area,
|
||||
zone_id="zone-1",
|
||||
geometry=AREA_GEOJSON["geometry"],
|
||||
points=AREA_GEOJSON["geometry"]["coordinates"][0][:-1],
|
||||
center={"longitude": 51.4088, "latitude": 35.6958},
|
||||
area_sqm=150000,
|
||||
area_hectares=15,
|
||||
sequence=1,
|
||||
processing_status=CropZone.STATUS_PENDING,
|
||||
task_id="legacy-shared-task-id",
|
||||
)
|
||||
stale_zone.updated_at = timezone.now() - timedelta(minutes=10)
|
||||
stale_zone.save(update_fields=["updated_at"])
|
||||
CropZone.objects.filter(id=stale_zone.id).update(updated_at=stale_time)
|
||||
|
||||
class Result:
|
||||
id = "requeued-zone-1"
|
||||
mock_async_result.side_effect = OperationalError("broker down")
|
||||
|
||||
mock_delay.return_value = Result()
|
||||
mock_async_result.return_value.state = "SUCCESS"
|
||||
|
||||
response = AreaView.as_view()(self._request())
|
||||
with patch("crop_zoning.services.dispatch_zone_processing_tasks") as mock_dispatch:
|
||||
response = AreaView.as_view()(self._request())
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(mock_delay.call_count, 1)
|
||||
stale_zone.refresh_from_db()
|
||||
self.assertEqual(stale_zone.task_id, "requeued-zone-1")
|
||||
mock_dispatch.assert_called_once_with(zone_ids=[stale_zone.id], force=True)
|
||||
|
||||
@@ -17,8 +17,8 @@ from .services import (
|
||||
get_products_payload,
|
||||
get_soil_quality_payload,
|
||||
get_water_need_payload,
|
||||
get_zone_page_request_params,
|
||||
get_zone_details_payload,
|
||||
get_zone_page_request_params,
|
||||
)
|
||||
|
||||
|
||||
@@ -27,26 +27,26 @@ class AreaView(APIView):
|
||||
tags=["Crop Zoning"],
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="sensor_uuid",
|
||||
name="farm_uuid",
|
||||
type=OpenApiTypes.UUID,
|
||||
location=OpenApiParameter.QUERY,
|
||||
required=True,
|
||||
description="UUID سنسور ارسالی کاربر برای گرفتن یا ساخت task فعال همان سنسور.",
|
||||
description="UUID مزرعه برای گرفتن يا ساخت آخرين پردازش محدوده همان مزرعه.",
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="page",
|
||||
type=OpenApiTypes.INT,
|
||||
location=OpenApiParameter.QUERY,
|
||||
required=False,
|
||||
description="شماره صفحه زونها. مقدار پیشفرض 1 است.",
|
||||
description="شماره صفحه زون ها. مقدار پيش فرض 1 است.",
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="page_size",
|
||||
type=OpenApiTypes.INT,
|
||||
location=OpenApiParameter.QUERY,
|
||||
required=False,
|
||||
description="تعداد زون در هر صفحه. مقدار پیشفرض 10 است.",
|
||||
)
|
||||
description="تعداد زون در هر صفحه. مقدار پيش فرض 10 است.",
|
||||
),
|
||||
],
|
||||
responses={
|
||||
200: status_response("CropZoningAreaResponse", data=serializers.JSONField()),
|
||||
@@ -55,10 +55,10 @@ class AreaView(APIView):
|
||||
},
|
||||
)
|
||||
def get(self, request):
|
||||
sensor_uuid = request.query_params.get("sensor_uuid")
|
||||
farm_uuid = request.query_params.get("farm_uuid")
|
||||
try:
|
||||
page, page_size = get_zone_page_request_params(request.query_params)
|
||||
crop_area = ensure_latest_area_ready_for_processing(sensor_uuid=sensor_uuid)
|
||||
crop_area = ensure_latest_area_ready_for_processing(farm_uuid=farm_uuid, owner=request.user)
|
||||
except ValueError as exc:
|
||||
return Response({"status": "error", "message": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except ImproperlyConfigured as exc:
|
||||
|
||||
Reference in New Issue
Block a user