UPDATE AUTH
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.backends import ModelBackend
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class MultiFieldBackend(ModelBackend):
|
||||||
|
"""
|
||||||
|
Authenticate against username, email, or phone_number.
|
||||||
|
Used for password-based login where the user can enter any of the three.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||||
|
if username is None or password is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = User.objects.get(
|
||||||
|
Q(username=username) | Q(email=username) | Q(phone_number=username)
|
||||||
|
)
|
||||||
|
except (User.DoesNotExist, User.MultipleObjectsReturned):
|
||||||
|
User().set_password(password)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if user.check_password(password) and self.user_can_authenticate(user):
|
||||||
|
return user
|
||||||
|
return None
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 5.1.15 on 2026-03-23 18:48
|
||||||
|
|
||||||
|
import account.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('account', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelManagers(
|
||||||
|
name='user',
|
||||||
|
managers=[
|
||||||
|
('objects', account.models.CustomUserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='email',
|
||||||
|
field=models.EmailField(db_index=True, max_length=254, unique=True, verbose_name='email address'),
|
||||||
|
),
|
||||||
|
]
|
||||||
+21
-3
@@ -1,19 +1,37 @@
|
|||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.contrib.auth.models import UserManager as BaseUserManager
|
||||||
|
from django.db.models import Q
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserManager(BaseUserManager):
|
||||||
|
"""Manager that allows lookup by username, email, or phone_number."""
|
||||||
|
|
||||||
|
def get_by_natural_key(self, username):
|
||||||
|
return self.get(
|
||||||
|
Q(username=username) | Q(email=username) | Q(phone_number=username)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractUser):
|
class User(AbstractUser):
|
||||||
phone_number = models.CharField(
|
phone_number = models.CharField(
|
||||||
max_length=32,
|
max_length=32,
|
||||||
unique=True,
|
unique=True,
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
|
email = models.EmailField(
|
||||||
|
"email address",
|
||||||
|
unique=True,
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
|
||||||
USERNAME_FIELD = "phone_number"
|
USERNAME_FIELD = "username"
|
||||||
REQUIRED_FIELDS = ["username"]
|
REQUIRED_FIELDS = ["email", "phone_number"]
|
||||||
|
|
||||||
|
objects = CustomUserManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = "users"
|
db_table = "users"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.phone_number
|
return self.username
|
||||||
|
|||||||
+21
-1
@@ -1,6 +1,27 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
# --- Register ---
|
||||||
|
class RegisterSerializer(serializers.Serializer):
|
||||||
|
"""Request body for POST /api/auth/register/."""
|
||||||
|
|
||||||
|
username = serializers.CharField(max_length=150)
|
||||||
|
email = serializers.EmailField()
|
||||||
|
phone_number = serializers.CharField(max_length=32)
|
||||||
|
password = serializers.CharField(min_length=8, write_only=True)
|
||||||
|
first_name = serializers.CharField(max_length=150, required=False, default="")
|
||||||
|
last_name = serializers.CharField(max_length=150, required=False, default="")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Login ---
|
||||||
|
class LoginSerializer(serializers.Serializer):
|
||||||
|
"""Request body for POST /api/auth/login/.
|
||||||
|
identifier can be username, email, or phone_number."""
|
||||||
|
|
||||||
|
identifier = serializers.CharField()
|
||||||
|
password = serializers.CharField()
|
||||||
|
|
||||||
|
|
||||||
# --- RequestOTP (request-otp/) ---
|
# --- RequestOTP (request-otp/) ---
|
||||||
class RequestOTPSerializer(serializers.Serializer):
|
class RequestOTPSerializer(serializers.Serializer):
|
||||||
"""Request body for POST /api/auth/request-otp/."""
|
"""Request body for POST /api/auth/request-otp/."""
|
||||||
@@ -26,4 +47,3 @@ class AuthUserSerializer(serializers.Serializer):
|
|||||||
first_name = serializers.CharField()
|
first_name = serializers.CharField()
|
||||||
last_name = serializers.CharField()
|
last_name = serializers.CharField()
|
||||||
phone_number = serializers.CharField()
|
phone_number = serializers.CharField()
|
||||||
|
|
||||||
|
|||||||
+5
-4
@@ -1,9 +1,10 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import AuthenticationView
|
from .views import AuthenticationView, LoginView, RegisterView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("request-otp/", AuthenticationView.as_view(), name="request-otp"),
|
path("register/", RegisterView.as_view(), name="register"),
|
||||||
path("verify-otp/", AuthenticationView.as_view(), name="verify-otp"),
|
path("login/", LoginView.as_view(), name="login"),
|
||||||
|
# path("request-otp/", AuthenticationView.as_view(), name="request-otp"),
|
||||||
|
# path("verify-otp/", AuthenticationView.as_view(), name="verify-otp"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+105
-2
@@ -1,15 +1,22 @@
|
|||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
|
from django.contrib.auth import authenticate
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.signing import BadSignature, SignatureExpired, TimestampSigner
|
from django.core.signing import BadSignature, SignatureExpired, TimestampSigner
|
||||||
|
from django.db import IntegrityError
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework_simplejwt.tokens import RefreshToken
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
|
||||||
from account.models import User
|
from account.models import User
|
||||||
from .serializers import RequestOTPSerializer, VerifyOTPSerializer
|
from .serializers import (
|
||||||
|
LoginSerializer,
|
||||||
|
RegisterSerializer,
|
||||||
|
RequestOTPSerializer,
|
||||||
|
VerifyOTPSerializer,
|
||||||
|
)
|
||||||
from .sms_service import send_otp_sms
|
from .sms_service import send_otp_sms
|
||||||
|
|
||||||
|
|
||||||
@@ -31,6 +38,99 @@ def _auth_user_to_data(user):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterView(APIView):
|
||||||
|
"""
|
||||||
|
POST /api/auth/register/
|
||||||
|
Creates a new user with username, email, phone_number, and password.
|
||||||
|
All fields are required (first_name, last_name optional).
|
||||||
|
Returns JWT tokens and user data on success.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
serializer = RegisterSerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
data = serializer.validated_data
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = User.objects.create_user(
|
||||||
|
username=data["username"],
|
||||||
|
email=data["email"],
|
||||||
|
phone_number=data["phone_number"],
|
||||||
|
password=data["password"],
|
||||||
|
first_name=data.get("first_name", ""),
|
||||||
|
last_name=data.get("last_name", ""),
|
||||||
|
)
|
||||||
|
except IntegrityError as exc:
|
||||||
|
msg = str(exc).lower()
|
||||||
|
if "username" in msg:
|
||||||
|
detail = "A user with this username already exists."
|
||||||
|
elif "email" in msg:
|
||||||
|
detail = "A user with this email already exists."
|
||||||
|
elif "phone_number" in msg:
|
||||||
|
detail = "A user with this phone number already exists."
|
||||||
|
else:
|
||||||
|
detail = "A user with these credentials already exists."
|
||||||
|
return Response(
|
||||||
|
{"code": 400, "msg": detail},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
refresh = RefreshToken.for_user(user)
|
||||||
|
user_data = _auth_user_to_data(user)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"code": 201,
|
||||||
|
"msg": "success",
|
||||||
|
"data": user_data,
|
||||||
|
"token": {
|
||||||
|
"access": str(refresh.access_token),
|
||||||
|
"refresh": str(refresh),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LoginView(APIView):
|
||||||
|
"""
|
||||||
|
POST /api/auth/login/
|
||||||
|
Accepts identifier (username, email, or phone_number) + password.
|
||||||
|
Returns JWT tokens and user data on success.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
serializer = LoginSerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
identifier = serializer.validated_data["identifier"]
|
||||||
|
password = serializer.validated_data["password"]
|
||||||
|
|
||||||
|
user = authenticate(request, username=identifier, password=password)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
return Response(
|
||||||
|
{"code": 401, "msg": "Invalid credentials."},
|
||||||
|
status=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
|
||||||
|
refresh = RefreshToken.for_user(user)
|
||||||
|
user_data = _auth_user_to_data(user)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": user_data,
|
||||||
|
"token": {
|
||||||
|
"access": str(refresh.access_token),
|
||||||
|
"refresh": str(refresh),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationView(APIView):
|
class AuthenticationView(APIView):
|
||||||
"""
|
"""
|
||||||
Single view for auth flows: request-otp and verify-otp.
|
Single view for auth flows: request-otp and verify-otp.
|
||||||
@@ -91,7 +191,10 @@ class AuthenticationView(APIView):
|
|||||||
|
|
||||||
user, created = User.objects.get_or_create(
|
user, created = User.objects.get_or_create(
|
||||||
phone_number=phone_number,
|
phone_number=phone_number,
|
||||||
defaults={"username": phone_number},
|
defaults={
|
||||||
|
"username": phone_number,
|
||||||
|
"email": f"{phone_number}@otp.local",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
refresh = RefreshToken.for_user(user)
|
refresh = RefreshToken.for_user(user)
|
||||||
|
|||||||
+5
-1
@@ -13,6 +13,10 @@ ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost,127.0.0.1").split(","
|
|||||||
|
|
||||||
AUTH_USER_MODEL = "account.User"
|
AUTH_USER_MODEL = "account.User"
|
||||||
|
|
||||||
|
AUTHENTICATION_BACKENDS = [
|
||||||
|
"account.backends.MultiFieldBackend",
|
||||||
|
]
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"django.contrib.admin",
|
"django.contrib.admin",
|
||||||
"django.contrib.auth",
|
"django.contrib.auth",
|
||||||
@@ -22,7 +26,7 @@ INSTALLED_APPS = [
|
|||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
"auth.apps.AuthConfig",
|
"auth.apps.AuthConfig",
|
||||||
"account.apps.AccountConfig",
|
"account.apps.AccountConfig",
|
||||||
"sensor_hub",
|
"sensor_hub.apps.SensorHubConfig",
|
||||||
"dashboard",
|
"dashboard",
|
||||||
"crop_zoning",
|
"crop_zoning",
|
||||||
"plant_simulator",
|
"plant_simulator",
|
||||||
|
|||||||
@@ -417,10 +417,10 @@ ECONOMIC_OVERVIEW = {
|
|||||||
|
|
||||||
# Unified response for GET /api/farm-dashboard (section 5)
|
# Unified response for GET /api/farm-dashboard (section 5)
|
||||||
ALL_CARDS = {
|
ALL_CARDS = {
|
||||||
"farmOverviewKpis": FARM_OVERVIEW_KPIS,
|
"farmOverviewKpis": FARM_OVERVIEW_KPIS , # این باید سه روز یکبار محتواش محاسبه بشه
|
||||||
"farmWeatherCard": FARM_WEATHER_CARD,
|
"farmWeatherCard": FARM_WEATHER_CARD, # هروز
|
||||||
"farmAlertsTracker": FARM_ALERTS_TRACKER,
|
"farmAlertsTracker": FARM_ALERTS_TRACKER, #هروز
|
||||||
"sensorValuesList": SENSOR_VALUES_LIST,
|
"sensorValuesList": SENSOR_VALUES_LIST,#هروز
|
||||||
"sensorRadarChart": SENSOR_RADAR_CHART,
|
"sensorRadarChart": SENSOR_RADAR_CHART,
|
||||||
"sensorComparisonChart": SENSOR_COMPARISON_CHART,
|
"sensorComparisonChart": SENSOR_COMPARISON_CHART,
|
||||||
"anomalyDetectionCard": ANOMALY_DETECTION_CARD,
|
"anomalyDetectionCard": ANOMALY_DETECTION_CARD,
|
||||||
@@ -430,6 +430,6 @@ ALL_CARDS = {
|
|||||||
"yieldPredictionChart": YIELD_PREDICTION_CHART,
|
"yieldPredictionChart": YIELD_PREDICTION_CHART,
|
||||||
"soilMoistureHeatmap": SOIL_MOISTURE_HEATMAP,
|
"soilMoistureHeatmap": SOIL_MOISTURE_HEATMAP,
|
||||||
"ndviHealthCard": NDVI_HEALTH_CARD,
|
"ndviHealthCard": NDVI_HEALTH_CARD,
|
||||||
"recommendationsList": RECOMMENDATIONS_LIST,
|
"recommendationsList": RECOMMENDATIONS_LIST, # این باید حتما از recommendetion ها گرفته بشه
|
||||||
"economicOverview": ECONOMIC_OVERVIEW,
|
"economicOverview": ECONOMIC_OVERVIEW,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class SensorHubConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "sensor_hub"
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Generated by Django 5.1.15 on 2026-03-23 18:48
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Sensor',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('uuid_sensor', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, unique=True)),
|
||||||
|
('name', models.CharField(max_length=255)),
|
||||||
|
('is_active', models.BooleanField(default=True)),
|
||||||
|
('specifications', models.JSONField(blank=True, default=dict)),
|
||||||
|
('power_source', models.JSONField(blank=True, default=dict)),
|
||||||
|
('customized_sensors', models.JSONField(blank=True, default=dict)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sensors', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'sensors',
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Sensor(models.Model):
|
||||||
|
uuid_sensor = models.UUIDField(default=uuid.uuid4, unique=True, editable=False, db_index=True)
|
||||||
|
owner = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="sensors",
|
||||||
|
)
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
specifications = models.JSONField(default=dict, blank=True)
|
||||||
|
power_source = models.JSONField(default=dict, blank=True)
|
||||||
|
customized_sensors = models.JSONField(default=dict, blank=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "sensors"
|
||||||
|
ordering = ["-created_at"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name} ({self.uuid_sensor})"
|
||||||
@@ -1,12 +1,32 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
class SensorStoreResponseSerializer(serializers.Serializer):
|
from .models import Sensor
|
||||||
"""Schema for static sensor store response (name, uuid_sensor, last_updated, specifications, power_source, customized_sensors)."""
|
|
||||||
|
|
||||||
name = serializers.CharField()
|
|
||||||
uuid_sensor = serializers.CharField()
|
class SensorSerializer(serializers.ModelSerializer):
|
||||||
last_updated = serializers.CharField()
|
last_updated = serializers.DateTimeField(source="updated_at", read_only=True)
|
||||||
specifications = serializers.JSONField()
|
|
||||||
power_source = serializers.JSONField()
|
class Meta:
|
||||||
customized_sensors = serializers.JSONField()
|
model = Sensor
|
||||||
|
fields = [
|
||||||
|
"uuid_sensor",
|
||||||
|
"name",
|
||||||
|
"is_active",
|
||||||
|
"specifications",
|
||||||
|
"power_source",
|
||||||
|
"customized_sensors",
|
||||||
|
"last_updated",
|
||||||
|
]
|
||||||
|
read_only_fields = ["uuid_sensor", "last_updated"]
|
||||||
|
|
||||||
|
|
||||||
|
class SensorCreateSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Sensor
|
||||||
|
fields = [
|
||||||
|
"name",
|
||||||
|
"specifications",
|
||||||
|
"power_source",
|
||||||
|
"customized_sensors",
|
||||||
|
]
|
||||||
|
|||||||
+77
-72
@@ -1,97 +1,102 @@
|
|||||||
"""
|
|
||||||
Sensor Hub module.
|
|
||||||
All endpoints require authenticated user (must be registered).
|
|
||||||
All responses are static; no processing or validation on inputs.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from .serializers import SensorStoreResponseSerializer
|
from .models import Sensor
|
||||||
|
from .serializers import SensorCreateSerializer, SensorSerializer
|
||||||
# Static sensor payload for store (list/get) response.
|
|
||||||
STORE_DATA = {
|
|
||||||
"name": "sensor-hub-static",
|
|
||||||
"uuid_sensor": "550e8400-e29b-41d4-a716-446655440000",
|
|
||||||
"last_updated": "2025-02-18T12:00:00Z",
|
|
||||||
"specifications": {
|
|
||||||
"model": "SH-1",
|
|
||||||
"firmware": "1.0.0",
|
|
||||||
"capabilities": ["temperature", "humidity", "light"],
|
|
||||||
},
|
|
||||||
"power_source": {
|
|
||||||
"type": "battery",
|
|
||||||
"voltage": 3.3,
|
|
||||||
"backup": "solar",
|
|
||||||
},
|
|
||||||
"customized_sensors": {
|
|
||||||
"thresholds": {"temperature_min": 10, "temperature_max": 35},
|
|
||||||
"report_interval_sec": 300,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Static payload for single-sensor detail response (same shape as store).
|
|
||||||
SENSOR_DETAIL_DATA = {
|
|
||||||
"name": "sensor-hub-static",
|
|
||||||
"uuid_sensor": "550e8400-e29b-41d4-a716-446655440000",
|
|
||||||
"last_updated": "2025-02-18T12:00:00Z",
|
|
||||||
"specifications": {
|
|
||||||
"model": "SH-1",
|
|
||||||
"firmware": "1.0.0",
|
|
||||||
"capabilities": ["temperature", "humidity", "light"],
|
|
||||||
},
|
|
||||||
"power_source": {
|
|
||||||
"type": "battery",
|
|
||||||
"voltage": 3.3,
|
|
||||||
"backup": "solar",
|
|
||||||
},
|
|
||||||
"customized_sensors": {
|
|
||||||
"thresholds": {"temperature_min": 10, "temperature_max": 35},
|
|
||||||
"report_interval_sec": 300,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class SensorHubView(APIView):
|
class SensorHubView(APIView):
|
||||||
"""
|
"""
|
||||||
Sensor-hub endpoints. Behavior depends on URL and HTTP method.
|
Sensor-hub CRUD endpoints connected to the database.
|
||||||
No processing or validation is performed on inputs; responses are static.
|
|
||||||
|
|
||||||
Routes:
|
Routes:
|
||||||
- GET "" → List: returns code 200, msg "success", data with static sensor list.
|
- GET "" → List sensors for authenticated user.
|
||||||
- GET "<uuid>/" → Detail: uuid (path). Returns code 200, msg "success", data with static sensor payload.
|
- GET "<uuid>/" → Detail of a single sensor.
|
||||||
- POST "" → Add: body/query may be sent but not used. Returns code 200, msg "success". No data field.
|
- POST "" → Create a new sensor.
|
||||||
- PATCH "<uuid>/" → Update: uuid (path), body/query may be sent but not used. Returns code 200, msg "success". No data field.
|
- PATCH "<uuid>/" → Update an existing sensor.
|
||||||
- DELETE "<uuid>/" → Delete: uuid (path). Returns code 200, msg "success". No data field.
|
- DELETE "<uuid>/" → Delete a sensor.
|
||||||
- POST "active/" → Activate: no input. Returns code 200, msg "success". No data field.
|
- POST "active/" → Activate a sensor (requires uuid_sensor in body).
|
||||||
- POST "deactive/" → Deactivate: no input. Returns code 200, msg "success". No data field.
|
- POST "deactive/" → Deactivate a sensor (requires uuid_sensor in body).
|
||||||
"""
|
"""
|
||||||
authentication_classes = [] # No authentication
|
|
||||||
permission_classes = [] # No permission
|
permission_classes = [IsAuthenticated]
|
||||||
# permission_classes = [IsAuthenticated]
|
|
||||||
|
def _get_sensor(self, request, uuid):
|
||||||
|
try:
|
||||||
|
return Sensor.objects.get(uuid_sensor=uuid, owner=request.user)
|
||||||
|
except Sensor.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
uuid = kwargs.get("uuid")
|
uuid = kwargs.get("uuid")
|
||||||
if uuid is not None:
|
if uuid is not None:
|
||||||
data = SensorStoreResponseSerializer(SENSOR_DETAIL_DATA).data
|
sensor = self._get_sensor(request, uuid)
|
||||||
else:
|
if sensor is None:
|
||||||
data = SensorStoreResponseSerializer(STORE_DATA).data
|
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)
|
||||||
|
|
||||||
|
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)
|
return Response({"code": 200, "msg": "success", "data": data}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
action = kwargs.get("action")
|
action = kwargs.get("action")
|
||||||
if action == "active":
|
if action in ("active", "deactive"):
|
||||||
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK)
|
return self._toggle_active(request, is_active=(action == "active"))
|
||||||
if action == "deactive":
|
|
||||||
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK)
|
serializer = SensorCreateSerializer(data=request.data)
|
||||||
# POST without action = add
|
serializer.is_valid(raise_exception=True)
|
||||||
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK)
|
sensor = serializer.save(owner=request.user)
|
||||||
|
data = SensorSerializer(sensor).data
|
||||||
|
return Response(
|
||||||
|
{"code": 201, "msg": "success", "data": data},
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
|
||||||
def patch(self, request, *args, **kwargs):
|
def patch(self, request, *args, **kwargs):
|
||||||
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK)
|
uuid = kwargs.get("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):
|
def delete(self, request, *args, **kwargs):
|
||||||
|
uuid = kwargs.get("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,
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
sensor = self._get_sensor(request, 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
|
||||||
|
sensor.save(update_fields=["is_active", "updated_at"])
|
||||||
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK)
|
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK)
|
||||||
|
|||||||
Reference in New Issue
Block a user