First commit
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
---
|
||||
alwaysApply: false
|
||||
---
|
||||
# Backend API Architecture & Postman
|
||||
|
||||
## 1. URL / Routing Architecture
|
||||
|
||||
- **Root (config/urls.py):** API mounts under `api/<app-prefix>/` via `include()`.
|
||||
- Example: `path("api/auth/", include("auth.urls"))`, `path("api/sensor-hub/", include("sensor_hub.urls"))`.
|
||||
- App prefix: kebab-case (e.g. `sensor-hub`).
|
||||
|
||||
- **App URLs (each app’s urls.py):** Only endpoint definitions with `path()`.
|
||||
- Same view can be used for several paths; distinguish by path or `kwargs` (e.g. `kwargs={"action": "active"}`).
|
||||
- Order matters: more specific paths first (e.g. `active/`, `deactive/`), then path-param routes (e.g. `<uuid:uuid>/`), then base `""` for list.
|
||||
- Example pattern:
|
||||
- `path("active/", View.as_view(), kwargs={"action": "active"})`
|
||||
- `path("deactive/", View.as_view(), kwargs={"action": "deactive"})`
|
||||
- `path("<uuid:uuid>/", View.as_view(), name="...-detail")`
|
||||
- `path("", View.as_view(), name="...-list")`
|
||||
|
||||
- **Views:** One `APIView` per resource (or per flow, e.g. auth). Dispatch by HTTP method and optionally by `request.path` or `kwargs` (e.g. `uuid`, `action`). No business logic in views; orchestration only.
|
||||
|
||||
---
|
||||
|
||||
## 2. Postman Collection Layout
|
||||
|
||||
- **Placement:** One collection per app: `<app_name>/postman/<collection_name>.json` (e.g. `sensor_hub/postman/sensor_hub.json`, `auth/postman/postman.json`).
|
||||
|
||||
- **Structure:**
|
||||
- `info`: `name`, `schema` (v2.1.0), optional `description`.
|
||||
- `item`: array of requests (one per endpoint variant/method).
|
||||
- `variable`: at least `baseUrl` (e.g. `http://localhost:8000`); add `token`, `uuid` etc. when needed.
|
||||
|
||||
- **Request style:**
|
||||
- One base URL per resource; multiple requests for different methods or path params (e.g. list vs `{{uuid}}/`).
|
||||
- URL: `{{baseUrl}}/api/<app-prefix>/...` (e.g. `{{baseUrl}}/api/sensor-hub/`, `{{baseUrl}}/api/sensor-hub/{{uuid}}/`).
|
||||
- Auth: where required, header `Authorization: Bearer {{token}}`.
|
||||
- No random/dynamic values in body or response examples.
|
||||
|
||||
---
|
||||
|
||||
## 3. Postman Request Generator (when I give you routes)
|
||||
|
||||
Your task is to take the API routes I provide and convert them into a valid Postman collection JSON (as above).
|
||||
|
||||
ROUTE STYLE:
|
||||
- When routes are defined as a single URL with different HTTP methods, generate one base URL and multiple requests (one per method/variant). Use the same URL for all; use path params (e.g. `<uuid>/`) or query for GET detail vs list when applicable.
|
||||
|
||||
RULES:
|
||||
|
||||
1. For each route (or each method/variant on the same URL), generate:
|
||||
- Name: A descriptive, concise name for the request based on the route and HTTP method.
|
||||
- Method: The HTTP method (GET, POST, PUT, DELETE, etc.).
|
||||
- URL: The route URL.
|
||||
- Body: If the endpoint accepts input, provide a JSON body example with appropriate keys; otherwise, leave it empty.
|
||||
- Response: Provide a sample JSON response in the following format:
|
||||
- If the endpoint returns no data:
|
||||
{
|
||||
"status": "success"
|
||||
}
|
||||
- If the endpoint returns data:
|
||||
{
|
||||
"status": "success",
|
||||
"data": {}
|
||||
}
|
||||
- All responses must use HTTP status 200.
|
||||
2. Do NOT generate random or dynamic values in the body or response.
|
||||
3. Output must be a valid Postman collection JSON structure:
|
||||
- Include "info" with collection name.
|
||||
- Include "item" array with all requests.
|
||||
4. Keep the JSON fully compatible with Postman import.
|
||||
5. Do NOT include explanations outside the JSON.
|
||||
|
||||
Wait for me to provide the route definitions.
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
## 2. Django App (Module) Naming
|
||||
|
||||
| Item | Convention | Example |
|
||||
|--------|------------|----------------------------|
|
||||
| App name | snake_case, **بدون** پسوند `_api` | `account`, `auth`, `sensor_hub` |
|
||||
|
||||
- نام اپها را با `_api` تمام **نکنید**. مثلاً بهجای `account_api` از `account` استفاده کنید.
|
||||
- برای ماژولهای فقط API، همان نام دامنه کافی است (مثلاً `auth`، `account`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Model and Database Field Naming
|
||||
|
||||
| Item | Convention | Example |
|
||||
|-------------------|-------------------------|----------------------------------------------|
|
||||
| Model | PascalCase | `UserProfile` |
|
||||
| Fields | snake_case | `first_name`, `email_address` |
|
||||
| Boolean | `is_` / `has_` + name | `is_active`, `has_paid` |
|
||||
| Date/Time | `created_at` / `updated_at` | `created_at`, `updated_at` |
|
||||
| ForeignKey / M2M | snake_case, often model name | `author = ForeignKey(UserProfile)` |
|
||||
| Choices / Enum | UPPER_SNAKE_CASE values | `role = CharField(choices=(("ADMIN","Admin"), ("USER","User")))` |
|
||||
|
||||
---
|
||||
|
||||
## 4. DRF Conventions
|
||||
|
||||
- **Serializer:** Validation + data transformation. Use PascalCase names.
|
||||
- **Service layer:** All business logic lives here.
|
||||
- **View:** Orchestration only — call services and return responses. No business logic in views.
|
||||
- **URLs:** Define endpoints only. Use kebab-case for URL paths.
|
||||
|
||||
---
|
||||
|
||||
## 5. API Response Format
|
||||
|
||||
همه پاسخهای API باید فیلد `code` را برگردانند؛ مقدار آن برابر **HTTP status code** درخواست است (مثلاً 200، 201، 400، 404).
|
||||
|
||||
| فیلد | توضیح |
|
||||
|-------|----------------------------------------|
|
||||
| `code` | کد وضعیت HTTP (مثلاً 200، 404، 500) |
|
||||
| `msg` | پیام (مثلاً "success" برای 2xx) |
|
||||
| `data` | دادهٔ برگشتی (اختیاری) |
|
||||
|
||||
مثال:
|
||||
```json
|
||||
{"code": 200, "msg": "success", "data": {...}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Simple Example: How the Layers Connect (users app)
|
||||
|
||||
```python
|
||||
# models.py
|
||||
class UserProfile(models.Model):
|
||||
first_name = models.CharField(max_length=50)
|
||||
last_name = models.CharField(max_length=50)
|
||||
email_address = models.EmailField(unique=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
||||
# serializers.py
|
||||
class UserCreateSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = UserProfile
|
||||
fields = ["first_name", "last_name", "email_address"]
|
||||
|
||||
|
||||
# services.py
|
||||
def create_user(first_name, last_name, email_address):
|
||||
return UserProfile.objects.create(
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
email_address=email_address,
|
||||
)
|
||||
|
||||
|
||||
# views.py
|
||||
class UserCreateAPIView(APIView):
|
||||
def post(self, request):
|
||||
serializer = UserCreateSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
user = create_user(**serializer.validated_data)
|
||||
return Response({"code": 201, "msg": "success", "data": {"id": user.id}}, status=201)
|
||||
```
|
||||
|
||||
- All names follow the conventions above.
|
||||
- Business logic is in `services.py`.
|
||||
- Serializer only validates and serializes.
|
||||
- View only orchestrates (calls service, returns response).
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
alwaysApply: false
|
||||
---
|
||||
You are a Django API code generator.
|
||||
|
||||
Your task is to generate a complete and runnable Django project based on the routes I provide.
|
||||
|
||||
ROUTE STYLE:
|
||||
- Routes may be defined as a single URL with different HTTP methods (e.g. one path, GET for list, GET with query for detail, PUT/PATCH for update, DELETE for delete, POST for action). Use one view class that implements get, post, put, patch, delete as needed. Use query parameters (e.g. sensor_id) to distinguish list vs detail when both use GET.
|
||||
|
||||
STRICT RULES:
|
||||
|
||||
- Use Django only.
|
||||
- Do NOT use Django REST Framework unless I explicitly request it.
|
||||
- Do NOT connect to any database.
|
||||
- Do NOT create any Models.
|
||||
- Do NOT generate random or dynamic data.
|
||||
- Input parameters must be accepted (body, query params, path params).
|
||||
- HOWEVER, absolutely NO processing, validation, transformation, or logic may be applied to them.
|
||||
- Do NOT use input values inside the response.
|
||||
- No conditional logic.
|
||||
- No business logic.
|
||||
- No validation.
|
||||
- All endpoints must always return static JSON responses only.
|
||||
- ALL responses must return HTTP status code 200 only.
|
||||
- No other status codes are allowed.
|
||||
- No explanations outside the code.
|
||||
- Return complete runnable code including project structure (views.py, urls.py, settings if needed, etc.).
|
||||
|
||||
--------------------------------------------------
|
||||
RESPONSE FORMAT (STRICTLY ENFORCED)
|
||||
|
||||
If the endpoint does NOT require returning data:
|
||||
|
||||
{
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
If the endpoint requires returning data:
|
||||
|
||||
{
|
||||
"status": "success",
|
||||
"data": {}
|
||||
}
|
||||
|
||||
Mandatory rules:
|
||||
- The "status" field MUST always be exactly "success".
|
||||
- If "data" is present, it MUST be exactly an empty object {}.
|
||||
- If data is not required, DO NOT include the "data" field.
|
||||
- No additional fields are allowed.
|
||||
--------------------------------------------------
|
||||
|
||||
COMMENTING REQUIREMENTS (VERY IMPORTANT):
|
||||
|
||||
Each endpoint MUST include professional, multi-line docstring documentation.
|
||||
|
||||
The documentation MUST include:
|
||||
|
||||
1. Clear description of the endpoint purpose.
|
||||
2. Complete description of ALL input parameters:
|
||||
- Parameter name
|
||||
- Data type
|
||||
- Location (body / query / path)
|
||||
- Description of its intended purpose
|
||||
3. Full description of the response structure:
|
||||
- status field
|
||||
- data field (if applicable)
|
||||
4. Explicit statement that no processing or validation is performed on inputs.
|
||||
|
||||
Use clean, professional API documentation style.
|
||||
Do not write anything outside the code.
|
||||
Wait for my route definitions.
|
||||
@@ -0,0 +1,16 @@
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
.git
|
||||
__pycache__
|
||||
*.pyc
|
||||
.venv
|
||||
venv
|
||||
*.egg-info
|
||||
.pytest_cache
|
||||
.coverage
|
||||
htmlcov
|
||||
*.log
|
||||
media
|
||||
staticfiles
|
||||
.cursor
|
||||
@@ -0,0 +1,15 @@
|
||||
# Django
|
||||
SECRET_KEY=your-secret-key-change-in-production
|
||||
DEBUG=1
|
||||
ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0
|
||||
|
||||
# Database (MySQL) - used by Django in Docker
|
||||
DB_ENGINE=django.db.backends.mysql
|
||||
DB_NAME=croplogic
|
||||
DB_USER=croplogic
|
||||
DB_PASSWORD=changeme
|
||||
DB_HOST=db
|
||||
DB_PORT=3306
|
||||
|
||||
# Optional: for running manage.py from host (local DB)
|
||||
# DB_HOST=127.0.0.1
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
*.env
|
||||
!*.env.example
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual environments
|
||||
.venv/
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Django
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
media/
|
||||
staticfiles/
|
||||
*.pot
|
||||
|
||||
# Testing / Coverage
|
||||
.coverage
|
||||
htmlcov/
|
||||
.pytest_cache/
|
||||
.tox/
|
||||
.nox/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# System deps for MySQL client (pkg-config required by mysqlclient to find libs)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
default-libmysqlclient-dev \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
|
||||
@@ -0,0 +1,145 @@
|
||||
{
|
||||
"info": {
|
||||
"name": "Account",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
"description": "Account API. GET list, GET by uuid (detail), POST add, PATCH update, DELETE delete, PATCH profile. Authenticated user required."
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "Update profile",
|
||||
"request": {
|
||||
"method": "PATCH",
|
||||
"header": [
|
||||
{"key": "Content-Type", "value": "application/json"},
|
||||
{"key": "Authorization", "value": "Bearer {{token}}", "description": "Required: user must be authenticated"}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"first_name\": \"\",\n \"last_name\": \"\",\n \"email\": \"\"\n}"
|
||||
},
|
||||
"url": "{{baseUrl}}/api/account/profile/",
|
||||
"description": "Update current user profile (first_name, last_name, email). Returns UpdateProfileResponse."
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "Success",
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"body": "{\n \"code\": 200,\n \"msg\": \"success\",\n \"data\": {\n \"id\": 0,\n \"username\": \"\",\n \"email\": \"\",\n \"first_name\": \"\",\n \"last_name\": \"\",\n \"phone_number\": \"\"\n }\n}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "List accounts",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{"key": "Content-Type", "value": "application/json"},
|
||||
{"key": "Authorization", "value": "Bearer {{token}}", "description": "Required: user must be authenticated"}
|
||||
],
|
||||
"url": "{{baseUrl}}/api/account/",
|
||||
"description": "Get list of accounts. GET on base route."
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "Success",
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"body": "{\n \"code\": 200,\n \"msg\": \"success\",\n \"data\": {}\n}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Get account detail (by uuid)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{"key": "Content-Type", "value": "application/json"},
|
||||
{"key": "Authorization", "value": "Bearer {{token}}", "description": "Required: user must be authenticated"}
|
||||
],
|
||||
"url": "{{baseUrl}}/api/account/{{uuid}}/",
|
||||
"description": "Get one account by uuid in path."
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "Success",
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"body": "{\n \"code\": 200,\n \"msg\": \"success\",\n \"data\": {}\n}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Add account",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{"key": "Content-Type", "value": "application/json"},
|
||||
{"key": "Authorization", "value": "Bearer {{token}}", "description": "Required: user must be authenticated"}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"first_name\": \"\",\n \"last_name\": \"\",\n \"phones\": []\n}"
|
||||
},
|
||||
"url": "{{baseUrl}}/api/account/",
|
||||
"description": "Add a new account. POST on base route."
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "Success",
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"body": "{\n \"code\": 200,\n \"msg\": \"success\"\n}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Update account",
|
||||
"request": {
|
||||
"method": "PATCH",
|
||||
"header": [
|
||||
{"key": "Content-Type", "value": "application/json"},
|
||||
{"key": "Authorization", "value": "Bearer {{token}}", "description": "Required: user must be authenticated"}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"first_name\": \"\",\n \"last_name\": \"\",\n \"phones\": []\n}"
|
||||
},
|
||||
"url": "{{baseUrl}}/api/account/{{uuid}}/",
|
||||
"description": "Update account by uuid in path. PATCH."
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "Success",
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"body": "{\n \"code\": 200,\n \"msg\": \"success\"\n}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Delete account",
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"header": [
|
||||
{"key": "Authorization", "value": "Bearer {{token}}", "description": "Required: user must be authenticated"}
|
||||
],
|
||||
"url": "{{baseUrl}}/api/account/{{uuid}}/",
|
||||
"description": "Delete account by uuid in path."
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "Success",
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"body": "{\n \"code\": 200,\n \"msg\": \"success\"\n}"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"variable": [
|
||||
{"key": "baseUrl", "value": "http://localhost:8000"},
|
||||
{"key": "token", "value": ""},
|
||||
{"key": "uuid", "value": "550e8400-e29b-41d4-a716-446655440000"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
Account API serializers.
|
||||
UpdateProfile request/response shapes aligned with frontend types.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class UpdateProfileSerializer(serializers.Serializer):
|
||||
"""
|
||||
Request body for PATCH /api/account/profile/ (UpdateProfilePayload).
|
||||
"""
|
||||
|
||||
first_name = serializers.CharField(max_length=150, required=False, allow_blank=True)
|
||||
last_name = serializers.CharField(max_length=150, required=False, allow_blank=True)
|
||||
email = serializers.EmailField(required=False, allow_blank=True)
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import AccountView, ProfileView
|
||||
|
||||
urlpatterns = [
|
||||
path("profile/", ProfileView.as_view(), name="profile-update"),
|
||||
path("<uuid:uuid>/", AccountView.as_view(), name="account-detail"),
|
||||
path("", AccountView.as_view(), name="account-list"),
|
||||
]
|
||||
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
Account API module.
|
||||
CRUD endpoints for user account profile (first name, last name, phone numbers).
|
||||
Profile update endpoint returns UpdateProfileResponse (code, msg, data: AuthUser).
|
||||
"""
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from .serializers import UpdateProfileSerializer
|
||||
|
||||
|
||||
def _auth_user_to_data(user):
|
||||
"""Build AuthUser-shaped dict from Django User."""
|
||||
if user is None or not getattr(user, "pk", None):
|
||||
return None
|
||||
# return {
|
||||
# "id": user.id,
|
||||
# "username": getattr(user, "username", "") or "",
|
||||
# "email": getattr(user, "email", "") or "",
|
||||
# "first_name": getattr(user, "first_name", "") or "",
|
||||
# "last_name": getattr(user, "last_name", "") or "",
|
||||
# "phone_number": getattr(user, "phone_number", "") or "",
|
||||
# }
|
||||
return {
|
||||
"id": 1,
|
||||
"username": "testuser",
|
||||
"email": "testuser@example.com",
|
||||
"first_name": "Test",
|
||||
"last_name": "User",
|
||||
"phone_number": "09123456789",
|
||||
}
|
||||
|
||||
|
||||
class ProfileView(APIView):
|
||||
"""
|
||||
PATCH /api/account/profile/
|
||||
UpdateProfilePayload: first_name, last_name, email.
|
||||
UpdateProfileResponse: code, msg, data (AuthUser).
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def patch(self, request):
|
||||
serializer = UpdateProfileSerializer(data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
# TODO: persist first_name, last_name, email via service layer
|
||||
user = request.user
|
||||
data = _auth_user_to_data(user)
|
||||
if data is None:
|
||||
data = {
|
||||
"id": 0,
|
||||
"username": "",
|
||||
"email": "",
|
||||
"first_name": "",
|
||||
"last_name": "",
|
||||
"phone_number": "",
|
||||
}
|
||||
return Response(
|
||||
{"code": 200, "msg": "success", "data": data},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
class AccountView(APIView):
|
||||
"""
|
||||
Account CRUD endpoints. Dispatch by HTTP method and path (uuid for detail/update/delete).
|
||||
No processing, validation, or transformation is applied to any input.
|
||||
All endpoints return HTTP 200 only. Response format: {"code": 200, "msg": "success"} or {"code": 200, "msg": "success", "data": {}}.
|
||||
|
||||
Routes:
|
||||
- GET "" → List: returns status "success", data {}.
|
||||
- GET "<uuid>/" → Detail: uuid (path). Returns status "success", data {}.
|
||||
- POST "" → Create: body/query may contain first_name, last_name, phones; not used. Returns status "success". No data field.
|
||||
- PATCH "<uuid>/" → Update: uuid (path), body/query may contain first_name, last_name, phones; not used. Returns status "success". No data field.
|
||||
- DELETE "<uuid>/" → Delete: uuid (path). Returns status "success". No data field.
|
||||
"""
|
||||
|
||||
# permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
List or detail account.
|
||||
|
||||
List (GET on base URL):
|
||||
- Input parameters: none required. Query params if sent are not processed.
|
||||
- Response: {"code": 200, "msg": "success", "data": {}}.
|
||||
- No processing or validation is performed on inputs.
|
||||
|
||||
Detail (GET on <uuid>/):
|
||||
- Input parameters: uuid (path, UUID). Description: identifier for the account resource.
|
||||
- Response: {"code": 200, "msg": "success", "data": {}}.
|
||||
- No processing or validation is performed on inputs.
|
||||
"""
|
||||
return Response({"code": 200, "msg": "success", "data": {}}, status=status.HTTP_200_OK)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Create account.
|
||||
|
||||
Input parameters (body, JSON): first_name (string), last_name (string), phones (array of strings).
|
||||
Description: intended for user first name, last name, and phone numbers. Not processed or validated.
|
||||
Response: {"code": 200, "msg": "success"}. No data field.
|
||||
No processing or validation is performed on inputs.
|
||||
"""
|
||||
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK)
|
||||
|
||||
def patch(self, request, *args, **kwargs):
|
||||
"""
|
||||
Update account.
|
||||
|
||||
Input parameters: uuid (path, UUID), body (JSON) may contain first_name, last_name, phones.
|
||||
Description: identifier in path; body fields intended for updated profile. Not processed or validated.
|
||||
Response: {"code": 200, "msg": "success"}. No data field.
|
||||
No processing or validation is performed on inputs.
|
||||
"""
|
||||
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
"""
|
||||
Delete account.
|
||||
|
||||
Input parameters: uuid (path, UUID). Description: identifier for the account resource to delete.
|
||||
Response: {"code": 200, "msg": "success"}. No data field.
|
||||
No processing or validation is performed on inputs.
|
||||
"""
|
||||
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuthConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "auth"
|
||||
label = "auth_api" # Avoid clash with django.contrib.auth (label "auth")
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"info": {
|
||||
"name": "Auth",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
"description": "Auth API. Request OTP, Verify OTP."
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "Request OTP",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{"key": "Content-Type", "value": "application/json"}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"phone_number\": \"\"\n}"
|
||||
},
|
||||
"url": "{{baseUrl}}/api/auth/request-otp/",
|
||||
"description": "Request OTP for the given phone number. In DEBUG mode, response includes debug_note."
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "Success",
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"body": "{\n \"code\": 200,\n \"msg\": \"success\",\n \"token\": \"\"\n}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Verify OTP",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{"key": "Content-Type", "value": "application/json"}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"token\": \"\",\n \"otp_code\": \"\"\n}"
|
||||
},
|
||||
"url": "{{baseUrl}}/api/auth/verify-otp/",
|
||||
"description": "Verify OTP with token from request-otp and otp_code sent to user."
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "Success",
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"body": "{\n \"code\": 200,\n \"msg\": \"success\",\n \"data\": {\n \"id\": 0,\n \"username\": \"\",\n \"email\": \"\",\n \"first_name\": \"\",\n \"last_name\": \"\",\n \"phone_number\": \"\"\n },\n \"token\": \"\"\n}"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"variable": [
|
||||
{"key": "baseUrl", "value": "http://localhost:8000"},
|
||||
{"key": "token", "value": ""}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
# --- RequestOTP (request-otp/) ---
|
||||
class RequestOTPSerializer(serializers.Serializer):
|
||||
"""Request body for POST /api/auth/request-otp/."""
|
||||
|
||||
phone_number = serializers.CharField(max_length=32)
|
||||
|
||||
|
||||
# --- VerifyOTP (verify-otp/) ---
|
||||
class VerifyOTPSerializer(serializers.Serializer):
|
||||
"""Request body for POST /api/auth/verify-otp/."""
|
||||
|
||||
token = serializers.CharField()
|
||||
otp_code = serializers.CharField(max_length=10)
|
||||
|
||||
|
||||
# --- AuthUser (used in VerifyOTPResponse and UpdateProfileResponse) ---
|
||||
class AuthUserSerializer(serializers.Serializer):
|
||||
"""User data returned in auth/account responses."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
username = serializers.CharField()
|
||||
email = serializers.EmailField(allow_blank=True)
|
||||
first_name = serializers.CharField()
|
||||
last_name = serializers.CharField()
|
||||
phone_number = serializers.CharField()
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import AuthenticationView
|
||||
|
||||
urlpatterns = [
|
||||
path("request-otp/", AuthenticationView.as_view(), name="request-otp"),
|
||||
path("verify-otp/", AuthenticationView.as_view(), name="verify-otp"),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import secrets
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.core.signing import TimestampSigner
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from .serializers import RequestOTPSerializer, VerifyOTPSerializer
|
||||
|
||||
|
||||
OTP_TTL_SECONDS = 300
|
||||
OTP_SIGNER = TimestampSigner(salt="auth.otp")
|
||||
|
||||
|
||||
def _auth_user_to_data(user):
|
||||
"""Build AuthUser-shaped dict from Django User (or mock)."""
|
||||
# if user is None or not getattr(user, "pk", None):
|
||||
# return None
|
||||
# return {
|
||||
# "id": user.id,
|
||||
# "username": getattr(user, "username", "") or "",
|
||||
# "email": getattr(user, "email", "") or "",
|
||||
# "first_name": getattr(user, "first_name", "") or "",
|
||||
# "last_name": getattr(user, "last_name", "") or "",
|
||||
# "phone_number": getattr(user, "phone_number", "") or "",
|
||||
# }
|
||||
return {
|
||||
"id": 1,
|
||||
"username": "testuser",
|
||||
"email": "testuser@example.com",
|
||||
"first_name": "Test",
|
||||
"last_name": "User",
|
||||
"phone_number": "09123456789",
|
||||
}
|
||||
|
||||
|
||||
class AuthenticationView(APIView):
|
||||
"""
|
||||
Single view for auth flows: request-otp and verify-otp.
|
||||
Dispatches by path: .../request-otp/ -> request_otp, .../verify-otp/ -> verify_otp.
|
||||
Response format: RequestOTPResponse / VerifyOTPResponse (code, msg, token, data when applicable).
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
if "verify-otp" in request.path:
|
||||
return self._verify_otp(request)
|
||||
return self._request_otp(request)
|
||||
|
||||
def _request_otp(self, request):
|
||||
serializer = RequestOTPSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
phone_number = serializer.validated_data["phone_number"].strip()
|
||||
otp_code = f"{secrets.randbelow(1_000_000):06d}"
|
||||
|
||||
cache.set(f"otp_code:{phone_number}", otp_code, timeout=OTP_TTL_SECONDS)
|
||||
otp_token = OTP_SIGNER.sign(phone_number)
|
||||
|
||||
payload = {"code": 200, "msg": "success", "token": otp_token}
|
||||
if settings.DEBUG:
|
||||
payload["debug_note"] = "OTP code is returned only when DEBUG=1."
|
||||
|
||||
return Response(payload, status=status.HTTP_200_OK)
|
||||
|
||||
def _verify_otp(self, request):
|
||||
serializer = VerifyOTPSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
# TODO: validate token + otp_code, load or create user, issue JWT/session token
|
||||
auth_token = "1234567890"
|
||||
user_data = _auth_user_to_data(getattr(request, "user", None))
|
||||
if user_data is None:
|
||||
user_data = {
|
||||
"id": 0,
|
||||
"username": "",
|
||||
"email": "",
|
||||
"first_name": "",
|
||||
"last_name": "",
|
||||
"phone_number": "",
|
||||
}
|
||||
|
||||
return Response(
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": user_data,
|
||||
"token": auth_token,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||
|
||||
application = get_asgi_application()
|
||||
@@ -0,0 +1,105 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
SECRET_KEY = os.environ.get("SECRET_KEY", "django-insecure-dev-only")
|
||||
DEBUG = os.environ.get("DEBUG", "0") == "1"
|
||||
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"auth.apps.AuthConfig",
|
||||
"account",
|
||||
"sensor_hub",
|
||||
"rest_framework",
|
||||
"corsheaders",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"corsheaders.middleware.CorsMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "config.urls"
|
||||
WSGI_APPLICATION = "config.wsgi.application"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": os.environ.get("DB_ENGINE", "django.db.backends.mysql"),
|
||||
"NAME": os.environ.get("DB_NAME", "croplogic"),
|
||||
"USER": os.environ.get("DB_USER", "croplogic"),
|
||||
"PASSWORD": os.environ.get("DB_PASSWORD", ""),
|
||||
"HOST": os.environ.get("DB_HOST", "127.0.0.1"),
|
||||
"PORT": os.environ.get("DB_PORT", "3306"),
|
||||
"OPTIONS": {
|
||||
"charset": "utf8mb4",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
||||
]
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
TIME_ZONE = "UTC"
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
STATIC_URL = "static/"
|
||||
STATIC_ROOT = BASE_DIR / "staticfiles"
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
||||
"LOCATION": "croplogic-auth-otp",
|
||||
}
|
||||
}
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_PERMISSION_CLASSES": [
|
||||
"rest_framework.permissions.AllowAny",
|
||||
],
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": [
|
||||
"rest_framework_simplejwt.authentication.JWTAuthentication",
|
||||
],
|
||||
}
|
||||
|
||||
CORS_ALLOW_ALL_ORIGINS = DEBUG
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("api/auth/", include("auth.urls")),
|
||||
path("api/account/", include("account.urls")),
|
||||
path("api/sensor-hub/", include("sensor_hub.urls")),
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||
|
||||
application = get_wsgi_application()
|
||||
@@ -0,0 +1,46 @@
|
||||
# Production: no source mount; image contains code
|
||||
services:
|
||||
db:
|
||||
image: mysql:8.0
|
||||
environment:
|
||||
MYSQL_DATABASE: ${DB_NAME}
|
||||
MYSQL_USER: ${DB_USER}
|
||||
MYSQL_PASSWORD: ${DB_PASSWORD}
|
||||
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${DB_ROOT_PASSWORD}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
phpmyadmin:
|
||||
image: phpmyadmin:latest
|
||||
environment:
|
||||
PMA_HOST: db
|
||||
PMA_PORT: 3306
|
||||
UPLOAD_LIMIT: 64M
|
||||
ports:
|
||||
- "8080:80"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
web:
|
||||
build: .
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
DB_HOST: db
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8000:8000"
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
@@ -0,0 +1,46 @@
|
||||
# Development: volumes mount source so code updates apply without rebuild
|
||||
services:
|
||||
db:
|
||||
image: mysql:8.0
|
||||
environment:
|
||||
MYSQL_DATABASE: ${DB_NAME:-croplogic}
|
||||
MYSQL_USER: ${DB_USER:-croplogic}
|
||||
MYSQL_PASSWORD: ${DB_PASSWORD:-changeme}
|
||||
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-changeme}
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${DB_PASSWORD:-changeme}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
phpmyadmin:
|
||||
image: phpmyadmin:latest
|
||||
environment:
|
||||
PMA_HOST: db
|
||||
PMA_PORT: 3306
|
||||
UPLOAD_LIMIT: 64M
|
||||
ports:
|
||||
- "8080:80"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
web:
|
||||
build: .
|
||||
command: python manage.py runserver 0.0.0.0:8000
|
||||
volumes:
|
||||
- .:/app
|
||||
ports:
|
||||
- "8000:8000"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
DB_HOST: db
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,7 @@
|
||||
Django>=5.0,<6
|
||||
djangorestframework>=3.14,<4
|
||||
djangorestframework-simplejwt>=5.3,<6
|
||||
django-cors-headers>=4.3,<5
|
||||
mysqlclient>=2.2,<3
|
||||
gunicorn>=22,<25
|
||||
python-dotenv>=1.0,<2
|
||||
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"info": {
|
||||
"name": "Sensor Hub",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
"description": "Sensor Hub API. GET list, GET by uuid (detail), POST add, PATCH update, DELETE delete, POST active/deactive. Authenticated user required. Static responses only."
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "List sensors",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{"key": "Content-Type", "value": "application/json"},
|
||||
{"key": "Authorization", "value": "Bearer {{token}}", "description": "Required: user must be authenticated"}
|
||||
],
|
||||
"url": "{{baseUrl}}/api/sensor-hub/",
|
||||
"description": "Get list of sensors. GET on base route."
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "Success",
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"body": "{\n \"status\": \"success\",\n \"data\": {\n \"name\": \"sensor-hub-static\",\n \"uuid_sensor\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"last_updated\": \"2025-02-18T12:00:00Z\",\n \"specifications\": {\n \"model\": \"SH-1\",\n \"firmware\": \"1.0.0\",\n \"capabilities\": [\"temperature\", \"humidity\", \"light\"]\n },\n \"power_source\": {\n \"type\": \"battery\",\n \"voltage\": 3.3,\n \"backup\": \"solar\"\n },\n \"customized_sensors\": {\n \"thresholds\": {\"temperature_min\": 10, \"temperature_max\": 35},\n \"report_interval_sec\": 300\n }\n }\n}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Get sensor details (by uuid)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{"key": "Content-Type", "value": "application/json"},
|
||||
{"key": "Authorization", "value": "Bearer {{token}}", "description": "Required: user must be authenticated"}
|
||||
],
|
||||
"url": "{{baseUrl}}/api/sensor-hub/{{uuid}}/",
|
||||
"description": "Get one sensor by uuid in path."
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "Success",
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"body": "{\n \"status\": \"success\",\n \"data\": {\n \"name\": \"sensor-hub-static\",\n \"uuid_sensor\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"last_updated\": \"2025-02-18T12:00:00Z\",\n \"specifications\": {\n \"model\": \"SH-1\",\n \"firmware\": \"1.0.0\",\n \"capabilities\": [\"temperature\", \"humidity\", \"light\"]\n },\n \"power_source\": {\n \"type\": \"battery\",\n \"voltage\": 3.3,\n \"backup\": \"solar\"\n },\n \"customized_sensors\": {\n \"thresholds\": {\"temperature_min\": 10, \"temperature_max\": 35},\n \"report_interval_sec\": 300\n }\n }\n}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Add sensor",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{"key": "Content-Type", "value": "application/json"},
|
||||
{"key": "Authorization", "value": "Bearer {{token}}", "description": "Required: user must be authenticated"}
|
||||
],
|
||||
"body": {"mode": "raw", "raw": "{}"},
|
||||
"url": "{{baseUrl}}/api/sensor-hub/",
|
||||
"description": "Add a new sensor. POST on base route."
|
||||
},
|
||||
"response": [
|
||||
{"name": "Success", "status": "OK", "code": 200, "body": "{\n \"status\": \"success\"\n}"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Update sensor",
|
||||
"request": {
|
||||
"method": "PATCH",
|
||||
"header": [
|
||||
{"key": "Content-Type", "value": "application/json"},
|
||||
{"key": "Authorization", "value": "Bearer {{token}}", "description": "Required: user must be authenticated"}
|
||||
],
|
||||
"body": {"mode": "raw", "raw": "{}"},
|
||||
"url": "{{baseUrl}}/api/sensor-hub/{{uuid}}/",
|
||||
"description": "Update sensor by uuid in path. PATCH."
|
||||
},
|
||||
"response": [
|
||||
{"name": "Success", "status": "OK", "code": 200, "body": "{\n \"status\": \"success\"\n}"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Delete sensor",
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"header": [
|
||||
{"key": "Authorization", "value": "Bearer {{token}}", "description": "Required: user must be authenticated"}
|
||||
],
|
||||
"url": "{{baseUrl}}/api/sensor-hub/{{uuid}}/",
|
||||
"description": "Delete sensor by uuid in path."
|
||||
},
|
||||
"response": [
|
||||
{"name": "Success", "status": "OK", "code": 200, "body": "{\n \"status\": \"success\"\n}"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Activate",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{"key": "Content-Type", "value": "application/json"},
|
||||
{"key": "Authorization", "value": "Bearer {{token}}", "description": "Required: user must be authenticated"}
|
||||
],
|
||||
"body": {"mode": "raw", "raw": "{}"},
|
||||
"url": "{{baseUrl}}/api/sensor-hub/active/",
|
||||
"description": "Activate. POST on active/ route."
|
||||
},
|
||||
"response": [
|
||||
{"name": "Success", "status": "OK", "code": 200, "body": "{\n \"status\": \"success\"\n}"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Deactivate",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{"key": "Content-Type", "value": "application/json"},
|
||||
{"key": "Authorization", "value": "Bearer {{token}}", "description": "Required: user must be authenticated"}
|
||||
],
|
||||
"body": {"mode": "raw", "raw": "{}"},
|
||||
"url": "{{baseUrl}}/api/sensor-hub/deactive/",
|
||||
"description": "Deactivate. POST on deactive/ route."
|
||||
},
|
||||
"response": [
|
||||
{"name": "Success", "status": "OK", "code": 200, "body": "{\n \"status\": \"success\"\n}"}
|
||||
]
|
||||
}
|
||||
],
|
||||
"variable": [
|
||||
{"key": "baseUrl", "value": "http://localhost:8000"},
|
||||
{"key": "token", "value": ""},
|
||||
{"key": "uuid", "value": "550e8400-e29b-41d4-a716-446655440000"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class SensorStoreResponseSerializer(serializers.Serializer):
|
||||
"""Schema for static sensor store response (name, uuid_sensor, last_updated, specifications, power_source, customized_sensors)."""
|
||||
|
||||
name = serializers.CharField()
|
||||
uuid_sensor = serializers.CharField()
|
||||
last_updated = serializers.CharField()
|
||||
specifications = serializers.JSONField()
|
||||
power_source = serializers.JSONField()
|
||||
customized_sensors = serializers.JSONField()
|
||||
@@ -0,0 +1,10 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import SensorHubView
|
||||
|
||||
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"),
|
||||
]
|
||||
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
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.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from .serializers import SensorStoreResponseSerializer
|
||||
|
||||
# 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):
|
||||
"""
|
||||
Sensor-hub endpoints. Behavior depends on URL and HTTP method.
|
||||
No processing or validation is performed on inputs; responses are static.
|
||||
|
||||
Routes:
|
||||
- GET "" → List: returns code 200, msg "success", data with static sensor list.
|
||||
- GET "<uuid>/" → Detail: uuid (path). Returns code 200, msg "success", data with static sensor payload.
|
||||
- POST "" → Add: body/query may be sent but not used. Returns code 200, msg "success". No data field.
|
||||
- PATCH "<uuid>/" → Update: uuid (path), body/query may be sent but not used. Returns code 200, msg "success". No data field.
|
||||
- DELETE "<uuid>/" → Delete: uuid (path). Returns code 200, msg "success". No data field.
|
||||
- POST "active/" → Activate: no input. Returns code 200, msg "success". No data field.
|
||||
- POST "deactive/" → Deactivate: no input. Returns code 200, msg "success". No data field.
|
||||
"""
|
||||
|
||||
# permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
uuid = kwargs.get("uuid")
|
||||
if uuid is not None:
|
||||
data = SensorStoreResponseSerializer(SENSOR_DETAIL_DATA).data
|
||||
else:
|
||||
data = SensorStoreResponseSerializer(STORE_DATA).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 == "active":
|
||||
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK)
|
||||
if action == "deactive":
|
||||
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK)
|
||||
# POST without action = add
|
||||
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK)
|
||||
|
||||
def patch(self, request, *args, **kwargs):
|
||||
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK)
|
||||
Reference in New Issue
Block a user