devicetypeAPIGET1

This commit is contained in:
QuangMinh_123
2026-05-21 12:01:10 +07:00
parent 86383e7c03
commit 7aebcf9567
35 changed files with 2784 additions and 145 deletions

View 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

View 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}
)

View 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)

View 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)

View 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
)

View 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

View File