Testys commited on
Commit
73d94a0
·
1 Parent(s): e0ac5b2

Making more commits

Browse files
Files changed (4) hide show
  1. main.py +2 -2
  2. requirements.txt +14 -14
  3. src/database.py +3 -0
  4. src/models.py +141 -39
main.py CHANGED
@@ -6,14 +6,14 @@ import uvicorn
6
  from contextlib import asynccontextmanager
7
 
8
 
9
- from src.database import create_db_tables, get_db
10
  from src.routers import students, devices, clearance, token, users, admin
11
 
12
  @asynccontextmanager
13
  async def lifespan(app_instance: FastAPI):
14
  """Handles application startup and shutdown events."""
15
  print("Application startup...")
16
- create_db_tables()
17
  print("Database tables checked/created.")
18
  yield
19
  print("Application shutdown...")
 
6
  from contextlib import asynccontextmanager
7
 
8
 
9
+ from src.database import create_db_and_tables, get_db
10
  from src.routers import students, devices, clearance, token, users, admin
11
 
12
  @asynccontextmanager
13
  async def lifespan(app_instance: FastAPI):
14
  """Handles application startup and shutdown events."""
15
  print("Application startup...")
16
+ create_db_and_tables()
17
  print("Database tables checked/created.")
18
  yield
19
  print("Application shutdown...")
requirements.txt CHANGED
@@ -1,14 +1,14 @@
1
- fastapi
2
- uvicorn
3
- databases[postgresql]
4
- asyncpg
5
- pydantic
6
- pydantic[email]
7
- pydantic[email,cryptography]
8
- python-dotenv
9
- sqlalchemy
10
- supabase
11
- psycopg2-binary
12
- bcrypt
13
- python-multipart
14
- python-jose
 
1
+ fastapi==0.104.1
2
+ uvicorn[standard]==0.24.0
3
+ databases[postgresql]==0.8.0
4
+ asyncpg==0.29.0
5
+ pydantic==2.5.0
6
+ pydantic[email]==2.5.0
7
+ pydantic-settings==2.1.0
8
+ python-dotenv==1.0.0
9
+ sqlalchemy==2.0.23
10
+ supabase==2.0.0
11
+ psycopg2-binary==2.9.9
12
+ bcrypt==4.1.2
13
+ python-multipart==0.0.6
14
+ python-jose[cryptography]==3.3.0
src/database.py CHANGED
@@ -91,3 +91,6 @@ def initialize_student_clearance_statuses(db: SQLAlchemySessionType, student_id_
91
  print(f"Error committing initial clearance statuses for student {student_id_str}: {e}")
92
  db.rollback()
93
  raise
 
 
 
 
91
  print(f"Error committing initial clearance statuses for student {student_id_str}: {e}")
92
  db.rollback()
93
  raise
94
+
95
+ # Alias for backward compatibility
96
+ initialize_student_clearance_statuses_orm = initialize_student_clearance_statuses
src/models.py CHANGED
@@ -5,17 +5,48 @@ SQLAlchemy Models for database table definitions.
5
  from pydantic import BaseModel, Field
6
  from typing import List, Optional, Union
7
  from enum import Enum
 
8
 
9
  from sqlalchemy import (
10
- Boolean, Column, ForeignKey, Integer, String,
11
  create_engine, Enum as SQLAlchemyEnum
12
  )
13
  from sqlalchemy.orm import relationship, sessionmaker, declarative_base
14
 
15
  # ==============================================================================
16
- # Database (SQLAlchemy) Models
17
  # ==============================================================================
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  Base = declarative_base()
20
 
21
  class User(Base):
@@ -25,9 +56,12 @@ class User(Base):
25
  username = Column(String, unique=True, index=True, nullable=False)
26
  hashed_password = Column(String, nullable=False)
27
  name = Column(String, nullable=False)
28
- role = Column(String, default="staff")
 
29
  is_active = Column(Boolean, default=True)
30
  tag_id = Column(String, unique=True, index=True, nullable=True)
 
 
31
 
32
 
33
  class Student(Base):
@@ -38,6 +72,8 @@ class Student(Base):
38
  name = Column(String, nullable=False)
39
  department = Column(String, nullable=False)
40
  tag_id = Column(String, unique=True, index=True, nullable=True)
 
 
41
 
42
  clearance_statuses = relationship("ClearanceStatus", back_populates="student")
43
 
@@ -47,46 +83,76 @@ class ClearanceStatus(Base):
47
  __tablename__ = "clearance_statuses"
48
  id = Column(Integer, primary_key=True, index=True)
49
  student_id = Column(String, ForeignKey("students.student_id"), nullable=False)
50
- department = Column(String, nullable=False)
51
- status = Column(String, default="PENDING")
 
 
 
 
52
 
53
  student = relationship("Student", back_populates="clearance_statuses")
54
-
55
-
56
- class UserTypeEnum(str, Enum):
57
- """Enum for user types."""
58
- STUDENT = "student"
59
- USER = "user"
60
 
61
 
62
  class Device(Base):
63
  """Database model for RFID reader devices."""
64
  __tablename__ = "devices"
65
  id = Column(Integer, primary_key=True, index=True)
66
- device_id_str = Column(String, unique=True, index=True, nullable=False)
 
 
67
  location = Column(String, nullable=True)
68
- # Fields to temporarily link a device to a user for tag registration
69
- link_for_user_id = Column(String, nullable=True)
70
- link_for_user_type = Column(SQLAlchemyEnum(UserTypeEnum), nullable=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
- # ==============================================================================
73
- # API (Pydantic) Models
74
- # ==============================================================================
75
 
76
  # --- User and Auth Models ---
77
  class UserBase(BaseModel):
78
  username: str
79
  name: str
 
 
 
 
80
 
81
  class UserCreate(UserBase):
82
  password: str
83
- role: str = "staff"
84
 
85
- class UserResponse(UserBase):
86
  id: int
87
- is_active: bool
88
- role: str
 
 
89
  tag_id: Optional[str] = None
 
 
 
90
 
91
  class Config:
92
  from_attributes = True
@@ -98,7 +164,6 @@ class Token(BaseModel):
98
  class TokenData(BaseModel):
99
  username: Optional[str] = None
100
 
101
-
102
  # --- Student and Clearance Models ---
103
  class StudentBase(BaseModel):
104
  student_id: str = Field(..., example="CST/18/123")
@@ -106,33 +171,41 @@ class StudentBase(BaseModel):
106
  department: str = Field(..., example="Computer Science")
107
 
108
  class StudentCreate(StudentBase):
109
- pass
110
 
111
  class StudentResponse(StudentBase):
112
  id: int
113
  tag_id: Optional[str] = None
 
 
114
 
115
  class Config:
116
  from_attributes = True
117
 
118
- class ClearanceStatusEnum(str, Enum):
119
- PENDING = "PENDING"
120
- COMPLETED = "COMPLETED"
121
- REJECTED = "REJECTED"
122
-
123
- class ClearanceDepartment(str, Enum):
124
- DEPARTMENT = "DEPARTMENT"
125
- BURSARY = "BURSARY"
126
- LIBRARY = "LIBRARY"
127
- ALUMNI = "ALUMNI"
128
 
129
- class OverallClearanceStatusEnum(str, Enum):
130
- PENDING = "PENDING"
131
- COMPLETED = "COMPLETED"
 
 
 
 
 
 
 
 
 
132
 
133
  class ClearanceStatusItem(BaseModel):
134
- department: str
135
  status: ClearanceStatusEnum
 
 
136
 
137
  class Config:
138
  from_attributes = True
@@ -145,9 +218,34 @@ class ClearanceDetail(BaseModel):
145
  clearance_items: List[ClearanceStatusItem]
146
 
147
  class ClearanceStatusUpdate(BaseModel):
148
- department: str
149
  status: ClearanceStatusEnum
 
150
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
  # --- Tag and Device Models ---
153
  class TagLinkRequest(BaseModel):
@@ -169,3 +267,7 @@ class RfidLinkSuccessResponse(BaseModel):
169
  message: str = "Tag linked successfully."
170
  user_id: str
171
  user_type: UserTypeEnum
 
 
 
 
 
5
  from pydantic import BaseModel, Field
6
  from typing import List, Optional, Union
7
  from enum import Enum
8
+ from datetime import datetime
9
 
10
  from sqlalchemy import (
11
+ Boolean, Column, ForeignKey, Integer, String, DateTime,
12
  create_engine, Enum as SQLAlchemyEnum
13
  )
14
  from sqlalchemy.orm import relationship, sessionmaker, declarative_base
15
 
16
  # ==============================================================================
17
+ # Shared Enums (used by both SQLAlchemy and Pydantic)
18
  # ==============================================================================
19
 
20
+ class ClearanceStatusEnum(str, Enum):
21
+ NOT_COMPLETED = "NOT_COMPLETED"
22
+ PENDING = "PENDING"
23
+ COMPLETED = "COMPLETED"
24
+ REJECTED = "REJECTED"
25
+
26
+ class ClearanceDepartment(str, Enum):
27
+ DEPARTMENT = "DEPARTMENT"
28
+ BURSARY = "BURSARY"
29
+ LIBRARY = "LIBRARY"
30
+ ALUMNI = "ALUMNI"
31
+
32
+ class UserRole(str, Enum):
33
+ ADMIN = "ADMIN"
34
+ STAFF = "STAFF"
35
+
36
+ class TargetUserType(str, Enum):
37
+ STUDENT = "STUDENT"
38
+ STAFF_ADMIN = "STAFF_ADMIN"
39
+
40
+ class OverallClearanceStatusEnum(str, Enum):
41
+ PENDING = "PENDING"
42
+ COMPLETED = "COMPLETED"
43
+
44
+ class UserTypeEnum(str, Enum):
45
+ """Enum for user types."""
46
+ STUDENT = "student"
47
+ USER = "user"
48
+
49
+
50
  Base = declarative_base()
51
 
52
  class User(Base):
 
56
  username = Column(String, unique=True, index=True, nullable=False)
57
  hashed_password = Column(String, nullable=False)
58
  name = Column(String, nullable=False)
59
+ role = Column(SQLAlchemyEnum(UserRole), default=UserRole.STAFF, nullable=False)
60
+ department = Column(SQLAlchemyEnum(ClearanceDepartment), nullable=True)
61
  is_active = Column(Boolean, default=True)
62
  tag_id = Column(String, unique=True, index=True, nullable=True)
63
+ created_at = Column(DateTime, default=datetime.utcnow)
64
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
65
 
66
 
67
  class Student(Base):
 
72
  name = Column(String, nullable=False)
73
  department = Column(String, nullable=False)
74
  tag_id = Column(String, unique=True, index=True, nullable=True)
75
+ created_at = Column(DateTime, default=datetime.utcnow)
76
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
77
 
78
  clearance_statuses = relationship("ClearanceStatus", back_populates="student")
79
 
 
83
  __tablename__ = "clearance_statuses"
84
  id = Column(Integer, primary_key=True, index=True)
85
  student_id = Column(String, ForeignKey("students.student_id"), nullable=False)
86
+ department = Column(SQLAlchemyEnum(ClearanceDepartment), nullable=False)
87
+ status = Column(SQLAlchemyEnum(ClearanceStatusEnum), default=ClearanceStatusEnum.NOT_COMPLETED, nullable=False)
88
+ remarks = Column(String, nullable=True)
89
+ cleared_by = Column(Integer, ForeignKey("users.id"), nullable=True)
90
+ created_at = Column(DateTime, default=datetime.utcnow)
91
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
92
 
93
  student = relationship("Student", back_populates="clearance_statuses")
94
+ cleared_by_user = relationship("User", foreign_keys=[cleared_by])
 
 
 
 
 
95
 
96
 
97
  class Device(Base):
98
  """Database model for RFID reader devices."""
99
  __tablename__ = "devices"
100
  id = Column(Integer, primary_key=True, index=True)
101
+ name = Column(String, nullable=False)
102
+ description = Column(String, nullable=True)
103
+ device_id = Column(String, unique=True, index=True, nullable=True)
104
  location = Column(String, nullable=True)
105
+ api_key = Column(String, unique=True, index=True, nullable=False)
106
+ is_active = Column(Boolean, default=True)
107
+ created_at = Column(DateTime, default=datetime.utcnow)
108
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
109
+
110
+
111
+ class PendingTagLink(Base):
112
+ """Database model for pending tag link requests."""
113
+ __tablename__ = "pending_tag_links"
114
+ id = Column(Integer, primary_key=True, index=True)
115
+ tag_id = Column(String, nullable=False)
116
+ target_user_id = Column(String, nullable=False)
117
+ target_user_type = Column(SQLAlchemyEnum(TargetUserType), nullable=False)
118
+ initiated_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
119
+ created_at = Column(DateTime, default=datetime.utcnow)
120
+
121
+
122
+ class DeviceLog(Base):
123
+ """Database model for device activity logs."""
124
+ __tablename__ = "device_logs"
125
+ id = Column(Integer, primary_key=True, index=True)
126
+ device_fk_id = Column(Integer, ForeignKey("devices.id"), nullable=True)
127
+ actual_device_id_str = Column(String, nullable=True)
128
+ tag_id_scanned = Column(String, nullable=True)
129
+ user_type = Column(String, nullable=True)
130
+ action = Column(String, nullable=False)
131
+ timestamp = Column(DateTime, default=datetime.utcnow)
132
 
 
 
 
133
 
134
  # --- User and Auth Models ---
135
  class UserBase(BaseModel):
136
  username: str
137
  name: str
138
+ role: UserRole = UserRole.STAFF
139
+ department: Optional[ClearanceDepartment] = None
140
+ tag_id: Optional[str] = None
141
+ is_active: Optional[bool] = True
142
 
143
  class UserCreate(UserBase):
144
  password: str
 
145
 
146
+ class UserResponse(BaseModel):
147
  id: int
148
+ username: str
149
+ name: str
150
+ role: UserRole
151
+ department: Optional[ClearanceDepartment] = None
152
  tag_id: Optional[str] = None
153
+ is_active: bool
154
+ created_at: datetime
155
+ updated_at: datetime
156
 
157
  class Config:
158
  from_attributes = True
 
164
  class TokenData(BaseModel):
165
  username: Optional[str] = None
166
 
 
167
  # --- Student and Clearance Models ---
168
  class StudentBase(BaseModel):
169
  student_id: str = Field(..., example="CST/18/123")
 
171
  department: str = Field(..., example="Computer Science")
172
 
173
  class StudentCreate(StudentBase):
174
+ tag_id: Optional[str] = None
175
 
176
  class StudentResponse(StudentBase):
177
  id: int
178
  tag_id: Optional[str] = None
179
+ created_at: datetime
180
+ updated_at: datetime
181
 
182
  class Config:
183
  from_attributes = True
184
 
185
+ class ClearanceStatusCreate(BaseModel):
186
+ student_id: str
187
+ department: ClearanceDepartment
188
+ status: ClearanceStatusEnum
189
+ remarks: Optional[str] = None
 
 
 
 
 
190
 
191
+ class ClearanceStatusResponse(BaseModel):
192
+ id: int
193
+ student_id: str
194
+ department: ClearanceDepartment
195
+ status: ClearanceStatusEnum
196
+ remarks: Optional[str] = None
197
+ cleared_by: Optional[int] = None
198
+ created_at: datetime
199
+ updated_at: datetime
200
+
201
+ class Config:
202
+ from_attributes = True
203
 
204
  class ClearanceStatusItem(BaseModel):
205
+ department: ClearanceDepartment
206
  status: ClearanceStatusEnum
207
+ remarks: Optional[str] = None
208
+ updated_at: datetime
209
 
210
  class Config:
211
  from_attributes = True
 
218
  clearance_items: List[ClearanceStatusItem]
219
 
220
  class ClearanceStatusUpdate(BaseModel):
221
+ department: ClearanceDepartment
222
  status: ClearanceStatusEnum
223
+ remarks: Optional[str] = None
224
 
225
+ # --- Device Models ---
226
+ class DeviceCreateAdmin(BaseModel):
227
+ name: str
228
+ description: Optional[str] = None
229
+ device_id: Optional[str] = None
230
+ location: Optional[str] = None
231
+
232
+ class DeviceRegister(BaseModel):
233
+ device_id: str
234
+ location: str
235
+
236
+ class DeviceResponse(BaseModel):
237
+ id: int
238
+ name: str
239
+ description: Optional[str] = None
240
+ device_id: Optional[str] = None
241
+ location: Optional[str] = None
242
+ api_key: str
243
+ is_active: bool
244
+ created_at: datetime
245
+ updated_at: datetime
246
+
247
+ class Config:
248
+ from_attributes = True
249
 
250
  # --- Tag and Device Models ---
251
  class TagLinkRequest(BaseModel):
 
267
  message: str = "Tag linked successfully."
268
  user_id: str
269
  user_type: UserTypeEnum
270
+
271
+ # JWT Configuration
272
+ import os
273
+ JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key")