From 4b0749b0d744e3de481da31391b436ea19f36a58 Mon Sep 17 00:00:00 2001 From: Mohammad Sajad Pourajam Date: Thu, 19 Feb 2026 17:54:50 +0330 Subject: [PATCH] First commit --- .cursor/postman.mdc | 74 +++++++++++++++++++++++++++ .cursor/project.mdc | 96 +++++++++++++++++++++++++++++++++++ .cursor/test-rule.mdc | 72 ++++++++++++++++++++++++++ .dockerignore | 16 ++++++ .env.example | 15 ++++++ .gitignore | 59 ++++++++++++++++++++++ Dockerfile | 22 ++++++++ config/__init__.py | 0 config/asgi.py | 7 +++ config/settings.py | 106 +++++++++++++++++++++++++++++++++++++++ config/urls.py | 11 ++++ config/wsgi.py | 7 +++ docker-compose-prod.yaml | 51 +++++++++++++++++++ docker-compose.yaml | 51 +++++++++++++++++++ manage.py | 22 ++++++++ requirements.txt | 7 +++ 16 files changed, 616 insertions(+) create mode 100644 .cursor/postman.mdc create mode 100644 .cursor/project.mdc create mode 100644 .cursor/test-rule.mdc create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 config/__init__.py create mode 100644 config/asgi.py create mode 100644 config/settings.py create mode 100644 config/urls.py create mode 100644 config/wsgi.py create mode 100644 docker-compose-prod.yaml create mode 100644 docker-compose.yaml create mode 100644 manage.py create mode 100644 requirements.txt diff --git a/.cursor/postman.mdc b/.cursor/postman.mdc new file mode 100644 index 0000000..fa8049d --- /dev/null +++ b/.cursor/postman.mdc @@ -0,0 +1,74 @@ +--- +alwaysApply: false +--- +# Backend API Architecture & Postman + +## 1. URL / Routing Architecture + +- **Root (config/urls.py):** API mounts under `api//` 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. `/`), then base `""` for list. + - Example pattern: + - `path("active/", View.as_view(), kwargs={"action": "active"})` + - `path("deactive/", View.as_view(), kwargs={"action": "deactive"})` + - `path("/", 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: `/postman/.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//...` (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. `/`) 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. diff --git a/.cursor/project.mdc b/.cursor/project.mdc new file mode 100644 index 0000000..8cc1be1 --- /dev/null +++ b/.cursor/project.mdc @@ -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). diff --git a/.cursor/test-rule.mdc b/.cursor/test-rule.mdc new file mode 100644 index 0000000..6a758c2 --- /dev/null +++ b/.cursor/test-rule.mdc @@ -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. diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..34fb1e8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +.env +.env.* +!.env.example +.git +__pycache__ +*.pyc +.venv +venv +*.egg-info +.pytest_cache +.coverage +htmlcov +*.log +media +staticfiles +.cursor diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e9a0485 --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f9cd5a --- /dev/null +++ b/.gitignore @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0e05676 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/asgi.py b/config/asgi.py new file mode 100644 index 0000000..856079b --- /dev/null +++ b/config/asgi.py @@ -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() diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 0000000..3a4b2fe --- /dev/null +++ b/config/settings.py @@ -0,0 +1,106 @@ +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", + "dashboard", + "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", "sensor_hub"), + "USER": os.environ.get("DB_USER", "sensor_hub"), + "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 diff --git a/config/urls.py b/config/urls.py new file mode 100644 index 0000000..60db6b8 --- /dev/null +++ b/config/urls.py @@ -0,0 +1,11 @@ +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")), + path("api/farm-dashboard-config/", include("dashboard.urls_config")), + path("api/farm-dashboard/", include("dashboard.urls")), +] diff --git a/config/wsgi.py b/config/wsgi.py new file mode 100644 index 0000000..8509335 --- /dev/null +++ b/config/wsgi.py @@ -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() diff --git a/docker-compose-prod.yaml b/docker-compose-prod.yaml new file mode 100644 index 0000000..ad4bf8d --- /dev/null +++ b/docker-compose-prod.yaml @@ -0,0 +1,51 @@ +# Production: no source mount; image contains code +name: sensor-hub + +services: + db: + image: mysql:8.0 + container_name: sensor-hub-db + environment: + MYSQL_DATABASE: ${DB_NAME:-sensor_hub} + MYSQL_USER: ${DB_USER:-sensor_hub} + MYSQL_PASSWORD: ${DB_PASSWORD} + MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} + volumes: + - sensor_hub_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 + container_name: sensor-hub-phpmyadmin + environment: + PMA_HOST: db + PMA_PORT: 3306 + UPLOAD_LIMIT: 64M + ports: + - "8081:80" + depends_on: + db: + condition: service_healthy + restart: unless-stopped + + web: + build: . + container_name: sensor-hub-web + env_file: + - .env + environment: + DB_HOST: db + depends_on: + db: + condition: service_healthy + restart: unless-stopped + ports: + - "8010:8000" + +volumes: + sensor_hub_mysql_data: diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..cf70af2 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,51 @@ +# Development: volumes mount source so code updates apply without rebuild +name: sensor-hub + +services: + db: + image: mysql:8.0 + container_name: sensor-hub-db + environment: + MYSQL_DATABASE: ${DB_NAME:-sensor_hub} + MYSQL_USER: ${DB_USER:-sensor_hub} + MYSQL_PASSWORD: ${DB_PASSWORD:-changeme} + MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-changeme} + volumes: + - sensor_hub_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 + container_name: sensor-hub-phpmyadmin + environment: + PMA_HOST: db + PMA_PORT: 3306 + UPLOAD_LIMIT: 64M + ports: + - "8081:80" + depends_on: + db: + condition: service_healthy + + web: + build: . + container_name: sensor-hub-web + command: python manage.py runserver 0.0.0.0:8000 + volumes: + - .:/app + ports: + - "8010:8000" + env_file: + - .env + environment: + DB_HOST: db + depends_on: + db: + condition: service_healthy + +volumes: + sensor_hub_mysql_data: diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..d28672e --- /dev/null +++ b/manage.py @@ -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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..36cb173 --- /dev/null +++ b/requirements.txt @@ -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