884 lines
30 KiB
Markdown
884 lines
30 KiB
Markdown
# Crop Zoning Code Logic
|
|
|
|
این فایل یک توضیح کامل و شفاف از منطق سه فایل زیر است:
|
|
|
|
- `crop_zoning/views.py`
|
|
- `crop_zoning/services.py`
|
|
- `crop_zoning/tests.py`
|
|
|
|
هدف این داکیومنت این است که بدون نیاز به خواندن مستقیم کد، بتوان فهمید هر endpoint چه میکند، دادهها چگونه ساخته میشوند، taskها چگونه مدیریت میشوند، و تستها چه رفتارهایی را پوشش میدهند.
|
|
|
|
---
|
|
|
|
## تصویر کلی ماژول
|
|
|
|
ماژول `crop_zoning` برای این ساخته شده که:
|
|
|
|
1. یک polygon مربوط به زمین را دریافت یا پیدا کند.
|
|
2. آن را به چند zone مربعی تقسیم کند.
|
|
3. برای هر zone داده اولیه تولید کند.
|
|
4. برای هر zone یک task پردازش جداگانه ثبت کند.
|
|
5. خروجی مناسب برای فرانت برگرداند تا هم وضعیت پردازش را بداند و هم zoneها را روی نقشه نمایش دهد.
|
|
|
|
این ماژول دو نوع داده برای zoneها دارد:
|
|
|
|
- داده اولیه و rule-based که سریع ساخته میشود و برای خالی نبودن UI استفاده میشود.
|
|
- داده تحلیلی که بعدا از طریق task و داده خاک تکمیل میشود.
|
|
|
|
---
|
|
|
|
## منطق `crop_zoning/views.py`
|
|
|
|
فایل `views.py` فقط لایه HTTP است.
|
|
یعنی کار اصلی را خودش انجام نمیدهد، بلکه:
|
|
|
|
- ورودی request را میخواند
|
|
- آن را validate میکند یا به serviceها میسپارد
|
|
- خروجی مناسب را به صورت JSON response برمیگرداند
|
|
|
|
### 1) `AreaView`
|
|
|
|
این مهمترین endpoint ماژول است.
|
|
|
|
### کار این view
|
|
|
|
- `farm_uuid` را از query params میگیرد.
|
|
- `page` و `page_size` را هم از query params میگیرد.
|
|
- از service میخواهد مطمئن شود برای این sensor یک area معتبر و zoneهای آن وجود دارند.
|
|
- اگر zoneها وجود نداشته باشند، ساخته میشوند.
|
|
- اگر taskهای پردازش لازم باشند، dispatch میشوند.
|
|
- در نهایت خروجی area + zoneهای همان صفحه + اطلاعات pagination را برمیگرداند.
|
|
|
|
### ورودیهای `AreaView`
|
|
|
|
- `farm_uuid`: اجباری
|
|
- `page`: اختیاری، پیشفرض `1`
|
|
- `page_size`: اختیاری، پیشفرض `10`
|
|
|
|
### خروجی `AreaView`
|
|
|
|
خروجی سه بخش مهم دارد:
|
|
|
|
- `task`: وضعیت پردازش کل area
|
|
- `area`: polygon اصلی زمین
|
|
- `zones`: فقط zoneهای مربوط به همان صفحه
|
|
- `pagination`: اطلاعات صفحهبندی zoneها
|
|
|
|
### مدیریت خطا در `AreaView`
|
|
|
|
اگر هر کدام از این موارد رخ بدهد، خطای `400` داده میشود:
|
|
|
|
- `farm_uuid` ارسال نشده باشد
|
|
- `farm_uuid` معتبر نباشد یا farm پیدا نشود
|
|
- `page` نامعتبر باشد
|
|
- `page_size` نامعتبر باشد
|
|
|
|
اگر تنظیمات سمت سرور مشکل داشته باشند، خطای `500` داده میشود.
|
|
|
|
---
|
|
|
|
### 2) `ProductsView`
|
|
|
|
این endpoint لیست محصولات قابل کشت را برمیگرداند.
|
|
|
|
### کار این view
|
|
|
|
- از service میخواهد محصولات پیشفرض داخل دیتابیس sync شوند.
|
|
- سپس لیست محصولات را به فرمت مناسب فرانت برمیگرداند.
|
|
|
|
این view ساده است و منطق تحلیلی ندارد.
|
|
|
|
---
|
|
|
|
### 3) `ZonesInitialView`
|
|
|
|
این view برای ساخت zoneها از روی یک polygon ورودی استفاده میشود.
|
|
|
|
### کار این view
|
|
|
|
- polygon را از یکی از این کلیدها میگیرد:
|
|
- `area`
|
|
- `area_geojson`
|
|
- `boundary`
|
|
- اگر هیچکدام نباشد، از area پیشفرض mock استفاده میکند.
|
|
- در صورت ارسال، `cell_side_km` را هم میگیرد.
|
|
- service را صدا میزند تا area و zoneها ساخته شوند.
|
|
- response اولیه zoneها را برمیگرداند.
|
|
|
|
### تفاوت با `AreaView`
|
|
|
|
- `AreaView` بر اساس `farm_uuid` کار میکند و وضعیت taskها را هم برمیگرداند.
|
|
- `ZonesInitialView` بیشتر برای ساخت اولیه zoneها از روی polygon مناسب است.
|
|
|
|
---
|
|
|
|
### 4) `ZonesWaterNeedView`
|
|
|
|
این view لایه نیاز آبی zoneها را برمیگرداند.
|
|
|
|
### کار این view
|
|
|
|
- از request، `zoneIds` را میگیرد.
|
|
- service را صدا میزند.
|
|
- برای هر zone، level و value و color مربوط به آب را برمیگرداند.
|
|
|
|
---
|
|
|
|
### 5) `ZonesSoilQualityView`
|
|
|
|
این view لایه کیفیت خاک zoneها را برمیگرداند.
|
|
|
|
### خروجی اصلی
|
|
|
|
برای هر zone:
|
|
|
|
- `level`
|
|
- `score`
|
|
- `color`
|
|
|
|
---
|
|
|
|
### 6) `ZonesCultivationRiskView`
|
|
|
|
این view لایه ریسک کشت zoneها را برمیگرداند.
|
|
|
|
### خروجی اصلی
|
|
|
|
برای هر zone:
|
|
|
|
- `level`
|
|
- `color`
|
|
|
|
---
|
|
|
|
### 7) `ZoneDetailsView`
|
|
|
|
این endpoint جزئیات یک zone را برمیگرداند.
|
|
|
|
### کار این view
|
|
|
|
- `zone_id` را از URL میگیرد.
|
|
- جزئیات recommendation آن zone را از service میخواند.
|
|
- اگر zone پیدا نشود، `404` برمیگرداند.
|
|
|
|
### خروجی اصلی
|
|
|
|
- crop پیشنهادی
|
|
- درصد تطابق
|
|
- نیاز آبی
|
|
- سود تخمینی
|
|
- reason
|
|
- criteria
|
|
- مساحت zone
|
|
|
|
---
|
|
|
|
## منطق `crop_zoning/services.py`
|
|
|
|
این فایل قلب اصلی ماژول است.
|
|
بیشتر منطق واقعی اینجا پیادهسازی شده.
|
|
|
|
برای فهم بهتر، این فایل را میتوان به 8 بخش تقسیم کرد.
|
|
|
|
---
|
|
|
|
## بخش 1: تنظیمات و utilityهای اولیه
|
|
|
|
### ثابتها
|
|
|
|
چند constant اصلی در ابتدای فایل تعریف شدهاند:
|
|
|
|
- `DEFAULT_CELL_SIDE_KM`: اندازه پیشفرض ضلع هر zone
|
|
- `DEFAULT_ZONE_PAGE_SIZE`: تعداد پیشفرض zoneها در هر صفحه response
|
|
- `RULE_BASED_ALGORITHM`: نام الگوریتم rule-based
|
|
- `RULE_BASED_PRODUCTS`: داده اولیه محصولات و اطلاعات نمایشی آنها
|
|
|
|
### `get_default_cell_side_km()`
|
|
|
|
این تابع اندازه پیشفرض ضلع هر zone را مشخص میکند.
|
|
|
|
اولویتها:
|
|
|
|
1. اگر `CROP_ZONE_CELL_SIDE_KM` در settings وجود داشته باشد، همان استفاده میشود.
|
|
2. اگر نبود، از `CROP_ZONE_CHUNK_AREA_SQM` استفاده میکند و از روی آن ضلع مربع را حساب میکند.
|
|
3. اگر هیچکدام نباشند، از `DEFAULT_CELL_SIDE_KM` استفاده میشود.
|
|
|
|
### `get_task_stale_seconds()`
|
|
|
|
این تابع مشخص میکند بعد از چند ثانیه یک task ممکن است stale محسوب شود.
|
|
یعنی اگر task گیر کرده باشد، دوباره dispatch شود.
|
|
|
|
### `get_cell_side_km(cell_side_km=None)`
|
|
|
|
اگر کاربر اندازه zone را داده باشد، آن را validate میکند.
|
|
اگر نداده باشد، مقدار پیشفرض را برمیگرداند.
|
|
|
|
### `get_chunk_area_sqm(cell_side_km=None)`
|
|
|
|
مساحت zone را از روی ضلع آن حساب میکند:
|
|
|
|
- ضلع بر حسب کیلومتر دریافت میشود
|
|
- به متر تبدیل میشود
|
|
- مربع آن به عنوان مساحت zone برگردانده میشود
|
|
|
|
### `parse_positive_int(...)`
|
|
|
|
برای validate کردن پارامترهای عددی مثبت استفاده میشود.
|
|
الان برای `page` و `page_size` استفاده میشود.
|
|
|
|
### `get_zone_page_request_params(query_params)`
|
|
|
|
این تابع پارامترهای pagination را از query params میگیرد:
|
|
|
|
- `page`
|
|
- `page_size`
|
|
|
|
اگر ارسال نشده باشند، از default استفاده میکند.
|
|
اگر نامعتبر باشند، `ValueError` میدهد.
|
|
|
|
---
|
|
|
|
## بخش 2: آمادهسازی polygon و محاسبات هندسی
|
|
|
|
این بخش مسئول کار با GeoJSON و polygon است.
|
|
|
|
### `get_default_area_feature()`
|
|
|
|
یک area پیشفرض از داده mock برمیگرداند.
|
|
|
|
### `normalize_area_feature(area_feature)`
|
|
|
|
این تابع ورودی area را normalize میکند تا همیشه ساختار `Feature` داشته باشد.
|
|
|
|
### کارهای این تابع
|
|
|
|
- بررسی میکند ورودی null نباشد
|
|
- بررسی میکند ورودی dict باشد
|
|
- اگر ورودی از نوع `Feature` نباشد، آن را به `Feature` تبدیل میکند
|
|
- بررسی میکند geometry از نوع `Polygon` باشد
|
|
- بررسی میکند polygon حداقل 4 نقطه داشته باشد
|
|
|
|
### `get_polygon_ring(area_feature)`
|
|
|
|
حلقه اصلی polygon را استخراج میکند.
|
|
|
|
### `polygon_area_sqm(ring)`
|
|
|
|
مساحت polygon را به متر مربع حساب میکند.
|
|
برای این کار نقاط جغرافیایی را به مختصات مسطح تقریبی تبدیل میکند و فرمول shoelace را اجرا میکند.
|
|
|
|
### `normalize_points(ring)`
|
|
|
|
اگر آخر polygon با نقطه اول بسته شده باشد، نقطه تکراری آخر را حذف میکند.
|
|
|
|
### `calculate_center(points)`
|
|
|
|
مرکز تقریبی polygon یا مربع را از میانگین نقاط حساب میکند.
|
|
|
|
### `get_bbox(points)`
|
|
|
|
کمینه و بیشینه طول و عرض جغرافیایی را برمیگرداند تا محدوده کلی polygon مشخص شود.
|
|
|
|
### `meters_to_latitude_delta(meters)` و `meters_to_longitude_delta(meters, latitude)`
|
|
|
|
این دو تابع فاصله متر را به اختلاف latitude و longitude تبدیل میکنند.
|
|
برای ساخت grid مربعی از این دو تابع استفاده میشود.
|
|
|
|
---
|
|
|
|
## بخش 3: تشخیص برخورد polygon و cell
|
|
|
|
این بخش مشخص میکند که آیا یک مربع grid واقعا با polygon زمین برخورد دارد یا نه.
|
|
|
|
### `point_in_polygon(point, polygon_points)`
|
|
|
|
چک میکند یک نقطه داخل polygon هست یا نه.
|
|
|
|
### `_orientation`, `_on_segment`, `segments_intersect`
|
|
|
|
این توابع utilityهای هندسی برای تشخیص برخورد دو خط هستند.
|
|
|
|
### `rectangle_contains_point(point, cell_points)`
|
|
|
|
چک میکند یک نقطه داخل مربع cell قرار دارد یا نه.
|
|
|
|
### `polygon_intersects_cell(polygon_points, cell_points)`
|
|
|
|
این مهمترین تابع تقاطع است.
|
|
اگر یکی از شرایط زیر برقرار باشد، cell معتبر در نظر گرفته میشود:
|
|
|
|
- مرکز cell داخل polygon باشد
|
|
- یکی از گوشههای cell داخل polygon باشد
|
|
- یکی از نقاط polygon داخل cell باشد
|
|
- یکی از اضلاع polygon با اضلاع cell برخورد داشته باشد
|
|
|
|
نتیجه: فقط مربعهایی zone میشوند که واقعا با زمین همپوشانی داشته باشند.
|
|
|
|
---
|
|
|
|
## بخش 4: ساخت zoneها از روی area
|
|
|
|
### `build_square_points(...)`
|
|
|
|
چهار گوشه یک مربع را از روی مرزهای آن میسازد.
|
|
|
|
### `build_zone_square(area_points, center, zone_area_sqm)`
|
|
|
|
اگر area خیلی کوچک باشد یا zoneی تولید نشود، یک مربع fallback حول center area ساخته میشود.
|
|
|
|
### `split_area_into_zones(area_feature, cell_side_km=None)`
|
|
|
|
این تابع مهمترین بخش ساخت zoneها است.
|
|
|
|
### مراحل اجرای آن
|
|
|
|
1. polygon area را میگیرد.
|
|
2. center و bbox و total area را حساب میکند.
|
|
3. اندازه ضلع zone را مشخص میکند.
|
|
4. روی bbox یک grid مربعی میسازد.
|
|
5. هر cell را با `polygon_intersects_cell` بررسی میکند.
|
|
6. اگر cell با polygon تقاطع داشته باشد، یک zone جدید میسازد.
|
|
7. برای هر zone این دادهها تولید میشود:
|
|
- `zone_id`
|
|
- `geometry`
|
|
- `points`
|
|
- `center`
|
|
- `area_sqm`
|
|
- `area_hectares`
|
|
- `sequence`
|
|
8. اگر هیچ zoneی ساخته نشود، یک zone fallback میسازد.
|
|
9. در نهایت area summary و لیست zoneها را برمیگرداند.
|
|
|
|
### نکته مهم
|
|
|
|
در این پروژه zoneها grid-based هستند، نه بر اساس تقسیم واقعی shape زمین.
|
|
یعنی ابتدا grid مربعی ساخته میشود و بعد فقط مربعهایی که با زمین برخورد دارند نگه داشته میشوند.
|
|
|
|
---
|
|
|
|
## بخش 5: تولید recommendation و لایههای تحلیلی
|
|
|
|
این بخش داده پیشنهادی هر zone را تولید میکند.
|
|
|
|
### `build_rule_based_zone_metrics(index, coords)`
|
|
|
|
این تابع بدون نیاز به API خارجی، برای هر zone یک recommendation اولیه میسازد.
|
|
|
|
### هدف آن
|
|
|
|
وقتی zone تازه ساخته میشود، فرانت از همان ابتدا داده خالی نداشته باشد.
|
|
|
|
### خروجی آن
|
|
|
|
- `recommended_crop`
|
|
- `match_percent`
|
|
- `water_need_level`
|
|
- `water_need_value`
|
|
- `soil_quality_score`
|
|
- `soil_level`
|
|
- `cultivation_risk_level`
|
|
- `estimated_profit`
|
|
- `reason`
|
|
- `criteria`
|
|
|
|
این دادهها از روی مختصات zone و `sequence` به صورت deterministic ساخته میشوند.
|
|
|
|
### `build_initial_zone_payload(zone)`
|
|
|
|
خروجی سبک و اولیه برای endpoint ساخت zoneها تولید میکند.
|
|
|
|
### `build_area_zone_payload(zone)`
|
|
|
|
خروجی کاملتر برای `AreaView` تولید میکند و این بخشها را شامل میشود:
|
|
|
|
- geometry
|
|
- center
|
|
- area
|
|
- processing status
|
|
- crop recommendation
|
|
- water layer
|
|
- soil layer
|
|
- risk layer
|
|
|
|
### `persist_zone_analysis_metrics(zone, metrics)`
|
|
|
|
metrics را داخل مدلهای مختلف ذخیره میکند:
|
|
|
|
- recommendation
|
|
- criteria
|
|
- water need layer
|
|
- soil quality layer
|
|
- cultivation risk layer
|
|
|
|
### `ensure_rule_based_zone_data(zone, force=False)`
|
|
|
|
اگر zone هنوز recommendation نداشته باشد، با rule-based data آن را پر میکند.
|
|
|
|
---
|
|
|
|
## بخش 6: تحلیل خاک واقعی و ذخیره نتیجه
|
|
|
|
### `_get_level_color_map(...)`
|
|
|
|
رنگ هر level را برای سه لایه water / soil / risk برمیگرداند.
|
|
|
|
### `_pick_level(...)`
|
|
|
|
از روی score مشخص میکند level برابر `low` یا `medium` یا `high` است.
|
|
|
|
### `_format_range(...)`
|
|
|
|
برای ساخت رشتههایی مثل `3000-4000 m³/ha` استفاده میشود.
|
|
|
|
### `_derive_analysis_metrics(depths)`
|
|
|
|
این تابع از داده depthهای خاک، recommendation نهایی را میسازد.
|
|
|
|
### ورودی آن
|
|
|
|
آرایهای از depthها که از سرویس خارجی خاک میآید.
|
|
|
|
### محاسبات مهم آن
|
|
|
|
از میانگین این فیلدها استفاده میکند:
|
|
|
|
- `phh2o`
|
|
- `soc`
|
|
- `clay`
|
|
- `nitrogen`
|
|
- `wv0033`
|
|
|
|
بعد از اینها محاسبه میشود:
|
|
|
|
- کیفیت خاک
|
|
- نیاز آبی
|
|
- ریسک کشت
|
|
- محصول پیشنهادی
|
|
- درصد تطابق
|
|
- reason
|
|
- criteria
|
|
|
|
### `fetch_soil_data_for_zone(zone)`
|
|
|
|
برای یک zone به سرویس خارجی AI درخواست میزند و داده خاک میگیرد.
|
|
|
|
### payload ارسالی
|
|
|
|
- longitude
|
|
- latitude
|
|
- geometry zone
|
|
- center
|
|
- area
|
|
|
|
### `analyze_and_store_zone_soil_data(zone_id)`
|
|
|
|
این تابع منطق اصلی پردازش هر zone در worker است.
|
|
|
|
### مراحل آن
|
|
|
|
1. zone از دیتابیس خوانده میشود.
|
|
2. اگر قبلا کامل شده باشد، دوباره کاری نمیکند.
|
|
3. status روی `processing` میرود.
|
|
4. از API خارجی داده خاک میگیرد.
|
|
5. depthها را استخراج میکند.
|
|
6. recommendation واقعیتر را از روی خاک میسازد.
|
|
7. نتیجه را داخل مدلهای analysis و recommendation ذخیره میکند.
|
|
8. status را `completed` میکند.
|
|
9. اگر هر خطایی رخ دهد، status روی `failed` میرود و متن خطا ذخیره میشود.
|
|
|
|
---
|
|
|
|
## بخش 7: مدیریت taskهای zone
|
|
|
|
چون هر zone جداگانه پردازش میشود، باید taskها مدیریت شوند.
|
|
|
|
### `_get_stale_zone_ids(zones)`
|
|
|
|
این تابع zoneهایی را پیدا میکند که task آنها stale شده است.
|
|
|
|
### چه zoneهایی stale محسوب میشوند؟
|
|
|
|
- zone کامل نشده باشد
|
|
- task_id داشته باشد
|
|
- task خیلی قدیمی شده باشد
|
|
- یا task_id آن با task یک zone completed مشترک باشد
|
|
- یا state task در celery یکی از stateهای نامعتبر برای ادامه باشد
|
|
|
|
### `dispatch_zone_processing_tasks(crop_area_id=None, zone_ids=None, force=False)`
|
|
|
|
این تابع برای zoneهای انتخابشده task celery ثبت میکند.
|
|
|
|
### رفتار آن
|
|
|
|
- zoneهای completed را رد میکند
|
|
- اگر zone pending/processing باشد و task_id معتبر داشته باشد، دوباره dispatch نمیکند مگر `force=True`
|
|
- برای هر zone یک task جدا ثبت میکند
|
|
- اگر celery broker در دسترس نباشد، باز هم یک `task_id` محلی تولید میکند
|
|
- متن خطا را در `processing_error` ذخیره میکند
|
|
|
|
### اهمیت این طراحی
|
|
|
|
این باعث میشود:
|
|
|
|
- هر zone مستقل پردازش شود
|
|
- fail شدن یک zone بقیه را متوقف نکند
|
|
- فرانت بتواند وضعیت هر zone را جدا ببیند
|
|
|
|
---
|
|
|
|
## بخش 8: ساخت area، بازیابی area و ساخت payload response
|
|
|
|
### `create_missing_zones_for_area(crop_area)`
|
|
|
|
اگر area در دیتابیس وجود داشته باشد ولی zoneهایش از بین رفته باشند یا ساخته نشده باشند، دوباره از روی geometry آن zoneها را میسازد.
|
|
|
|
### `get_farm_for_uuid(farm_uuid)`
|
|
|
|
اعتبارسنجی میکند که:
|
|
|
|
- `farm_uuid` ارسال شده باشد
|
|
- farm واقعا در دیتابیس وجود داشته باشد
|
|
|
|
### `ensure_latest_area_ready_for_processing(farm_uuid, area_feature=None)`
|
|
|
|
این یکی از مهمترین توابع کل فایل است.
|
|
|
|
### منطق آن
|
|
|
|
1. sensor را پیدا میکند.
|
|
2. آخرین area مربوط به آن sensor را میگیرد.
|
|
3. اگر area وجود نداشته باشد:
|
|
- area پیشفرض یا area ورودی را میگیرد
|
|
- area و zoneها را میسازد
|
|
- taskها را dispatch میکند
|
|
4. اگر area وجود داشته باشد:
|
|
- مطمئن میشود zoneها وجود دارند
|
|
- برای هر zone، rule-based data را در صورت نبود ایجاد میکند
|
|
- zoneهای stale را پیدا میکند
|
|
- zoneهای لازم را دوباره dispatch میکند
|
|
- area تازه از دیتابیس خوانده میشود و برگردانده میشود
|
|
|
|
### نتیجه این تابع
|
|
|
|
وقتی `AreaView` این تابع را صدا میزند، همیشه یک area آماده برای نمایش و پردازش دارد.
|
|
|
|
### `create_zones_and_dispatch(area_feature, cell_side_km=None, sensor=None)`
|
|
|
|
این تابع area جدید را میسازد.
|
|
|
|
### مراحل آن
|
|
|
|
1. productها sync میشوند.
|
|
2. area normalize میشود.
|
|
3. area به zoneها تقسیم میشود.
|
|
4. داخل transaction:
|
|
- یک `CropArea` ساخته میشود
|
|
- همه `CropZone`ها bulk create میشوند
|
|
5. zoneها دوباره از دیتابیس خوانده میشوند
|
|
6. rule-based data برای هر zone ساخته میشود
|
|
7. taskهای پردازش dispatch میشوند
|
|
8. area و zones برگردانده میشوند
|
|
|
|
### `_zones_queryset(zone_ids=None)`
|
|
|
|
یک queryset آماده برمیگرداند که relationهای لازم را از قبل load میکند:
|
|
|
|
- recommendation
|
|
- product
|
|
- criteria
|
|
- water layer
|
|
- soil layer
|
|
- risk layer
|
|
|
|
این باعث میشود responseسازی سریعتر و با query کمتر انجام شود.
|
|
|
|
### `get_latest_area_payload(area=None, page=1, page_size=DEFAULT_ZONE_PAGE_SIZE)`
|
|
|
|
این تابع خروجی نهایی `AreaView` را میسازد.
|
|
|
|
### کارهای این تابع
|
|
|
|
1. area را پیدا میکند.
|
|
2. وضعیت همه zoneها را میخواند.
|
|
3. تعداد completed / pending / processing / failed را حساب میکند.
|
|
4. `task.status` را تعیین میکند.
|
|
5. `stage` و `stage_label` را تعیین میکند.
|
|
6. درصد پیشرفت را حساب میکند.
|
|
7. zoneهای همان صفحه را با slicing برمیدارد.
|
|
8. `pagination` را میسازد.
|
|
9. payload نهایی را برمیگرداند.
|
|
|
|
### منطق `task.status`
|
|
|
|
- اگر zone failed داشته باشیم: `FAILURE`
|
|
- اگر همه complete باشند: `SUCCESS`
|
|
- اگر بخشی complete یا processing باشند: `PROCESSING`
|
|
- در غیر این صورت: `PENDING`
|
|
|
|
### منطق pagination
|
|
|
|
- `page` و `page_size` از request گرفته میشوند
|
|
- `total_pages` از تقسیم تعداد کل zoneها بر `page_size` محاسبه میشود
|
|
- فقط zoneهای همان بازه برگردانده میشوند
|
|
- اطلاعات page فعلی، تعداد صفحات و وجود صفحه قبل/بعد در body قرار میگیرد
|
|
|
|
### `get_initial_zones_payload(crop_area)`
|
|
|
|
payload سادهتر برای endpoint اولیه zoneها میسازد.
|
|
|
|
### `get_water_need_payload(zone_ids=None)`
|
|
|
|
خروجی لایه نیاز آبی را برمیگرداند.
|
|
|
|
### `get_soil_quality_payload(zone_ids=None)`
|
|
|
|
خروجی لایه کیفیت خاک را برمیگرداند.
|
|
|
|
### `get_cultivation_risk_payload(zone_ids=None)`
|
|
|
|
خروجی لایه ریسک کشت را برمیگرداند.
|
|
|
|
### `get_zone_details_payload(zone_id)`
|
|
|
|
خروجی دیتیل یک zone را میسازد.
|
|
|
|
---
|
|
|
|
## جریان کامل اجرای `GET /api/crop-zoning/area/`
|
|
|
|
اگر بخواهیم کل flow را از ابتدا تا انتها خیلی ساده توضیح بدهیم:
|
|
|
|
1. فرانت `farm_uuid` و احتمالا `page` و `page_size` را میفرستد.
|
|
2. `AreaView` پارامترها را میخواند.
|
|
3. `ensure_latest_area_ready_for_processing` اجرا میشود.
|
|
4. اگر area وجود نداشته باشد، area و zoneها ساخته میشوند.
|
|
5. اگر zoneها ناقص باشند، کامل میشوند.
|
|
6. اگر recommendation اولیه نباشد، ساخته میشود.
|
|
7. اگر taskهای لازم وجود نداشته باشند یا stale باشند، dispatch میشوند.
|
|
8. `get_latest_area_payload` اجرا میشود.
|
|
9. وضعیت کلی task و zoneهای صفحه فعلی ساخته میشود.
|
|
10. response نهایی به فرانت برمیگردد.
|
|
|
|
---
|
|
|
|
## منطق `crop_zoning/tests.py`
|
|
|
|
این فایل تست رفتار کلیدی API را پوشش میدهد.
|
|
|
|
تستها با `Django TestCase` و `APIRequestFactory` نوشته شدهاند.
|
|
|
|
### تنظیمات مشترک تستها
|
|
|
|
در تستها از این تنظیمات استفاده شده:
|
|
|
|
- `USE_EXTERNAL_API_MOCK=True`
|
|
- `CROP_ZONE_CHUNK_AREA_SQM=200000`
|
|
|
|
هدف این است که:
|
|
|
|
- وابستگی به API خارجی واقعی حذف شود
|
|
- zoneها با اندازه مشخص و قابل پیشبینی ساخته شوند
|
|
|
|
---
|
|
|
|
## کلاس `ZonesInitialViewTests`
|
|
|
|
### `test_post_accepts_area_geojson_alias`
|
|
|
|
این تست بررسی میکند که اگر polygon با کلید `area_geojson` ارسال شود:
|
|
|
|
- endpoint آن را قبول کند
|
|
- پاسخ `200` بدهد
|
|
- zone ساخته شود
|
|
- تعداد zoneهای خروجی با `zone_count` یکسان باشد
|
|
|
|
این تست در عمل alias بودن `area_geojson` را validate میکند.
|
|
|
|
---
|
|
|
|
## کلاس `AreaViewTests`
|
|
|
|
این کلاس رفتارهای اصلی `AreaView` را تست میکند.
|
|
|
|
### `setUp`
|
|
|
|
در شروع هر تست:
|
|
|
|
- یک user ساخته میشود
|
|
- یک sensor برای آن user ساخته میشود
|
|
- `APIRequestFactory` آماده میشود
|
|
|
|
### `_create_area(...)`
|
|
|
|
یک helper برای ساخت سریع `CropArea` در تستها است.
|
|
|
|
### `_request()`
|
|
|
|
یک request استاندارد برای `AreaView` با `farm_uuid` معتبر میسازد.
|
|
|
|
### `_request_with_pagination(page, page_size)`
|
|
|
|
یک request برای تست pagination میسازد.
|
|
|
|
---
|
|
|
|
### تستهای اصلی `AreaView`
|
|
|
|
#### `test_get_requires_farm_uuid`
|
|
|
|
بررسی میکند اگر `farm_uuid` ارسال نشود، پاسخ `400` برگردد.
|
|
|
|
#### `test_get_returns_pending_task_status_until_all_zones_complete`
|
|
|
|
بررسی میکند اگر zoneها pending و processing باشند:
|
|
|
|
- status کلی `PROCESSING` باشد
|
|
- area برگردد
|
|
- zoneها در response باشند
|
|
- فیلد `processing_status` برای zone موجود باشد
|
|
|
|
#### `test_get_returns_area_when_all_tasks_complete`
|
|
|
|
بررسی میکند وقتی همه zoneها complete باشند:
|
|
|
|
- status کلی `SUCCESS` باشد
|
|
- zoneها برگردند
|
|
- فیلدهای recommendation و layerها موجود باشند
|
|
|
|
#### `test_get_returns_paginated_zones`
|
|
|
|
تست جدید pagination است.
|
|
|
|
بررسی میکند که:
|
|
|
|
- با `page=2` و `page_size=1`
|
|
- فقط zone دوم برگردد
|
|
- اطلاعات pagination درست باشد
|
|
- `total_pages`, `has_next`, `has_previous` درست باشند
|
|
|
|
#### `test_get_rejects_invalid_pagination_params`
|
|
|
|
بررسی میکند اگر `page=0` باشد:
|
|
|
|
- پاسخ `400` بدهد
|
|
- پیام خطا مناسب برگردد
|
|
|
|
#### `test_get_dispatches_zone_task_when_task_id_is_missing`
|
|
|
|
با mock کردن `dispatch_zone_processing_tasks` بررسی میکند که:
|
|
|
|
- اگر zone task_id نداشته باشد
|
|
- در زمان فراخوانی `AreaView`
|
|
- dispatch انجام شود
|
|
|
|
#### `test_get_creates_area_when_sensor_has_no_data`
|
|
|
|
با mock کردن `create_zones_and_dispatch` بررسی میکند که:
|
|
|
|
- اگر sensor هنوز area نداشته باشد
|
|
- سیستم area جدید بسازد
|
|
- همان sensor را به service پاس بدهد
|
|
|
|
#### `test_each_zone_gets_its_own_task`
|
|
|
|
بررسی میکند برای دو zone جدا:
|
|
|
|
- دو task مستقل ایجاد شود
|
|
- هر zone task_id جدا داشته باشد
|
|
|
|
این تست خیلی مهم است چون تایید میکند taskها shared نیستند و per-zone هستند.
|
|
|
|
#### `test_get_generates_local_task_id_when_broker_is_unavailable`
|
|
|
|
با mock کردن celery و ایجاد `OperationalError` بررسی میکند که:
|
|
|
|
- حتی وقتی broker در دسترس نیست
|
|
- سیستم task_id محلی بسازد
|
|
- response خراب نشود
|
|
- وضعیت کلی درست بماند
|
|
|
|
#### `test_get_stores_task_id_and_reuses_it_on_next_request`
|
|
|
|
بررسی میکند:
|
|
|
|
- وقتی اولین request task_id را ثبت کرد
|
|
- request بعدی دوباره task تازه نسازد
|
|
- همان task_id قبلی reuse شود
|
|
|
|
این تست جلوی dispatch تکراری را میگیرد.
|
|
|
|
#### `test_get_redispatches_pending_zone_when_shared_task_already_completed`
|
|
|
|
این تست سناریوی قدیمی یا خراب را پوشش میدهد.
|
|
|
|
سناریو:
|
|
|
|
- یک zone completed شده
|
|
- zone دیگر pending مانده
|
|
- هر دو task_id یکسان دارند
|
|
|
|
در این حالت سیستم باید zone stale را دوباره dispatch کند.
|
|
|
|
این تست نشان میدهد منطق stale detection واقعا کار میکند.
|
|
|
|
---
|
|
|
|
## جمعبندی معماری
|
|
|
|
اگر خیلی خلاصه بخواهیم نقش هر فایل را بگوییم:
|
|
|
|
### `views.py`
|
|
|
|
لایه HTTP است.
|
|
|
|
- request را میگیرد
|
|
- service مناسب را صدا میزند
|
|
- response برمیگرداند
|
|
|
|
### `services.py`
|
|
|
|
لایه business logic است.
|
|
|
|
- area را validate میکند
|
|
- polygon را به zone تبدیل میکند
|
|
- recommendation اولیه و نهایی میسازد
|
|
- taskها را مدیریت میکند
|
|
- payload response را میسازد
|
|
|
|
### `tests.py`
|
|
|
|
لایه اطمینان از رفتار سیستم است.
|
|
|
|
- ساخت area
|
|
- ساخت zone
|
|
- status task
|
|
- dispatch task
|
|
- stale task
|
|
- pagination
|
|
- خطاهای ورودی
|
|
|
|
را تست میکند.
|
|
|
|
---
|
|
|
|
## نکات مهم برای فرانت
|
|
|
|
- endpoint `area` الان pagination دارد و `zones` همیشه همه zoneها را برنمیگرداند.
|
|
- تعداد کل zoneها از `task.total_zones` یا `pagination.total_zones` قابل خواندن است.
|
|
- تعداد کل صفحهها از `pagination.total_pages` قابل خواندن است.
|
|
- برای نمایش progress باید از `task.progress_percent` و `task.status` استفاده شود.
|
|
- `task.status` وضعیت کلی area است، نه وضعیت تکتک zoneها.
|
|
- وضعیت هر zone داخل `processing_status` قرار دارد.
|
|
- در صورت نیاز به جزئیات بیشتر برای یک zone باید `ZoneDetailsView` صدا زده شود.
|
|
|
|
---
|
|
|
|
## نکات مهم برای بکاند
|
|
|
|
- منطق grid سازی و پردازش zoneها تقریبا کامل داخل `services.py` متمرکز شده است.
|
|
- `AreaView` عمدا thin نگه داشته شده تا business logic وارد view نشود.
|
|
- rule-based data نقش fallback سریع برای UI را دارد.
|
|
- data واقعیتر بعدا با taskهای تحلیل خاک جایگزین یا تکمیل میشود.
|
|
- تستها بیشتر روی پایداری flow پردازش و task dispatch تمرکز دارند.
|
|
|