Testys commited on
Commit
3358b33
·
1 Parent(s): 17d3149

Let's just finish this

Browse files
.gitignore CHANGED
@@ -135,6 +135,7 @@ venv/
135
  ENV/
136
  env.bak/
137
  venv.bak/
 
138
 
139
  # Spyder project settings
140
  .spyderproject
 
135
  ENV/
136
  env.bak/
137
  venv.bak/
138
+ streamlit_app.py
139
 
140
  # Spyder project settings
141
  .spyderproject
src/auth.py CHANGED
@@ -107,3 +107,6 @@ async def get_api_key(api_key: str = Security(api_key_header), db: Session = Dep
107
  )
108
 
109
  return api_key
 
 
 
 
107
  )
108
 
109
  return api_key
110
+
111
+ def require_super_admin(current_user: User = Depends(get_current_active_user(required_roles=[Role.ADMIN]))):
112
+ return current_user # Or raise if not super, but since Role.ADMIN is "super", it's fine.
src/crud/__init__.py CHANGED
@@ -24,7 +24,8 @@ from .users import (
24
  update_user, # FIX: was update_user_tag_id
25
  delete_user,
26
  hash_password,
27
- get_all_users
 
28
  )
29
  from .devices import (
30
  create_device, # ADD: missing
 
24
  update_user, # FIX: was update_user_tag_id
25
  delete_user,
26
  hash_password,
27
+ get_all_users,
28
+ get_user_by_email
29
  )
30
  from .devices import (
31
  create_device, # ADD: missing
src/crud/clearance.py CHANGED
@@ -1,6 +1,7 @@
1
  from sqlmodel import Session, select
2
  from typing import List, Optional
3
  from src.models import ClearanceStatus, Student, ClearanceUpdate, ClearanceStatusEnum
 
4
 
5
  def get_clearance_status_for_student(db: Session, student: Student) -> List[ClearanceStatus]:
6
  """
@@ -24,6 +25,7 @@ def update_clearance_status(db: Session, update_data: ClearanceUpdate) -> Cleara
24
  ClearanceStatus.department == update_data.department
25
  )
26
  clearance_record = db.exec(status_statement).first()
 
27
 
28
  if not clearance_record:
29
  # This case should ideally not happen if students are created correctly
 
1
  from sqlmodel import Session, select
2
  from typing import List, Optional
3
  from src.models import ClearanceStatus, Student, ClearanceUpdate, ClearanceStatusEnum
4
+ from datetime import datetime
5
 
6
  def get_clearance_status_for_student(db: Session, student: Student) -> List[ClearanceStatus]:
7
  """
 
25
  ClearanceStatus.department == update_data.department
26
  )
27
  clearance_record = db.exec(status_statement).first()
28
+ clearance_record.updated_at = datetime.utcnow()
29
 
30
  if not clearance_record:
31
  # This case should ideally not happen if students are created correctly
src/crud/devices.py CHANGED
@@ -14,6 +14,10 @@ def create_device(db: Session, device: DeviceCreate) -> Optional[Device]:
14
  existing_device = db.exec(select(Device).where(Device.device_name == device.device_name)).first()
15
  if existing_device:
16
  return None
 
 
 
 
17
 
18
  # Generate a secure, URL-safe API key
19
  api_key = secrets.token_urlsafe(32)
 
14
  existing_device = db.exec(select(Device).where(Device.device_name == device.device_name)).first()
15
  if existing_device:
16
  return None
17
+
18
+ existing_location = db.exec(select(Device).where(Device.location == device.location)).first()
19
+ if existing_location:
20
+ return None
21
 
22
  # Generate a secure, URL-safe API key
23
  api_key = secrets.token_urlsafe(32)
src/crud/students.py CHANGED
@@ -2,7 +2,7 @@ from sqlmodel import Session, select
2
  from typing import List, Optional
3
 
4
  from src.models import (
5
- Student, StudentCreate, StudentUpdate, User, Role, ClearanceStatus, ClearanceDepartment, RFIDTag
6
  )
7
  from src.crud import users as user_crud
8
  # --- Read Operations ---
@@ -35,7 +35,7 @@ def create_student(db: Session, student_data: StudentCreate) -> Student:
35
  """
36
  # 1. Create the associated User account for the student to log in
37
  # The student's username is their matriculation number.
38
- user_for_student = user_crud.UserCreate(
39
  username=student_data.matric_no,
40
  password=student_data.password,
41
  email=student_data.email,
@@ -69,9 +69,6 @@ def create_student(db: Session, student_data: StudentCreate) -> Student:
69
 
70
  def update_student(db: Session, student: Student, updates: StudentUpdate) -> Student:
71
  """Updates a student's profile information."""
72
- student = get_student_by_id(db, student_id=student.id)
73
- if not student:
74
- return None
75
 
76
  update_data = updates.model_dump(exclude_unset=True)
77
  student.sqlmodel_update(update_data)
 
2
  from typing import List, Optional
3
 
4
  from src.models import (
5
+ Student, StudentCreate, StudentUpdate, User, Role, ClearanceStatus, ClearanceDepartment, RFIDTag, UserCreate
6
  )
7
  from src.crud import users as user_crud
8
  # --- Read Operations ---
 
35
  """
36
  # 1. Create the associated User account for the student to log in
37
  # The student's username is their matriculation number.
38
+ user_for_student = UserCreate(
39
  username=student_data.matric_no,
40
  password=student_data.password,
41
  email=student_data.email,
 
69
 
70
  def update_student(db: Session, student: Student, updates: StudentUpdate) -> Student:
71
  """Updates a student's profile information."""
 
 
 
72
 
73
  update_data = updates.model_dump(exclude_unset=True)
74
  student.sqlmodel_update(update_data)
src/crud/users.py CHANGED
@@ -47,11 +47,7 @@ def create_user(db: Session, user: UserCreate) -> User:
47
  return db_user
48
 
49
  def update_user(db: Session, user: User, updates: UserUpdate) -> User:
50
- """Updates a user's information."""
51
- user = get_user_by_id(db, user_id=user.id)
52
- if not user:
53
- return None
54
-
55
  update_data = updates.model_dump(exclude_unset=True)
56
 
57
  if "password" in update_data:
 
47
  return db_user
48
 
49
  def update_user(db: Session, user: User, updates: UserUpdate) -> User:
50
+ """Updates a user's information."""
 
 
 
 
51
  update_data = updates.model_dump(exclude_unset=True)
52
 
53
  if "password" in update_data:
src/models.py CHANGED
@@ -1,7 +1,7 @@
1
  from typing import List, Optional
2
  from sqlmodel import Field, Relationship, SQLModel
3
  from enum import Enum
4
-
5
  # --- Enums for choices ---
6
 
7
  class Role(str, Enum):
@@ -27,6 +27,10 @@ class ClearanceStatusEnum(str, Enum):
27
  PENDING = "pending"
28
  APPROVED = "approved"
29
  REJECTED = "rejected"
 
 
 
 
30
 
31
  # --- Database Table Models ---
32
 
@@ -57,6 +61,7 @@ class ClearanceStatus(SQLModel, table=True):
57
  remarks: Optional[str] = None
58
  student_id: int = Field(foreign_key="student.id")
59
  student: "Student" = Relationship(back_populates="clearance_statuses")
 
60
 
61
  class RFIDTag(SQLModel, table=True):
62
  tag_id: str = Field(primary_key=True, index=True)
@@ -129,12 +134,27 @@ class ClearanceStatusRead(SQLModel):
129
  department: ClearanceDepartment
130
  status: ClearanceStatusEnum
131
  remarks: Optional[str] = None
 
132
 
133
  class ClearanceUpdate(SQLModel):
134
  matric_no: str
135
  department: ClearanceDepartment
136
  status: ClearanceStatusEnum
137
  remarks: Optional[str] = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
  # Combined Read Model
140
  class StudentReadWithClearance(StudentRead):
 
1
  from typing import List, Optional
2
  from sqlmodel import Field, Relationship, SQLModel
3
  from enum import Enum
4
+ from datetime import datetime
5
  # --- Enums for choices ---
6
 
7
  class Role(str, Enum):
 
27
  PENDING = "pending"
28
  APPROVED = "approved"
29
  REJECTED = "rejected"
30
+
31
+ class OverallClearanceStatusEnum(str, Enum):
32
+ COMPLETED = "completed"
33
+ PENDING = "pending"
34
 
35
  # --- Database Table Models ---
36
 
 
61
  remarks: Optional[str] = None
62
  student_id: int = Field(foreign_key="student.id")
63
  student: "Student" = Relationship(back_populates="clearance_statuses")
64
+ updated_at: Optional[datetime] = Field(default=None)
65
 
66
  class RFIDTag(SQLModel, table=True):
67
  tag_id: str = Field(primary_key=True, index=True)
 
134
  department: ClearanceDepartment
135
  status: ClearanceStatusEnum
136
  remarks: Optional[str] = None
137
+
138
 
139
  class ClearanceUpdate(SQLModel):
140
  matric_no: str
141
  department: ClearanceDepartment
142
  status: ClearanceStatusEnum
143
  remarks: Optional[str] = None
144
+
145
+
146
+ class ClearanceStatusItem(SQLModel):
147
+ department: ClearanceDepartment
148
+ status: ClearanceStatusEnum
149
+ remarks: Optional[str] = None
150
+ updated_at: Optional[datetime] = None # Will be added to table model; see below
151
+
152
+ class ClearanceDetail(SQLModel):
153
+ student_id: int
154
+ name: str
155
+ department: Department
156
+ clearance_items: List[ClearanceStatusItem]
157
+ overall_status: OverallClearanceStatusEnum
158
 
159
  # Combined Read Model
160
  class StudentReadWithClearance(StudentRead):
src/routers/admin.py CHANGED
@@ -3,7 +3,7 @@ from sqlmodel import Session, SQLModel
3
  from typing import List, Optional, Dict
4
 
5
  from src.database import get_session
6
- from src.auth import get_current_active_user, get_api_key
7
  from src.models import (
8
  User, UserCreate, UserRead, UserUpdate, Role,
9
  Student, StudentCreate, StudentReadWithClearance, StudentUpdate, StudentRead,
@@ -154,7 +154,10 @@ def lookup_user(
154
  @router.put("/users/{user_id}", response_model=UserRead, dependencies=[Depends(require_super_admin)])
155
  def update_user_details(user_id: int, user: UserUpdate, db: Session = Depends(get_session)):
156
  """(Super Admin Only) Updates a user's details (e.g., role)."""
157
- updated_user = user_crud.update_user(db, user_id=user_id, user_update=user)
 
 
 
158
  if not updated_user:
159
  raise HTTPException(status_code=404, detail="User not found")
160
  return updated_user
@@ -186,9 +189,8 @@ def create_device(device: DeviceCreate, db: Session = Depends(get_session)):
186
  return device_crud.create_device(db=db, device=device)
187
 
188
  @router.get("/devices/", response_model=List[DeviceRead], dependencies=[Depends(require_super_admin)])
189
- def read_all_devices(db: Session = Depends(get_session)):
190
- """(Super Admin Only) Retrieves a list of all registered devices."""
191
- return device_crud.get_all_devices(db)
192
 
193
  @router.delete("/devices/{device_id}", response_model=DeviceRead, dependencies=[Depends(require_super_admin)])
194
  def delete_device_registration(device_id: int, db: Session = Depends(get_session)):
 
3
  from typing import List, Optional, Dict
4
 
5
  from src.database import get_session
6
+ from src.auth import get_current_active_user, get_api_key, require_super_admin
7
  from src.models import (
8
  User, UserCreate, UserRead, UserUpdate, Role,
9
  Student, StudentCreate, StudentReadWithClearance, StudentUpdate, StudentRead,
 
154
  @router.put("/users/{user_id}", response_model=UserRead, dependencies=[Depends(require_super_admin)])
155
  def update_user_details(user_id: int, user: UserUpdate, db: Session = Depends(get_session)):
156
  """(Super Admin Only) Updates a user's details (e.g., role)."""
157
+ db_user = user_crud.get_user_by_id(db, user_id=user_id)
158
+ if not db_user:
159
+ raise HTTPException(status_code=404, detail="User not found")
160
+ updated_user = user_crud.update_user(db, user=db_user, updates=user)
161
  if not updated_user:
162
  raise HTTPException(status_code=404, detail="User not found")
163
  return updated_user
 
189
  return device_crud.create_device(db=db, device=device)
190
 
191
  @router.get("/devices/", response_model=List[DeviceRead], dependencies=[Depends(require_super_admin)])
192
+ def read_all_devices(skip: int = 0, limit: int = 100, db: Session = Depends(get_session)):
193
+ return device_crud.get_all_devices(db, skip=skip, limit=limit)
 
194
 
195
  @router.delete("/devices/{device_id}", response_model=DeviceRead, dependencies=[Depends(require_super_admin)])
196
  def delete_device_registration(device_id: int, db: Session = Depends(get_session)):
src/routers/clearance.py CHANGED
@@ -26,6 +26,8 @@ def update_student_clearance_status(
26
  """
27
  # A potential security check: ensure staff's department matches clearance_update.department
28
  # For now, we trust the role.
 
 
29
 
30
  updated_status = clearance_crud.update_clearance_status(db, clearance_update)
31
 
 
26
  """
27
  # A potential security check: ensure staff's department matches clearance_update.department
28
  # For now, we trust the role.
29
+ if current_user.department != clearance_update.department:
30
+ raise HTTPException(status_code=403, detail="You can only update clearances for your department.")
31
 
32
  updated_status = clearance_crud.update_clearance_status(db, clearance_update)
33
 
src/routers/devices.py CHANGED
@@ -27,14 +27,17 @@ def create_device(
27
  """
28
  # Check if a device with the same location already exists
29
  db_device = device_crud.get_device_by_location(db, location=device.location)
 
30
  if db_device:
31
  raise HTTPException(
32
  status_code=status.HTTP_400_BAD_REQUEST,
33
  detail=f"A device at location '{device.location}' already exists."
34
  )
35
-
36
- # Create the new device and its API key
37
- return device_crud.create_device(db=db, device=device)
 
 
38
 
39
 
40
  @router.get("/", response_model=List[DeviceRead])
 
27
  """
28
  # Check if a device with the same location already exists
29
  db_device = device_crud.get_device_by_location(db, location=device.location)
30
+
31
  if db_device:
32
  raise HTTPException(
33
  status_code=status.HTTP_400_BAD_REQUEST,
34
  detail=f"A device at location '{device.location}' already exists."
35
  )
36
+
37
+ db_device = device_crud.create_device(db=db, device=device)
38
+ if not db_device:
39
+ raise HTTPException(status_code=400, detail="Device already exists (duplicate name or location).")
40
+ return db_device
41
 
42
 
43
  @router.get("/", response_model=List[DeviceRead])
src/routers/rfid.py CHANGED
@@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status, Security
2
  from fastapi.security import APIKeyHeader
3
  from sqlmodel import Session, SQLModel
4
  from typing import Dict
5
-
6
  from src.database import get_session
7
  from src.auth import get_current_active_user, get_api_key
8
  from src.models import (
@@ -18,11 +18,15 @@ router = APIRouter(prefix="/rfid", tags=["RFID"])
18
  api_key_header = APIKeyHeader(name="x-api-key", auto_error=False)
19
 
20
  # --- Moved from admin.py: State for secure admin scanning ---
21
- # Maps a device's API key to the admin user ID who activated it (changed to str key for api_key).
22
  activated_scanners: Dict[str, int] = {}
23
 
24
  # Stores the last tag scanned by a device, keyed by the admin ID who was waiting.
25
  admin_scanned_tags: Dict[int, tuple[str, str]] = {}
 
 
 
 
26
 
27
  # --- Moved from admin.py: Secure Scanning Workflow ---
28
  class ActivationRequest(SQLModel):
@@ -41,28 +45,35 @@ def activate_admin_scanner(
41
  device = device_crud.get_device_by_api_key(db, api_key=activation.api_key)
42
  if not device:
43
  raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Device not found.")
44
-
45
- # Map the device's API key to the currently logged-in admin's ID.
46
- activated_scanners[device.api_key] = current_user.id
 
47
  return
48
 
49
  @router.post("/scanners/scan", status_code=status.HTTP_204_NO_CONTENT)
50
  def receive_scan_from_activated_device(
51
  scan_data: TagScan,
52
- # Removed api_key dependency for "free use" (easier RFID data submission).
53
- # Assumes api_key is in scan_data for association.
54
  ):
55
  """
56
  STEP 2 (Device): The ESP32 device sends the scanned tag to this endpoint.
57
- No authentication required for free use.
58
  """
59
- # Check if this device was activated by an admin (using api_key from scan_data).
60
- admin_id = activated_scanners.pop(scan_data.api_key, None)
 
 
 
 
 
 
61
  if admin_id is None:
62
  raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="This scanner has not been activated for a scan.")
63
-
64
- # Store the scanned tag against the admin who was waiting for it.
65
- admin_scanned_tags[admin_id] = (scan_data.tag_id, scan_data.api_key)
 
66
  return
67
 
68
  @router.get("/scanners/retrieve", response_model=TagScan)
@@ -73,7 +84,9 @@ def retrieve_scanned_tag_for_ui(
73
  STEP 3 (Browser): The browser polls this endpoint to get the tag ID
74
  that the device reported in STEP 2.
75
  """
76
- tag_data = admin_scanned_tags.pop(current_user.id, None)
 
 
77
  if not tag_data:
78
  raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No tag has been scanned by the activated device yet.")
79
  tag_id, api_key = tag_data
@@ -102,7 +115,6 @@ def check_rfid_status(
102
  for clearance in student.clearance_statuses
103
  )
104
  clearance_status_str = "Fully Cleared" if is_cleared else "Pending Clearance"
105
-
106
  return RFIDStatusResponse(
107
  status="found",
108
  full_name=student.full_name,
@@ -126,4 +138,5 @@ def check_rfid_status(
126
  full_name=None,
127
  entity_type=None,
128
  clearance_status=None,
129
- )
 
 
2
  from fastapi.security import APIKeyHeader
3
  from sqlmodel import Session, SQLModel
4
  from typing import Dict
5
+ from threading import Lock # Added for thread-safety on global state
6
  from src.database import get_session
7
  from src.auth import get_current_active_user, get_api_key
8
  from src.models import (
 
18
  api_key_header = APIKeyHeader(name="x-api-key", auto_error=False)
19
 
20
  # --- Moved from admin.py: State for secure admin scanning ---
21
+ # Maps a device's API key to the admin user ID who activated it.
22
  activated_scanners: Dict[str, int] = {}
23
 
24
  # Stores the last tag scanned by a device, keyed by the admin ID who was waiting.
25
  admin_scanned_tags: Dict[int, tuple[str, str]] = {}
26
+ # Locks for thread-safe access to global dicts (since FastAPI is async/multi-threaded)
27
+
28
+ activated_scanners_lock = Lock()
29
+ admin_scanned_tags_lock = Lock()
30
 
31
  # --- Moved from admin.py: Secure Scanning Workflow ---
32
  class ActivationRequest(SQLModel):
 
45
  device = device_crud.get_device_by_api_key(db, api_key=activation.api_key)
46
  if not device:
47
  raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Device not found.")
48
+
49
+ # Map the device's API key to the currently logged-in admin's ID (thread-safe).
50
+ with activated_scanners_lock:
51
+ activated_scanners[device.api_key] = current_user.id
52
  return
53
 
54
  @router.post("/scanners/scan", status_code=status.HTTP_204_NO_CONTENT)
55
  def receive_scan_from_activated_device(
56
  scan_data: TagScan,
57
+ db: Session = Depends(get_session) # Added back for device validation
 
58
  ):
59
  """
60
  STEP 2 (Device): The ESP32 device sends the scanned tag to this endpoint.
61
+ No authentication required for free use, but validates device existence.
62
  """
63
+ # Validate that the device exists (minimal security check without full auth).
64
+ device = device_crud.get_device_by_api_key(db, api_key=scan_data.api_key)
65
+ if not device:
66
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Device not found.")
67
+
68
+ # Check if this device was activated by an admin (thread-safe).
69
+ with activated_scanners_lock:
70
+ admin_id = activated_scanners.pop(scan_data.api_key, None)
71
  if admin_id is None:
72
  raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="This scanner has not been activated for a scan.")
73
+
74
+ # Store the scanned tag against the admin who was waiting for it (thread-safe).
75
+ with admin_scanned_tags_lock:
76
+ admin_scanned_tags[admin_id] = (scan_data.tag_id, scan_data.api_key)
77
  return
78
 
79
  @router.get("/scanners/retrieve", response_model=TagScan)
 
84
  STEP 3 (Browser): The browser polls this endpoint to get the tag ID
85
  that the device reported in STEP 2.
86
  """
87
+ # Retrieve the tag data (thread-safe).
88
+ with admin_scanned_tags_lock:
89
+ tag_data = admin_scanned_tags.pop(current_user.id, None)
90
  if not tag_data:
91
  raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No tag has been scanned by the activated device yet.")
92
  tag_id, api_key = tag_data
 
115
  for clearance in student.clearance_statuses
116
  )
117
  clearance_status_str = "Fully Cleared" if is_cleared else "Pending Clearance"
 
118
  return RFIDStatusResponse(
119
  status="found",
120
  full_name=student.full_name,
 
138
  full_name=None,
139
  entity_type=None,
140
  clearance_status=None,
141
+
142
+ )
src/routers/students.py CHANGED
@@ -17,7 +17,7 @@ router = APIRouter(
17
  dependencies=[Depends(get_current_active_user)]
18
  )
19
 
20
- @router.post("/lookup", response_model=StudentReadWithClearance)
21
  def lookup_student_by_matric_no(
22
  request: StudentLookupRequest,
23
  db: Session = Depends(get_session)
 
17
  dependencies=[Depends(get_current_active_user)]
18
  )
19
 
20
+ @router.get("/lookup", response_model=StudentReadWithClearance)
21
  def lookup_student_by_matric_no(
22
  request: StudentLookupRequest,
23
  db: Session = Depends(get_session)
src/utils.py CHANGED
@@ -1,6 +1,7 @@
1
  from typing import List, Dict, Any
2
  from sqlalchemy.orm import Session as SQLAlchemySessionType
3
  from fastapi.concurrency import run_in_threadpool
 
4
 
5
  from src import crud, models
6
 
@@ -18,7 +19,9 @@ async def format_student_clearance_details(
18
  Returns:
19
  models.ClearanceDetail: Formatted clearance details.
20
  """
21
- statuses_orm_list = await run_in_threadpool(crud.get_clearance_statuses_by_student_id, db, student_orm.student_id)
 
 
22
 
23
  clearance_items_models: List[models.ClearanceStatusItem] = []
24
  overall_status_val = models.OverallClearanceStatusEnum.COMPLETED
@@ -35,15 +38,12 @@ async def format_student_clearance_details(
35
  updated_at=status_orm.updated_at
36
  )
37
  clearance_items_models.append(item)
38
- if item.status != models.ClearanceStatusEnum.COMPLETED:
39
  overall_status_val = models.OverallClearanceStatusEnum.PENDING
40
 
41
- if not statuses_orm_list and overall_status_val == models.OverallClearanceStatusEnum.COMPLETED:
42
- overall_status_val = models.OverallClearanceStatusEnum.PENDING
43
-
44
  return models.ClearanceDetail(
45
- student_id=student_orm.student_id,
46
- name=student_orm.name,
47
  department=student_orm.department,
48
  clearance_items=clearance_items_models,
49
  overall_status=overall_status_val
 
1
  from typing import List, Dict, Any
2
  from sqlalchemy.orm import Session as SQLAlchemySessionType
3
  from fastapi.concurrency import run_in_threadpool
4
+ from sqlmodel import select
5
 
6
  from src import crud, models
7
 
 
19
  Returns:
20
  models.ClearanceDetail: Formatted clearance details.
21
  """
22
+ statuses_orm_list = await run_in_threadpool(
23
+ lambda: db.exec(select(models.ClearanceStatus).where(models.ClearanceStatus.student_id == student_orm.id)).all()
24
+ )
25
 
26
  clearance_items_models: List[models.ClearanceStatusItem] = []
27
  overall_status_val = models.OverallClearanceStatusEnum.COMPLETED
 
38
  updated_at=status_orm.updated_at
39
  )
40
  clearance_items_models.append(item)
41
+ if item.status != models.ClearanceStatusEnum.APPROVED:
42
  overall_status_val = models.OverallClearanceStatusEnum.PENDING
43
 
 
 
 
44
  return models.ClearanceDetail(
45
+ student_id=student_orm.id,
46
+ name=student_orm.full_name,
47
  department=student_orm.department,
48
  clearance_items=clearance_items_models,
49
  overall_status=overall_status_val