UPDATE
This commit is contained in:
@@ -0,0 +1,357 @@
|
|||||||
|
# مستند ارتباط فرانت با API تنظیمات داشبورد فارم
|
||||||
|
|
||||||
|
این فایل مشخص میکند فرانتاند برای endpoint زیر چه request و responseی انتظار دارد:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://localhost:8000/api/farm-dashboard-config
|
||||||
|
```
|
||||||
|
|
||||||
|
این endpoint در فرانت از طریق فایل `src/libs/api/services/farmDashboardService.ts` مصرف میشود.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## خلاصه رفتار فرانت
|
||||||
|
|
||||||
|
- فرانت برای دریافت تنظیمات از `GET /api/farm-dashboard-config` استفاده میکند.
|
||||||
|
- فرانت برای ذخیره تغییرات از `PATCH /api/farm-dashboard-config` استفاده میکند.
|
||||||
|
- در `PATCH` فقط فیلدهای تغییرکرده ارسال میشوند.
|
||||||
|
- اما در response بهتر است همیشه **کل تنظیمات نهایی** برگردانده شود.
|
||||||
|
- فرانت response را هم در حالت wrapper شده و هم بدون wrapper میپذیرد.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## فرمت response قابل قبول
|
||||||
|
|
||||||
|
فرانت هر دو فرمت زیر را میپذیرد.
|
||||||
|
|
||||||
|
### فرمت پیشنهادی
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "OK",
|
||||||
|
"data": {
|
||||||
|
"disabled_card_ids": ["farmWeatherCard", "sensorRadarChart"],
|
||||||
|
"row_order": [
|
||||||
|
"overviewKpis",
|
||||||
|
"weatherAlerts",
|
||||||
|
"sensorMonitoring",
|
||||||
|
"sensorCharts",
|
||||||
|
"alertsWater",
|
||||||
|
"predictions",
|
||||||
|
"soilHeatmap",
|
||||||
|
"ndviRecommendations",
|
||||||
|
"economic"
|
||||||
|
],
|
||||||
|
"enable_drag_reorder": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### فرمت قابل قبول بدون wrapper
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"disabled_card_ids": ["farmWeatherCard", "sensorRadarChart"],
|
||||||
|
"row_order": [
|
||||||
|
"overviewKpis",
|
||||||
|
"weatherAlerts",
|
||||||
|
"sensorMonitoring",
|
||||||
|
"sensorCharts",
|
||||||
|
"alertsWater",
|
||||||
|
"predictions",
|
||||||
|
"soilHeatmap",
|
||||||
|
"ndviRecommendations",
|
||||||
|
"economic"
|
||||||
|
],
|
||||||
|
"enable_drag_reorder": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GET
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/farm-dashboard-config
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
> هدر `Authorization` فقط وقتی ارسال میشود که توکن در `localStorage` موجود باشد.
|
||||||
|
|
||||||
|
### Response مورد انتظار
|
||||||
|
|
||||||
|
فرانت در خروجی GET این ساختار را انتظار دارد:
|
||||||
|
|
||||||
|
| فیلد | نوع | توضیح |
|
||||||
|
|---|---|---|
|
||||||
|
| `disabled_card_ids` | `string[]` | لیست کارتهای مخفیشده |
|
||||||
|
| `row_order` | `string[]` | ترتیب ردیفهای داشبورد |
|
||||||
|
| `enable_drag_reorder` | `boolean` | فعال/غیرفعال بودن drag reorder |
|
||||||
|
|
||||||
|
### مثال response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "OK",
|
||||||
|
"data": {
|
||||||
|
"disabled_card_ids": ["farmWeatherCard", "sensorRadarChart"],
|
||||||
|
"row_order": [
|
||||||
|
"overviewKpis",
|
||||||
|
"weatherAlerts",
|
||||||
|
"sensorMonitoring",
|
||||||
|
"sensorCharts",
|
||||||
|
"alertsWater",
|
||||||
|
"predictions",
|
||||||
|
"soilHeatmap",
|
||||||
|
"ndviRecommendations",
|
||||||
|
"economic"
|
||||||
|
],
|
||||||
|
"enable_drag_reorder": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### نکات مهم GET
|
||||||
|
|
||||||
|
- اگر `enable_drag_reorder` برنگردد، فرانت مقدار پیشفرض `true` در نظر میگیرد.
|
||||||
|
- اگر `row_order` نامعتبر یا خالی باشد، فرانت از ترتیب پیشفرض خودش استفاده میکند.
|
||||||
|
- اگر request خطا بخورد، فرانت اول `localStorage` را چک میکند؛ اگر چیزی نبود، config پیشفرض را میسازد.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PATCH
|
||||||
|
|
||||||
|
### رفتار request
|
||||||
|
|
||||||
|
فرانت فقط فیلدهای تغییرکرده را میفرستد. یعنی body میتواند یکی از حالتهای زیر باشد یا ترکیبی از آنها:
|
||||||
|
|
||||||
|
#### 1) تغییر کارتهای غیرفعال
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"disabled_card_ids": ["farmWeatherCard", "sensorRadarChart"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2) تغییر ترتیب ردیفها
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"row_order": [
|
||||||
|
"overviewKpis",
|
||||||
|
"weatherAlerts",
|
||||||
|
"predictions",
|
||||||
|
"sensorMonitoring",
|
||||||
|
"sensorCharts",
|
||||||
|
"alertsWater",
|
||||||
|
"soilHeatmap",
|
||||||
|
"ndviRecommendations",
|
||||||
|
"economic"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3) فعال/غیرفعال کردن drag reorder
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enable_drag_reorder": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4) ترکیبی
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"disabled_card_ids": ["farmWeatherCard"],
|
||||||
|
"row_order": [
|
||||||
|
"overviewKpis",
|
||||||
|
"weatherAlerts",
|
||||||
|
"sensorMonitoring",
|
||||||
|
"sensorCharts",
|
||||||
|
"alertsWater",
|
||||||
|
"predictions",
|
||||||
|
"soilHeatmap",
|
||||||
|
"ndviRecommendations",
|
||||||
|
"economic"
|
||||||
|
],
|
||||||
|
"enable_drag_reorder": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response مورد انتظار برای PATCH
|
||||||
|
|
||||||
|
هرچند request میتواند partial باشد، ولی response بهتر است همیشه **کل state نهایی config** را برگرداند:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "OK",
|
||||||
|
"data": {
|
||||||
|
"disabled_card_ids": ["farmWeatherCard"],
|
||||||
|
"row_order": [
|
||||||
|
"overviewKpis",
|
||||||
|
"weatherAlerts",
|
||||||
|
"sensorMonitoring",
|
||||||
|
"sensorCharts",
|
||||||
|
"alertsWater",
|
||||||
|
"predictions",
|
||||||
|
"soilHeatmap",
|
||||||
|
"ndviRecommendations",
|
||||||
|
"economic"
|
||||||
|
],
|
||||||
|
"enable_drag_reorder": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### نکته خیلی مهم برای PATCH
|
||||||
|
|
||||||
|
در پیادهسازی فعلی فرانت، response باید حداقل یکی از این دو فیلد را داشته باشد:
|
||||||
|
|
||||||
|
- `disabled_card_ids`
|
||||||
|
- `row_order`
|
||||||
|
|
||||||
|
اگر backend فقط این را برگرداند:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enable_drag_reorder": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
ممکن است فرانت این response را نامعتبر تشخیص دهد.
|
||||||
|
بنابراین برای جلوگیری از مشکل، **همیشه کل object نهایی config را برگردانید**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## mapping بین فرانت و API
|
||||||
|
|
||||||
|
فرانت state داخلی را با نامهای camelCase نگه میدارد، اما در request به snake_case تبدیل میکند:
|
||||||
|
|
||||||
|
| Frontend field | API field |
|
||||||
|
|---|---|
|
||||||
|
| `disabledCardIds` | `disabled_card_ids` |
|
||||||
|
| `rowOrder` | `row_order` |
|
||||||
|
| `enableDragReorder` | `enable_drag_reorder` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## مقادیر معتبر پیشنهادی
|
||||||
|
|
||||||
|
### Row ID های معتبر
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
"overviewKpis",
|
||||||
|
"weatherAlerts",
|
||||||
|
"sensorMonitoring",
|
||||||
|
"sensorCharts",
|
||||||
|
"alertsWater",
|
||||||
|
"predictions",
|
||||||
|
"soilHeatmap",
|
||||||
|
"ndviRecommendations",
|
||||||
|
"economic"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Card ID های معتبر
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
"farmOverviewKpis",
|
||||||
|
"farmWeatherCard",
|
||||||
|
"farmAlertsTracker",
|
||||||
|
"sensorValuesList",
|
||||||
|
"sensorRadarChart",
|
||||||
|
"sensorComparisonChart",
|
||||||
|
"anomalyDetectionCard",
|
||||||
|
"farmAlertsTimeline",
|
||||||
|
"waterNeedPrediction",
|
||||||
|
"harvestPredictionCard",
|
||||||
|
"yieldPredictionChart",
|
||||||
|
"soilMoistureHeatmap",
|
||||||
|
"ndviHealthCard",
|
||||||
|
"recommendationsList",
|
||||||
|
"economicOverview"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## فرمت canonical پیشنهادی برای backend
|
||||||
|
|
||||||
|
اگر بخواهی backend کاملاً سازگار و بدون ambiguity باشد، بهترین قرارداد این است:
|
||||||
|
|
||||||
|
- `disabled_card_ids` فقط شامل `Card ID` باشد
|
||||||
|
- `row_order` فقط شامل `Row ID` باشد
|
||||||
|
- response همیشه full object برگرداند
|
||||||
|
- status موفق `200` باشد
|
||||||
|
- `Content-Type` برابر `application/json` باشد
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## نمونه نهایی پیشنهادی
|
||||||
|
|
||||||
|
### GET success
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "OK",
|
||||||
|
"data": {
|
||||||
|
"disabled_card_ids": [],
|
||||||
|
"row_order": [
|
||||||
|
"overviewKpis",
|
||||||
|
"weatherAlerts",
|
||||||
|
"sensorMonitoring",
|
||||||
|
"sensorCharts",
|
||||||
|
"alertsWater",
|
||||||
|
"predictions",
|
||||||
|
"soilHeatmap",
|
||||||
|
"ndviRecommendations",
|
||||||
|
"economic"
|
||||||
|
],
|
||||||
|
"enable_drag_reorder": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PATCH success
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "OK",
|
||||||
|
"data": {
|
||||||
|
"disabled_card_ids": ["farmWeatherCard"],
|
||||||
|
"row_order": [
|
||||||
|
"overviewKpis",
|
||||||
|
"weatherAlerts",
|
||||||
|
"sensorMonitoring",
|
||||||
|
"sensorCharts",
|
||||||
|
"alertsWater",
|
||||||
|
"predictions",
|
||||||
|
"soilHeatmap",
|
||||||
|
"ndviRecommendations",
|
||||||
|
"economic"
|
||||||
|
],
|
||||||
|
"enable_drag_reorder": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## منبع این مستند
|
||||||
|
|
||||||
|
این مستند بر اساس رفتار واقعی فرانت در فایلهای زیر نوشته شده است:
|
||||||
|
|
||||||
|
- `src/libs/api/services/farmDashboardService.ts`
|
||||||
|
- `src/views/dashboards/farm/farmDashboardConfig.ts`
|
||||||
|
- `src/views/dashboards/farm/FarmDashboardWrapper.tsx`
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
|
||||||
|
from account.seeds import ADMIN_USER_DATA, seed_admin_user
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Create or update the default admin user."
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
try:
|
||||||
|
user, created = seed_admin_user()
|
||||||
|
except ValueError as exc:
|
||||||
|
raise CommandError(str(exc)) from exc
|
||||||
|
|
||||||
|
action = "created" if created else "updated"
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f"Admin user {action}: username={user.username}, email={user.email}, "
|
||||||
|
f"phone_number={user.phone_number}, password={ADMIN_USER_DATA['password']}"
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db import transaction
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
|
||||||
|
ADMIN_USER_DATA = {
|
||||||
|
"username": "admin",
|
||||||
|
"email": "admin@example.com",
|
||||||
|
"phone_number": "0912345678",
|
||||||
|
"first_name": "admin",
|
||||||
|
"last_name": "admin",
|
||||||
|
"password": "admin123456",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def seed_admin_user():
|
||||||
|
user_model = get_user_model()
|
||||||
|
lookup = (
|
||||||
|
Q(username=ADMIN_USER_DATA["username"])
|
||||||
|
| Q(email=ADMIN_USER_DATA["email"])
|
||||||
|
| Q(phone_number=ADMIN_USER_DATA["phone_number"])
|
||||||
|
)
|
||||||
|
matched_users = list(user_model.objects.filter(lookup).order_by("id"))
|
||||||
|
|
||||||
|
if len(matched_users) > 1:
|
||||||
|
raise ValueError(
|
||||||
|
"Multiple users matched the admin seeder lookup. Resolve duplicates before seeding."
|
||||||
|
)
|
||||||
|
|
||||||
|
created = not matched_users
|
||||||
|
user = matched_users[0] if matched_users else user_model()
|
||||||
|
user.username = ADMIN_USER_DATA["username"]
|
||||||
|
user.email = ADMIN_USER_DATA["email"]
|
||||||
|
user.phone_number = ADMIN_USER_DATA["phone_number"]
|
||||||
|
user.first_name = ADMIN_USER_DATA["first_name"]
|
||||||
|
user.last_name = ADMIN_USER_DATA["last_name"]
|
||||||
|
user.is_staff = True
|
||||||
|
user.is_superuser = True
|
||||||
|
user.is_active = True
|
||||||
|
user.set_password(ADMIN_USER_DATA["password"])
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
return user, created
|
||||||
+1
-1
@@ -19,7 +19,7 @@ class LoginSerializer(serializers.Serializer):
|
|||||||
identifier can be username, email, or phone_number."""
|
identifier can be username, email, or phone_number."""
|
||||||
|
|
||||||
identifier = serializers.CharField()
|
identifier = serializers.CharField()
|
||||||
password = serializers.CharField()
|
password = serializers.CharField(min_length=8, write_only=True)
|
||||||
|
|
||||||
|
|
||||||
# --- RequestOTP (request-otp/) ---
|
# --- RequestOTP (request-otp/) ---
|
||||||
|
|||||||
+51
-11
@@ -1,28 +1,68 @@
|
|||||||
"""
|
"""
|
||||||
Static mock data for Farm Dashboard API.
|
Static mock data for Farm Dashboard API.
|
||||||
No database, no dynamic values. Pure static payloads.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Config payload for GET/PATCH farm-dashboard-config (section 2.1)
|
from copy import deepcopy
|
||||||
# row_order must use valid row IDs only: overviewKpis, weatherAlerts, sensorMonitoring,
|
from threading import Lock
|
||||||
# sensorCharts, alertsWater, predictions, soilHeatmap, ndviRecommendations, economic
|
|
||||||
CONFIG = {
|
|
||||||
"disabled_card_ids": [
|
VALID_ROW_IDS = [
|
||||||
"predictions",
|
|
||||||
],
|
|
||||||
"row_order": [
|
|
||||||
"overviewKpis",
|
"overviewKpis",
|
||||||
"weatherAlerts",
|
"weatherAlerts",
|
||||||
"sensorMonitoring",
|
"sensorMonitoring",
|
||||||
"sensorCharts",
|
"sensorCharts",
|
||||||
"alertsWater",
|
"alertsWater",
|
||||||
|
"predictions",
|
||||||
"soilHeatmap",
|
"soilHeatmap",
|
||||||
"ndviRecommendations",
|
"ndviRecommendations",
|
||||||
"economic",
|
"economic",
|
||||||
],
|
]
|
||||||
"enable_drag_reorder": False,
|
|
||||||
|
VALID_CARD_IDS = [
|
||||||
|
"farmOverviewKpis",
|
||||||
|
"farmWeatherCard",
|
||||||
|
"farmAlertsTracker",
|
||||||
|
"sensorValuesList",
|
||||||
|
"sensorRadarChart",
|
||||||
|
"sensorComparisonChart",
|
||||||
|
"anomalyDetectionCard",
|
||||||
|
"farmAlertsTimeline",
|
||||||
|
"waterNeedPrediction",
|
||||||
|
"harvestPredictionCard",
|
||||||
|
"yieldPredictionChart",
|
||||||
|
"soilMoistureHeatmap",
|
||||||
|
"ndviHealthCard",
|
||||||
|
"recommendationsList",
|
||||||
|
"economicOverview",
|
||||||
|
]
|
||||||
|
|
||||||
|
DEFAULT_CONFIG = {
|
||||||
|
"disabled_card_ids": [],
|
||||||
|
"row_order": VALID_ROW_IDS.copy(),
|
||||||
|
"enable_drag_reorder": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_config_lock = Lock()
|
||||||
|
_config_state = deepcopy(DEFAULT_CONFIG)
|
||||||
|
|
||||||
|
|
||||||
|
def get_config():
|
||||||
|
with _config_lock:
|
||||||
|
return deepcopy(_config_state)
|
||||||
|
|
||||||
|
|
||||||
|
def update_config(changes):
|
||||||
|
with _config_lock:
|
||||||
|
_config_state.update(deepcopy(changes))
|
||||||
|
return deepcopy(_config_state)
|
||||||
|
|
||||||
|
|
||||||
|
def reset_config():
|
||||||
|
with _config_lock:
|
||||||
|
_config_state.clear()
|
||||||
|
_config_state.update(deepcopy(DEFAULT_CONFIG))
|
||||||
|
return deepcopy(_config_state)
|
||||||
|
|
||||||
# 4.1 farmOverviewKpis
|
# 4.1 farmOverviewKpis
|
||||||
FARM_OVERVIEW_KPIS = {
|
FARM_OVERVIEW_KPIS = {
|
||||||
"kpis": [
|
"kpis": [
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from .mock_data import VALID_CARD_IDS, VALID_ROW_IDS
|
||||||
|
|
||||||
|
|
||||||
|
class FarmDashboardConfigSerializer(serializers.Serializer):
|
||||||
|
disabled_card_ids = serializers.ListField(
|
||||||
|
child=serializers.CharField(),
|
||||||
|
allow_empty=True,
|
||||||
|
)
|
||||||
|
row_order = serializers.ListField(
|
||||||
|
child=serializers.CharField(),
|
||||||
|
allow_empty=False,
|
||||||
|
)
|
||||||
|
enable_drag_reorder = serializers.BooleanField()
|
||||||
|
|
||||||
|
def validate_disabled_card_ids(self, value):
|
||||||
|
invalid_ids = [card_id for card_id in value if card_id not in VALID_CARD_IDS]
|
||||||
|
if invalid_ids:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
f"Invalid card IDs: {', '.join(invalid_ids)}."
|
||||||
|
)
|
||||||
|
if len(set(value)) != len(value):
|
||||||
|
raise serializers.ValidationError("disabled_card_ids must not contain duplicates.")
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate_row_order(self, value):
|
||||||
|
invalid_ids = [row_id for row_id in value if row_id not in VALID_ROW_IDS]
|
||||||
|
if invalid_ids:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
f"Invalid row IDs: {', '.join(invalid_ids)}."
|
||||||
|
)
|
||||||
|
if len(set(value)) != len(value):
|
||||||
|
raise serializers.ValidationError("row_order must not contain duplicates.")
|
||||||
|
if set(value) != set(VALID_ROW_IDS):
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
"row_order must contain each valid row ID exactly once."
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class FarmDashboardConfigPatchSerializer(FarmDashboardConfigSerializer):
|
||||||
|
disabled_card_ids = serializers.ListField(
|
||||||
|
child=serializers.CharField(),
|
||||||
|
allow_empty=True,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
row_order = serializers.ListField(
|
||||||
|
child=serializers.CharField(),
|
||||||
|
allow_empty=False,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
enable_drag_reorder = serializers.BooleanField(required=False)
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
attrs = super().validate(attrs)
|
||||||
|
if not attrs:
|
||||||
|
raise serializers.ValidationError("At least one config field must be provided.")
|
||||||
|
return attrs
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
from django.test import SimpleTestCase
|
||||||
|
from rest_framework.test import APIRequestFactory
|
||||||
|
|
||||||
|
from .mock_data import DEFAULT_CONFIG, reset_config
|
||||||
|
from .views import FarmDashboardConfigView
|
||||||
|
|
||||||
|
|
||||||
|
class FarmDashboardConfigViewTests(SimpleTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.factory = APIRequestFactory()
|
||||||
|
reset_config()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
reset_config()
|
||||||
|
|
||||||
|
def test_get_returns_canonical_config(self):
|
||||||
|
request = self.factory.get("/api/farm-dashboard-config/")
|
||||||
|
response = FarmDashboardConfigView.as_view()(request)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.data["code"], 200)
|
||||||
|
self.assertEqual(response.data["msg"], "OK")
|
||||||
|
self.assertEqual(response.data["data"], DEFAULT_CONFIG)
|
||||||
|
|
||||||
|
def test_patch_partial_update_returns_full_final_config(self):
|
||||||
|
request = self.factory.patch(
|
||||||
|
"/api/farm-dashboard-config/",
|
||||||
|
{"disabled_card_ids": ["farmWeatherCard"]},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
response = FarmDashboardConfigView.as_view()(request)
|
||||||
|
|
||||||
|
expected = deepcopy(DEFAULT_CONFIG)
|
||||||
|
expected["disabled_card_ids"] = ["farmWeatherCard"]
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.data["data"], expected)
|
||||||
|
|
||||||
|
def test_patch_only_drag_flag_still_returns_full_config(self):
|
||||||
|
request = self.factory.patch(
|
||||||
|
"/api/farm-dashboard-config/",
|
||||||
|
{"enable_drag_reorder": False},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
response = FarmDashboardConfigView.as_view()(request)
|
||||||
|
|
||||||
|
expected = deepcopy(DEFAULT_CONFIG)
|
||||||
|
expected["enable_drag_reorder"] = False
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.data["data"], expected)
|
||||||
|
self.assertIn("disabled_card_ids", response.data["data"])
|
||||||
|
self.assertIn("row_order", response.data["data"])
|
||||||
|
|
||||||
|
def test_patch_rejects_invalid_row_order(self):
|
||||||
|
request = self.factory.patch(
|
||||||
|
"/api/farm-dashboard-config/",
|
||||||
|
{"row_order": ["overviewKpis"]},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
response = FarmDashboardConfigView.as_view()(request)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertIn("row_order", response.data)
|
||||||
+1
-1
@@ -3,6 +3,6 @@ from django.urls import path
|
|||||||
from .views import FarmDashboardCardsView
|
from .views import FarmDashboardCardsView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("cards/", FarmDashboardCardsView.as_view(), name="farm-dashboard-cards"),
|
# path("cards/", FarmDashboardCardsView.as_view(), name="farm-dashboard-cards"),
|
||||||
path("", FarmDashboardCardsView.as_view(), name="farm-dashboard"),
|
path("", FarmDashboardCardsView.as_view(), name="farm-dashboard"),
|
||||||
]
|
]
|
||||||
|
|||||||
+23
-13
@@ -1,43 +1,51 @@
|
|||||||
"""
|
"""
|
||||||
Farm Dashboard API views.
|
Farm Dashboard API views.
|
||||||
No database connection. All responses use static mock data from mock_data.py.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from rest_framework.permissions import AllowAny
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from drf_spectacular.types import OpenApiTypes
|
|
||||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||||
|
|
||||||
from config.swagger import code_response
|
from config.swagger import code_response
|
||||||
from external_api_adapter import request as external_api_request
|
from .mock_data import get_config, update_config
|
||||||
from .mock_data import CONFIG
|
from .serializers import FarmDashboardConfigPatchSerializer, FarmDashboardConfigSerializer
|
||||||
|
|
||||||
|
|
||||||
@extend_schema_view(
|
@extend_schema_view(
|
||||||
get=extend_schema(
|
get=extend_schema(
|
||||||
tags=["Farm Dashboard"],
|
tags=["Farm Dashboard"],
|
||||||
responses={200: code_response("FarmDashboardConfigGetResponse", data=serializers.JSONField())},
|
responses={200: code_response("FarmDashboardConfigGetResponse", data=FarmDashboardConfigSerializer())},
|
||||||
),
|
),
|
||||||
patch=extend_schema(
|
patch=extend_schema(
|
||||||
tags=["Farm Dashboard"],
|
tags=["Farm Dashboard"],
|
||||||
request=OpenApiTypes.OBJECT,
|
request=FarmDashboardConfigPatchSerializer,
|
||||||
responses={200: code_response("FarmDashboardConfigPatchResponse", data=serializers.JSONField())},
|
responses={200: code_response("FarmDashboardConfigPatchResponse", data=FarmDashboardConfigSerializer())},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
class FarmDashboardConfigView(APIView):
|
class FarmDashboardConfigView(APIView):
|
||||||
"""
|
"""
|
||||||
Farm dashboard config endpoints: GET and PATCH.
|
Farm dashboard config endpoints.
|
||||||
GET returns static config (disabled_card_ids, row_order, enable_drag_reorder).
|
GET returns the current config.
|
||||||
PATCH accepts body but returns same static config; no processing or validation.
|
PATCH accepts partial updates and returns the full final config.
|
||||||
No database. No input values used in response.
|
|
||||||
"""
|
"""
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return Response({"code": 200, "msg": "OK", "data": CONFIG}, status=status.HTTP_200_OK)
|
config = get_config()
|
||||||
|
return Response({"code": 200, "msg": "OK", "data": config}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def patch(self, request):
|
def patch(self, request):
|
||||||
return Response({"code": 200, "msg": "OK", "data": CONFIG}, status=status.HTTP_200_OK)
|
serializer = FarmDashboardConfigPatchSerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
config = update_config(serializer.validated_data)
|
||||||
|
response_serializer = FarmDashboardConfigSerializer(config)
|
||||||
|
return Response(
|
||||||
|
{"code": 200, "msg": "OK", "data": response_serializer.data},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@extend_schema_view(
|
@extend_schema_view(
|
||||||
@@ -53,5 +61,7 @@ class FarmDashboardCardsView(APIView):
|
|||||||
No database. Static mock data only.
|
No database. Static mock data only.
|
||||||
"""
|
"""
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
from external_api_adapter import request as external_api_request
|
||||||
|
|
||||||
adapter_response = external_api_request("ai", "/dashboard-data/status", method="GET")
|
adapter_response = external_api_request("ai", "/dashboard-data/status", method="GET")
|
||||||
return Response(adapter_response.data, status=adapter_response.status_code)
|
return Response(adapter_response.data, status=adapter_response.status_code)
|
||||||
|
|||||||
@@ -8,32 +8,602 @@
|
|||||||
"sensor_id": "550e8400-e29b-41d4-a716-446655440000",
|
"sensor_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
"all_cards": {
|
"all_cards": {
|
||||||
"farmOverviewKpis": {
|
"farmOverviewKpis": {
|
||||||
"healthScore": 82,
|
"kpis": [
|
||||||
"activeAlerts": 2,
|
{
|
||||||
"waterNeedMm": 18.4
|
"id": "farm_health_score",
|
||||||
|
"title": "امتیاز سلامت مزرعه",
|
||||||
|
"subtitle": "تحلیل هوشمند",
|
||||||
|
"stats": "87%",
|
||||||
|
"avatarColor": "success",
|
||||||
|
"avatarIcon": "tabler-heartbeat",
|
||||||
|
"chipText": "خوب",
|
||||||
|
"chipColor": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "water_stress_index",
|
||||||
|
"title": "شاخص تنش آبی",
|
||||||
|
"subtitle": "فعلی",
|
||||||
|
"stats": "12%",
|
||||||
|
"avatarColor": "info",
|
||||||
|
"avatarIcon": "tabler-droplet",
|
||||||
|
"chipText": "پایین",
|
||||||
|
"chipColor": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "disease_risk",
|
||||||
|
"title": "ریسک بیماری",
|
||||||
|
"subtitle": "۷ روز اخیر",
|
||||||
|
"stats": "پایین",
|
||||||
|
"avatarColor": "success",
|
||||||
|
"avatarIcon": "tabler-bug",
|
||||||
|
"chipText": "5%",
|
||||||
|
"chipColor": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "avg_soil_moisture",
|
||||||
|
"title": "میانگین رطوبت خاک",
|
||||||
|
"subtitle": "کل مزرعه",
|
||||||
|
"stats": "65%",
|
||||||
|
"avatarColor": "primary",
|
||||||
|
"avatarIcon": "tabler-plant-2",
|
||||||
|
"chipText": "بهینه",
|
||||||
|
"chipColor": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "yield_prediction",
|
||||||
|
"title": "پیشبینی عملکرد",
|
||||||
|
"subtitle": "این فصل",
|
||||||
|
"stats": "42 تن",
|
||||||
|
"avatarColor": "secondary",
|
||||||
|
"avatarIcon": "tabler-chart-bar",
|
||||||
|
"chipText": "+8%",
|
||||||
|
"chipColor": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pest_risk",
|
||||||
|
"title": "ریسک آفات",
|
||||||
|
"subtitle": "پیشبینی هوشمند",
|
||||||
|
"stats": "15%",
|
||||||
|
"avatarColor": "warning",
|
||||||
|
"avatarIcon": "tabler-bug-off",
|
||||||
|
"chipText": "تحت نظر",
|
||||||
|
"chipColor": "warning"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"farmWeatherCard": {
|
||||||
|
"condition": "صاف",
|
||||||
|
"temperature": 24,
|
||||||
|
"unit": "°C",
|
||||||
|
"humidity": 45,
|
||||||
|
"windSpeed": 12,
|
||||||
|
"windUnit": "km/h",
|
||||||
|
"chartData": {
|
||||||
|
"labels": [
|
||||||
|
"۶ صبح",
|
||||||
|
"۹ صبح",
|
||||||
|
"۱۲ ظهر",
|
||||||
|
"۳ بعدازظهر",
|
||||||
|
"۶ عصر",
|
||||||
|
"۹ شب",
|
||||||
|
"۱۲ شب"
|
||||||
|
],
|
||||||
|
"series": [
|
||||||
|
[
|
||||||
|
18,
|
||||||
|
22,
|
||||||
|
26,
|
||||||
|
28,
|
||||||
|
25,
|
||||||
|
20,
|
||||||
|
18
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"farmAlertsTracker": {
|
||||||
|
"totalAlerts": 3,
|
||||||
|
"radialBarValue": 30,
|
||||||
|
"alertStats": [
|
||||||
|
{
|
||||||
|
"title": "کمبود آب",
|
||||||
|
"count": "2",
|
||||||
|
"avatarColor": "error",
|
||||||
|
"avatarIcon": "tabler-droplet-half-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "ریسک قارچی",
|
||||||
|
"count": "1",
|
||||||
|
"avatarColor": "warning",
|
||||||
|
"avatarIcon": "tabler-mushroom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "هشدار یخبندان",
|
||||||
|
"count": "0",
|
||||||
|
"avatarColor": "info",
|
||||||
|
"avatarIcon": "tabler-snowflake"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"sensorValuesList": {
|
"sensorValuesList": {
|
||||||
"items": [
|
"sensors": [
|
||||||
{
|
{
|
||||||
"label": "رطوبت خاک",
|
"title": "28°C",
|
||||||
"value": 45.2,
|
"subtitle": "دمای هوا",
|
||||||
|
"trendNumber": 2.1,
|
||||||
|
"trend": "positive",
|
||||||
|
"unit": "°C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "24°C",
|
||||||
|
"subtitle": "دمای خاک",
|
||||||
|
"trendNumber": -0.5,
|
||||||
|
"trend": "negative",
|
||||||
|
"unit": "°C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "65%",
|
||||||
|
"subtitle": "رطوبت هوا",
|
||||||
|
"trendNumber": 3.2,
|
||||||
|
"trend": "positive",
|
||||||
"unit": "%"
|
"unit": "%"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "دما خاک",
|
"title": "42%",
|
||||||
"value": 22.5,
|
"subtitle": "رطوبت خاک (۱۰ سانتیمتر)",
|
||||||
"unit": "°C"
|
"trendNumber": -1.8,
|
||||||
|
"trend": "negative",
|
||||||
|
"unit": "%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "6.8",
|
||||||
|
"subtitle": "pH خاک",
|
||||||
|
"trendNumber": 0.2,
|
||||||
|
"trend": "positive",
|
||||||
|
"unit": "pH"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "1.2",
|
||||||
|
"subtitle": "هدایت الکتریکی (dS/m)",
|
||||||
|
"trendNumber": 0.1,
|
||||||
|
"trend": "positive",
|
||||||
|
"unit": "dS/m"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "850",
|
||||||
|
"subtitle": "شدت نور (لوکس)",
|
||||||
|
"trendNumber": 15.3,
|
||||||
|
"trend": "positive",
|
||||||
|
"unit": "lux"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "12",
|
||||||
|
"subtitle": "سرعت باد (کیلومتر/ساعت)",
|
||||||
|
"trendNumber": -2.4,
|
||||||
|
"trend": "negative",
|
||||||
|
"unit": "km/h"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sensorRadarChart": {
|
||||||
|
"labels": [
|
||||||
|
"دما",
|
||||||
|
"رطوبت",
|
||||||
|
"pH",
|
||||||
|
"هدایت الکتریکی",
|
||||||
|
"نور",
|
||||||
|
"باد"
|
||||||
|
],
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"name": "امروز",
|
||||||
|
"data": [
|
||||||
|
75,
|
||||||
|
65,
|
||||||
|
80,
|
||||||
|
70,
|
||||||
|
85,
|
||||||
|
60
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ایدهآل",
|
||||||
|
"data": [
|
||||||
|
80,
|
||||||
|
70,
|
||||||
|
75,
|
||||||
|
75,
|
||||||
|
90,
|
||||||
|
50
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sensorComparisonChart": {
|
||||||
|
"currentValue": 48,
|
||||||
|
"vsLastWeek": "+5%",
|
||||||
|
"vsLastWeekValue": 5,
|
||||||
|
"categories": [
|
||||||
|
"دوشنبه",
|
||||||
|
"سهشنبه",
|
||||||
|
"چهارشنبه",
|
||||||
|
"پنجشنبه",
|
||||||
|
"جمعه",
|
||||||
|
"شنبه",
|
||||||
|
"یکشنبه"
|
||||||
|
],
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"name": "امروز",
|
||||||
|
"data": [
|
||||||
|
42,
|
||||||
|
45,
|
||||||
|
48,
|
||||||
|
52,
|
||||||
|
50,
|
||||||
|
48,
|
||||||
|
46
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "هفته قبل",
|
||||||
|
"data": [
|
||||||
|
38,
|
||||||
|
40,
|
||||||
|
42,
|
||||||
|
45,
|
||||||
|
43,
|
||||||
|
40,
|
||||||
|
38
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"anomalyDetectionCard": {
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"sensor": "رطوبت خاک زون ۳",
|
||||||
|
"value": "38%",
|
||||||
|
"expected": "45-65%",
|
||||||
|
"deviation": "-12%",
|
||||||
|
"severity": "warning"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sensor": "pH بخش ۲",
|
||||||
|
"value": "5.2",
|
||||||
|
"expected": "6.0-7.0",
|
||||||
|
"deviation": "-0.8",
|
||||||
|
"severity": "error"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"farmAlertsTimeline": {
|
||||||
|
"alerts": [
|
||||||
|
{
|
||||||
|
"title": "ریسک کمبود آب",
|
||||||
|
"description": "رطوبت خاک در عمق ۱۰ سانتیمتر (۴۲٪) کمتر از حد بهینه است. پیشبینی: در صورت عدم آبیاری، تنش طی ۲ تا ۳ روز. توصیه: آبیاری ظرف ۲۴ ساعت.",
|
||||||
|
"time": "۱۵ دقیقه پیش",
|
||||||
|
"color": "warning"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "ریسک بیماری قارچی",
|
||||||
|
"description": "رطوبت بالا (۶۵٪) و دمای ۲۴ درجه شرایط مساعد برای رشد قارچ. استفاده از قارچکش پیشگیرانه یا کاهش آبیاری را در نظر بگیرید.",
|
||||||
|
"time": "۱ ساعت پیش",
|
||||||
|
"color": "error"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "پیشنهاد آبیاری",
|
||||||
|
"description": "بازه بهینه آبیاری: ۶:۰۰ تا ۸:۰۰ صبح. حجم پیشنهادی: ۴۵۰ مترمکعب برای زون آ. بهبود راندمان مورد انتظار: ۱۲٪.",
|
||||||
|
"time": "۲ ساعت پیش",
|
||||||
|
"color": "info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "بررسی شوری خاک",
|
||||||
|
"description": "مقدار هدایت الکتریکی ۱/۲ dS/m در محدوده مجاز است. نیازی به اقدام نیست. بررسی بعدی توصیه میشود ظرف ۵ روز.",
|
||||||
|
"time": "۴ ساعت پیش",
|
||||||
|
"color": "success"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"waterNeedPrediction": {
|
||||||
|
"totalNext7Days": 3290,
|
||||||
|
"unit": "m³",
|
||||||
|
"categories": [
|
||||||
|
"روز ۱",
|
||||||
|
"روز ۲",
|
||||||
|
"روز ۳",
|
||||||
|
"روز ۴",
|
||||||
|
"روز ۵",
|
||||||
|
"روز ۶",
|
||||||
|
"روز ۷"
|
||||||
|
],
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"name": "نیاز آبی",
|
||||||
|
"data": [
|
||||||
|
420,
|
||||||
|
450,
|
||||||
|
480,
|
||||||
|
460,
|
||||||
|
490,
|
||||||
|
510,
|
||||||
|
480
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"harvestPredictionCard": {
|
||||||
|
"date": "2025-10-15",
|
||||||
|
"dateFormatted": "۱۵ اکتبر ۲۰۲۵",
|
||||||
|
"daysUntil": 58,
|
||||||
|
"description": "بر اساس تجمع GDD فعلی و پیشبینی آب و هوا. بازه بهینه برداشت: ۱۲ تا ۱۸ اکتبر.",
|
||||||
|
"optimalWindowStart": "2025-10-12",
|
||||||
|
"optimalWindowEnd": "2025-10-18"
|
||||||
|
},
|
||||||
|
"yieldPredictionChart": {
|
||||||
|
"categories": [
|
||||||
|
"ژانویه",
|
||||||
|
"فوریه",
|
||||||
|
"مارس",
|
||||||
|
"آوریل",
|
||||||
|
"می",
|
||||||
|
"ژوئن",
|
||||||
|
"ژوئیه",
|
||||||
|
"آگوست",
|
||||||
|
"سپتامبر",
|
||||||
|
"اکتبر",
|
||||||
|
"نوامبر",
|
||||||
|
"دسامبر"
|
||||||
|
],
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"name": "امسال",
|
||||||
|
"data": [
|
||||||
|
35,
|
||||||
|
38,
|
||||||
|
40,
|
||||||
|
42,
|
||||||
|
45,
|
||||||
|
48,
|
||||||
|
50,
|
||||||
|
48,
|
||||||
|
46,
|
||||||
|
44,
|
||||||
|
42,
|
||||||
|
42
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "سال گذشته",
|
||||||
|
"data": [
|
||||||
|
32,
|
||||||
|
34,
|
||||||
|
36,
|
||||||
|
38,
|
||||||
|
40,
|
||||||
|
42,
|
||||||
|
44,
|
||||||
|
42,
|
||||||
|
40,
|
||||||
|
38,
|
||||||
|
36,
|
||||||
|
38
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"title": "عملکرد پیشبینیشده",
|
||||||
|
"subtitle": "این فصل",
|
||||||
|
"amount": "42 تن",
|
||||||
|
"avatarColor": "primary",
|
||||||
|
"avatarIcon": "tabler-chart-bar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "تاریخ برداشت",
|
||||||
|
"subtitle": "حدود ۱۵ اکتبر",
|
||||||
|
"amount": "+8%",
|
||||||
|
"avatarColor": "success",
|
||||||
|
"avatarIcon": "tabler-calendar"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"soilMoistureHeatmap": {
|
||||||
|
"zones": [
|
||||||
|
"زون ۱",
|
||||||
|
"زون ۲",
|
||||||
|
"زون ۳",
|
||||||
|
"زون ۴",
|
||||||
|
"زون ۵",
|
||||||
|
"زون ۶",
|
||||||
|
"زون ۷"
|
||||||
|
],
|
||||||
|
"hours": [
|
||||||
|
"۶ ص",
|
||||||
|
"۸ ص",
|
||||||
|
"۱۰ ص",
|
||||||
|
"۱۲ ظ",
|
||||||
|
"۱۴ ع",
|
||||||
|
"۱۶ ع",
|
||||||
|
"۱۸ ع"
|
||||||
|
],
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"name": "زون ۱",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"x": "۶ ص",
|
||||||
|
"y": 52
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "۸ ص",
|
||||||
|
"y": 48
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "۱۰ ص",
|
||||||
|
"y": 55
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "۱۲ ظ",
|
||||||
|
"y": 60
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "۱۴ ع",
|
||||||
|
"y": 58
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "۱۶ ع",
|
||||||
|
"y": 54
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "۱۸ ع",
|
||||||
|
"y": 50
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "زون ۲",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"x": "۶ ص",
|
||||||
|
"y": 45
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "۸ ص",
|
||||||
|
"y": 42
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "۱۰ ص",
|
||||||
|
"y": 48
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "۱۲ ظ",
|
||||||
|
"y": 52
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "۱۴ ع",
|
||||||
|
"y": 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "۱۶ ع",
|
||||||
|
"y": 47
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "۱۸ ع",
|
||||||
|
"y": 44
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ndviHealthCard": {
|
||||||
|
"ndviIndex": 0.78,
|
||||||
|
"healthData": [
|
||||||
|
{
|
||||||
|
"title": "تنش نیتروژن",
|
||||||
|
"value": "پایین",
|
||||||
|
"color": "success",
|
||||||
|
"icon": "tabler-leaf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "سلامت محصول",
|
||||||
|
"value": "خوب",
|
||||||
|
"color": "success",
|
||||||
|
"icon": "tabler-plant"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"recommendationsList": {
|
"recommendationsList": {
|
||||||
"items": [
|
"recommendations": [
|
||||||
{
|
{
|
||||||
"recommendation_title": "تنظیم نوبت آبیاری",
|
"title": "آبیاری: ۶:۰۰ تا ۸:۰۰ صبح",
|
||||||
"suggested_action": "آبیاری بعدی را صبح فردا انجام دهید.",
|
"subtitle": "۴۵۰ مترمکعب برای زون آ. بدون آبیاری، عملکرد ممکن است حدود ۸٪ کاهش یابد.",
|
||||||
"urgency_level": "high"
|
"avatarIcon": "tabler-droplet",
|
||||||
|
"avatarColor": "primary"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "کود: NPK 20-20-20",
|
||||||
|
"subtitle": "اعمال ۲۵ کیلوگرم در هکتار ظرف ۷ روز. کمبود نیتروژن فعلی در بخش ۲.",
|
||||||
|
"avatarIcon": "tabler-leaf",
|
||||||
|
"avatarColor": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "قارچکش: پیشگیرانه",
|
||||||
|
"subtitle": "رطوبت و دما مساعد قارچ. سمپاشی بر پایه مس را در نظر بگیرید.",
|
||||||
|
"avatarIcon": "tabler-mushroom",
|
||||||
|
"avatarColor": "warning"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "بازه برداشت: ۱۲ تا ۱۸ اکتبر",
|
||||||
|
"subtitle": "اوج رسیدگی حدود ۱۵ اکتبر. نیروی کار را متناسب برنامهریزی کنید.",
|
||||||
|
"avatarIcon": "tabler-calendar-event",
|
||||||
|
"avatarColor": "info"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"economicOverview": {
|
||||||
|
"economicData": [
|
||||||
|
{
|
||||||
|
"title": "هزینه آب",
|
||||||
|
"value": "€720",
|
||||||
|
"subtitle": "این ماه",
|
||||||
|
"avatarIcon": "tabler-droplet",
|
||||||
|
"avatarColor": "primary"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "صرفهجویی آب هوشمند",
|
||||||
|
"value": "€156",
|
||||||
|
"subtitle": "۱۸٪ صرفهجویی شده",
|
||||||
|
"avatarIcon": "tabler-bulb",
|
||||||
|
"avatarColor": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "بازده سرمایه پلتفرم",
|
||||||
|
"value": "127%",
|
||||||
|
"subtitle": "نسبت به سال گذشته",
|
||||||
|
"avatarIcon": "tabler-chart-line",
|
||||||
|
"avatarColor": "info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "پیشبینی درآمد",
|
||||||
|
"value": "€42k",
|
||||||
|
"subtitle": "این فصل",
|
||||||
|
"avatarIcon": "tabler-currency-euro",
|
||||||
|
"avatarColor": "success"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"chartSeries": [
|
||||||
|
{
|
||||||
|
"name": "هزینه آب",
|
||||||
|
"data": [
|
||||||
|
120,
|
||||||
|
115,
|
||||||
|
110,
|
||||||
|
125,
|
||||||
|
118,
|
||||||
|
122
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "کود",
|
||||||
|
"data": [
|
||||||
|
80,
|
||||||
|
85,
|
||||||
|
90,
|
||||||
|
75,
|
||||||
|
82,
|
||||||
|
78
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"chartCategories": [
|
||||||
|
"ژانویه",
|
||||||
|
"فوریه",
|
||||||
|
"مارس",
|
||||||
|
"آوریل",
|
||||||
|
"می",
|
||||||
|
"ژوئن"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class FertilizationFarmDataSerializer(serializers.Serializer):
|
||||||
|
soilType = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
organicMatter = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
waterEC = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class FertilizationRecommendRequestSerializer(serializers.Serializer):
|
||||||
|
crop_id = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
growth_stage = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
farm_data = FertilizationFarmDataSerializer(required=False)
|
||||||
|
soilType = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
organicMatter = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
waterEC = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class FertilizationPlanSerializer(serializers.Serializer):
|
||||||
|
npkRatio = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
amountPerHectare = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
applicationMethod = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
applicationInterval = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
reasoning = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class FertilizationRecommendResponseDataSerializer(serializers.Serializer):
|
||||||
|
plan = FertilizationPlanSerializer(required=False)
|
||||||
|
|
||||||
|
|
||||||
|
class FertilizationTaskSubmitDataSerializer(serializers.Serializer):
|
||||||
|
task_id = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
status = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class FertilizationTaskProgressSerializer(serializers.Serializer):
|
||||||
|
message = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class FertilizationTaskStatusDataSerializer(serializers.Serializer):
|
||||||
|
task_id = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
status = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
progress = FertilizationTaskProgressSerializer(required=False)
|
||||||
|
result = FertilizationRecommendResponseDataSerializer(required=False)
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import ConfigView, RecommendView
|
from .views import ConfigView, RecommendTaskStatusView, RecommendView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("config/", ConfigView.as_view(), name="fertilization-recommendation-config"),
|
path("config/", ConfigView.as_view(), name="fertilization-recommendation-config"),
|
||||||
path("recommend/", RecommendView.as_view(), name="fertilization-recommendation-recommend"),
|
path("recommend/", RecommendView.as_view(), name="fertilization-recommendation-recommend"),
|
||||||
|
# path("recommend/task/", RecommendTaskCreateView.as_view(), name="fertilization-recommendation-task-create"),
|
||||||
|
path("recommend/<str:task_id>/status/", RecommendTaskStatusView.as_view(), name="fertilization-recommendation-task-status"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,78 +1,38 @@
|
|||||||
"""
|
"""
|
||||||
Fertilization Recommendation API views.
|
Fertilization Recommendation API views.
|
||||||
No database. All responses are static mock data.
|
|
||||||
Response format: {"status": "success", "data": <payload>}. HTTP 200 only.
|
|
||||||
No processing, validation, or use of input parameters in responses.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from rest_framework import serializers, status
|
from rest_framework import serializers, status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import extend_schema
|
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
||||||
|
|
||||||
from config.swagger import status_response
|
from config.swagger import status_response
|
||||||
from external_api_adapter import request as external_api_request
|
from external_api_adapter import request as external_api_request
|
||||||
from .mock_data import CONFIG_RESPONSE_DATA
|
from .mock_data import CONFIG_RESPONSE_DATA
|
||||||
|
from .serializers import (
|
||||||
|
FertilizationRecommendRequestSerializer,
|
||||||
|
FertilizationRecommendResponseDataSerializer,
|
||||||
|
FertilizationTaskStatusDataSerializer,
|
||||||
|
FertilizationTaskSubmitDataSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConfigView(APIView):
|
class ConfigView(APIView):
|
||||||
"""
|
|
||||||
GET endpoint for fertilization config (farm data, growth stages, crop options).
|
|
||||||
|
|
||||||
Purpose:
|
|
||||||
Returns static farm data (soilType, organicMatter, waterEC), growth
|
|
||||||
stages list, and crop options for the fertilization recommendation form.
|
|
||||||
Used when loading the fertilization recommendation page.
|
|
||||||
|
|
||||||
Input parameters:
|
|
||||||
None. Query parameters, if sent, are not read or used.
|
|
||||||
|
|
||||||
Response structure:
|
|
||||||
- status: string, always "success".
|
|
||||||
- data: object with keys farmData (object), growthStages (array of
|
|
||||||
{ id, icon }), cropOptions (array of { id, labelKey, icon }).
|
|
||||||
|
|
||||||
No processing or validation is performed on inputs.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
tags=["Fertilization Recommendation"],
|
tags=["Fertilization Recommendation"],
|
||||||
responses={200: status_response("FertilizationConfigResponse", data=serializers.JSONField())},
|
responses={200: status_response("FertilizationConfigResponse", data=serializers.JSONField())},
|
||||||
)
|
)
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return Response(
|
return Response({"status": "success", "data": CONFIG_RESPONSE_DATA}, status=status.HTTP_200_OK)
|
||||||
{"status": "success", "data": CONFIG_RESPONSE_DATA},
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RecommendView(APIView):
|
class RecommendView(APIView):
|
||||||
"""
|
|
||||||
POST endpoint for fertilization recommendation.
|
|
||||||
|
|
||||||
Purpose:
|
|
||||||
Returns a static fertilization plan (npkRatio, amountPerHectare,
|
|
||||||
applicationMethod, applicationInterval, reasoning). Body may contain
|
|
||||||
crop_id, growth_stage, farm_data; not read or used in response.
|
|
||||||
|
|
||||||
Input parameters:
|
|
||||||
- body (optional): JSON. May contain "crop_id", "growth_stage",
|
|
||||||
"soilType", "organicMatter", "waterEC". Data type: object.
|
|
||||||
Location: body. Not read or validated; not used in response.
|
|
||||||
|
|
||||||
Response structure:
|
|
||||||
- status: string, always "success".
|
|
||||||
- data: object with key "plan" (object with npkRatio, amountPerHectare,
|
|
||||||
applicationMethod, applicationInterval, reasoning).
|
|
||||||
|
|
||||||
No processing or validation is performed on inputs.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
tags=["Fertilization Recommendation"],
|
tags=["Fertilization Recommendation"],
|
||||||
request=OpenApiTypes.OBJECT,
|
request=FertilizationRecommendRequestSerializer,
|
||||||
responses={200: status_response("FertilizationRecommendResponse", data=serializers.JSONField())},
|
responses={200: status_response("FertilizationRecommendResponse", data=FertilizationRecommendResponseDataSerializer())},
|
||||||
)
|
)
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
adapter_response = external_api_request(
|
adapter_response = external_api_request(
|
||||||
@@ -82,3 +42,22 @@ class RecommendView(APIView):
|
|||||||
payload=request.data,
|
payload=request.data,
|
||||||
)
|
)
|
||||||
return Response(adapter_response.data, status=adapter_response.status_code)
|
return Response(adapter_response.data, status=adapter_response.status_code)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RecommendTaskStatusView(APIView):
|
||||||
|
@extend_schema(
|
||||||
|
tags=["Fertilization Recommendation"],
|
||||||
|
parameters=[
|
||||||
|
OpenApiParameter(name="task_id", type=OpenApiTypes.STR, location=OpenApiParameter.PATH),
|
||||||
|
],
|
||||||
|
responses={200: status_response("FertilizationRecommendTaskStatusResponse", data=FertilizationTaskStatusDataSerializer())},
|
||||||
|
)
|
||||||
|
def get(self, request, task_id):
|
||||||
|
adapter_response = external_api_request(
|
||||||
|
"ai",
|
||||||
|
f"/fertilization/status/{task_id}",
|
||||||
|
method="GET",
|
||||||
|
)
|
||||||
|
return Response(adapter_response.data, status=adapter_response.status_code)
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class IrrigationFarmDataSerializer(serializers.Serializer):
|
||||||
|
soilType = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
waterQuality = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
climateZone = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class IrrigationRecommendRequestSerializer(serializers.Serializer):
|
||||||
|
crop_id = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
farm_data = IrrigationFarmDataSerializer(required=False)
|
||||||
|
soilType = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
waterQuality = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
climateZone = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class IrrigationPlanSerializer(serializers.Serializer):
|
||||||
|
frequencyPerWeek = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
durationMinutes = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
bestTimeOfDay = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
moistureLevel = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
warning = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class IrrigationWaterBalanceDaySerializer(serializers.Serializer):
|
||||||
|
forecast_date = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
et0_mm = serializers.FloatField(required=False)
|
||||||
|
etc_mm = serializers.FloatField(required=False)
|
||||||
|
effective_rainfall_mm = serializers.FloatField(required=False)
|
||||||
|
gross_irrigation_mm = serializers.FloatField(required=False)
|
||||||
|
irrigation_timing = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class IrrigationCropProfileSerializer(serializers.Serializer):
|
||||||
|
kc_initial = serializers.FloatField(required=False)
|
||||||
|
kc_mid = serializers.FloatField(required=False)
|
||||||
|
kc_end = serializers.FloatField(required=False)
|
||||||
|
|
||||||
|
|
||||||
|
class IrrigationWaterBalanceSerializer(serializers.Serializer):
|
||||||
|
daily = IrrigationWaterBalanceDaySerializer(many=True, required=False)
|
||||||
|
crop_profile = IrrigationCropProfileSerializer(required=False)
|
||||||
|
active_kc = serializers.FloatField(required=False)
|
||||||
|
|
||||||
|
|
||||||
|
class IrrigationRecommendResponseDataSerializer(serializers.Serializer):
|
||||||
|
plan = IrrigationPlanSerializer(required=False)
|
||||||
|
raw_response = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
water_balance = IrrigationWaterBalanceSerializer(required=False)
|
||||||
|
status = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class IrrigationTaskSubmitDataSerializer(serializers.Serializer):
|
||||||
|
task_id = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
status = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class IrrigationTaskStatusDataSerializer(serializers.Serializer):
|
||||||
|
task_id = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
status = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
result = IrrigationRecommendResponseDataSerializer(required=False)
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import ConfigView, RecommendView
|
from .views import ConfigView, RecommendTaskCreateView, RecommendTaskStatusView, RecommendView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("config/", ConfigView.as_view(), name="irrigation-recommendation-config"),
|
path("config/", ConfigView.as_view(), name="irrigation-recommendation-config"),
|
||||||
path("recommend/", RecommendView.as_view(), name="irrigation-recommendation-recommend"),
|
path("recommend/", RecommendView.as_view(), name="irrigation-recommendation-recommend"),
|
||||||
|
path("recommend/task/", RecommendTaskCreateView.as_view(), name="irrigation-recommendation-task-create"),
|
||||||
|
path("recommend/<str:task_id>/status/", RecommendTaskStatusView.as_view(), name="irrigation-recommendation-task-status"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,78 +1,38 @@
|
|||||||
"""
|
"""
|
||||||
Irrigation Recommendation API views.
|
Irrigation Recommendation API views.
|
||||||
No database. All responses are static mock data.
|
|
||||||
Response format: {"status": "success", "data": <payload>}. HTTP 200 only.
|
|
||||||
No processing, validation, or use of input parameters in responses.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from rest_framework import serializers, status
|
from rest_framework import serializers, status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import extend_schema
|
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
||||||
|
|
||||||
from config.swagger import status_response
|
from config.swagger import status_response
|
||||||
from external_api_adapter import request as external_api_request
|
from external_api_adapter import request as external_api_request
|
||||||
from .mock_data import CONFIG_RESPONSE_DATA
|
from .mock_data import CONFIG_RESPONSE_DATA
|
||||||
|
from .serializers import (
|
||||||
|
IrrigationRecommendRequestSerializer,
|
||||||
|
IrrigationRecommendResponseDataSerializer,
|
||||||
|
IrrigationTaskStatusDataSerializer,
|
||||||
|
IrrigationTaskSubmitDataSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConfigView(APIView):
|
class ConfigView(APIView):
|
||||||
"""
|
|
||||||
GET endpoint for irrigation config (farm info and crop options).
|
|
||||||
|
|
||||||
Purpose:
|
|
||||||
Returns static farm info (soilType, waterQuality, climateZone) and
|
|
||||||
crop options list for the irrigation recommendation form. Used when
|
|
||||||
loading the irrigation recommendation page.
|
|
||||||
|
|
||||||
Input parameters:
|
|
||||||
None. Query parameters, if sent, are not read or used.
|
|
||||||
|
|
||||||
Response structure:
|
|
||||||
- status: string, always "success".
|
|
||||||
- data: object with keys farmInfo (object), cropOptions (array of
|
|
||||||
{ id, labelKey, icon }).
|
|
||||||
|
|
||||||
No processing or validation is performed on inputs.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
tags=["Irrigation Recommendation"],
|
tags=["Irrigation Recommendation"],
|
||||||
responses={200: status_response("IrrigationConfigResponse", data=serializers.JSONField())},
|
responses={200: status_response("IrrigationConfigResponse", data=serializers.JSONField())},
|
||||||
)
|
)
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return Response(
|
return Response({"status": "success", "data": CONFIG_RESPONSE_DATA}, status=status.HTTP_200_OK)
|
||||||
{"status": "success", "data": CONFIG_RESPONSE_DATA},
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RecommendView(APIView):
|
class RecommendView(APIView):
|
||||||
"""
|
|
||||||
POST endpoint for irrigation recommendation.
|
|
||||||
|
|
||||||
Purpose:
|
|
||||||
Returns a static irrigation plan (frequencyPerWeek, durationMinutes,
|
|
||||||
bestTimeOfDay, moistureLevel, warning). Body may contain crop_id
|
|
||||||
and farm info; not read or used in response.
|
|
||||||
|
|
||||||
Input parameters:
|
|
||||||
- body (optional): JSON. May contain "crop_id", "soilType", "waterQuality",
|
|
||||||
"climateZone". Data type: object. Location: body. Not read or validated;
|
|
||||||
not used in response.
|
|
||||||
|
|
||||||
Response structure:
|
|
||||||
- status: string, always "success".
|
|
||||||
- data: object with key "plan" (object with frequencyPerWeek,
|
|
||||||
durationMinutes, bestTimeOfDay, moistureLevel, warning).
|
|
||||||
|
|
||||||
No processing or validation is performed on inputs.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
tags=["Irrigation Recommendation"],
|
tags=["Irrigation Recommendation"],
|
||||||
request=OpenApiTypes.OBJECT,
|
request=IrrigationRecommendRequestSerializer,
|
||||||
responses={200: status_response("IrrigationRecommendResponse", data=serializers.JSONField())},
|
responses={200: status_response("IrrigationRecommendResponse", data=IrrigationRecommendResponseDataSerializer())},
|
||||||
)
|
)
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
adapter_response = external_api_request(
|
adapter_response = external_api_request(
|
||||||
@@ -82,3 +42,36 @@ class RecommendView(APIView):
|
|||||||
payload=request.data,
|
payload=request.data,
|
||||||
)
|
)
|
||||||
return Response(adapter_response.data, status=adapter_response.status_code)
|
return Response(adapter_response.data, status=adapter_response.status_code)
|
||||||
|
|
||||||
|
|
||||||
|
class RecommendTaskCreateView(APIView):
|
||||||
|
@extend_schema(
|
||||||
|
tags=["Irrigation Recommendation"],
|
||||||
|
request=IrrigationRecommendRequestSerializer,
|
||||||
|
responses={200: status_response("IrrigationRecommendTaskCreateResponse", data=IrrigationTaskSubmitDataSerializer())},
|
||||||
|
)
|
||||||
|
def post(self, request):
|
||||||
|
adapter_response = external_api_request(
|
||||||
|
"ai",
|
||||||
|
"/irrigation/recommend",
|
||||||
|
method="POST",
|
||||||
|
payload=request.data,
|
||||||
|
)
|
||||||
|
return Response(adapter_response.data, status=adapter_response.status_code)
|
||||||
|
|
||||||
|
|
||||||
|
class RecommendTaskStatusView(APIView):
|
||||||
|
@extend_schema(
|
||||||
|
tags=["Irrigation Recommendation"],
|
||||||
|
parameters=[
|
||||||
|
OpenApiParameter(name="task_id", type=OpenApiTypes.STR, location=OpenApiParameter.PATH),
|
||||||
|
],
|
||||||
|
responses={200: status_response("IrrigationRecommendTaskStatusResponse", data=IrrigationTaskStatusDataSerializer())},
|
||||||
|
)
|
||||||
|
def get(self, request, task_id):
|
||||||
|
adapter_response = external_api_request(
|
||||||
|
"ai",
|
||||||
|
f"/irrigation/recommend/status/{task_id}",
|
||||||
|
method="GET",
|
||||||
|
)
|
||||||
|
return Response(adapter_response.data, status=adapter_response.status_code)
|
||||||
|
|||||||
@@ -8,32 +8,602 @@
|
|||||||
"sensor_id": "550e8400-e29b-41d4-a716-446655440000",
|
"sensor_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
"all_cards": {
|
"all_cards": {
|
||||||
"farmOverviewKpis": {
|
"farmOverviewKpis": {
|
||||||
"healthScore": 82,
|
"kpis": [
|
||||||
"activeAlerts": 2,
|
{
|
||||||
"waterNeedMm": 18.4
|
"id": "farm_health_score",
|
||||||
|
"title": "امتیاز سلامت مزرعه",
|
||||||
|
"subtitle": "تحلیل هوشمند",
|
||||||
|
"stats": "87%",
|
||||||
|
"avatarColor": "success",
|
||||||
|
"avatarIcon": "tabler-heartbeat",
|
||||||
|
"chipText": "خوب",
|
||||||
|
"chipColor": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "water_stress_index",
|
||||||
|
"title": "شاخص تنش آبی",
|
||||||
|
"subtitle": "فعلی",
|
||||||
|
"stats": "12%",
|
||||||
|
"avatarColor": "info",
|
||||||
|
"avatarIcon": "tabler-droplet",
|
||||||
|
"chipText": "پایین",
|
||||||
|
"chipColor": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "disease_risk",
|
||||||
|
"title": "ریسک بیماری",
|
||||||
|
"subtitle": "۷ روز اخیر",
|
||||||
|
"stats": "پایین",
|
||||||
|
"avatarColor": "success",
|
||||||
|
"avatarIcon": "tabler-bug",
|
||||||
|
"chipText": "5%",
|
||||||
|
"chipColor": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "avg_soil_moisture",
|
||||||
|
"title": "میانگین رطوبت خاک",
|
||||||
|
"subtitle": "کل مزرعه",
|
||||||
|
"stats": "65%",
|
||||||
|
"avatarColor": "primary",
|
||||||
|
"avatarIcon": "tabler-plant-2",
|
||||||
|
"chipText": "بهینه",
|
||||||
|
"chipColor": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "yield_prediction",
|
||||||
|
"title": "پیشبینی عملکرد",
|
||||||
|
"subtitle": "این فصل",
|
||||||
|
"stats": "42 تن",
|
||||||
|
"avatarColor": "secondary",
|
||||||
|
"avatarIcon": "tabler-chart-bar",
|
||||||
|
"chipText": "+8%",
|
||||||
|
"chipColor": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pest_risk",
|
||||||
|
"title": "ریسک آفات",
|
||||||
|
"subtitle": "پیشبینی هوشمند",
|
||||||
|
"stats": "15%",
|
||||||
|
"avatarColor": "warning",
|
||||||
|
"avatarIcon": "tabler-bug-off",
|
||||||
|
"chipText": "تحت نظر",
|
||||||
|
"chipColor": "warning"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"farmWeatherCard": {
|
||||||
|
"condition": "صاف",
|
||||||
|
"temperature": 24,
|
||||||
|
"unit": "°C",
|
||||||
|
"humidity": 45,
|
||||||
|
"windSpeed": 12,
|
||||||
|
"windUnit": "km/h",
|
||||||
|
"chartData": {
|
||||||
|
"labels": [
|
||||||
|
"۶ صبح",
|
||||||
|
"۹ صبح",
|
||||||
|
"۱۲ ظهر",
|
||||||
|
"۳ بعدازظهر",
|
||||||
|
"۶ عصر",
|
||||||
|
"۹ شب",
|
||||||
|
"۱۲ شب"
|
||||||
|
],
|
||||||
|
"series": [
|
||||||
|
[
|
||||||
|
18,
|
||||||
|
22,
|
||||||
|
26,
|
||||||
|
28,
|
||||||
|
25,
|
||||||
|
20,
|
||||||
|
18
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"farmAlertsTracker": {
|
||||||
|
"totalAlerts": 3,
|
||||||
|
"radialBarValue": 30,
|
||||||
|
"alertStats": [
|
||||||
|
{
|
||||||
|
"title": "کمبود آب",
|
||||||
|
"count": "2",
|
||||||
|
"avatarColor": "error",
|
||||||
|
"avatarIcon": "tabler-droplet-half-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "ریسک قارچی",
|
||||||
|
"count": "1",
|
||||||
|
"avatarColor": "warning",
|
||||||
|
"avatarIcon": "tabler-mushroom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "هشدار یخبندان",
|
||||||
|
"count": "0",
|
||||||
|
"avatarColor": "info",
|
||||||
|
"avatarIcon": "tabler-snowflake"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"sensorValuesList": {
|
"sensorValuesList": {
|
||||||
"items": [
|
"sensors": [
|
||||||
{
|
{
|
||||||
"label": "رطوبت خاک",
|
"title": "28°C",
|
||||||
"value": 45.2,
|
"subtitle": "دمای هوا",
|
||||||
|
"trendNumber": 2.1,
|
||||||
|
"trend": "positive",
|
||||||
|
"unit": "°C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "24°C",
|
||||||
|
"subtitle": "دمای خاک",
|
||||||
|
"trendNumber": -0.5,
|
||||||
|
"trend": "negative",
|
||||||
|
"unit": "°C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "65%",
|
||||||
|
"subtitle": "رطوبت هوا",
|
||||||
|
"trendNumber": 3.2,
|
||||||
|
"trend": "positive",
|
||||||
"unit": "%"
|
"unit": "%"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "دما خاک",
|
"title": "42%",
|
||||||
"value": 22.5,
|
"subtitle": "رطوبت خاک (۱۰ سانتیمتر)",
|
||||||
"unit": "°C"
|
"trendNumber": -1.8,
|
||||||
|
"trend": "negative",
|
||||||
|
"unit": "%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "6.8",
|
||||||
|
"subtitle": "pH خاک",
|
||||||
|
"trendNumber": 0.2,
|
||||||
|
"trend": "positive",
|
||||||
|
"unit": "pH"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "1.2",
|
||||||
|
"subtitle": "هدایت الکتریکی (dS/m)",
|
||||||
|
"trendNumber": 0.1,
|
||||||
|
"trend": "positive",
|
||||||
|
"unit": "dS/m"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "850",
|
||||||
|
"subtitle": "شدت نور (لوکس)",
|
||||||
|
"trendNumber": 15.3,
|
||||||
|
"trend": "positive",
|
||||||
|
"unit": "lux"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "12",
|
||||||
|
"subtitle": "سرعت باد (کیلومتر/ساعت)",
|
||||||
|
"trendNumber": -2.4,
|
||||||
|
"trend": "negative",
|
||||||
|
"unit": "km/h"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sensorRadarChart": {
|
||||||
|
"labels": [
|
||||||
|
"دما",
|
||||||
|
"رطوبت",
|
||||||
|
"pH",
|
||||||
|
"هدایت الکتریکی",
|
||||||
|
"نور",
|
||||||
|
"باد"
|
||||||
|
],
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"name": "امروز",
|
||||||
|
"data": [
|
||||||
|
75,
|
||||||
|
65,
|
||||||
|
80,
|
||||||
|
70,
|
||||||
|
85,
|
||||||
|
60
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ایدهآل",
|
||||||
|
"data": [
|
||||||
|
80,
|
||||||
|
70,
|
||||||
|
75,
|
||||||
|
75,
|
||||||
|
90,
|
||||||
|
50
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sensorComparisonChart": {
|
||||||
|
"currentValue": 48,
|
||||||
|
"vsLastWeek": "+5%",
|
||||||
|
"vsLastWeekValue": 5,
|
||||||
|
"categories": [
|
||||||
|
"دوشنبه",
|
||||||
|
"سهشنبه",
|
||||||
|
"چهارشنبه",
|
||||||
|
"پنجشنبه",
|
||||||
|
"جمعه",
|
||||||
|
"شنبه",
|
||||||
|
"یکشنبه"
|
||||||
|
],
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"name": "امروز",
|
||||||
|
"data": [
|
||||||
|
42,
|
||||||
|
45,
|
||||||
|
48,
|
||||||
|
52,
|
||||||
|
50,
|
||||||
|
48,
|
||||||
|
46
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "هفته قبل",
|
||||||
|
"data": [
|
||||||
|
38,
|
||||||
|
40,
|
||||||
|
42,
|
||||||
|
45,
|
||||||
|
43,
|
||||||
|
40,
|
||||||
|
38
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"anomalyDetectionCard": {
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"sensor": "رطوبت خاک زون ۳",
|
||||||
|
"value": "38%",
|
||||||
|
"expected": "45-65%",
|
||||||
|
"deviation": "-12%",
|
||||||
|
"severity": "warning"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sensor": "pH بخش ۲",
|
||||||
|
"value": "5.2",
|
||||||
|
"expected": "6.0-7.0",
|
||||||
|
"deviation": "-0.8",
|
||||||
|
"severity": "error"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"farmAlertsTimeline": {
|
||||||
|
"alerts": [
|
||||||
|
{
|
||||||
|
"title": "ریسک کمبود آب",
|
||||||
|
"description": "رطوبت خاک در عمق ۱۰ سانتیمتر (۴۲٪) کمتر از حد بهینه است. پیشبینی: در صورت عدم آبیاری، تنش طی ۲ تا ۳ روز. توصیه: آبیاری ظرف ۲۴ ساعت.",
|
||||||
|
"time": "۱۵ دقیقه پیش",
|
||||||
|
"color": "warning"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "ریسک بیماری قارچی",
|
||||||
|
"description": "رطوبت بالا (۶۵٪) و دمای ۲۴ درجه شرایط مساعد برای رشد قارچ. استفاده از قارچکش پیشگیرانه یا کاهش آبیاری را در نظر بگیرید.",
|
||||||
|
"time": "۱ ساعت پیش",
|
||||||
|
"color": "error"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "پیشنهاد آبیاری",
|
||||||
|
"description": "بازه بهینه آبیاری: ۶:۰۰ تا ۸:۰۰ صبح. حجم پیشنهادی: ۴۵۰ مترمکعب برای زون آ. بهبود راندمان مورد انتظار: ۱۲٪.",
|
||||||
|
"time": "۲ ساعت پیش",
|
||||||
|
"color": "info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "بررسی شوری خاک",
|
||||||
|
"description": "مقدار هدایت الکتریکی ۱/۲ dS/m در محدوده مجاز است. نیازی به اقدام نیست. بررسی بعدی توصیه میشود ظرف ۵ روز.",
|
||||||
|
"time": "۴ ساعت پیش",
|
||||||
|
"color": "success"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"waterNeedPrediction": {
|
||||||
|
"totalNext7Days": 3290,
|
||||||
|
"unit": "m³",
|
||||||
|
"categories": [
|
||||||
|
"روز ۱",
|
||||||
|
"روز ۲",
|
||||||
|
"روز ۳",
|
||||||
|
"روز ۴",
|
||||||
|
"روز ۵",
|
||||||
|
"روز ۶",
|
||||||
|
"روز ۷"
|
||||||
|
],
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"name": "نیاز آبی",
|
||||||
|
"data": [
|
||||||
|
420,
|
||||||
|
450,
|
||||||
|
480,
|
||||||
|
460,
|
||||||
|
490,
|
||||||
|
510,
|
||||||
|
480
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"harvestPredictionCard": {
|
||||||
|
"date": "2025-10-15",
|
||||||
|
"dateFormatted": "۱۵ اکتبر ۲۰۲۵",
|
||||||
|
"daysUntil": 58,
|
||||||
|
"description": "بر اساس تجمع GDD فعلی و پیشبینی آب و هوا. بازه بهینه برداشت: ۱۲ تا ۱۸ اکتبر.",
|
||||||
|
"optimalWindowStart": "2025-10-12",
|
||||||
|
"optimalWindowEnd": "2025-10-18"
|
||||||
|
},
|
||||||
|
"yieldPredictionChart": {
|
||||||
|
"categories": [
|
||||||
|
"ژانویه",
|
||||||
|
"فوریه",
|
||||||
|
"مارس",
|
||||||
|
"آوریل",
|
||||||
|
"می",
|
||||||
|
"ژوئن",
|
||||||
|
"ژوئیه",
|
||||||
|
"آگوست",
|
||||||
|
"سپتامبر",
|
||||||
|
"اکتبر",
|
||||||
|
"نوامبر",
|
||||||
|
"دسامبر"
|
||||||
|
],
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"name": "امسال",
|
||||||
|
"data": [
|
||||||
|
35,
|
||||||
|
38,
|
||||||
|
40,
|
||||||
|
42,
|
||||||
|
45,
|
||||||
|
48,
|
||||||
|
50,
|
||||||
|
48,
|
||||||
|
46,
|
||||||
|
44,
|
||||||
|
42,
|
||||||
|
42
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "سال گذشته",
|
||||||
|
"data": [
|
||||||
|
32,
|
||||||
|
34,
|
||||||
|
36,
|
||||||
|
38,
|
||||||
|
40,
|
||||||
|
42,
|
||||||
|
44,
|
||||||
|
42,
|
||||||
|
40,
|
||||||
|
38,
|
||||||
|
36,
|
||||||
|
38
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"title": "عملکرد پیشبینیشده",
|
||||||
|
"subtitle": "این فصل",
|
||||||
|
"amount": "42 تن",
|
||||||
|
"avatarColor": "primary",
|
||||||
|
"avatarIcon": "tabler-chart-bar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "تاریخ برداشت",
|
||||||
|
"subtitle": "حدود ۱۵ اکتبر",
|
||||||
|
"amount": "+8%",
|
||||||
|
"avatarColor": "success",
|
||||||
|
"avatarIcon": "tabler-calendar"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"soilMoistureHeatmap": {
|
||||||
|
"zones": [
|
||||||
|
"زون ۱",
|
||||||
|
"زون ۲",
|
||||||
|
"زون ۳",
|
||||||
|
"زون ۴",
|
||||||
|
"زون ۵",
|
||||||
|
"زون ۶",
|
||||||
|
"زون ۷"
|
||||||
|
],
|
||||||
|
"hours": [
|
||||||
|
"۶ ص",
|
||||||
|
"۸ ص",
|
||||||
|
"۱۰ ص",
|
||||||
|
"۱۲ ظ",
|
||||||
|
"۱۴ ع",
|
||||||
|
"۱۶ ع",
|
||||||
|
"۱۸ ع"
|
||||||
|
],
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"name": "زون ۱",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"x": "۶ ص",
|
||||||
|
"y": 52
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "۸ ص",
|
||||||
|
"y": 48
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "۱۰ ص",
|
||||||
|
"y": 55
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "۱۲ ظ",
|
||||||
|
"y": 60
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "۱۴ ع",
|
||||||
|
"y": 58
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "۱۶ ع",
|
||||||
|
"y": 54
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "۱۸ ع",
|
||||||
|
"y": 50
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "زون ۲",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"x": "۶ ص",
|
||||||
|
"y": 45
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "۸ ص",
|
||||||
|
"y": 42
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "۱۰ ص",
|
||||||
|
"y": 48
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "۱۲ ظ",
|
||||||
|
"y": 52
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "۱۴ ع",
|
||||||
|
"y": 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "۱۶ ع",
|
||||||
|
"y": 47
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "۱۸ ع",
|
||||||
|
"y": 44
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ndviHealthCard": {
|
||||||
|
"ndviIndex": 0.78,
|
||||||
|
"healthData": [
|
||||||
|
{
|
||||||
|
"title": "تنش نیتروژن",
|
||||||
|
"value": "پایین",
|
||||||
|
"color": "success",
|
||||||
|
"icon": "tabler-leaf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "سلامت محصول",
|
||||||
|
"value": "خوب",
|
||||||
|
"color": "success",
|
||||||
|
"icon": "tabler-plant"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"recommendationsList": {
|
"recommendationsList": {
|
||||||
"items": [
|
"recommendations": [
|
||||||
{
|
{
|
||||||
"recommendation_title": "تنظیم نوبت آبیاری",
|
"title": "آبیاری: ۶:۰۰ تا ۸:۰۰ صبح",
|
||||||
"suggested_action": "آبیاری بعدی را صبح فردا انجام دهید.",
|
"subtitle": "۴۵۰ مترمکعب برای زون آ. بدون آبیاری، عملکرد ممکن است حدود ۸٪ کاهش یابد.",
|
||||||
"urgency_level": "high"
|
"avatarIcon": "tabler-droplet",
|
||||||
|
"avatarColor": "primary"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "کود: NPK 20-20-20",
|
||||||
|
"subtitle": "اعمال ۲۵ کیلوگرم در هکتار ظرف ۷ روز. کمبود نیتروژن فعلی در بخش ۲.",
|
||||||
|
"avatarIcon": "tabler-leaf",
|
||||||
|
"avatarColor": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "قارچکش: پیشگیرانه",
|
||||||
|
"subtitle": "رطوبت و دما مساعد قارچ. سمپاشی بر پایه مس را در نظر بگیرید.",
|
||||||
|
"avatarIcon": "tabler-mushroom",
|
||||||
|
"avatarColor": "warning"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "بازه برداشت: ۱۲ تا ۱۸ اکتبر",
|
||||||
|
"subtitle": "اوج رسیدگی حدود ۱۵ اکتبر. نیروی کار را متناسب برنامهریزی کنید.",
|
||||||
|
"avatarIcon": "tabler-calendar-event",
|
||||||
|
"avatarColor": "info"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"economicOverview": {
|
||||||
|
"economicData": [
|
||||||
|
{
|
||||||
|
"title": "هزینه آب",
|
||||||
|
"value": "€720",
|
||||||
|
"subtitle": "این ماه",
|
||||||
|
"avatarIcon": "tabler-droplet",
|
||||||
|
"avatarColor": "primary"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "صرفهجویی آب هوشمند",
|
||||||
|
"value": "€156",
|
||||||
|
"subtitle": "۱۸٪ صرفهجویی شده",
|
||||||
|
"avatarIcon": "tabler-bulb",
|
||||||
|
"avatarColor": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "بازده سرمایه پلتفرم",
|
||||||
|
"value": "127%",
|
||||||
|
"subtitle": "نسبت به سال گذشته",
|
||||||
|
"avatarIcon": "tabler-chart-line",
|
||||||
|
"avatarColor": "info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "پیشبینی درآمد",
|
||||||
|
"value": "€42k",
|
||||||
|
"subtitle": "این فصل",
|
||||||
|
"avatarIcon": "tabler-currency-euro",
|
||||||
|
"avatarColor": "success"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"chartSeries": [
|
||||||
|
{
|
||||||
|
"name": "هزینه آب",
|
||||||
|
"data": [
|
||||||
|
120,
|
||||||
|
115,
|
||||||
|
110,
|
||||||
|
125,
|
||||||
|
118,
|
||||||
|
122
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "کود",
|
||||||
|
"data": [
|
||||||
|
80,
|
||||||
|
85,
|
||||||
|
90,
|
||||||
|
75,
|
||||||
|
82,
|
||||||
|
78
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"chartCategories": [
|
||||||
|
"ژانویه",
|
||||||
|
"فوریه",
|
||||||
|
"مارس",
|
||||||
|
"آوریل",
|
||||||
|
"می",
|
||||||
|
"ژوئن"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
|
||||||
|
from sensor_hub.seeds import seed_admin_sensor
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Create or update the default full sensor for the admin user."
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
try:
|
||||||
|
sensor, created = seed_admin_sensor()
|
||||||
|
except ValueError as exc:
|
||||||
|
raise CommandError(str(exc)) from exc
|
||||||
|
|
||||||
|
action = "created" if created else "updated"
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f"Admin sensor {action}: uuid_sensor={sensor.uuid_sensor}, "
|
||||||
|
f"name={sensor.name}, owner={sensor.owner.username}"
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
from account.seeds import seed_admin_user
|
||||||
|
|
||||||
|
from .models import Sensor
|
||||||
|
|
||||||
|
|
||||||
|
ADMIN_SENSOR_UUID = uuid.UUID("11111111-1111-1111-1111-111111111111")
|
||||||
|
ADMIN_SENSOR_DATA = {
|
||||||
|
"name": "Admin Smart Farm Sensor",
|
||||||
|
"is_active": True,
|
||||||
|
"specifications": {
|
||||||
|
"model": "CL-SENSE-PRO-X",
|
||||||
|
"firmware": "2.4.1",
|
||||||
|
"manufacturer": "CropLogic",
|
||||||
|
"serial_number": "CL-ADMIN-0001",
|
||||||
|
"capabilities": [
|
||||||
|
"temperature",
|
||||||
|
"humidity",
|
||||||
|
"soil_moisture",
|
||||||
|
"soil_temperature",
|
||||||
|
"light_intensity",
|
||||||
|
"ph",
|
||||||
|
"ec",
|
||||||
|
"wind_speed",
|
||||||
|
],
|
||||||
|
"connectivity": {
|
||||||
|
"protocol": "LoRaWAN",
|
||||||
|
"sim_enabled": True,
|
||||||
|
"bluetooth": True,
|
||||||
|
"wifi_fallback": True,
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"label": "Admin Demo Field",
|
||||||
|
"lat": 35.6892,
|
||||||
|
"lng": 51.389,
|
||||||
|
"altitude_m": 1190,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"power_source": {
|
||||||
|
"type": "hybrid",
|
||||||
|
"battery": {
|
||||||
|
"capacity_mah": 12000,
|
||||||
|
"voltage": 12,
|
||||||
|
"health_percent": 98,
|
||||||
|
},
|
||||||
|
"solar": {
|
||||||
|
"panel_watt": 40,
|
||||||
|
"controller": "MPPT",
|
||||||
|
},
|
||||||
|
"backup": "dc_adapter",
|
||||||
|
},
|
||||||
|
"customized_sensors": {
|
||||||
|
"thresholds": {
|
||||||
|
"temperature_c": {"min": 10, "max": 36},
|
||||||
|
"humidity_percent": {"min": 30, "max": 85},
|
||||||
|
"soil_moisture_percent": {"min": 25, "max": 70},
|
||||||
|
"ph": {"min": 5.8, "max": 7.2},
|
||||||
|
"ec_ds_m": {"min": 1.1, "max": 2.4},
|
||||||
|
},
|
||||||
|
"report_interval_sec": 300,
|
||||||
|
"alerts": {
|
||||||
|
"sms": True,
|
||||||
|
"email": True,
|
||||||
|
"push": True,
|
||||||
|
},
|
||||||
|
"calibration": {
|
||||||
|
"last_calibrated_at": "2025-03-01T08:30:00Z",
|
||||||
|
"technician": "system",
|
||||||
|
"status": "passed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def seed_admin_sensor():
|
||||||
|
owner, _ = seed_admin_user()
|
||||||
|
sensor, created = Sensor.objects.update_or_create(
|
||||||
|
uuid_sensor=ADMIN_SENSOR_UUID,
|
||||||
|
defaults={
|
||||||
|
"owner": owner,
|
||||||
|
"name": ADMIN_SENSOR_DATA["name"],
|
||||||
|
"is_active": ADMIN_SENSOR_DATA["is_active"],
|
||||||
|
"specifications": ADMIN_SENSOR_DATA["specifications"],
|
||||||
|
"power_source": ADMIN_SENSOR_DATA["power_source"],
|
||||||
|
"customized_sensors": ADMIN_SENSOR_DATA["customized_sensors"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return sensor, created
|
||||||
@@ -30,3 +30,7 @@ class SensorCreateSerializer(serializers.ModelSerializer):
|
|||||||
"power_source",
|
"power_source",
|
||||||
"customized_sensors",
|
"customized_sensors",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class SensorToggleSerializer(serializers.Serializer):
|
||||||
|
uuid_sensor = serializers.UUIDField()
|
||||||
|
|||||||
+5
-5
@@ -1,10 +1,10 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import SensorHubView
|
from .views import SensorActiveView, SensorDeactiveView, SensorDetailView, SensorListCreateView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("active/", SensorHubView.as_view(), name="sensor-hub-active", kwargs={"action": "active"}),
|
path("active/", SensorActiveView.as_view(), name="sensor-hub-active"),
|
||||||
path("deactive/", SensorHubView.as_view(), name="sensor-hub-deactive", kwargs={"action": "deactive"}),
|
path("deactive/", SensorDeactiveView.as_view(), name="sensor-hub-deactive"),
|
||||||
path("<uuid:uuid>/", SensorHubView.as_view(), name="sensor-hub-detail"),
|
path("<uuid:uuid>/", SensorDetailView.as_view(), name="sensor-hub-detail"),
|
||||||
path("", SensorHubView.as_view(), name="sensor-hub-list"),
|
path("", SensorListCreateView.as_view(), name="sensor-hub-list"),
|
||||||
]
|
]
|
||||||
|
|||||||
+79
-96
@@ -1,64 +1,15 @@
|
|||||||
from rest_framework import status
|
from rest_framework import serializers, status
|
||||||
from rest_framework import serializers
|
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.utils import extend_schema
|
||||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
|
||||||
|
|
||||||
from config.swagger import code_response
|
from config.swagger import code_response
|
||||||
from .models import Sensor
|
from .models import Sensor
|
||||||
from .serializers import SensorCreateSerializer, SensorSerializer
|
from .serializers import SensorCreateSerializer, SensorSerializer, SensorToggleSerializer
|
||||||
|
|
||||||
|
|
||||||
@extend_schema_view(
|
class SensorHubBaseView(APIView):
|
||||||
get=extend_schema(
|
|
||||||
tags=["Sensor Hub"],
|
|
||||||
responses={
|
|
||||||
200: code_response("SensorHubGetResponse", data=serializers.JSONField()),
|
|
||||||
404: code_response("SensorHubNotFoundResponse"),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
post=extend_schema(
|
|
||||||
tags=["Sensor Hub"],
|
|
||||||
request=OpenApiTypes.OBJECT,
|
|
||||||
responses={
|
|
||||||
201: code_response("SensorCreateResponse", data=serializers.JSONField()),
|
|
||||||
200: code_response("SensorToggleResponse"),
|
|
||||||
400: code_response("SensorToggleValidationResponse"),
|
|
||||||
404: code_response("SensorToggleNotFoundResponse"),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
patch=extend_schema(
|
|
||||||
tags=["Sensor Hub"],
|
|
||||||
request=SensorCreateSerializer,
|
|
||||||
responses={
|
|
||||||
200: code_response("SensorUpdateResponse", data=SensorSerializer()),
|
|
||||||
404: code_response("SensorUpdateNotFoundResponse"),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
delete=extend_schema(
|
|
||||||
tags=["Sensor Hub"],
|
|
||||||
responses={
|
|
||||||
200: code_response("SensorDeleteResponse"),
|
|
||||||
404: code_response("SensorDeleteNotFoundResponse"),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
class SensorHubView(APIView):
|
|
||||||
"""
|
|
||||||
Sensor-hub CRUD endpoints connected to the database.
|
|
||||||
|
|
||||||
Routes:
|
|
||||||
- GET "" → List sensors for authenticated user.
|
|
||||||
- GET "<uuid>/" → Detail of a single sensor.
|
|
||||||
- POST "" → Create a new sensor.
|
|
||||||
- PATCH "<uuid>/" → Update an existing sensor.
|
|
||||||
- DELETE "<uuid>/" → Delete a sensor.
|
|
||||||
- POST "active/" → Activate a sensor (requires uuid_sensor in body).
|
|
||||||
- POST "deactive/" → Deactivate a sensor (requires uuid_sensor in body).
|
|
||||||
"""
|
|
||||||
|
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def _get_sensor(self, request, uuid):
|
def _get_sensor(self, request, uuid):
|
||||||
@@ -67,74 +18,106 @@ class SensorHubView(APIView):
|
|||||||
except Sensor.DoesNotExist:
|
except Sensor.DoesNotExist:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
uuid = kwargs.get("uuid")
|
|
||||||
if uuid is not None:
|
|
||||||
sensor = self._get_sensor(request, uuid)
|
|
||||||
if sensor is None:
|
|
||||||
return Response(
|
|
||||||
{"code": 404, "msg": "Sensor not found."},
|
|
||||||
status=status.HTTP_404_NOT_FOUND,
|
|
||||||
)
|
|
||||||
data = SensorSerializer(sensor).data
|
|
||||||
return Response({"code": 200, "msg": "success", "data": data}, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
|
class SensorListCreateView(SensorHubBaseView):
|
||||||
|
@extend_schema(
|
||||||
|
tags=["Sensor Hub"],
|
||||||
|
responses={200: code_response("SensorListResponse", data=SensorSerializer(many=True))},
|
||||||
|
)
|
||||||
|
def get(self, request):
|
||||||
sensors = Sensor.objects.filter(owner=request.user)
|
sensors = Sensor.objects.filter(owner=request.user)
|
||||||
data = SensorSerializer(sensors, many=True).data
|
data = SensorSerializer(sensors, many=True).data
|
||||||
return Response({"code": 200, "msg": "success", "data": data}, status=status.HTTP_200_OK)
|
return Response({"code": 200, "msg": "success", "data": data}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
@extend_schema(
|
||||||
action = kwargs.get("action")
|
tags=["Sensor Hub"],
|
||||||
if action in ("active", "deactive"):
|
request=SensorCreateSerializer,
|
||||||
return self._toggle_active(request, is_active=(action == "active"))
|
responses={201: code_response("SensorCreateResponse", data=SensorSerializer())},
|
||||||
|
)
|
||||||
|
def post(self, request):
|
||||||
serializer = SensorCreateSerializer(data=request.data)
|
serializer = SensorCreateSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
sensor = serializer.save(owner=request.user)
|
sensor = serializer.save(owner=request.user)
|
||||||
data = SensorSerializer(sensor).data
|
data = SensorSerializer(sensor).data
|
||||||
return Response(
|
return Response({"code": 201, "msg": "success", "data": data}, status=status.HTTP_201_CREATED)
|
||||||
{"code": 201, "msg": "success", "data": data},
|
|
||||||
status=status.HTTP_201_CREATED,
|
|
||||||
)
|
|
||||||
|
|
||||||
def patch(self, request, *args, **kwargs):
|
|
||||||
uuid = kwargs.get("uuid")
|
class SensorDetailView(SensorHubBaseView):
|
||||||
|
@extend_schema(
|
||||||
|
tags=["Sensor Hub"],
|
||||||
|
responses={
|
||||||
|
200: code_response("SensorDetailResponse", data=SensorSerializer()),
|
||||||
|
404: code_response("SensorNotFoundResponse"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def get(self, request, uuid):
|
||||||
sensor = self._get_sensor(request, uuid)
|
sensor = self._get_sensor(request, uuid)
|
||||||
if sensor is None:
|
if sensor is None:
|
||||||
return Response(
|
return Response({"code": 404, "msg": "Sensor not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||||
{"code": 404, "msg": "Sensor not found."},
|
data = SensorSerializer(sensor).data
|
||||||
status=status.HTTP_404_NOT_FOUND,
|
return Response({"code": 200, "msg": "success", "data": data}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
tags=["Sensor Hub"],
|
||||||
|
request=SensorCreateSerializer,
|
||||||
|
responses={
|
||||||
|
200: code_response("SensorUpdateResponse", data=SensorSerializer()),
|
||||||
|
404: code_response("SensorUpdateNotFoundResponse"),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
def patch(self, request, uuid):
|
||||||
|
sensor = self._get_sensor(request, uuid)
|
||||||
|
if sensor is None:
|
||||||
|
return Response({"code": 404, "msg": "Sensor not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||||
serializer = SensorCreateSerializer(sensor, data=request.data, partial=True)
|
serializer = SensorCreateSerializer(sensor, data=request.data, partial=True)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
data = SensorSerializer(sensor).data
|
data = SensorSerializer(sensor).data
|
||||||
return Response({"code": 200, "msg": "success", "data": data}, status=status.HTTP_200_OK)
|
return Response({"code": 200, "msg": "success", "data": data}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def delete(self, request, *args, **kwargs):
|
@extend_schema(
|
||||||
uuid = kwargs.get("uuid")
|
tags=["Sensor Hub"],
|
||||||
|
responses={
|
||||||
|
200: code_response("SensorDeleteResponse"),
|
||||||
|
404: code_response("SensorDeleteNotFoundResponse"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def delete(self, request, uuid):
|
||||||
sensor = self._get_sensor(request, uuid)
|
sensor = self._get_sensor(request, uuid)
|
||||||
if sensor is None:
|
if sensor is None:
|
||||||
return Response(
|
return Response({"code": 404, "msg": "Sensor not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||||
{"code": 404, "msg": "Sensor not found."},
|
|
||||||
status=status.HTTP_404_NOT_FOUND,
|
|
||||||
)
|
|
||||||
sensor.delete()
|
sensor.delete()
|
||||||
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK)
|
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def _toggle_active(self, request, is_active):
|
|
||||||
uuid_sensor = request.data.get("uuid_sensor")
|
class SensorToggleView(SensorHubBaseView):
|
||||||
if not uuid_sensor:
|
action = None
|
||||||
return Response(
|
|
||||||
{"code": 400, "msg": "uuid_sensor is required."},
|
@extend_schema(
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
tags=["Sensor Hub"],
|
||||||
|
request=SensorToggleSerializer,
|
||||||
|
responses={
|
||||||
|
200: code_response("SensorToggleResponse"),
|
||||||
|
400: code_response("SensorToggleValidationResponse"),
|
||||||
|
404: code_response("SensorToggleNotFoundResponse"),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
sensor = self._get_sensor(request, uuid_sensor)
|
def post(self, request):
|
||||||
|
serializer = SensorToggleSerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
sensor = self._get_sensor(request, serializer.validated_data["uuid_sensor"])
|
||||||
if sensor is None:
|
if sensor is None:
|
||||||
return Response(
|
return Response({"code": 404, "msg": "Sensor not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||||
{"code": 404, "msg": "Sensor not found."},
|
|
||||||
status=status.HTTP_404_NOT_FOUND,
|
sensor.is_active = self.action == "active"
|
||||||
)
|
|
||||||
sensor.is_active = is_active
|
|
||||||
sensor.save(update_fields=["is_active", "updated_at"])
|
sensor.save(update_fields=["is_active", "updated_at"])
|
||||||
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK)
|
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class SensorActiveView(SensorToggleView):
|
||||||
|
action = "active"
|
||||||
|
|
||||||
|
|
||||||
|
class SensorDeactiveView(SensorToggleView):
|
||||||
|
action = "deactive"
|
||||||
|
|||||||
Reference in New Issue
Block a user