Skip to content

Commit bb60ce5

Browse files
authored
Merge pull request #1161 from Benji918/feat/filtered_job_search
feat: implemeted filtered job search endpoint
2 parents ff9238c + 1eef5d5 commit bb60ce5

3 files changed

Lines changed: 178 additions & 16 deletions

File tree

api/v1/routes/jobs.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from api.v1.schemas.jobs import PostJobSchema, AddJobSchema, JobCreateResponseSchema, UpdateJobSchema
55
from fastapi.exceptions import HTTPException
66
from fastapi.encoders import jsonable_encoder
7-
from typing import Annotated
7+
from typing import Annotated, Optional
88
from fastapi import APIRouter, HTTPException, Depends, status, Query
99

1010
from api.v1.services.user import user_service
@@ -60,9 +60,38 @@ async def add_jobs(
6060
data=jsonable_encoder(JobCreateResponseSchema.model_validate(new_job))
6161
)
6262

63+
@jobs.get("/filter", response_model=success_response)
64+
async def filter(
65+
title: Optional[str] = None,
66+
location: Optional[str] = None,
67+
job_type: Optional[str] = None,
68+
db: Session = Depends(get_db)
69+
):
70+
"""
71+
Retrieve job details by specified search parameters salary range, location and job_type.
72+
This endpoint to handle job filtering based on user preferences. This endpoint will allow users to filter
73+
job listings by parameters such as salary range, location, and job type to find positions that match
74+
their specific needs
75+
76+
Parameters:
77+
- title: str (optional)
78+
The job title
79+
- location: str (optional)
80+
The job location
81+
- job_type: str (optional)
82+
The type of job
83+
- db: The database session
84+
"""
85+
jobs = job_service.fetch_by_filters(db, title, location, job_type)
86+
87+
return success_response(
88+
status_code=status.HTTP_200_OK,
89+
data=jsonable_encoder(jobs),
90+
message= f"Successfully retrieved {len(jobs)} jobs"
91+
)
6392

6493
@jobs.get("/{job_id}", response_model=success_response)
65-
async def get_job(
94+
async def retrieveJob(
6695
job_id: str,
6796
db: Session = Depends(get_db)
6897
):
@@ -75,15 +104,14 @@ async def get_job(
75104
The ID of the job to retrieve.
76105
- db: The database session
77106
"""
78-
job = job_service.fetch(db, job_id)
107+
job = job_service.retrieve(db, job_id)
79108

80109
return success_response(
81110
message="Retrieved Job successfully",
82-
status_code=200,
111+
status_code=status.HTTP_200_OK,
83112
data=jsonable_encoder(job)
84113
)
85114

86-
87115
@jobs.get("")
88116
async def fetch_all_jobs(
89117
db: Session = Depends(get_db),
@@ -146,6 +174,7 @@ async def update_job(
146174
)
147175

148176

177+
149178
# -------------------- JOB APPLICATION ROUTES ------------------------
150179
# --------------------------------------------------------------------
151180

@@ -211,7 +240,7 @@ async def fetch_all_job_applications(
211240
Args:
212241
- job_id (str): The Job ID
213242
- db (Annotated[Session, Depends): the database session
214-
- current_user: The current authenticated super admin
243+
- current_user: The current authenticated super admin
215244
- per_page: Number of customers per page (default: 10, minimum: 1)
216245
- page: Page number (starts from 1)
217246
@@ -242,6 +271,7 @@ async def delete_application(job_id: str,
242271
job_application_service.delete(job_id, application_id, db)
243272

244273

274+
245275
# -------------------- JOB BOOKMARK ROUTES ------------------------
246276
# --------------------------------------------------------------------
247277

@@ -277,7 +307,7 @@ async def create_bookmark(
277307
"status": "failure",
278308
"message": "Job not listed",
279309
"status_code": 400,
280-
"data": {}
310+
"data": {}
281311
}
282312
except HTTPException as e:
283313
return {

api/v1/services/jobs.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
from api.core.base.services import Service
55
from api.v1.models.job import Job
66
from fastapi import HTTPException
7+
from api.utils.db_validators import check_model_existence
78

89

9-
class JobService(Service):
10+
class JobService():
1011
"""Job service functionality"""
1112

1213
def create(self, db: Session, schema) -> Job:
@@ -32,20 +33,35 @@ def fetch_all(self, db: Session, **query_params: Optional[Any]):
3233

3334
return query.all()
3435

35-
36-
@staticmethod
37-
def fetch(db: Session, id: str) -> Optional[Job]:
36+
def retrieve(self, db: Session, job_id):
3837
"""Fetches a job by ID"""
39-
return db.query(Job).filter(Job.id == id).first()
40-
41-
def fetch(self, db: Session, id: str):
42-
"""Fetches a job by id"""
43-
job = db.query(Job).filter(Job.id == id).first()
38+
job = db.query(Job).filter(Job.id == job_id).first()
4439
if not job:
4540
raise HTTPException(status_code=404, detail="Job not found")
4641
return job
4742

4843

44+
def fetch_by_filters(self, db: Session, title: str = None, location: str = None, job_type: str = None):
45+
"""Fetch jobs by the specified filters"""
46+
query = db.query(Job)
47+
48+
if title:
49+
query = query.filter(Job.title.ilike(f"%{title}%"))
50+
if location:
51+
query = query.filter(Job.location.ilike(f"%{location}%"))
52+
if job_type:
53+
query = query.filter(Job.job_type == job_type)
54+
55+
jobs = query.all()
56+
57+
if not jobs:
58+
raise HTTPException(
59+
status_code=404,
60+
detail="No jobs found matching the search parameters."
61+
)
62+
return jobs
63+
64+
4965
def update(self, db: Session, id: str, schema):
5066
"""Updates a job"""
5167

tests/v1/job/test_filter_jobs.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import pytest
2+
from fastapi.testclient import TestClient
3+
from main import app
4+
from api.db.database import get_db
5+
from api.v1.models.job import Job
6+
from faker import Faker
7+
8+
fake = Faker()
9+
client = TestClient(app)
10+
11+
class FakeJob:
12+
def __init__(self, title, location, job_type):
13+
self.title = title
14+
self.location = location
15+
self.job_type = job_type
16+
def dict(self):
17+
return {"title": self.title, "location": self.location, "job_type": self.job_type}
18+
19+
class FakeQuery:
20+
def __init__(self, jobs):
21+
self.jobs = jobs
22+
def filter(self, predicate):
23+
filtered = list(filter(predicate, self.jobs))
24+
return FakeQuery(filtered)
25+
def all(self):
26+
return self.jobs
27+
28+
class FakeSession:
29+
def __init__(self, jobs):
30+
self.jobs = jobs
31+
def query(self, model):
32+
return FakeQuery(self.jobs)
33+
34+
class FakeColumn:
35+
def __init__(self, attr_name):
36+
self.attr_name = attr_name
37+
def ilike(self, pattern):
38+
def predicate(job):
39+
value = getattr(job, self.attr_name, "")
40+
return pattern.strip("%").lower() in value.lower()
41+
return predicate
42+
def __eq__(self, other):
43+
def predicate(job):
44+
return getattr(job, self.attr_name, None) == other
45+
return predicate
46+
47+
Job.title = FakeColumn("title")
48+
Job.location = FakeColumn("location")
49+
Job.job_type = FakeColumn("job_type")
50+
51+
fake_jobs = [
52+
FakeJob("Software Engineer", "New York", "Full Time"),
53+
FakeJob("Data Scientist", "San Francisco", "Part Time"),
54+
FakeJob("Backend Developer", "New York", "Contract")
55+
]
56+
57+
@pytest.fixture
58+
def fake_db():
59+
yield FakeSession(fake_jobs)
60+
61+
@pytest.fixture(autouse=True)
62+
def override_get_db(fake_db):
63+
def get_db_override():
64+
yield fake_db
65+
app.dependency_overrides[get_db] = get_db_override
66+
yield
67+
app.dependency_overrides = {}
68+
69+
def test_filter_no_parameters():
70+
response = client.get("/api/v1/jobs/filter")
71+
assert response.status_code == 200
72+
json_data = response.json()
73+
assert json_data["status_code"] == 200
74+
assert len(json_data["data"]) == 3
75+
assert json_data["message"] == "Successfully retrieved 3 jobs"
76+
77+
def test_filter_by_title():
78+
response = client.get("/api/v1/jobs/filter", params={"title": "Software"})
79+
assert response.status_code == 200
80+
json_data = response.json()
81+
assert len(json_data["data"]) == 1
82+
assert json_data["data"][0]["title"] == "Software Engineer"
83+
assert json_data["message"] == "Successfully retrieved 1 jobs"
84+
85+
def test_filter_by_location():
86+
response = client.get("/api/v1/jobs/filter", params={"location": "New York"})
87+
assert response.status_code == 200
88+
json_data = response.json()
89+
assert len(json_data["data"]) == 2
90+
titles = [job["title"] for job in json_data["data"]]
91+
assert "Software Engineer" in titles
92+
assert "Backend Developer" in titles
93+
assert json_data["message"] == "Successfully retrieved 2 jobs"
94+
95+
def test_filter_by_job_type():
96+
response = client.get("/api/v1/jobs/filter", params={"job_type": "Part Time"})
97+
assert response.status_code == 200
98+
json_data = response.json()
99+
assert len(json_data["data"]) == 1
100+
assert json_data["data"][0]["job_type"] == "Part Time"
101+
assert json_data["message"] == "Successfully retrieved 1 jobs"
102+
103+
def test_filter_by_multiple_parameters():
104+
response = client.get("/api/v1/jobs/filter", params={"title": "Developer", "location": "New York"})
105+
assert response.status_code == 200
106+
json_data = response.json()
107+
assert len(json_data["data"]) == 1
108+
assert json_data["data"][0]["title"] == "Backend Developer"
109+
assert json_data["data"][0]["location"] == "New York"
110+
assert json_data["message"] == "Successfully retrieved 1 jobs"
111+
112+
def test_filter_no_match():
113+
response = client.get("/api/v1/jobs/filter", params={"title": "Manager"})
114+
assert response.status_code == 404
115+
json_data = response.json()
116+
assert json_data["message"] == "No jobs found matching the search parameters."

0 commit comments

Comments
 (0)