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 = serializers.CharField()
|
||||
password = serializers.CharField()
|
||||
password = serializers.CharField(min_length=8, write_only=True)
|
||||
|
||||
|
||||
# --- RequestOTP (request-otp/) ---
|
||||
|
||||
+51
-11
@@ -1,28 +1,68 @@
|
||||
"""
|
||||
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)
|
||||
# row_order must use valid row IDs only: overviewKpis, weatherAlerts, sensorMonitoring,
|
||||
# sensorCharts, alertsWater, predictions, soilHeatmap, ndviRecommendations, economic
|
||||
CONFIG = {
|
||||
"disabled_card_ids": [
|
||||
"predictions",
|
||||
],
|
||||
"row_order": [
|
||||
from copy import deepcopy
|
||||
from threading import Lock
|
||||
|
||||
|
||||
VALID_ROW_IDS = [
|
||||
"overviewKpis",
|
||||
"weatherAlerts",
|
||||
"sensorMonitoring",
|
||||
"sensorCharts",
|
||||
"alertsWater",
|
||||
"predictions",
|
||||
"soilHeatmap",
|
||||
"ndviRecommendations",
|
||||
"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
|
||||
FARM_OVERVIEW_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
|
||||
|
||||
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"),
|
||||
]
|
||||
|
||||
+23
-13
@@ -1,43 +1,51 @@
|
||||
"""
|
||||
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 serializers
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
|
||||
from config.swagger import code_response
|
||||
from external_api_adapter import request as external_api_request
|
||||
from .mock_data import CONFIG
|
||||
from .mock_data import get_config, update_config
|
||||
from .serializers import FarmDashboardConfigPatchSerializer, FarmDashboardConfigSerializer
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
tags=["Farm Dashboard"],
|
||||
responses={200: code_response("FarmDashboardConfigGetResponse", data=serializers.JSONField())},
|
||||
responses={200: code_response("FarmDashboardConfigGetResponse", data=FarmDashboardConfigSerializer())},
|
||||
),
|
||||
patch=extend_schema(
|
||||
tags=["Farm Dashboard"],
|
||||
request=OpenApiTypes.OBJECT,
|
||||
responses={200: code_response("FarmDashboardConfigPatchResponse", data=serializers.JSONField())},
|
||||
request=FarmDashboardConfigPatchSerializer,
|
||||
responses={200: code_response("FarmDashboardConfigPatchResponse", data=FarmDashboardConfigSerializer())},
|
||||
),
|
||||
)
|
||||
class FarmDashboardConfigView(APIView):
|
||||
"""
|
||||
Farm dashboard config endpoints: GET and PATCH.
|
||||
GET returns static config (disabled_card_ids, row_order, enable_drag_reorder).
|
||||
PATCH accepts body but returns same static config; no processing or validation.
|
||||
No database. No input values used in response.
|
||||
Farm dashboard config endpoints.
|
||||
GET returns the current config.
|
||||
PATCH accepts partial updates and returns the full final config.
|
||||
"""
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
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):
|
||||
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(
|
||||
@@ -53,5 +61,7 @@ class FarmDashboardCardsView(APIView):
|
||||
No database. Static mock data only.
|
||||
"""
|
||||
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")
|
||||
return Response(adapter_response.data, status=adapter_response.status_code)
|
||||
|
||||
@@ -8,32 +8,602 @@
|
||||
"sensor_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"all_cards": {
|
||||
"farmOverviewKpis": {
|
||||
"healthScore": 82,
|
||||
"activeAlerts": 2,
|
||||
"waterNeedMm": 18.4
|
||||
"kpis": [
|
||||
{
|
||||
"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": {
|
||||
"items": [
|
||||
"sensors": [
|
||||
{
|
||||
"label": "رطوبت خاک",
|
||||
"value": 45.2,
|
||||
"title": "28°C",
|
||||
"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": "%"
|
||||
},
|
||||
{
|
||||
"label": "دما خاک",
|
||||
"value": 22.5,
|
||||
"unit": "°C"
|
||||
"title": "42%",
|
||||
"subtitle": "رطوبت خاک (۱۰ سانتیمتر)",
|
||||
"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": {
|
||||
"items": [
|
||||
"recommendations": [
|
||||
{
|
||||
"recommendation_title": "تنظیم نوبت آبیاری",
|
||||
"suggested_action": "آبیاری بعدی را صبح فردا انجام دهید.",
|
||||
"urgency_level": "high"
|
||||
"title": "آبیاری: ۶:۰۰ تا ۸:۰۰ صبح",
|
||||
"subtitle": "۴۵۰ مترمکعب برای زون آ. بدون آبیاری، عملکرد ممکن است حدود ۸٪ کاهش یابد.",
|
||||
"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 .views import ConfigView, RecommendView
|
||||
from .views import ConfigView, RecommendTaskStatusView, RecommendView
|
||||
|
||||
urlpatterns = [
|
||||
path("config/", ConfigView.as_view(), name="fertilization-recommendation-config"),
|
||||
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.
|
||||
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.response import Response
|
||||
from rest_framework.views import APIView
|
||||
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 external_api_adapter import request as external_api_request
|
||||
from .mock_data import CONFIG_RESPONSE_DATA
|
||||
from .serializers import (
|
||||
FertilizationRecommendRequestSerializer,
|
||||
FertilizationRecommendResponseDataSerializer,
|
||||
FertilizationTaskStatusDataSerializer,
|
||||
FertilizationTaskSubmitDataSerializer,
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
tags=["Fertilization Recommendation"],
|
||||
responses={200: status_response("FertilizationConfigResponse", data=serializers.JSONField())},
|
||||
)
|
||||
def get(self, request):
|
||||
return Response(
|
||||
{"status": "success", "data": CONFIG_RESPONSE_DATA},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
return Response({"status": "success", "data": CONFIG_RESPONSE_DATA}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
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(
|
||||
tags=["Fertilization Recommendation"],
|
||||
request=OpenApiTypes.OBJECT,
|
||||
responses={200: status_response("FertilizationRecommendResponse", data=serializers.JSONField())},
|
||||
request=FertilizationRecommendRequestSerializer,
|
||||
responses={200: status_response("FertilizationRecommendResponse", data=FertilizationRecommendResponseDataSerializer())},
|
||||
)
|
||||
def post(self, request):
|
||||
adapter_response = external_api_request(
|
||||
@@ -82,3 +42,22 @@ class RecommendView(APIView):
|
||||
payload=request.data,
|
||||
)
|
||||
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 .views import ConfigView, RecommendView
|
||||
from .views import ConfigView, RecommendTaskCreateView, RecommendTaskStatusView, RecommendView
|
||||
|
||||
urlpatterns = [
|
||||
path("config/", ConfigView.as_view(), name="irrigation-recommendation-config"),
|
||||
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.
|
||||
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.response import Response
|
||||
from rest_framework.views import APIView
|
||||
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 external_api_adapter import request as external_api_request
|
||||
from .mock_data import CONFIG_RESPONSE_DATA
|
||||
from .serializers import (
|
||||
IrrigationRecommendRequestSerializer,
|
||||
IrrigationRecommendResponseDataSerializer,
|
||||
IrrigationTaskStatusDataSerializer,
|
||||
IrrigationTaskSubmitDataSerializer,
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
tags=["Irrigation Recommendation"],
|
||||
responses={200: status_response("IrrigationConfigResponse", data=serializers.JSONField())},
|
||||
)
|
||||
def get(self, request):
|
||||
return Response(
|
||||
{"status": "success", "data": CONFIG_RESPONSE_DATA},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
return Response({"status": "success", "data": CONFIG_RESPONSE_DATA}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
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(
|
||||
tags=["Irrigation Recommendation"],
|
||||
request=OpenApiTypes.OBJECT,
|
||||
responses={200: status_response("IrrigationRecommendResponse", data=serializers.JSONField())},
|
||||
request=IrrigationRecommendRequestSerializer,
|
||||
responses={200: status_response("IrrigationRecommendResponse", data=IrrigationRecommendResponseDataSerializer())},
|
||||
)
|
||||
def post(self, request):
|
||||
adapter_response = external_api_request(
|
||||
@@ -82,3 +42,36 @@ class RecommendView(APIView):
|
||||
payload=request.data,
|
||||
)
|
||||
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",
|
||||
"all_cards": {
|
||||
"farmOverviewKpis": {
|
||||
"healthScore": 82,
|
||||
"activeAlerts": 2,
|
||||
"waterNeedMm": 18.4
|
||||
"kpis": [
|
||||
{
|
||||
"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": {
|
||||
"items": [
|
||||
"sensors": [
|
||||
{
|
||||
"label": "رطوبت خاک",
|
||||
"value": 45.2,
|
||||
"title": "28°C",
|
||||
"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": "%"
|
||||
},
|
||||
{
|
||||
"label": "دما خاک",
|
||||
"value": 22.5,
|
||||
"unit": "°C"
|
||||
"title": "42%",
|
||||
"subtitle": "رطوبت خاک (۱۰ سانتیمتر)",
|
||||
"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": {
|
||||
"items": [
|
||||
"recommendations": [
|
||||
{
|
||||
"recommendation_title": "تنظیم نوبت آبیاری",
|
||||
"suggested_action": "آبیاری بعدی را صبح فردا انجام دهید.",
|
||||
"urgency_level": "high"
|
||||
"title": "آبیاری: ۶:۰۰ تا ۸:۰۰ صبح",
|
||||
"subtitle": "۴۵۰ مترمکعب برای زون آ. بدون آبیاری، عملکرد ممکن است حدود ۸٪ کاهش یابد.",
|
||||
"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",
|
||||
"customized_sensors",
|
||||
]
|
||||
|
||||
|
||||
class SensorToggleSerializer(serializers.Serializer):
|
||||
uuid_sensor = serializers.UUIDField()
|
||||
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import SensorHubView
|
||||
from .views import SensorActiveView, SensorDeactiveView, SensorDetailView, SensorListCreateView
|
||||
|
||||
urlpatterns = [
|
||||
path("active/", SensorHubView.as_view(), name="sensor-hub-active", kwargs={"action": "active"}),
|
||||
path("deactive/", SensorHubView.as_view(), name="sensor-hub-deactive", kwargs={"action": "deactive"}),
|
||||
path("<uuid:uuid>/", SensorHubView.as_view(), name="sensor-hub-detail"),
|
||||
path("", SensorHubView.as_view(), name="sensor-hub-list"),
|
||||
path("active/", SensorActiveView.as_view(), name="sensor-hub-active"),
|
||||
path("deactive/", SensorDeactiveView.as_view(), name="sensor-hub-deactive"),
|
||||
path("<uuid:uuid>/", SensorDetailView.as_view(), name="sensor-hub-detail"),
|
||||
path("", SensorListCreateView.as_view(), name="sensor-hub-list"),
|
||||
]
|
||||
|
||||
+79
-96
@@ -1,64 +1,15 @@
|
||||
from rest_framework import status
|
||||
from rest_framework import serializers
|
||||
from rest_framework import serializers, status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
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
|
||||
|
||||
from config.swagger import code_response
|
||||
from .models import Sensor
|
||||
from .serializers import SensorCreateSerializer, SensorSerializer
|
||||
from .serializers import SensorCreateSerializer, SensorSerializer, SensorToggleSerializer
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
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).
|
||||
"""
|
||||
|
||||
class SensorHubBaseView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def _get_sensor(self, request, uuid):
|
||||
@@ -67,74 +18,106 @@ class SensorHubView(APIView):
|
||||
except Sensor.DoesNotExist:
|
||||
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)
|
||||
data = SensorSerializer(sensors, many=True).data
|
||||
return Response({"code": 200, "msg": "success", "data": data}, status=status.HTTP_200_OK)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
action = kwargs.get("action")
|
||||
if action in ("active", "deactive"):
|
||||
return self._toggle_active(request, is_active=(action == "active"))
|
||||
|
||||
@extend_schema(
|
||||
tags=["Sensor Hub"],
|
||||
request=SensorCreateSerializer,
|
||||
responses={201: code_response("SensorCreateResponse", data=SensorSerializer())},
|
||||
)
|
||||
def post(self, request):
|
||||
serializer = SensorCreateSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
sensor = serializer.save(owner=request.user)
|
||||
data = SensorSerializer(sensor).data
|
||||
return Response(
|
||||
{"code": 201, "msg": "success", "data": data},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
return Response({"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)
|
||||
if sensor is None:
|
||||
return Response(
|
||||
{"code": 404, "msg": "Sensor not found."},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
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)
|
||||
|
||||
@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.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
data = SensorSerializer(sensor).data
|
||||
return Response({"code": 200, "msg": "success", "data": data}, status=status.HTTP_200_OK)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
uuid = kwargs.get("uuid")
|
||||
@extend_schema(
|
||||
tags=["Sensor Hub"],
|
||||
responses={
|
||||
200: code_response("SensorDeleteResponse"),
|
||||
404: code_response("SensorDeleteNotFoundResponse"),
|
||||
},
|
||||
)
|
||||
def delete(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,
|
||||
)
|
||||
return Response({"code": 404, "msg": "Sensor not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
sensor.delete()
|
||||
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")
|
||||
if not uuid_sensor:
|
||||
return Response(
|
||||
{"code": 400, "msg": "uuid_sensor is required."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
|
||||
class SensorToggleView(SensorHubBaseView):
|
||||
action = None
|
||||
|
||||
@extend_schema(
|
||||
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:
|
||||
return Response(
|
||||
{"code": 404, "msg": "Sensor not found."},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
sensor.is_active = is_active
|
||||
return Response({"code": 404, "msg": "Sensor not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
sensor.is_active = self.action == "active"
|
||||
sensor.save(update_fields=["is_active", "updated_at"])
|
||||
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