Coverage for server / models / clients.py: 89%
260 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-10-04 09:33 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-10-04 09:33 +0000
1import re
2from datetime import datetime, timezone
3from typing import Annotated, Optional
4from beanie import Document, Indexed, PydanticObjectId
5from fastapi import HTTPException
6from pydantic import (
7 BaseModel,
8 EmailStr,
9 Field,
10 ValidationInfo,
11 field_validator,
12 model_validator,
13)
14from server.validators.user_enum import DesignationEnum
17class Client(Document):
18 first_name: str
19 middle_name: Optional[str] = None
20 last_name: str
21 full_name: str
22 role: str = "client"
23 email: Annotated[str, Indexed(unique=True)]
24 password: str
25 district: Optional[str]
26 designation: Optional[str]
27 office_address: Optional[str]
28 contact_no: Optional[str]
29 profile_picture: Optional[str] = None
30 created_by: Optional[str] = None
31 updated_by: Optional[str] = None
32 created_at: Optional[datetime] = None
33 updated_at: Optional[datetime] = None
35 class Settings:
36 name = "client_collection"
37 indexes = [[("role", 1)]] # Additional index on the role field
39 @field_validator("updated_at", mode="before", check_fields=True)
40 def set_updated_at_now(v):
41 return v or datetime.now(timezone.utc)
43 @field_validator("created_at", mode="before", check_fields=True)
44 def set_created_at_now(v):
45 return v or datetime.now(timezone.utc)
48class ClientRegistrationRequestSchema(BaseModel):
49 first_name: str
50 middle_name: Optional[str] = None
51 last_name: str
52 full_name: Optional[str] = None
53 email: EmailStr
54 password: str
55 repeat_password: str
56 district: str
57 designation: str
58 office_address: str
59 contact_no: str
60 created_by: Optional[str] = None
61 created_at: Optional[datetime] = None
62 updated_at: Optional[datetime] = None
64 @field_validator("*", mode="before")
65 def blank_strings(cls, v):
66 if isinstance(v, str):
67 v = v.strip()
68 if v == "":
69 return None
70 return v
72 @model_validator(mode="before")
73 def set_full_name(self):
74 if "first_name" in self and "last_name" in self:
75 first_name = self["first_name"]
76 last_name = self["last_name"]
77 if "middle_name" in self:
78 middle_name = self["middle_name"]
79 self["full_name"] = f"{first_name} {middle_name} {last_name}"
80 else:
81 self["full_name"] = f"{first_name} {last_name}"
83 return self
85 @field_validator("email")
86 def validate_email(cls, v):
87 if not re.match(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", v):
88 raise ValueError("Invalid email address")
89 return v
91 @field_validator("designation", mode="before")
92 @classmethod
93 def validate_designation(cls, v):
94 valid_des = [
95 "superintendent",
96 "assistant superintendent",
97 "director",
98 "executive director",
99 "technology administrator",
100 "specialist",
101 "principal",
102 "assistant principal",
103 "vice principal",
104 "dean",
105 "councilor",
106 "other",
107 ]
108 # Normalize input to title case
109 normalized_designation = v.strip().lower()
111 if normalized_designation not in valid_des:
112 raise ValueError(
113 f"Invalid designation. Valid designation options: {valid_des}"
114 )
115 return normalized_designation
117 @field_validator("password")
118 def validate_password(cls, v):
119 if not re.match(
120 r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{10,25}$", v
121 ):
122 raise ValueError(
123 "Password must be between 10 and 25 characters long and contain at least one uppercase letter, one lowercase letter, one digit, and one special character"
124 )
125 return v
127 @field_validator("repeat_password")
128 def validate_repeat_password(cls, v, info: ValidationInfo, **kwargs):
129 if "password" in info.data and v != info.data["password"]:
130 raise HTTPException(status_code=400, detail="Passwords do not match")
131 return v
133 @field_validator("updated_at", mode="before")
134 @classmethod
135 def set_updated_at_now(cls, v):
136 return v or datetime.now(timezone.utc)
138 @field_validator("created_at", mode="before")
139 @classmethod
140 def set_created_at_now(cls, v):
141 return v or datetime.now(timezone.utc)
143 class Config:
144 json_schema_extra = {
145 "example": {
146 "first_name": "John",
147 "middle_name": "David",
148 "last_name": "Doe",
149 "email": "validemail@gmail.com",
150 "district": "district",
151 "designation": "superintendent",
152 "office_address": "office address",
153 "contact_no": "contact number",
154 "password": "HeyyWorld123!",
155 "repeat_password": "HeyyWorld123!",
156 }
157 }
160class ClientLoginRequest(BaseModel):
161 email: Optional[str] = None
162 password: Optional[str] = None
164 @field_validator("*", mode="before")
165 def blank_strings(cls, v):
166 if isinstance(v, str):
167 v = v.strip()
168 if v == "":
169 return None
170 return v
172 class Config:
173 json_schema_extra = {
174 "example": {
175 "email": "example@gmail.com",
176 "password": "password123!",
177 }
178 }
181class IndividualClientSchema(BaseModel):
182 id: PydanticObjectId = Field(alias="_id")
183 first_name: str
184 middle_name: Optional[str] = None
185 last_name: str
186 role: str
187 email: str
188 created_by: Optional[str] = None
189 created_at: Optional[datetime] = None
190 updated_by: Optional[str] = None
191 updated_at: Optional[datetime] = None
194class School(Document):
195 client_id: Optional[PydanticObjectId] = None
196 district_id: PydanticObjectId # Required reference to the district
197 school_name: Annotated[str, Indexed(unique=True)]
198 address: str
199 state: str
200 city: str
201 zip_code: str
202 street: Optional[str] = None # Made optional since we use address
203 status: str = "active" # active, inactive, deleted
204 phone: str
205 email: Annotated[str, Indexed(unique=True)]
206 school_logo: Optional[str] = None # URL for uploaded school logo
207 bulk_users_file: Optional[str] = None # URL of the uploaded CSV file
208 no_of_students: Optional[int] = 0
209 no_of_teachers: Optional[int] = 0
210 student_total_usage_time_in_minutes: Optional[float] = 0
211 teacher_total_usage_time_in_minutes: Optional[float] = 0
212 total_usage_time_in_minutes: Optional[float] = 0
213 student_total_no_of_visits: Optional[int] = 0
214 teacher_total_no_of_visits: Optional[int] = 0
215 total_no_of_visits: Optional[int] = 0
216 created_at: Optional[datetime] = None
217 updated_at: Optional[datetime] = None
218 created_by: Optional[PydanticObjectId] = None
219 updated_by: Optional[PydanticObjectId] = None
221 class Settings:
222 name = "school_collection"
223 indexes = [
224 [("district_id", 1)], # Index on district_id for faster queries
225 [("status", 1)], # Index on status for filtering
226 [("school_name", 1), ("district_id", 1)] # Compound index for unique school names within a district
227 ]
230class StudentCSVData(BaseModel):
231 """Schema for validating student data from CSV"""
232 email: EmailStr
233 first_name: str
234 middle_name: Optional[str] = None
235 last_name: str
236 grade: Optional[str] = None
237 section: Optional[str] = None # For class assignment
238 semester: Optional[str] = "Fall" # Default semester
239 status: Optional[str] = "active" # Student status in the system
241 class Config:
242 json_schema_extra = {
243 "example": {
244 "email": "student@example.com",
245 "first_name": "John",
246 "middle_name": "M",
247 "last_name": "Doe",
248 "grade": "10",
249 "section": "A",
250 "semester": "Fall"
251 }
252 }
255class SchoolRegistration(BaseModel):
256 district_id: PydanticObjectId # Required reference to the district
257 school_name: str
258 state: str
259 city: str
260 zip_code: str
261 street: str
262 phone: str
263 email: EmailStr
264 school_logo: Optional[str] = None # URL for uploaded school logo
265 created_by: Optional[PydanticObjectId] = None
267 @field_validator("email")
268 @classmethod
269 def check_email(cls, v):
270 if v:
271 regex = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
272 if re.fullmatch(regex, v):
273 return v
274 else:
275 raise ValueError("email is invalid")
276 else:
277 raise ValueError("email field should not be empty")
279 class Config:
280 json_schema_extra = {
281 "example": {
282 "district_id": "507f1f77bcf86cd799439011",
283 "school_name": "Harvard University",
284 "state": "Massachusetts",
285 "city": "Cambridge",
286 "zip_code": "02138",
287 "street": "Massachusetts Hall",
288 "phone": "0992929394",
289 "email": "harvard@edu.com"
290 }
291 }
294class SchoolResponseSchema(BaseModel):
295 id: PydanticObjectId = Field(alias="_id")
296 district_id: PydanticObjectId
297 school_name: str
298 state: str
299 city: str
300 zip_code: str
301 street: str
302 status: str
303 phone: str
304 email: str
305 school_logo: Optional[str] = None
306 no_of_students: Optional[int] = 0
307 no_of_teachers: Optional[int] = 0
308 student_total_usage_time_in_minutes: Optional[float] = 0
309 teacher_total_usage_time_in_minutes: Optional[float] = 0
310 total_usage_time_in_minutes: Optional[float] = 0
311 student_total_no_of_visits: Optional[int] = 0
312 teacher_total_no_of_visits: Optional[int] = 0
313 total_no_of_visits: Optional[int] = 0
314 created_at: Optional[datetime] = None
315 updated_at: Optional[datetime] = None
316 created_by: Optional[PydanticObjectId] = None
317 updated_by: Optional[PydanticObjectId] = None
320class ClientUserRegistrationRequestSchema(BaseModel):
321 client_id: Optional[PydanticObjectId] = None
322 school_id: PydanticObjectId
323 first_name: str
324 middle_name: Optional[str] = None
325 last_name: str
326 email: EmailStr
327 role: str
328 status: Optional[str] = "inactive"
329 password: Optional[str] = None
330 created_at: Optional[datetime] = None
331 updated_at: Optional[datetime] = None
333 @field_validator("*", mode="before")
334 def blank_strings(cls, v):
335 if isinstance(v, str):
336 v = v.strip()
337 if v == "":
338 return None
339 return v
341 @field_validator("email")
342 @classmethod
343 def check_email(cls, v):
344 if v:
345 regex = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
346 if re.fullmatch(regex, v):
347 return v
348 else:
349 raise ValueError("email is invalid")
350 else:
351 raise ValueError("email field should not be empty")
353 @field_validator("updated_at", mode="before")
354 @classmethod
355 def set_updated_at_now(cls, v):
356 return v or datetime.now(timezone.utc)
358 @field_validator("created_at", mode="before")
359 @classmethod
360 def set_created_at_now(cls, v):
361 return v or datetime.now(timezone.utc)
363 class Config:
364 json_schema_extra = {
365 "example": {
366 "first_name": "John",
367 "middle_name": "David",
368 "last_name": "Doe",
369 "email": "validemail@gmail.com",
370 "role": "teacher",
371 }
372 }
375class UserResponseSchema(BaseModel):
376 id: PydanticObjectId = Field(alias="_id")
377 first_name: str
378 middle_name: Optional[str] = None
379 last_name: str
380 role: str
381 email: str
382 client_id: PydanticObjectId
383 school_id: PydanticObjectId
384 total_usage_time_in_minutes: Optional[float] = 0
385 total_no_of_visits: Optional[int] = 0
386 created_at: Optional[datetime] = None
387 updated_at: Optional[datetime] = None
390class ClientUserSessionDuration(BaseModel):
391 session_duration_in_minutes: float
394# Removed duplicate SchoolResponseSchema - using the complete one defined earlier