Coverage for backend/app/schemas.py: 100%
258 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-09-22 15:38 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-09-22 15:38 +0000
1"""Schemas for the JAM database
2Create schemas should be used to create entries in the database.
3Out schemas should be used to return data to the user.
4Min schemas should be used to return minimal data to the user (enough to display the entry as a badge) and should not
5contain reference to other tables.
6Update schemas should be used to update existing entries in the database."""
8from datetime import datetime, UTC
10from pydantic import BaseModel, EmailStr, computed_field
13class Out(BaseModel):
14 """Base model for all output schemas"""
16 id: int
17 created_at: datetime
18 modified_at: datetime
21class OwnedOut(Out):
22 """Base model for all output schemas owned by a user"""
24 owner_id: int
27# ------------------------------------------------------- SETTINGS ------------------------------------------------------
30class SettingCreate(BaseModel):
31 """Setting create schema"""
33 name: str
34 value: str
35 description: str | None = None
38class SettingOut(SettingCreate, Out):
39 """Setting output schema"""
41 pass
44class SettingUpdate(SettingCreate):
45 """Keyword update schema"""
47 name: str | None = None
48 value: str | None = None
51# -------------------------------------------------------- USER --------------------------------------------------------
54class UserCreate(BaseModel):
55 password: str
56 email: EmailStr
59class UserOut(Out):
60 email: EmailStr
61 theme: str
62 is_admin: bool = False
63 last_login: datetime | None = None
64 chase_threshold: int
65 deadline_threshold: int
66 update_limit: int
69class UserLogin(BaseModel):
70 email: EmailStr
71 password: str
74class UserUpdate(BaseModel):
75 current_password: str | None = None
76 email: EmailStr | None = None
77 theme: str | None = None
78 password: str | None = None
79 is_admin: bool | None = None
80 last_login: datetime | None = None
81 chase_threshold: int | None = None
82 deadline_threshold: int | None = None
83 update_limit: int | None = None
86# -------------------------------------------------------- TOKEN -------------------------------------------------------
89class Token(BaseModel):
90 access_token: str
91 token_type: str
94class TokenData(BaseModel):
95 id: str | None = None
98# ------------------------------------------------------- KEYWORD ------------------------------------------------------
101class KeywordCreate(BaseModel):
102 """Keyword create schema"""
104 name: str
107class KeywordOut(KeywordCreate, OwnedOut):
108 """Keyword output schema with full job data"""
110 jobs: list["JobOut"] = []
113class KeywordMinOut(KeywordCreate, OwnedOut):
114 """Bare Keyword output schema"""
116 pass
119class KeywordUpdate(KeywordCreate):
120 """Keyword update schema"""
122 name: str | None = None
125# ----------------------------------------------------- AGGREGATOR -----------------------------------------------------
128class AggregatorCreate(BaseModel):
129 """Aggregator create schema"""
131 name: str
132 url: str | None = None
135class AggregatorOut(AggregatorCreate, OwnedOut):
136 """Aggregator output schema with full job data and job applications"""
138 jobs: list["JobOut"] = []
139 job_applications: list["JobOut"] = []
142class AggregatorMinOut(AggregatorCreate, OwnedOut):
143 """Bare aggregator output schema"""
145 pass
148class AggregatorUpdate(AggregatorCreate):
149 """Aggregator update schema"""
151 name: str | None = None
154# ------------------------------------------------------- COMPANY ------------------------------------------------------
157class CompanyCreate(BaseModel):
158 """Company create schema"""
160 name: str
161 description: str | None = None
162 url: str | None = None
165class CompanyOut(CompanyCreate, OwnedOut):
166 """Company output schema with job data and individuals"""
168 jobs: list["JobOut"] = []
169 persons: list["PersonOut"] = []
172class CompanyMinOut(CompanyCreate, OwnedOut):
173 """Bare company output schema"""
175 pass
178class CompanyUpdate(CompanyCreate):
179 """Company update schema"""
181 name: str | None = None
184# ------------------------------------------------------ LOCATION ------------------------------------------------------
187class LocationCreate(BaseModel):
188 """Location create schema"""
190 postcode: str | None = None
191 city: str | None = None
192 country: str | None = None
195class LocationOut(LocationCreate, OwnedOut):
196 """Location output schema with job and interview data"""
198 name: str | None = None
199 jobs: list["JobOut"] = []
200 interviews: list["InterviewOut"] = []
203class LocationMinOut(LocationCreate, OwnedOut):
204 """Bare location output schema"""
206 name: str | None = None
209class LocationUpdate(LocationCreate):
210 """Location update schema"""
212 pass
215# -------------------------------------------------------- FILES -------------------------------------------------------
218class FileCreate(BaseModel):
219 """File create schema"""
221 filename: str
222 type: str
223 content: str
224 size: int
227class FileOut(FileCreate, OwnedOut):
228 """File output schema"""
230 pass
233class FileUpdate(FileCreate):
234 """File update schema"""
236 filename: str | None = None
237 type: str | None = None
238 content: str | None = None
239 size: int | None = None
242# ------------------------------------------------------- PERSON -------------------------------------------------------
245class PersonCreate(BaseModel):
246 """Person create schema"""
248 first_name: str
249 last_name: str
250 email: EmailStr | None = None
251 phone: str | None = None
252 linkedin_url: str | None = None
253 role: str | None = None
255 # Foreign keys
256 company_id: int | None = None
259class PersonOut(PersonCreate, OwnedOut):
260 """Person out schema with job data and bare interview data"""
262 company: CompanyMinOut | None = None
263 interviews: list["InterviewMinOut"] = []
264 jobs: list["JobOut"] = []
265 name: str | None = None
266 name_company: str | None = None
269class PersonMinOut(PersonCreate, OwnedOut):
270 """Bare person output schema"""
272 name: str | None = None
273 name_company: str | None = None
276class PersonUpdate(PersonCreate):
277 """Person update schema"""
279 first_name: str | None = None
280 last_name: str | None = None
283# --------------------------------------------------------- JOB --------------------------------------------------------
286class JobCreate(BaseModel):
287 """Job create schema"""
289 title: str
290 description: str | None = None
291 salary_min: float | None = None
292 salary_max: float | None = None
293 personal_rating: int | None = None
294 url: str | None = None
295 deadline: datetime | None = None
296 note: str | None = None
297 attendance_type: str | None = None
298 application_date: datetime | None = None
299 application_url: str | None = None
300 application_status: str | None = None
301 application_note: str | None = None
302 applied_via: str | None = None
304 # Foreign keys
305 company_id: int | None = None
306 location_id: int | None = None
307 duplicate_id: int | None = None
308 source_id: int | None = None
309 application_aggregator_id: int | None = None
310 cv_id: int | None = None
311 cover_letter_id: int | None = None
312 keywords: list[int] = []
313 contacts: list[int] = []
316class JobOut(JobCreate, OwnedOut):
317 """Job output schema with bare company, location, aggregator, keywords, contacts data and semi-full interview and update data"""
319 company: CompanyMinOut | None = None
320 location: LocationMinOut | None = None
321 source: AggregatorMinOut | None = None
322 keywords: list[KeywordMinOut] = []
323 contacts: list[PersonMinOut] = []
324 application_aggregator: AggregatorMinOut | None = None
325 interviews: list["InterviewAppOut"] = [] # get the full interviews
326 updates: list["JobApplicationUpdateAppOut"] = [] # get the full updates
327 name: str
329 @computed_field
330 @property
331 def last_update_date(self) -> datetime | None:
332 """Computed property that returns the most recent activity date from application date, interviews, or updates"""
334 if self.application_date is None:
335 return None
337 dates = [self.application_date]
339 # Add interview dates
340 if self.interviews:
341 dates.extend([interview.date for interview in self.interviews])
343 # Add update dates
344 if self.updates:
345 dates.extend([update.date for update in self.updates])
347 # Filter out None values and return the maximum date
348 valid_dates = [d for d in dates if d is not None]
349 return max(valid_dates) if valid_dates else self.created_at
351 @computed_field
352 @property
353 def last_update_type(self) -> str | None:
354 """Computed property that returns the type of the most recent activity"""
356 if self.application_date is None:
357 return None
359 most_recent_date = self.application_date
360 most_recent_type = "Application"
362 # Check interviews
363 if self.interviews:
364 latest_interview = max(self.interviews, key=lambda x: x.date, default=None)
365 if latest_interview and latest_interview.date > most_recent_date:
366 most_recent_date = latest_interview.date
367 most_recent_type = f"Interview ({len(self.interviews)})"
369 # Check updates
370 if self.updates:
371 latest_update = max(self.updates, key=lambda x: x.date, default=None)
372 if latest_update and latest_update.date > most_recent_date:
373 most_recent_type = f"Update ({len(self.updates)})"
375 return most_recent_type
377 @computed_field
378 @property
379 def days_since_last_update(self) -> int | None:
380 """Calculate days since the last update"""
382 if self.application_date is None:
383 return None
384 now = datetime.now(UTC)
385 return (now - self.last_update_date).days
387 @computed_field
388 @property
389 def days_until_deadline(self) -> int | None:
390 """Calculate the number of days before the deadline"""
392 if self.deadline is None:
393 return None
394 now = datetime.now(UTC)
395 return (self.deadline - now).days
398class JobMinOut(OwnedOut):
399 """Bare job output schema"""
401 title: str
402 description: str | None
403 salary_min: float | None
404 salary_max: float | None
405 personal_rating: int | None
406 url: str | None
407 deadline: datetime | None
408 note: str | None
409 attendance_type: str | None
410 application_date: datetime
411 application_url: str | None = None
412 application_status: str
413 application_note: str | None = None
414 applied_via: str | None = None
415 name: str
417 # Foreign keys
418 company_id: int | None = None
419 location_id: int | None = None
420 duplicate_id: int | None = None
421 source_id: int | None = None
422 application_aggregator_id: int | None = None
425class JobUpdate(JobCreate):
426 """Job update schema"""
428 title: str | None = None
431# ------------------------------------------------------ INTERVIEW -----------------------------------------------------
434class InterviewCreate(BaseModel):
435 """Interview create schema"""
437 date: datetime
438 type: str
439 job_id: int
440 attendance_type: str | None = None
441 location_id: int | None = None
442 note: str | None = None
443 interviewers: list[int] | None = None
446class InterviewOut(InterviewCreate, OwnedOut):
447 """Interview output with bare location and person data, and job data"""
449 location: LocationMinOut | None = None
450 interviewers: list[PersonMinOut] = []
451 job: JobOut | None = None
454class InterviewAppOut(InterviewCreate, OwnedOut):
455 """Interview output with bare location and person data"""
457 location: LocationMinOut | None = None
458 interviewers: list[PersonMinOut] = []
461class InterviewMinOut(OwnedOut):
462 """Bare interview output schema"""
464 date: datetime
465 type: str
466 location_id: int | None
467 job_id: int
468 note: str | None
469 attendance_type: str | None
472class InterviewUpdate(InterviewCreate):
473 """Interview update schema"""
475 date: datetime | None = None
476 type: str | None = None
477 job_id: int | None = None
480# ----------------------------------------------- JOB APPLICATION UPDATE -----------------------------------------------
483class JobApplicationUpdateCreate(BaseModel):
484 """Job Application Update create schema"""
486 date: datetime
487 type: str
488 job_id: int
489 note: str | None = None
492class JobApplicationUpdateOut(JobApplicationUpdateCreate, OwnedOut):
493 """Job Application Update output schema with job data"""
495 job: JobOut | None = None
498class JobApplicationUpdateAppOut(JobApplicationUpdateCreate, OwnedOut):
499 """Job Application Update output"""
501 pass
504class JobApplicationUpdateUpdate(JobApplicationUpdateCreate):
505 """Job Application Update update schema"""
507 date: datetime | None = None
508 type: str | None = None
509 job_id: int | None = None