UPDATE
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
# Farm Hub & Sensor Catalog Changes
|
||||
|
||||
این فایل برای تیم فرانت آماده شده و فقط تغییرات موثر روی قرارداد API و دادهها را توضیح میدهد.
|
||||
|
||||
## 1) تغییر اصلی
|
||||
|
||||
مدل قدیمی `plant` از جریان اصلی حذف شده و اطلاعات محصول حالا از مدل `Product` در اپ `farm_hub` خوانده میشود.
|
||||
|
||||
به همین خاطر endpoint زیر:
|
||||
|
||||
`/api/farm-hub/farm-types/{farm_type_uuid}/products/`
|
||||
|
||||
اکنون باید دادههای هر محصول را از جدول `products` برگرداند، نه از جدول قدیمی `plant`.
|
||||
|
||||
## 2) endpoint های مرتبط
|
||||
|
||||
### لیست نوع مزرعه
|
||||
|
||||
`GET /api/farm-hub/farm-types/`
|
||||
|
||||
نمونه پاسخ:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": [
|
||||
{
|
||||
"uuid": "farm-type-uuid",
|
||||
"name": "زراعی",
|
||||
"description": "",
|
||||
"metadata": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### لیست محصولات بر اساس نوع مزرعه
|
||||
|
||||
`GET /api/farm-hub/farm-types/{farm_type_uuid}/products/`
|
||||
|
||||
نمونه پاسخ جدید:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": [
|
||||
{
|
||||
"uuid": "product-uuid",
|
||||
"name": "گندم",
|
||||
"description": "",
|
||||
"metadata": {},
|
||||
"light": "",
|
||||
"watering": "",
|
||||
"soil": "لومی",
|
||||
"temperature": "",
|
||||
"planting_season": "پاییز",
|
||||
"harvest_time": "اواخر بهار",
|
||||
"spacing": "",
|
||||
"fertilizer": "",
|
||||
"health_profile": {},
|
||||
"irrigation_profile": {},
|
||||
"growth_profile": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 3) فیلدهای جدید هر product
|
||||
|
||||
قبلا فرانت فقط اینها را میگرفت:
|
||||
|
||||
- `uuid`
|
||||
- `name`
|
||||
- `description`
|
||||
- `metadata`
|
||||
|
||||
الان این فیلدها هم اضافه شدهاند:
|
||||
|
||||
- `light`
|
||||
- `watering`
|
||||
- `soil`
|
||||
- `temperature`
|
||||
- `planting_season`
|
||||
- `harvest_time`
|
||||
- `spacing`
|
||||
- `fertilizer`
|
||||
- `health_profile`
|
||||
- `irrigation_profile`
|
||||
- `growth_profile`
|
||||
|
||||
## 4) نکات مهم برای فرانت
|
||||
|
||||
- `health_profile`، `irrigation_profile` و `growth_profile` از نوع `JSON object` هستند
|
||||
- بعضی فیلدها ممکن است رشته خالی `""` یا آبجکت خالی `{}` برگردانند
|
||||
- بهتر است UI برای نبودن داده fallback داشته باشد
|
||||
- مرتبسازی محصولات در endpoint بر اساس `name` است
|
||||
|
||||
## 5) سیدرهای جدید catalog
|
||||
|
||||
برای farm type ها و product ها سیدر گستردهتر اضافه شده است.
|
||||
|
||||
### farm type ها
|
||||
|
||||
- `زراعی`
|
||||
- `درختی`
|
||||
- `غرقابی`
|
||||
- `گلخانه ای`
|
||||
|
||||
### محصولات seed شده
|
||||
|
||||
#### زراعی
|
||||
|
||||
- `گندم`
|
||||
- `ذرت`
|
||||
- `جو`
|
||||
- `کلزا`
|
||||
- `پنبه`
|
||||
|
||||
#### درختی
|
||||
|
||||
- `سیب`
|
||||
- `پسته`
|
||||
- `انگور`
|
||||
- `انار`
|
||||
|
||||
#### غرقابی
|
||||
|
||||
- `برنج`
|
||||
|
||||
#### گلخانه ای
|
||||
|
||||
- `گوجه فرنگی`
|
||||
- `خیار`
|
||||
- `فلفل دلمه ای`
|
||||
|
||||
## 6) تغییرات مرتبط با farm object
|
||||
|
||||
در پاسخ farm، این فیلدها هم الان مهم هستند:
|
||||
|
||||
- `area_uuid`
|
||||
- `sensors[].sensor_catalog_uuid`
|
||||
- `sensors[].physical_device_uuid`
|
||||
|
||||
و این فیلدها از مدل حذف شدهاند:
|
||||
|
||||
- `customization` در سطح farm
|
||||
- `customization` در سطح sensor
|
||||
|
||||
## 7) sensor_catalog/apps.py
|
||||
|
||||
فایل:
|
||||
|
||||
`sensor_catalog/apps.py`
|
||||
|
||||
محتوا:
|
||||
|
||||
- اپ با نام `sensor_catalog` ثبت شده
|
||||
- کلاس کانفیگ آن `SensorCatalogConfig` است
|
||||
- `verbose_name` برابر `Sensor Catalog` است
|
||||
|
||||
نکته مهم برای فرانت:
|
||||
|
||||
- خود `sensor_catalog/apps.py` خروجی API را تغییر نمیدهد
|
||||
- اثر عملی آن این است که اپ `sensor_catalog` به صورت رسمی در پروژه register شده و دادههای سنسور حالا در `farm_hub` استفاده میشوند
|
||||
- از این به بعد فرانت میتواند روی `sensor_catalog_uuid` برای سنسورها حساب کند
|
||||
|
||||
## 8) نتیجه نهایی برای فرانت
|
||||
|
||||
- منبع نمایش محصولات باید `farm_hub.products` باشد
|
||||
- endpoint اصلی برای لیست محصولات هر نوع مزرعه:
|
||||
- `GET /api/farm-hub/farm-types/{farm_type_uuid}/products/`
|
||||
- UI جزئیات محصول میتواند فیلدهای زراعی/رشد/آبیاری را مستقیم از response بخواند
|
||||
- برای سنسورها باید از `sensor_catalog_uuid` و `physical_device_uuid` استفاده شود
|
||||
+1
-1
@@ -28,7 +28,7 @@ INSTALLED_APPS = [
|
||||
"auth.apps.AuthConfig",
|
||||
"account.apps.AccountConfig",
|
||||
"farm_hub.apps.FarmHubConfig",
|
||||
"plant.apps.PlantConfig",
|
||||
"sensor_catalog.apps.SensorCatalogConfig",
|
||||
"dashboard",
|
||||
"crop_zoning",
|
||||
"plant_simulator",
|
||||
|
||||
@@ -10,6 +10,7 @@ urlpatterns = [
|
||||
path("api/auth/", include("auth.urls")),
|
||||
path("api/account/", include("account.urls")),
|
||||
path("api/farm-hub/", include("farm_hub.urls")),
|
||||
path("api/sensor-catalog/", include("sensor_catalog.urls")),
|
||||
path("api/farm-dashboard-config/", include("dashboard.urls_config")),
|
||||
path("api/farm-dashboard/", include("dashboard.urls")),
|
||||
path("api/crop-zoning/", include("crop_zoning.urls")),
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
CATALOG_SEED_DATA = {
|
||||
"زراعی": [
|
||||
{"name": "گندم", "planting_season": "پاییز", "harvest_time": "اواخر بهار", "soil": "لومی"},
|
||||
{"name": "ذرت", "planting_season": "بهار", "harvest_time": "تابستان", "soil": "لومی شنی"},
|
||||
{"name": "جو", "planting_season": "پاییز", "harvest_time": "اواخر بهار", "soil": "لومی"},
|
||||
{"name": "کلزا", "planting_season": "پاییز", "harvest_time": "بهار", "soil": "لومی رسی"},
|
||||
{"name": "پنبه", "planting_season": "بهار", "harvest_time": "پاییز", "soil": "لومی"},
|
||||
],
|
||||
"درختی": [
|
||||
{"name": "سیب", "planting_season": "زمستان", "harvest_time": "پاییز", "soil": "لومی"},
|
||||
{"name": "پسته", "planting_season": "زمستان", "harvest_time": "اواخر تابستان", "soil": "شنی لومی"},
|
||||
{"name": "انگور", "planting_season": "اواخر زمستان", "harvest_time": "تابستان", "soil": "لومی"},
|
||||
{"name": "انار", "planting_season": "اواخر زمستان", "harvest_time": "پاییز", "soil": "لومی شنی"},
|
||||
],
|
||||
"غرقابی": [
|
||||
{"name": "برنج", "planting_season": "بهار", "harvest_time": "اواخر تابستان", "soil": "رسی"},
|
||||
],
|
||||
"گلخانه ای": [
|
||||
{"name": "گوجه فرنگی", "planting_season": "چهار فصل", "harvest_time": "چند مرحله ای", "soil": "کوکوپیت"},
|
||||
{"name": "خیار", "planting_season": "چهار فصل", "harvest_time": "چند مرحله ای", "soil": "پرلیت"},
|
||||
{"name": "فلفل دلمه ای", "planting_season": "چهار فصل", "harvest_time": "چند مرحله ای", "soil": "بستر هیدروپونیک"},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from farm_hub.catalog import CATALOG_SEED_DATA
|
||||
from farm_hub.models import FarmType, Product
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Seed farm types and products catalog data."
|
||||
|
||||
def handle(self, *args, **options):
|
||||
farm_type_count = 0
|
||||
product_count = 0
|
||||
|
||||
for farm_type_name, products in CATALOG_SEED_DATA.items():
|
||||
farm_type, created = FarmType.objects.get_or_create(name=farm_type_name)
|
||||
farm_type_count += int(created)
|
||||
for product_data in products:
|
||||
_, product_created = Product.objects.update_or_create(
|
||||
farm_type=farm_type,
|
||||
name=product_data["name"],
|
||||
defaults={key: value for key, value in product_data.items() if key != "name"},
|
||||
)
|
||||
product_count += int(product_created)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Farm catalog seeded successfully. Created farm types: {farm_type_count}, products: {product_count}."
|
||||
)
|
||||
)
|
||||
@@ -2,9 +2,10 @@ from django.db import migrations
|
||||
|
||||
|
||||
FARM_TYPES = {
|
||||
"زراعی": ["گندم", "ذرت"],
|
||||
"درختی": ["سیب", "پسته"],
|
||||
"زراعی": ["گندم", "ذرت", "جو", "کلزا", "پنبه"],
|
||||
"درختی": ["سیب", "پسته", "انگور", "انار"],
|
||||
"غرقابی": ["برنج"],
|
||||
"گلخانه ای": ["گوجه فرنگی", "خیار", "فلفل دلمه ای"],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 5.2.12 on 2026-03-20 00:30
|
||||
|
||||
import uuid
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("sensor_catalog", "0002_sensorcatalog_supported_power_sources"),
|
||||
("farm_hub", "0002_seed_default_catalog"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="farmsensor",
|
||||
name="physical_device_uuid",
|
||||
field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="farmsensor",
|
||||
name="sensor_catalog",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="farm_sensors",
|
||||
to="sensor_catalog.sensorcatalog",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.2.12 on 2026-03-20 01:30
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("crop_zoning", "0004_croparea_farm"),
|
||||
("farm_hub", "0003_farmsensor_catalog_and_physical_device"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="farmhub",
|
||||
name="customization",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="farmsensor",
|
||||
name="customization",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="farmhub",
|
||||
name="current_crop_area",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="current_for_farms",
|
||||
to="crop_zoning.croparea",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,163 @@
|
||||
import json
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
DEFAULT_FARM_TYPE_NAME = "زراعی"
|
||||
|
||||
|
||||
def _table_exists(schema_editor, table_name):
|
||||
with schema_editor.connection.cursor() as cursor:
|
||||
existing_tables = set(schema_editor.connection.introspection.table_names(cursor))
|
||||
return table_name in existing_tables
|
||||
|
||||
|
||||
def _deserialize_json(value):
|
||||
if value in (None, "", b""):
|
||||
return {}
|
||||
if isinstance(value, (dict, list)):
|
||||
return value
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode("utf-8")
|
||||
try:
|
||||
return json.loads(value)
|
||||
except (TypeError, ValueError):
|
||||
return {}
|
||||
|
||||
|
||||
def migrate_plant_rows_to_products(apps, schema_editor):
|
||||
if not _table_exists(schema_editor, "plant_plant"):
|
||||
return
|
||||
|
||||
FarmType = apps.get_model("farm_hub", "FarmType")
|
||||
Product = apps.get_model("farm_hub", "Product")
|
||||
|
||||
farm_type, _ = FarmType.objects.get_or_create(name=DEFAULT_FARM_TYPE_NAME)
|
||||
|
||||
with schema_editor.connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT
|
||||
name,
|
||||
light,
|
||||
watering,
|
||||
soil,
|
||||
temperature,
|
||||
planting_season,
|
||||
harvest_time,
|
||||
spacing,
|
||||
fertilizer,
|
||||
health_profile,
|
||||
irrigation_profile,
|
||||
growth_profile,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM plant_plant
|
||||
"""
|
||||
)
|
||||
columns = [column[0] for column in cursor.description]
|
||||
rows = [dict(zip(columns, row)) for row in cursor.fetchall()]
|
||||
|
||||
for row in rows:
|
||||
Product.objects.update_or_create(
|
||||
farm_type=farm_type,
|
||||
name=row["name"],
|
||||
defaults={
|
||||
"light": row["light"] or "",
|
||||
"watering": row["watering"] or "",
|
||||
"soil": row["soil"] or "",
|
||||
"temperature": row["temperature"] or "",
|
||||
"planting_season": row["planting_season"] or "",
|
||||
"harvest_time": row["harvest_time"] or "",
|
||||
"spacing": row["spacing"] or "",
|
||||
"fertilizer": row["fertilizer"] or "",
|
||||
"health_profile": _deserialize_json(row["health_profile"]),
|
||||
"irrigation_profile": _deserialize_json(row["irrigation_profile"]),
|
||||
"growth_profile": _deserialize_json(row["growth_profile"]),
|
||||
"created_at": row["created_at"],
|
||||
"updated_at": row["updated_at"],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def drop_legacy_plant_table(apps, schema_editor):
|
||||
if _table_exists(schema_editor, "plant_plant"):
|
||||
schema_editor.execute("DROP TABLE plant_plant")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("farm_hub", "0004_remove_customization_add_current_crop_area"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="product",
|
||||
name="fertilizer",
|
||||
field=models.CharField(blank=True, default="", help_text="کود مناسب", max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="product",
|
||||
name="growth_profile",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text='پروفایل رشد محصول برای مدل GDD. {"base_temperature": 10, "required_gdd_for_maturity": 1200, "stage_thresholds": {"flowering": 500, "fruiting": 850}, "current_cumulative_gdd": 320}',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="product",
|
||||
name="harvest_time",
|
||||
field=models.CharField(blank=True, default="", help_text="زمان برداشت", max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="product",
|
||||
name="health_profile",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text='پروفایل سلامت محصول برای KPIها. ساختار نمونه: {"moisture": {"ideal_value": 65, "min_range": 45, "max_range": 75, "weight": 0.4}}',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="product",
|
||||
name="irrigation_profile",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text='پروفایل آبیاری محصول برای محاسبات ETc. {"kc_initial": 0.6, "kc_mid": 1.15, "kc_end": 0.8, "growth_stage_duration": {"initial": 20, "mid": 30, "late": 25}}',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="product",
|
||||
name="light",
|
||||
field=models.CharField(blank=True, default="", help_text="نور مورد نیاز", max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="product",
|
||||
name="planting_season",
|
||||
field=models.CharField(blank=True, default="", help_text="فصل کاشت", max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="product",
|
||||
name="soil",
|
||||
field=models.CharField(blank=True, default="", help_text="خاک مناسب", max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="product",
|
||||
name="spacing",
|
||||
field=models.CharField(blank=True, default="", help_text="فاصله کاشت", max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="product",
|
||||
name="temperature",
|
||||
field=models.CharField(blank=True, default="", help_text="دمای مناسب", max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="product",
|
||||
name="watering",
|
||||
field=models.CharField(blank=True, default="", help_text="آبیاری", max_length=255),
|
||||
),
|
||||
migrations.RunPython(migrate_plant_rows_to_products, migrations.RunPython.noop),
|
||||
migrations.RunPython(drop_legacy_plant_table, migrations.RunPython.noop),
|
||||
]
|
||||
@@ -0,0 +1,55 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
CATALOG_SEED_DATA = {
|
||||
"زراعی": [
|
||||
{"name": "گندم", "planting_season": "پاییز", "harvest_time": "اواخر بهار", "soil": "لومی"},
|
||||
{"name": "ذرت", "planting_season": "بهار", "harvest_time": "تابستان", "soil": "لومی شنی"},
|
||||
{"name": "جو", "planting_season": "پاییز", "harvest_time": "اواخر بهار", "soil": "لومی"},
|
||||
{"name": "کلزا", "planting_season": "پاییز", "harvest_time": "بهار", "soil": "لومی رسی"},
|
||||
{"name": "پنبه", "planting_season": "بهار", "harvest_time": "پاییز", "soil": "لومی"},
|
||||
],
|
||||
"درختی": [
|
||||
{"name": "سیب", "planting_season": "زمستان", "harvest_time": "پاییز", "soil": "لومی"},
|
||||
{"name": "پسته", "planting_season": "زمستان", "harvest_time": "اواخر تابستان", "soil": "شنی لومی"},
|
||||
{"name": "انگور", "planting_season": "اواخر زمستان", "harvest_time": "تابستان", "soil": "لومی"},
|
||||
{"name": "انار", "planting_season": "اواخر زمستان", "harvest_time": "پاییز", "soil": "لومی شنی"},
|
||||
],
|
||||
"غرقابی": [
|
||||
{"name": "برنج", "planting_season": "بهار", "harvest_time": "اواخر تابستان", "soil": "رسی"},
|
||||
],
|
||||
"گلخانه ای": [
|
||||
{"name": "گوجه فرنگی", "planting_season": "چهار فصل", "harvest_time": "چند مرحله ای", "soil": "کوکوپیت"},
|
||||
{"name": "خیار", "planting_season": "چهار فصل", "harvest_time": "چند مرحله ای", "soil": "پرلیت"},
|
||||
{"name": "فلفل دلمه ای", "planting_season": "چهار فصل", "harvest_time": "چند مرحله ای", "soil": "بستر هیدروپونیک"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def seed_expanded_catalog(apps, schema_editor):
|
||||
FarmType = apps.get_model("farm_hub", "FarmType")
|
||||
Product = apps.get_model("farm_hub", "Product")
|
||||
|
||||
for farm_type_name, products in CATALOG_SEED_DATA.items():
|
||||
farm_type, _ = FarmType.objects.get_or_create(name=farm_type_name)
|
||||
for product_data in products:
|
||||
Product.objects.update_or_create(
|
||||
farm_type=farm_type,
|
||||
name=product_data["name"],
|
||||
defaults={key: value for key, value in product_data.items() if key != "name"},
|
||||
)
|
||||
|
||||
|
||||
def unseed_expanded_catalog(apps, schema_editor):
|
||||
FarmType = apps.get_model("farm_hub", "FarmType")
|
||||
FarmType.objects.filter(name__in=CATALOG_SEED_DATA.keys()).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("farm_hub", "0005_product_profiles_and_plant_migration"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(seed_expanded_catalog, unseed_expanded_catalog),
|
||||
]
|
||||
+55
-7
@@ -1,11 +1,12 @@
|
||||
import uuid
|
||||
import uuid as uuid_lib
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from sensor_catalog.models import SensorCatalog
|
||||
|
||||
|
||||
class FarmType(models.Model):
|
||||
uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False, db_index=True)
|
||||
uuid = models.UUIDField(default=uuid_lib.uuid4, unique=True, editable=False, db_index=True)
|
||||
name = models.CharField(max_length=255, unique=True, db_index=True)
|
||||
description = models.TextField(blank=True, default="")
|
||||
metadata = models.JSONField(default=dict, blank=True)
|
||||
@@ -21,7 +22,7 @@ class FarmType(models.Model):
|
||||
|
||||
|
||||
class Product(models.Model):
|
||||
uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False, db_index=True)
|
||||
uuid = models.UUIDField(default=uuid_lib.uuid4, unique=True, editable=False, db_index=True)
|
||||
farm_type = models.ForeignKey(
|
||||
FarmType,
|
||||
on_delete=models.CASCADE,
|
||||
@@ -30,6 +31,40 @@ class Product(models.Model):
|
||||
name = models.CharField(max_length=255, db_index=True)
|
||||
description = models.TextField(blank=True, default="")
|
||||
metadata = models.JSONField(default=dict, blank=True)
|
||||
light = models.CharField(max_length=255, blank=True, default="", help_text="نور مورد نیاز")
|
||||
watering = models.CharField(max_length=255, blank=True, default="", help_text="آبیاری")
|
||||
soil = models.CharField(max_length=255, blank=True, default="", help_text="خاک مناسب")
|
||||
temperature = models.CharField(max_length=255, blank=True, default="", help_text="دمای مناسب")
|
||||
planting_season = models.CharField(max_length=255, blank=True, default="", help_text="فصل کاشت")
|
||||
harvest_time = models.CharField(max_length=255, blank=True, default="", help_text="زمان برداشت")
|
||||
spacing = models.CharField(max_length=255, blank=True, default="", help_text="فاصله کاشت")
|
||||
fertilizer = models.CharField(max_length=255, blank=True, default="", help_text="کود مناسب")
|
||||
health_profile = models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text=(
|
||||
"پروفایل سلامت محصول برای KPIها. ساختار نمونه: "
|
||||
'{"moisture": {"ideal_value": 65, "min_range": 45, "max_range": 75, "weight": 0.4}}'
|
||||
),
|
||||
)
|
||||
irrigation_profile = models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text=(
|
||||
"پروفایل آبیاری محصول برای محاسبات ETc. "
|
||||
'{"kc_initial": 0.6, "kc_mid": 1.15, "kc_end": 0.8, '
|
||||
'"growth_stage_duration": {"initial": 20, "mid": 30, "late": 25}}'
|
||||
),
|
||||
)
|
||||
growth_profile = models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text=(
|
||||
"پروفایل رشد محصول برای مدل GDD. "
|
||||
'{"base_temperature": 10, "required_gdd_for_maturity": 1200, '
|
||||
'"stage_thresholds": {"flowering": 500, "fruiting": 850}, "current_cumulative_gdd": 320}'
|
||||
),
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
@@ -45,7 +80,7 @@ class Product(models.Model):
|
||||
|
||||
|
||||
class FarmHub(models.Model):
|
||||
farm_uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False, db_index=True)
|
||||
farm_uuid = models.UUIDField(default=uuid_lib.uuid4, unique=True, editable=False, db_index=True)
|
||||
owner = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
@@ -58,7 +93,13 @@ class FarmHub(models.Model):
|
||||
)
|
||||
name = models.CharField(max_length=255)
|
||||
is_active = models.BooleanField(default=True)
|
||||
customization = models.JSONField(default=dict, blank=True)
|
||||
current_crop_area = models.ForeignKey(
|
||||
"crop_zoning.CropArea",
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="current_for_farms",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
products = models.ManyToManyField(Product, related_name="farms", blank=True)
|
||||
@@ -72,18 +113,25 @@ class FarmHub(models.Model):
|
||||
|
||||
|
||||
class FarmSensor(models.Model):
|
||||
uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False, db_index=True)
|
||||
uuid = models.UUIDField(default=uuid_lib.uuid4, unique=True, editable=False, db_index=True)
|
||||
farm = models.ForeignKey(
|
||||
FarmHub,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="sensors",
|
||||
)
|
||||
sensor_catalog = models.ForeignKey(
|
||||
SensorCatalog,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="farm_sensors",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
physical_device_uuid = models.UUIDField(default=uuid_lib.uuid4, unique=True, db_index=True)
|
||||
name = models.CharField(max_length=255)
|
||||
sensor_type = models.CharField(max_length=255, blank=True, default="")
|
||||
is_active = models.BooleanField(default=True)
|
||||
specifications = models.JSONField(default=dict, blank=True)
|
||||
power_source = models.JSONField(default=dict, blank=True)
|
||||
customization = models.JSONField(default=dict, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
||||
+35
-31
@@ -3,7 +3,9 @@ import uuid
|
||||
from django.db import transaction
|
||||
|
||||
from account.seeds import seed_admin_user
|
||||
from sensor_catalog.models import SensorCatalog
|
||||
|
||||
from .catalog import CATALOG_SEED_DATA
|
||||
from .models import FarmHub, FarmType, Product
|
||||
from .services import dispatch_farm_zoning
|
||||
|
||||
@@ -12,19 +14,10 @@ ADMIN_FARM_UUID = uuid.UUID("11111111-1111-1111-1111-111111111111")
|
||||
ADMIN_FARM_DATA = {
|
||||
"name": "Admin Smart Farm",
|
||||
"is_active": True,
|
||||
"customization": {
|
||||
"irrigation": {
|
||||
"mode": "smart",
|
||||
"report_interval_sec": 300,
|
||||
},
|
||||
"alerts": {
|
||||
"sms": True,
|
||||
"email": True,
|
||||
"push": True,
|
||||
},
|
||||
},
|
||||
"sensors": [
|
||||
{
|
||||
"sensor_catalog_name": "Sensor 7 - Soil Moisture Sensor v1.2",
|
||||
"physical_device_uuid": uuid.UUID("22222222-2222-2222-2222-222222222221"),
|
||||
"name": "Station 1",
|
||||
"sensor_type": "weather_station",
|
||||
"is_active": True,
|
||||
@@ -38,14 +31,10 @@ ADMIN_FARM_DATA = {
|
||||
"battery": {"capacity_mah": 12000, "voltage": 12},
|
||||
"solar": {"panel_watt": 40, "controller": "MPPT"},
|
||||
},
|
||||
"customization": {
|
||||
"thresholds": {
|
||||
"temperature_c": {"min": 10, "max": 36},
|
||||
"humidity_percent": {"min": 30, "max": 85},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"sensor_catalog_name": "Sensor 7 - Soil Moisture Sensor v1.2",
|
||||
"physical_device_uuid": uuid.UUID("22222222-2222-2222-2222-222222222222"),
|
||||
"name": "Soil Probe 1",
|
||||
"sensor_type": "soil_probe",
|
||||
"is_active": True,
|
||||
@@ -53,13 +42,6 @@ ADMIN_FARM_DATA = {
|
||||
"capabilities": ["soil_moisture", "soil_temperature", "ph", "ec"],
|
||||
},
|
||||
"power_source": {"type": "battery", "backup": "solar"},
|
||||
"customization": {
|
||||
"depth_cm": [20, 40],
|
||||
"thresholds": {
|
||||
"soil_moisture_percent": {"min": 25, "max": 70},
|
||||
"ph": {"min": 5.8, "max": 7.2},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -82,10 +64,25 @@ ADMIN_FARM_AREA_GEOJSON = {
|
||||
|
||||
|
||||
def _get_default_catalog():
|
||||
farm_type, _ = FarmType.objects.get_or_create(name="زراعی")
|
||||
wheat, _ = Product.objects.get_or_create(farm_type=farm_type, name="گندم")
|
||||
corn, _ = Product.objects.get_or_create(farm_type=farm_type, name="ذرت")
|
||||
return farm_type, [wheat, corn]
|
||||
default_farm_type_name = "زراعی"
|
||||
created_products = []
|
||||
|
||||
for farm_type_name, products in CATALOG_SEED_DATA.items():
|
||||
farm_type, _ = FarmType.objects.get_or_create(name=farm_type_name)
|
||||
for product_data in products:
|
||||
product, _ = Product.objects.update_or_create(
|
||||
farm_type=farm_type,
|
||||
name=product_data["name"],
|
||||
defaults={key: value for key, value in product_data.items() if key != "name"},
|
||||
)
|
||||
if farm_type_name == default_farm_type_name:
|
||||
created_products.append(product)
|
||||
|
||||
return FarmType.objects.get(name=default_farm_type_name), created_products[:2]
|
||||
|
||||
|
||||
def _get_sensor_catalog_by_name(name):
|
||||
return SensorCatalog.objects.filter(name=name).first()
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
@@ -99,12 +96,19 @@ def seed_admin_farm():
|
||||
"farm_type": farm_type,
|
||||
"name": ADMIN_FARM_DATA["name"],
|
||||
"is_active": ADMIN_FARM_DATA["is_active"],
|
||||
"customization": ADMIN_FARM_DATA["customization"],
|
||||
},
|
||||
)
|
||||
farm.products.set(products)
|
||||
farm.sensors.all().delete()
|
||||
farm.sensors.bulk_create([farm.sensors.model(farm=farm, **sensor_data) for sensor_data in ADMIN_FARM_DATA["sensors"]])
|
||||
sensors = []
|
||||
for sensor_data in ADMIN_FARM_DATA["sensors"]:
|
||||
sensor_data = sensor_data.copy()
|
||||
sensor_catalog_name = sensor_data.pop("sensor_catalog_name", None)
|
||||
sensor_data["sensor_catalog"] = _get_sensor_catalog_by_name(sensor_catalog_name) if sensor_catalog_name else None
|
||||
sensors.append(farm.sensors.model(farm=farm, **sensor_data))
|
||||
farm.sensors.bulk_create(sensors)
|
||||
if created:
|
||||
dispatch_farm_zoning(ADMIN_FARM_AREA_GEOJSON, farm)
|
||||
crop_area, _zoning_payload = dispatch_farm_zoning(ADMIN_FARM_AREA_GEOJSON, farm)
|
||||
farm.current_crop_area = crop_area
|
||||
farm.save(update_fields=["current_crop_area", "updated_at"])
|
||||
return farm, created
|
||||
|
||||
+45
-5
@@ -1,6 +1,7 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import FarmHub, FarmSensor, FarmType, Product
|
||||
from sensor_catalog.models import SensorCatalog
|
||||
|
||||
|
||||
class FarmTypeSerializer(serializers.ModelSerializer):
|
||||
@@ -12,22 +13,40 @@ class FarmTypeSerializer(serializers.ModelSerializer):
|
||||
class ProductSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ["uuid", "name", "description", "metadata"]
|
||||
fields = [
|
||||
"uuid",
|
||||
"name",
|
||||
"description",
|
||||
"metadata",
|
||||
"light",
|
||||
"watering",
|
||||
"soil",
|
||||
"temperature",
|
||||
"planting_season",
|
||||
"harvest_time",
|
||||
"spacing",
|
||||
"fertilizer",
|
||||
"health_profile",
|
||||
"irrigation_profile",
|
||||
"growth_profile",
|
||||
]
|
||||
|
||||
|
||||
class FarmSensorSerializer(serializers.ModelSerializer):
|
||||
last_updated = serializers.DateTimeField(source="updated_at", read_only=True)
|
||||
sensor_catalog_uuid = serializers.UUIDField(source="sensor_catalog.uuid", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = FarmSensor
|
||||
fields = [
|
||||
"uuid",
|
||||
"sensor_catalog_uuid",
|
||||
"physical_device_uuid",
|
||||
"name",
|
||||
"sensor_type",
|
||||
"is_active",
|
||||
"specifications",
|
||||
"power_source",
|
||||
"customization",
|
||||
"last_updated",
|
||||
]
|
||||
read_only_fields = ["uuid", "last_updated"]
|
||||
@@ -38,14 +57,15 @@ class FarmHubSerializer(serializers.ModelSerializer):
|
||||
farm_type = FarmTypeSerializer(read_only=True)
|
||||
products = ProductSerializer(many=True, read_only=True)
|
||||
sensors = FarmSensorSerializer(many=True, read_only=True)
|
||||
area_uuid = serializers.UUIDField(source="current_crop_area.uuid", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = FarmHub
|
||||
fields = [
|
||||
"farm_uuid",
|
||||
"area_uuid",
|
||||
"name",
|
||||
"is_active",
|
||||
"customization",
|
||||
"farm_type",
|
||||
"products",
|
||||
"sensors",
|
||||
@@ -55,17 +75,32 @@ class FarmHubSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class FarmSensorWriteSerializer(serializers.ModelSerializer):
|
||||
sensor_catalog_uuid = serializers.UUIDField(write_only=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = FarmSensor
|
||||
fields = [
|
||||
"sensor_catalog_uuid",
|
||||
"physical_device_uuid",
|
||||
"name",
|
||||
"sensor_type",
|
||||
"is_active",
|
||||
"specifications",
|
||||
"power_source",
|
||||
"customization",
|
||||
]
|
||||
|
||||
def validate(self, attrs):
|
||||
sensor_catalog_uuid = attrs.pop("sensor_catalog_uuid", None)
|
||||
if sensor_catalog_uuid is not None:
|
||||
try:
|
||||
sensor_catalog = SensorCatalog.objects.get(uuid=sensor_catalog_uuid)
|
||||
except SensorCatalog.DoesNotExist as exc:
|
||||
raise serializers.ValidationError({"sensor_catalog_uuid": ["Sensor catalog not found."]}) from exc
|
||||
attrs["sensor_catalog"] = sensor_catalog
|
||||
attrs.setdefault("name", sensor_catalog.name)
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
class FarmHubCreateSerializer(serializers.ModelSerializer):
|
||||
area_geojson = serializers.JSONField(write_only=True, required=False)
|
||||
@@ -82,13 +117,18 @@ class FarmHubCreateSerializer(serializers.ModelSerializer):
|
||||
fields = [
|
||||
"name",
|
||||
"is_active",
|
||||
"customization",
|
||||
"farm_type_uuid",
|
||||
"product_uuids",
|
||||
"sensors",
|
||||
"area_geojson",
|
||||
]
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if hasattr(data, "copy"):
|
||||
data = data.copy()
|
||||
data.pop("farm_uuid", None)
|
||||
return super().to_internal_value(data)
|
||||
|
||||
def validate_area_geojson(self, value):
|
||||
if not isinstance(value, dict):
|
||||
raise serializers.ValidationError("`area_geojson` must be a GeoJSON object.")
|
||||
|
||||
+11
-7
@@ -1,21 +1,25 @@
|
||||
from django.db import transaction
|
||||
|
||||
from crop_zoning.services import create_zones_and_dispatch, get_initial_zones_payload, normalize_area_feature
|
||||
from crop_zoning.services import (
|
||||
create_zones_and_dispatch,
|
||||
get_default_area_feature,
|
||||
get_initial_zones_payload,
|
||||
normalize_area_feature,
|
||||
)
|
||||
|
||||
|
||||
def dispatch_farm_zoning(area_feature, farm):
|
||||
crop_area, _zones = create_zones_and_dispatch(normalize_area_feature(area_feature), farm=farm)
|
||||
return get_initial_zones_payload(crop_area)
|
||||
return crop_area, get_initial_zones_payload(crop_area)
|
||||
|
||||
|
||||
def create_farm_with_zoning(serializer, owner):
|
||||
area_feature = serializer.validated_data.pop("area_geojson", None)
|
||||
area_feature = serializer.validated_data.pop("area_geojson", None) or get_default_area_feature()
|
||||
|
||||
with transaction.atomic():
|
||||
farm = serializer.save(owner=owner)
|
||||
zoning_payload = None
|
||||
|
||||
if area_feature is not None:
|
||||
zoning_payload = dispatch_farm_zoning(area_feature, farm)
|
||||
crop_area, zoning_payload = dispatch_farm_zoning(area_feature, farm)
|
||||
farm.current_crop_area = crop_area
|
||||
farm.save(update_fields=["current_crop_area", "updated_at"])
|
||||
|
||||
return farm, zoning_payload
|
||||
|
||||
+118
-3
@@ -5,7 +5,8 @@ from rest_framework.test import APIRequestFactory, force_authenticate
|
||||
from crop_zoning.models import CropArea
|
||||
from farm_hub.models import FarmType, Product
|
||||
from farm_hub.seeds import seed_admin_farm
|
||||
from farm_hub.views import FarmListCreateView
|
||||
from farm_hub.views import FarmListCreateView, FarmTypeListView, FarmTypeProductsView
|
||||
from sensor_catalog.models import SensorCatalog
|
||||
|
||||
|
||||
AREA_GEOJSON = {
|
||||
@@ -40,22 +41,27 @@ class FarmListCreateViewTests(TestCase):
|
||||
)
|
||||
self.farm_type, _ = FarmType.objects.get_or_create(name="زراعی")
|
||||
self.wheat, _ = Product.objects.get_or_create(farm_type=self.farm_type, name="گندم")
|
||||
self.weather_station, _ = SensorCatalog.objects.get_or_create(
|
||||
name="Sensor 7 - Soil Moisture Sensor v1.2",
|
||||
defaults={"supported_power_sources": ["solar", "direct_power"]},
|
||||
)
|
||||
|
||||
def test_create_farm_with_area_geojson_creates_crop_zoning_payload(self):
|
||||
physical_device_uuid = "33333333-3333-3333-3333-333333333333"
|
||||
request = self.factory.post(
|
||||
"/api/farm-hub/",
|
||||
{
|
||||
"name": "farm-1",
|
||||
"farm_type_uuid": str(self.farm_type.uuid),
|
||||
"product_uuids": [str(self.wheat.uuid)],
|
||||
"customization": {"report_interval_sec": 300},
|
||||
"sensors": [
|
||||
{
|
||||
"sensor_catalog_uuid": str(self.weather_station.uuid),
|
||||
"physical_device_uuid": physical_device_uuid,
|
||||
"name": "zone-sensor",
|
||||
"sensor_type": "weather_station",
|
||||
"specifications": {"model": "FH-1"},
|
||||
"power_source": {"type": "battery"},
|
||||
"customization": {"report_interval_sec": 300},
|
||||
}
|
||||
],
|
||||
"area_geojson": AREA_GEOJSON,
|
||||
@@ -70,7 +76,10 @@ class FarmListCreateViewTests(TestCase):
|
||||
self.assertEqual(response.data["code"], 201)
|
||||
self.assertEqual(response.data["data"]["name"], "farm-1")
|
||||
self.assertIn("zoning", response.data["data"])
|
||||
self.assertIsNotNone(response.data["data"]["area_uuid"])
|
||||
self.assertEqual(len(response.data["data"]["sensors"]), 1)
|
||||
self.assertEqual(response.data["data"]["sensors"][0]["sensor_catalog_uuid"], str(self.weather_station.uuid))
|
||||
self.assertEqual(response.data["data"]["sensors"][0]["physical_device_uuid"], physical_device_uuid)
|
||||
self.assertGreater(response.data["data"]["zoning"]["zone_count"], 1)
|
||||
self.assertEqual(
|
||||
response.data["data"]["zoning"]["zone_count"],
|
||||
@@ -78,6 +87,48 @@ class FarmListCreateViewTests(TestCase):
|
||||
)
|
||||
self.assertEqual(CropArea.objects.count(), 1)
|
||||
|
||||
def test_create_farm_ignores_client_farm_uuid_and_generates_new_one(self):
|
||||
request = self.factory.post(
|
||||
"/api/farm-hub/",
|
||||
{
|
||||
"farm_uuid": "11111111-1111-1111-1111-111111111111",
|
||||
"name": "farm-2",
|
||||
"farm_type_uuid": str(self.farm_type.uuid),
|
||||
"product_uuids": [str(self.wheat.uuid)],
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
force_authenticate(request, user=self.user)
|
||||
|
||||
response = FarmListCreateView.as_view()(request)
|
||||
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertNotEqual(response.data["data"]["farm_uuid"], "11111111-1111-1111-1111-111111111111")
|
||||
self.assertIsNotNone(response.data["data"]["area_uuid"])
|
||||
|
||||
def test_create_farm_rejects_unknown_sensor_catalog_uuid(self):
|
||||
request = self.factory.post(
|
||||
"/api/farm-hub/",
|
||||
{
|
||||
"name": "farm-3",
|
||||
"farm_type_uuid": str(self.farm_type.uuid),
|
||||
"product_uuids": [str(self.wheat.uuid)],
|
||||
"sensors": [
|
||||
{
|
||||
"sensor_catalog_uuid": "44444444-4444-4444-4444-444444444444",
|
||||
"name": "zone-sensor",
|
||||
}
|
||||
],
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
force_authenticate(request, user=self.user)
|
||||
|
||||
response = FarmListCreateView.as_view()(request)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn("sensor_catalog_uuid", response.data["sensors"][0])
|
||||
|
||||
|
||||
@override_settings(
|
||||
USE_EXTERNAL_API_MOCK=True,
|
||||
@@ -85,14 +136,23 @@ class FarmListCreateViewTests(TestCase):
|
||||
)
|
||||
class FarmSeedTests(TestCase):
|
||||
def test_seed_admin_farm_dispatches_crop_logic_flow_on_create(self):
|
||||
SensorCatalog.objects.get_or_create(
|
||||
name="Sensor 7 - Soil Moisture Sensor v1.2",
|
||||
defaults={"supported_power_sources": ["solar", "direct_power"]},
|
||||
)
|
||||
farm, created = seed_admin_farm()
|
||||
|
||||
self.assertTrue(created)
|
||||
self.assertEqual(farm.farm_uuid.hex, "11111111111111111111111111111111")
|
||||
self.assertEqual(CropArea.objects.count(), 1)
|
||||
self.assertEqual(farm.sensors.count(), 2)
|
||||
self.assertIsNotNone(farm.sensors.first().physical_device_uuid)
|
||||
|
||||
def test_seed_admin_farm_does_not_dispatch_twice_for_existing_seed(self):
|
||||
SensorCatalog.objects.get_or_create(
|
||||
name="Sensor 7 - Soil Moisture Sensor v1.2",
|
||||
defaults={"supported_power_sources": ["solar", "direct_power"]},
|
||||
)
|
||||
first_farm, first_created = seed_admin_farm()
|
||||
second_farm, second_created = seed_admin_farm()
|
||||
|
||||
@@ -100,3 +160,58 @@ class FarmSeedTests(TestCase):
|
||||
self.assertFalse(second_created)
|
||||
self.assertEqual(first_farm.id, second_farm.id)
|
||||
self.assertEqual(CropArea.objects.count(), 1)
|
||||
|
||||
|
||||
class FarmCatalogViewsTests(TestCase):
|
||||
def setUp(self):
|
||||
self.factory = APIRequestFactory()
|
||||
self.user = get_user_model().objects.create_user(
|
||||
username="catalog-user",
|
||||
password="secret123",
|
||||
email="catalog@example.com",
|
||||
phone_number="09120000001",
|
||||
)
|
||||
self.field_farm_type = FarmType.objects.create(name="زراعی")
|
||||
self.tree_farm_type = FarmType.objects.create(name="درختی")
|
||||
self.wheat = Product.objects.create(
|
||||
farm_type=self.field_farm_type,
|
||||
name="گندم",
|
||||
planting_season="پاییز",
|
||||
harvest_time="بهار",
|
||||
health_profile={"moisture": {"ideal_value": 65}},
|
||||
)
|
||||
self.corn = Product.objects.create(farm_type=self.field_farm_type, name="ذرت")
|
||||
Product.objects.create(farm_type=self.tree_farm_type, name="سیب")
|
||||
|
||||
def test_farm_type_list_returns_all_farm_types(self):
|
||||
request = self.factory.get("/api/farm-hub/farm-types/")
|
||||
force_authenticate(request, user=self.user)
|
||||
|
||||
response = FarmTypeListView.as_view()(request)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data["code"], 200)
|
||||
self.assertEqual(len(response.data["data"]), 2)
|
||||
|
||||
def test_farm_type_products_returns_products_for_selected_type(self):
|
||||
request = self.factory.get(f"/api/farm-hub/farm-types/{self.field_farm_type.uuid}/products/")
|
||||
force_authenticate(request, user=self.user)
|
||||
|
||||
response = FarmTypeProductsView.as_view()(request, farm_type_uuid=self.field_farm_type.uuid)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data["code"], 200)
|
||||
self.assertEqual({item["name"] for item in response.data["data"]}, {self.wheat.name, self.corn.name})
|
||||
wheat_payload = next(item for item in response.data["data"] if item["name"] == self.wheat.name)
|
||||
self.assertEqual(wheat_payload["planting_season"], "پاییز")
|
||||
self.assertEqual(wheat_payload["health_profile"]["moisture"]["ideal_value"], 65)
|
||||
|
||||
def test_farm_type_products_returns_404_for_unknown_type(self):
|
||||
unknown_farm_type_uuid = "11111111-1111-1111-1111-111111111111"
|
||||
request = self.factory.get(f"/api/farm-hub/farm-types/{unknown_farm_type_uuid}/products/")
|
||||
force_authenticate(request, user=self.user)
|
||||
|
||||
response = FarmTypeProductsView.as_view()(request, farm_type_uuid=unknown_farm_type_uuid)
|
||||
|
||||
self.assertEqual(response.status_code, 404)
|
||||
self.assertEqual(response.data["msg"], "Farm type not found.")
|
||||
|
||||
+10
-1
@@ -1,10 +1,19 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import FarmActiveView, FarmDeactiveView, FarmDetailView, FarmListCreateView
|
||||
from .views import (
|
||||
FarmActiveView,
|
||||
FarmDeactiveView,
|
||||
FarmDetailView,
|
||||
FarmListCreateView,
|
||||
FarmTypeListView,
|
||||
FarmTypeProductsView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("active/", FarmActiveView.as_view(), name="farm-hub-active"),
|
||||
path("deactive/", FarmDeactiveView.as_view(), name="farm-hub-deactive"),
|
||||
path("farm-types/", FarmTypeListView.as_view(), name="farm-type-list"),
|
||||
path("farm-types/<uuid:farm_type_uuid>/products/", FarmTypeProductsView.as_view(), name="farm-type-products"),
|
||||
path("<uuid:farm_uuid>/", FarmDetailView.as_view(), name="farm-hub-detail"),
|
||||
path("", FarmListCreateView.as_view(), name="farm-hub-list"),
|
||||
]
|
||||
|
||||
+44
-4
@@ -6,8 +6,14 @@ from rest_framework.views import APIView
|
||||
from drf_spectacular.utils import extend_schema
|
||||
|
||||
from config.swagger import code_response
|
||||
from .models import FarmHub
|
||||
from .serializers import FarmHubCreateSerializer, FarmHubSerializer, FarmToggleSerializer
|
||||
from .models import FarmHub, FarmType, Product
|
||||
from .serializers import (
|
||||
FarmHubCreateSerializer,
|
||||
FarmHubSerializer,
|
||||
FarmToggleSerializer,
|
||||
FarmTypeSerializer,
|
||||
ProductSerializer,
|
||||
)
|
||||
from .services import create_farm_with_zoning
|
||||
|
||||
|
||||
@@ -16,7 +22,10 @@ class FarmHubBaseView(APIView):
|
||||
|
||||
def _get_farm(self, request, farm_uuid):
|
||||
try:
|
||||
return FarmHub.objects.prefetch_related("products", "sensors").select_related("farm_type").get(
|
||||
return FarmHub.objects.prefetch_related("products", "sensors", "sensors__sensor_catalog").select_related(
|
||||
"farm_type",
|
||||
"current_crop_area",
|
||||
).get(
|
||||
farm_uuid=farm_uuid,
|
||||
owner=request.user,
|
||||
)
|
||||
@@ -30,9 +39,10 @@ class FarmListCreateView(FarmHubBaseView):
|
||||
responses={200: code_response("FarmListResponse", data=FarmHubSerializer(many=True))},
|
||||
)
|
||||
def get(self, request):
|
||||
farms = FarmHub.objects.filter(owner=request.user).select_related("farm_type").prefetch_related(
|
||||
farms = FarmHub.objects.filter(owner=request.user).select_related("farm_type", "current_crop_area").prefetch_related(
|
||||
"products",
|
||||
"sensors",
|
||||
"sensors__sensor_catalog",
|
||||
)
|
||||
data = FarmHubSerializer(farms, many=True).data
|
||||
return Response({"code": 200, "msg": "success", "data": data}, status=status.HTTP_200_OK)
|
||||
@@ -57,6 +67,36 @@ class FarmListCreateView(FarmHubBaseView):
|
||||
return Response({"code": 201, "msg": "success", "data": data}, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class FarmTypeListView(FarmHubBaseView):
|
||||
@extend_schema(
|
||||
tags=["Farm Hub"],
|
||||
responses={200: code_response("FarmTypeListResponse", data=FarmTypeSerializer(many=True))},
|
||||
)
|
||||
def get(self, request):
|
||||
farm_types = FarmType.objects.order_by("name")
|
||||
data = FarmTypeSerializer(farm_types, many=True).data
|
||||
return Response({"code": 200, "msg": "success", "data": data}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class FarmTypeProductsView(FarmHubBaseView):
|
||||
@extend_schema(
|
||||
tags=["Farm Hub"],
|
||||
responses={
|
||||
200: code_response("FarmTypeProductsResponse", data=ProductSerializer(many=True)),
|
||||
404: code_response("FarmTypeProductsNotFoundResponse"),
|
||||
},
|
||||
)
|
||||
def get(self, request, farm_type_uuid):
|
||||
try:
|
||||
farm_type = FarmType.objects.get(uuid=farm_type_uuid)
|
||||
except FarmType.DoesNotExist:
|
||||
return Response({"code": 404, "msg": "Farm type not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
products = Product.objects.filter(farm_type=farm_type).order_by("name")
|
||||
data = ProductSerializer(products, many=True).data
|
||||
return Response({"code": 200, "msg": "success", "data": data}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class FarmDetailView(FarmHubBaseView):
|
||||
@extend_schema(
|
||||
tags=["Farm Hub"],
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PlantConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "plant"
|
||||
@@ -1,35 +0,0 @@
|
||||
# Generated by Django 5.2.12 on 2026-03-19 15:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Plant',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(db_index=True, help_text='نام گیاه', max_length=255, unique=True)),
|
||||
('light', models.CharField(blank=True, help_text='نور مورد نیاز', max_length=255)),
|
||||
('watering', models.CharField(blank=True, help_text='آبیاری', max_length=255)),
|
||||
('soil', models.CharField(blank=True, help_text='خاک مناسب', max_length=255)),
|
||||
('temperature', models.CharField(blank=True, help_text='دمای مناسب', max_length=255)),
|
||||
('planting_season', models.CharField(blank=True, help_text='فصل کاشت', max_length=255)),
|
||||
('harvest_time', models.CharField(blank=True, help_text='زمان برداشت', max_length=255)),
|
||||
('spacing', models.CharField(blank=True, help_text='فاصله کاشت', max_length=255)),
|
||||
('fertilizer', models.CharField(blank=True, help_text='کود مناسب', max_length=255)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'گیاه',
|
||||
'verbose_name_plural': 'گیاهان',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("plant", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="plant",
|
||||
name="health_profile",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text=(
|
||||
"پروفایل سلامت گیاه برای KPIها. ساختار نمونه: "
|
||||
'{"moisture": {"ideal_value": 65, "min_range": 45, "max_range": 75, "weight": 0.4}}'
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("plant", "0002_plant_health_profile"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="plant",
|
||||
name="irrigation_profile",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text=(
|
||||
"پروفایل آبیاری گیاه برای محاسبات ETc. "
|
||||
'{"kc_initial": 0.6, "kc_mid": 1.15, "kc_end": 0.8, '
|
||||
'"growth_stage_duration": {"initial": 20, "mid": 30, "late": 25}}'
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("plant", "0003_plant_irrigation_profile"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="plant",
|
||||
name="growth_profile",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text=(
|
||||
"پروفایل رشد گیاه برای مدل GDD. "
|
||||
'{"base_temperature": 10, "required_gdd_for_maturity": 1200, '
|
||||
'"stage_thresholds": {"flowering": 500, "fruiting": 850}, "current_cumulative_gdd": 320}'
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,49 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Plant(models.Model):
|
||||
name = models.CharField(max_length=255, unique=True, db_index=True, help_text="نام گیاه")
|
||||
light = models.CharField(max_length=255, blank=True, help_text="نور مورد نیاز")
|
||||
watering = models.CharField(max_length=255, blank=True, help_text="آبیاری")
|
||||
soil = models.CharField(max_length=255, blank=True, help_text="خاک مناسب")
|
||||
temperature = models.CharField(max_length=255, blank=True, help_text="دمای مناسب")
|
||||
planting_season = models.CharField(max_length=255, blank=True, help_text="فصل کاشت")
|
||||
harvest_time = models.CharField(max_length=255, blank=True, help_text="زمان برداشت")
|
||||
spacing = models.CharField(max_length=255, blank=True, help_text="فاصله کاشت")
|
||||
fertilizer = models.CharField(max_length=255, blank=True, help_text="کود مناسب")
|
||||
health_profile = models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text=(
|
||||
"پروفایل سلامت گیاه برای KPIها. ساختار نمونه: "
|
||||
'{"moisture": {"ideal_value": 65, "min_range": 45, "max_range": 75, "weight": 0.4}}'
|
||||
),
|
||||
)
|
||||
irrigation_profile = models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text=(
|
||||
"پروفایل آبیاری گیاه برای محاسبات ETc. "
|
||||
'{"kc_initial": 0.6, "kc_mid": 1.15, "kc_end": 0.8, '
|
||||
'"growth_stage_duration": {"initial": 20, "mid": 30, "late": 25}}'
|
||||
),
|
||||
)
|
||||
growth_profile = models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text=(
|
||||
"پروفایل رشد گیاه برای مدل GDD. "
|
||||
'{"base_temperature": 10, "required_gdd_for_maturity": 1200, '
|
||||
'"stage_thresholds": {"flowering": 500, "fruiting": 850}, "current_cumulative_gdd": 320}'
|
||||
),
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "گیاه"
|
||||
verbose_name_plural = "گیاهان"
|
||||
ordering = ["name"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SensorCatalogConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "sensor_catalog"
|
||||
verbose_name = "Sensor Catalog"
|
||||
@@ -0,0 +1,57 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from sensor_catalog.models import SensorCatalog
|
||||
|
||||
|
||||
SENSOR_CATALOG_ITEMS = [
|
||||
{
|
||||
"name": "Sensor 7 - Soil Moisture Sensor v1.2",
|
||||
"description": (
|
||||
"This sensor is typically the YL-69 or FC-28 soil moisture sensor. "
|
||||
"It measures only soil moisture and provides analog and digital outputs. "
|
||||
"It does not report soil temperature, pH, or nutrients."
|
||||
),
|
||||
"customizable_fields": [],
|
||||
"supported_power_sources": ["solar", "direct_power"],
|
||||
"returned_data_fields": ["soil_moisture", "analog_output", "digital_output"],
|
||||
"sample_payload": {
|
||||
"soil_moisture": 42,
|
||||
"analog_output": 610,
|
||||
"digital_output": 1,
|
||||
},
|
||||
"is_active": True,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Seed sensor catalog data."
|
||||
|
||||
def handle(self, *args, **options):
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
|
||||
for item in SENSOR_CATALOG_ITEMS:
|
||||
sensor, created = SensorCatalog.objects.update_or_create(
|
||||
name=item["name"],
|
||||
defaults={
|
||||
"description": item["description"],
|
||||
"customizable_fields": item["customizable_fields"],
|
||||
"supported_power_sources": item["supported_power_sources"],
|
||||
"returned_data_fields": item["returned_data_fields"],
|
||||
"sample_payload": item["sample_payload"],
|
||||
"is_active": item["is_active"],
|
||||
},
|
||||
)
|
||||
if created:
|
||||
created_count += 1
|
||||
self.stdout.write(self.style.SUCCESS(f"Created sensor catalog item: {sensor.name}"))
|
||||
else:
|
||||
updated_count += 1
|
||||
self.stdout.write(self.style.WARNING(f"Updated sensor catalog item: {sensor.name}"))
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Sensor catalog seeding complete. Created: {created_count}, Updated: {updated_count}"
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
# Generated by Django 5.2.12 on 2026-03-20 00:00
|
||||
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="SensorCatalog",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("uuid", models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, unique=True)),
|
||||
("name", models.CharField(db_index=True, max_length=255, unique=True)),
|
||||
("description", models.TextField(blank=True, default="")),
|
||||
("customizable_fields", models.JSONField(blank=True, default=list)),
|
||||
("returned_data_fields", models.JSONField(blank=True, default=list)),
|
||||
("sample_payload", models.JSONField(blank=True, default=dict)),
|
||||
("is_active", models.BooleanField(default=True)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
"db_table": "sensor_catalogs",
|
||||
"ordering": ["name"],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.2.12 on 2026-03-20 01:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("sensor_catalog", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="sensorcatalog",
|
||||
name="supported_power_sources",
|
||||
field=models.JSONField(blank=True, default=list),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class SensorCatalog(models.Model):
|
||||
uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False, db_index=True)
|
||||
name = models.CharField(max_length=255, unique=True, db_index=True)
|
||||
description = models.TextField(blank=True, default="")
|
||||
customizable_fields = models.JSONField(default=list, blank=True)
|
||||
supported_power_sources = models.JSONField(default=list, blank=True)
|
||||
returned_data_fields = models.JSONField(default=list, blank=True)
|
||||
sample_payload = models.JSONField(default=dict, blank=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "sensor_catalogs"
|
||||
ordering = ["name"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -0,0 +1,19 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import SensorCatalog
|
||||
|
||||
|
||||
class SensorCatalogSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = SensorCatalog
|
||||
fields = [
|
||||
"uuid",
|
||||
"name",
|
||||
"description",
|
||||
"customizable_fields",
|
||||
"supported_power_sources",
|
||||
"returned_data_fields",
|
||||
"sample_payload",
|
||||
"is_active",
|
||||
]
|
||||
read_only_fields = fields
|
||||
@@ -0,0 +1,55 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from rest_framework.test import APIRequestFactory, force_authenticate
|
||||
|
||||
from sensor_catalog.models import SensorCatalog
|
||||
from sensor_catalog.views import SensorCatalogListView
|
||||
|
||||
|
||||
class SensorCatalogListViewTests(TestCase):
|
||||
def setUp(self):
|
||||
self.factory = APIRequestFactory()
|
||||
self.user = get_user_model().objects.create_user(
|
||||
username="sensor-user",
|
||||
password="secret123",
|
||||
email="sensor@example.com",
|
||||
phone_number="09120000002",
|
||||
)
|
||||
SensorCatalog.objects.update_or_create(
|
||||
name="Sensor 7 - Soil Moisture Sensor v1.2",
|
||||
defaults={
|
||||
"description": (
|
||||
"Measures only soil moisture using electrical resistance between two metal probes. "
|
||||
"Provides analog and digital outputs."
|
||||
),
|
||||
"customizable_fields": [],
|
||||
"supported_power_sources": ["solar", "direct_power"],
|
||||
"returned_data_fields": ["soil_moisture", "analog_output", "digital_output"],
|
||||
"sample_payload": {"soil_moisture": 42, "analog_output": 610, "digital_output": 1},
|
||||
"is_active": True,
|
||||
},
|
||||
)
|
||||
SensorCatalog.objects.update_or_create(
|
||||
name="Legacy Sensor",
|
||||
defaults={
|
||||
"customizable_fields": [],
|
||||
"supported_power_sources": ["direct_power"],
|
||||
"returned_data_fields": ["status"],
|
||||
"sample_payload": {"status": "offline"},
|
||||
"is_active": False,
|
||||
},
|
||||
)
|
||||
|
||||
def test_list_returns_all_existing_sensors(self):
|
||||
request = self.factory.get("/api/sensor-catalog/")
|
||||
force_authenticate(request, user=self.user)
|
||||
|
||||
response = SensorCatalogListView.as_view()(request)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data["code"], 200)
|
||||
self.assertEqual(len(response.data["data"]), 2)
|
||||
self.assertEqual(
|
||||
{item["name"] for item in response.data["data"]},
|
||||
{"Sensor 7 - Soil Moisture Sensor v1.2", "Legacy Sensor"},
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import SensorCatalogListView
|
||||
|
||||
urlpatterns = [
|
||||
path("", SensorCatalogListView.as_view(), name="sensor-catalog-list"),
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from drf_spectacular.utils import extend_schema
|
||||
|
||||
from config.swagger import code_response
|
||||
from .models import SensorCatalog
|
||||
from .serializers import SensorCatalogSerializer
|
||||
|
||||
|
||||
class SensorCatalogListView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@extend_schema(
|
||||
tags=["Sensor Catalog"],
|
||||
responses={200: code_response("SensorCatalogListResponse", data=SensorCatalogSerializer(many=True))},
|
||||
)
|
||||
def get(self, request):
|
||||
sensors = SensorCatalog.objects.order_by("name")
|
||||
data = SensorCatalogSerializer(sensors, many=True).data
|
||||
return Response({"code": 200, "msg": "success", "data": data}, status=status.HTTP_200_OK)
|
||||
Reference in New Issue
Block a user