devicetypeAPIGET1
This commit is contained in:
83
backend/modules/device_type/controller.py
Normal file
83
backend/modules/device_type/controller.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# ============================================
|
||||
# CONTROLLER: Nhận request → Validate → Gọi Service → Trả response
|
||||
# ============================================
|
||||
# Controller KHÔNG chứa business logic (check trùng tên, tính toán...)
|
||||
# Controller CHỈ làm 3 việc:
|
||||
# 1. Lấy dữ liệu từ request (body, params, query string)
|
||||
# 2. Validate dữ liệu (bằng Marshmallow schema)
|
||||
# 3. Gọi service → trả response
|
||||
# ============================================
|
||||
|
||||
from flask import request
|
||||
from common.response.api_response import success_response
|
||||
from common.constants.status_code import HTTP_CREATED
|
||||
from modules.device_type.service import (
|
||||
get_device_types_service,
|
||||
create_device_type_service
|
||||
)
|
||||
from modules.device_type.schemas import CreateDeviceTypeSchema
|
||||
|
||||
|
||||
def get_device_types():
|
||||
"""GET /api/device-types — Lấy danh sách tất cả"""
|
||||
device_types = get_device_types_service()
|
||||
return success_response(
|
||||
data=device_types,
|
||||
message="Device types retrieved successfully"
|
||||
)
|
||||
|
||||
|
||||
def get_device_type_by_id(device_type_id):
|
||||
"""GET /api/device-types/<device_type_id> — Lấy chi tiết theo ID"""
|
||||
# TODO: Triển khai sau
|
||||
pass
|
||||
|
||||
|
||||
def create_device_type():
|
||||
"""
|
||||
POST /api/device-types — Tạo mới device type
|
||||
|
||||
Luồng xử lý:
|
||||
1. Lấy JSON body từ request
|
||||
2. Validate bằng Marshmallow schema
|
||||
- Nếu sai → Marshmallow tự throw ValidationError
|
||||
- Global handler bắt → trả 400 kèm chi tiết lỗi
|
||||
3. Gọi service (service sẽ check trùng tên + insert DB)
|
||||
4. Trả 201 Created + data vừa tạo
|
||||
"""
|
||||
# Bước 1: Lấy JSON body từ request
|
||||
body = request.get_json()
|
||||
# request.get_json() parse body thành dict Python
|
||||
# VD: {"name": "Router", "color": "#3B82F6"} → {'name': 'Router', 'color': '#3B82F6'}
|
||||
|
||||
# Bước 2: Validate bằng schema
|
||||
schema = CreateDeviceTypeSchema()
|
||||
data = schema.load(body)
|
||||
# schema.load() làm 2 việc:
|
||||
# a) Validate: check required, type, regex... → nếu sai throw ValidationError
|
||||
# b) Deserialize: loại bỏ field thừa, áp dụng load_default
|
||||
# VD: body = {"name": "Router", "color": "#3B82F6", "hack": "xss"}
|
||||
# → data = {"name": "Router", "color": "#3B82F6", "sort_order": 0, "is_active": True}
|
||||
# → "hack" bị loại bỏ, sort_order/is_active được thêm default
|
||||
|
||||
# Bước 3: Gọi service
|
||||
new_device_type = create_device_type_service(data)
|
||||
|
||||
# Bước 4: Trả response 201 Created
|
||||
return success_response(
|
||||
data=new_device_type,
|
||||
message="Device type created successfully",
|
||||
status_code=HTTP_CREATED # 201 thay vì 200 — đúng chuẩn REST cho POST thành công
|
||||
)
|
||||
|
||||
|
||||
def update_device_type(device_type_id):
|
||||
"""PUT /api/device-types/<device_type_id> — Cập nhật"""
|
||||
# TODO: Triển khai sau
|
||||
pass
|
||||
|
||||
|
||||
def delete_device_type(device_type_id):
|
||||
"""DELETE /api/device-types/<device_type_id> — Xóa"""
|
||||
# TODO: Triển khai sau
|
||||
pass
|
||||
30
backend/modules/device_type/exceptions.py
Normal file
30
backend/modules/device_type/exceptions.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Đây là exception cho riêng cho từng module
|
||||
from common.exceptions.app_exception import (
|
||||
NotFoundException,
|
||||
ConflictException,
|
||||
BadRequestException
|
||||
)
|
||||
|
||||
|
||||
class DeviceTypeNotFoundException(NotFoundException):
|
||||
def __init__(self, device_type_id):
|
||||
super().__init__(
|
||||
message=f"Device type not found with id={device_type_id}",
|
||||
payload={"device_type_id": device_type_id}
|
||||
)
|
||||
|
||||
|
||||
class DeviceTypeAlreadyExistsException(ConflictException):
|
||||
def __init__(self, name):
|
||||
super().__init__(
|
||||
message=f"Device type already exists with name={name}",
|
||||
payload={"name": name}
|
||||
)
|
||||
|
||||
|
||||
class DeviceTypeInUseException(BadRequestException):
|
||||
def __init__(self, device_type_id):
|
||||
super().__init__(
|
||||
message="Cannot delete device type because it is being used by devices",
|
||||
payload={"device_type_id": device_type_id}
|
||||
)
|
||||
120
backend/modules/device_type/repository.py
Normal file
120
backend/modules/device_type/repository.py
Normal file
@@ -0,0 +1,120 @@
|
||||
from config.database import get_connection, release_connection
|
||||
|
||||
# ============================================
|
||||
# Hàm chung: Chuyển 1 row (tuple) thành dict
|
||||
# Tại sao tách riêng? Vì nhiều hàm đều cần chuyển row → dict,
|
||||
# viết 1 lần dùng lại, tránh copy-paste
|
||||
# ============================================
|
||||
def _row_to_dict(row):
|
||||
return {
|
||||
"id": str(row[0]),
|
||||
"name": row[1],
|
||||
"description": row[2],
|
||||
"icon_url": row[3],
|
||||
"color": row[4],
|
||||
"sort_order": row[5],
|
||||
"is_active": row[6],
|
||||
"created": row[7].isoformat() if row[7] else None,
|
||||
"modified": row[8].isoformat() if row[8] else None
|
||||
}
|
||||
|
||||
|
||||
# ============================================
|
||||
# GET ALL: Lấy danh sách tất cả device types
|
||||
# ============================================
|
||||
def find_all_device_types():
|
||||
conn = get_connection()
|
||||
cur = None
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
SELECT id, name, description, icon_url, color, sort_order, is_active, created, modified
|
||||
FROM device_types
|
||||
ORDER BY sort_order ASC, name ASC
|
||||
""")
|
||||
|
||||
rows = cur.fetchall()
|
||||
|
||||
device_types = []
|
||||
for row in rows:
|
||||
device_types.append(_row_to_dict(row))
|
||||
|
||||
# ✅ FIX: return PHẢI nằm NGOÀI vòng for
|
||||
# Trước đó return nằm trong for → chỉ trả về 1 row rồi dừng
|
||||
return device_types
|
||||
|
||||
finally:
|
||||
if cur:
|
||||
cur.close()
|
||||
# ✅ FIX: PHẢI trả connection về pool, không thì sau 10 request app sẽ đứng
|
||||
release_connection(conn)
|
||||
|
||||
|
||||
# ============================================
|
||||
# FIND BY NAME: Tìm device type theo tên (để check trùng tên khi tạo mới)
|
||||
# Trả về dict nếu tìm thấy, None nếu không
|
||||
# ============================================
|
||||
def find_device_type_by_name(name):
|
||||
conn = get_connection()
|
||||
cur = None
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
# Dùng LOWER() để so sánh không phân biệt hoa/thường
|
||||
# VD: "Router" và "router" coi như trùng
|
||||
cur.execute("""
|
||||
SELECT id, name, description, icon_url, color, sort_order, is_active, created, modified
|
||||
FROM device_types
|
||||
WHERE LOWER(name) = LOWER(%s)
|
||||
""", (name,))
|
||||
# ⚠️ LƯU Ý: (name,) có dấu phẩy → tạo tuple 1 phần tử
|
||||
# Nếu viết (name) thì Python hiểu là string, không phải tuple → lỗi
|
||||
|
||||
row = cur.fetchone() # fetchone() vì chỉ cần 1 kết quả
|
||||
if row:
|
||||
return _row_to_dict(row)
|
||||
return None
|
||||
|
||||
finally:
|
||||
if cur:
|
||||
cur.close()
|
||||
release_connection(conn)
|
||||
|
||||
|
||||
# ============================================
|
||||
# INSERT: Thêm mới 1 device type vào database
|
||||
# Trả về dict của device type vừa tạo (có id, created, modified từ DB)
|
||||
# ============================================
|
||||
def insert_device_type(data):
|
||||
conn = get_connection()
|
||||
cur = None
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
INSERT INTO device_types (name, description, icon_url, color, sort_order, is_active)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
RETURNING id, name, description, icon_url, color, sort_order, is_active, created, modified
|
||||
""", (
|
||||
data["name"],
|
||||
data.get("description"), # .get() trả về None nếu không có key
|
||||
data.get("icon_url"),
|
||||
data["color"],
|
||||
data.get("sort_order", 0), # default = 0 nếu không truyền
|
||||
data.get("is_active", True) # default = True nếu không truyền
|
||||
))
|
||||
# RETURNING: PostgreSQL trả về row vừa INSERT (bao gồm id, created, modified do DB tự tạo)
|
||||
# → Không cần SELECT lại sau khi INSERT
|
||||
|
||||
row = cur.fetchone()
|
||||
conn.commit() # ⚠️ QUAN TRỌNG: INSERT/UPDATE/DELETE phải commit() để lưu vào DB
|
||||
# Nếu không commit → dữ liệu chỉ nằm trong transaction, khi connection đóng → mất hết
|
||||
|
||||
return _row_to_dict(row)
|
||||
|
||||
except Exception:
|
||||
conn.rollback() # Nếu có lỗi → rollback để hủy transaction, tránh dữ liệu bẩn
|
||||
raise # raise lại exception để tầng trên (service/controller) xử lý
|
||||
|
||||
finally:
|
||||
if cur:
|
||||
cur.close()
|
||||
release_connection(conn)
|
||||
21
backend/modules/device_type/routes.py
Normal file
21
backend/modules/device_type/routes.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from flask import Blueprint
|
||||
|
||||
from modules.device_type.controller import (
|
||||
get_device_types,
|
||||
get_device_type_by_id,
|
||||
create_device_type,
|
||||
update_device_type,
|
||||
delete_device_type
|
||||
)
|
||||
|
||||
device_type_bp = Blueprint(
|
||||
"device_type",
|
||||
__name__,
|
||||
url_prefix="/device-types"
|
||||
)
|
||||
|
||||
device_type_bp.route("", methods=["GET"])(get_device_types)
|
||||
device_type_bp.route("/<device_type_id>", methods=["GET"])(get_device_type_by_id)
|
||||
device_type_bp.route("", methods=["POST"])(create_device_type)
|
||||
device_type_bp.route("/<device_type_id>", methods=["PUT"])(update_device_type)
|
||||
device_type_bp.route("/<device_type_id>", methods=["DELETE"])(delete_device_type)
|
||||
67
backend/modules/device_type/schemas.py
Normal file
67
backend/modules/device_type/schemas.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from marshmallow import Schema, fields, validate
|
||||
|
||||
HEX_COLOR_REGEX = r"^#[0-9A-Fa-f]{6}$"
|
||||
|
||||
|
||||
class CreateDeviceTypeSchema(Schema):
|
||||
name = fields.String(
|
||||
required=True,
|
||||
validate=validate.Length(min=1, max=100)
|
||||
)
|
||||
|
||||
description = fields.String(
|
||||
required=False,
|
||||
allow_none=True
|
||||
)
|
||||
|
||||
icon_url = fields.String(
|
||||
required=False,
|
||||
allow_none=True,
|
||||
validate=validate.Length(max=512)
|
||||
)
|
||||
|
||||
color = fields.String(
|
||||
required=True,
|
||||
validate=validate.Regexp(HEX_COLOR_REGEX)
|
||||
)
|
||||
|
||||
sort_order = fields.Integer(
|
||||
required=False,
|
||||
load_default=0
|
||||
)
|
||||
|
||||
is_active = fields.Boolean(
|
||||
required=False,
|
||||
load_default=True
|
||||
)
|
||||
|
||||
|
||||
class UpdateDeviceTypeSchema(Schema):
|
||||
name = fields.String(
|
||||
required=False,
|
||||
validate=validate.Length(min=1, max=100)
|
||||
)
|
||||
|
||||
description = fields.String(
|
||||
required=False,
|
||||
allow_none=True
|
||||
)
|
||||
|
||||
icon_url = fields.String(
|
||||
required=False,
|
||||
allow_none=True,
|
||||
validate=validate.Length(max=512)
|
||||
)
|
||||
|
||||
color = fields.String(
|
||||
required=False,
|
||||
validate=validate.Regexp(HEX_COLOR_REGEX)
|
||||
)
|
||||
|
||||
sort_order = fields.Integer(
|
||||
required=False
|
||||
)
|
||||
|
||||
is_active = fields.Boolean(
|
||||
required=False
|
||||
)
|
||||
38
backend/modules/device_type/service.py
Normal file
38
backend/modules/device_type/service.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# ============================================
|
||||
# SERVICE LAYER: Chứa Business Logic
|
||||
# ============================================
|
||||
# Controller chỉ nhận request + trả response
|
||||
# Repository chỉ truy vấn DB
|
||||
# Service ở giữa: chứa LOGIC NGHIỆP VỤ (validate, check điều kiện, tính toán)
|
||||
# ============================================
|
||||
|
||||
from modules.device_type.repository import (
|
||||
find_all_device_types,
|
||||
find_device_type_by_name,
|
||||
insert_device_type
|
||||
)
|
||||
from modules.device_type.exceptions import DeviceTypeAlreadyExistsException
|
||||
|
||||
|
||||
def get_device_types_service():
|
||||
"""Lấy danh sách tất cả device types — hiện tại không có logic gì thêm"""
|
||||
return find_all_device_types()
|
||||
|
||||
|
||||
def create_device_type_service(data):
|
||||
"""
|
||||
Tạo mới device type.
|
||||
Business logic:
|
||||
1. Check xem tên đã tồn tại chưa (UNIQUE constraint)
|
||||
2. Nếu trùng → throw exception
|
||||
3. Nếu không trùng → insert vào DB
|
||||
"""
|
||||
# Bước 1: Check trùng tên
|
||||
existing = find_device_type_by_name(data["name"])
|
||||
if existing:
|
||||
# Throw exception → global handler bắt → trả 409 Conflict
|
||||
raise DeviceTypeAlreadyExistsException(data["name"])
|
||||
|
||||
# Bước 2: Insert vào DB
|
||||
new_device_type = insert_device_type(data)
|
||||
return new_device_type
|
||||
0
backend/modules/device_type/validator.py
Normal file
0
backend/modules/device_type/validator.py
Normal file
Reference in New Issue
Block a user