Skip to content

Commit 62dab3c

Browse files
authored
Add profile management and single opportunity endpoints (#340)
2 parents bcbff90 + dcc07ff commit 62dab3c

File tree

6 files changed

+401
-1
lines changed

6 files changed

+401
-1
lines changed

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,9 @@ env/
66
*.db
77
.coverage
88
*.vscode
9-
.env
9+
.env
10+
11+
labconnect/bin
12+
labconnect/include
13+
labconnect/lib
14+
labconnect/pyvenv.cfg

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ posted by professors, graduate students, or lab staff.</p>
112112
```bash
113113
$ python3 -m pytest --cov
114114
```
115+
or individual tests
116+
```bash
117+
$ python3 -m pytest -q tests/(file_name).py -q
118+
```
115119

116120
## Development
117121
* Run flask with python directly

labconnect/main/opportunity_routes.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,62 @@
2727
from . import main_blueprint
2828

2929

30+
def opportunity_to_dict(opportunity: Opportunities) -> dict:
31+
"""Return a plain dict representation of an Opportunities model instance."""
32+
if opportunity is None:
33+
return {}
34+
35+
return {
36+
"id": opportunity.id,
37+
"name": opportunity.name,
38+
"description": opportunity.description,
39+
"recommended_experience": opportunity.recommended_experience,
40+
"pay": opportunity.pay,
41+
"one_credit": bool(opportunity.one_credit),
42+
"two_credits": bool(opportunity.two_credits),
43+
"three_credits": bool(opportunity.three_credits),
44+
"four_credits": bool(opportunity.four_credits),
45+
"semester": str(opportunity.semester)
46+
if opportunity.semester is not None
47+
else None,
48+
"year": opportunity.year,
49+
"active": bool(opportunity.active),
50+
}
51+
52+
53+
# Single opportunity endpoints used by the frontend/tests
54+
@main_blueprint.get("/opportunity/<int:opportunity_id>")
55+
def get_single_opportunity(opportunity_id: int):
56+
"""Return a single opportunity by id. Returns 404 if not found"""
57+
opp = db.session.get(Opportunities, opportunity_id)
58+
if not opp:
59+
abort(404)
60+
61+
return opportunity_to_dict(opp)
62+
63+
64+
@main_blueprint.get("/opportunity")
65+
def get_opportunity_via_json():
66+
"""GET /opportunity expects a JSON payload with {"id": <int>}."""
67+
data = request.get_json()
68+
if not data:
69+
abort(400)
70+
71+
if "id" not in data:
72+
abort(400)
73+
74+
try:
75+
opp_id = int(data["id"])
76+
except ValueError:
77+
abort(400)
78+
79+
opp = db.session.get(Opportunities, opp_id)
80+
if not opp:
81+
abort(404)
82+
83+
return opportunity_to_dict(opp)
84+
85+
3086
@main_blueprint.get("/searchOpportunity/<string:query>")
3187
def searchOpportunity(query: str):
3288
# Perform a search

labconnect/main/profile_routes.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
from flask import Response, jsonify, make_response, request
2+
from flask_jwt_extended import get_jwt_identity, jwt_required
3+
4+
from labconnect import db
5+
from labconnect.models import Majors, RPIDepartments, User, UserDepartments, UserMajors
6+
7+
from . import main_blueprint
8+
9+
10+
def user_to_dict(user: User) -> dict:
11+
"""Helper function to serialize User object data."""
12+
user_departments = (
13+
db.session.execute(
14+
db.select(UserDepartments.department_id).where(
15+
UserDepartments.user_id == user.id
16+
)
17+
)
18+
.scalars()
19+
.all()
20+
)
21+
22+
user_majors = (
23+
db.session.execute(
24+
db.select(UserMajors.major_code).where(UserMajors.user_id == user.id)
25+
)
26+
.scalars()
27+
.all()
28+
)
29+
30+
return {
31+
"id": user.id,
32+
"email": user.email,
33+
"first_name": user.first_name,
34+
"last_name": user.last_name,
35+
"preferred_name": user.preferred_name,
36+
"class_year": user.class_year,
37+
"profile_picture": user.profile_picture,
38+
"website": user.website,
39+
"description": user.description,
40+
"departments": user_departments,
41+
"majors": user_majors,
42+
}
43+
44+
45+
@main_blueprint.route("/profile", methods=["GET"])
46+
@jwt_required()
47+
def get_profile() -> Response:
48+
"""GET /profile: current user profile"""
49+
user_email = get_jwt_identity()
50+
user = db.session.execute(
51+
db.select(User).where(User.email == user_email)
52+
).scalar_one_or_none()
53+
54+
if not user:
55+
return make_response(jsonify({"msg": "User not found"}), 404)
56+
57+
return jsonify(user_to_dict(user))
58+
59+
60+
@main_blueprint.route("/profile", methods=["PUT"])
61+
@jwt_required()
62+
def update_profile() -> Response:
63+
"""PUT /profile: Updates current user profile"""
64+
user_email = get_jwt_identity()
65+
user = db.session.execute(
66+
db.select(User).where(User.email == user_email)
67+
).scalar_one_or_none()
68+
69+
if not user:
70+
return make_response(jsonify({"msg": "User not found"}), 404)
71+
72+
json_data = request.get_json()
73+
if not json_data:
74+
return make_response(jsonify({"msg": "Missing JSON in request"}), 400)
75+
76+
# Update basic User fields
77+
user.first_name = json_data.get("first_name", user.first_name)
78+
user.last_name = json_data.get("last_name", user.last_name)
79+
user.preferred_name = json_data.get("preferred_name", user.preferred_name)
80+
user.class_year = json_data.get("class_year", user.class_year)
81+
user.website = json_data.get("website", user.website)
82+
user.description = json_data.get("description", user.description)
83+
84+
if "departments" in json_data:
85+
db.session.query(UserDepartments).filter(
86+
UserDepartments.user_id == user.id
87+
).delete()
88+
for dept_id in json_data["departments"]:
89+
if db.session.get(
90+
RPIDepartments, dept_id
91+
): # Ensure department exists before adding
92+
new_user_dept = UserDepartments(user_id=user.id, department_id=dept_id)
93+
db.session.add(new_user_dept)
94+
95+
if "majors" in json_data:
96+
db.session.query(UserMajors).filter(UserMajors.user_id == user.id).delete()
97+
for major_code in json_data["majors"]:
98+
if db.session.get(Majors, major_code): # Ensure major exists before adding
99+
new_user_major = UserMajors(user_id=user.id, major_code=major_code)
100+
db.session.add(new_user_major)
101+
102+
db.session.commit()
103+
104+
return jsonify({"msg": "Profile updated successfully"})

tests/test_profile_routes.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import json
2+
3+
from flask.testing import FlaskClient
4+
5+
from labconnect import db
6+
from labconnect.models import User, UserDepartments, UserMajors
7+
8+
9+
def login_as_student(test_client: FlaskClient):
10+
"""Helper function to log in a user and handle the auth flow."""
11+
response = test_client.get("/login")
12+
assert response.status_code == 302
13+
14+
redirect_url = response.headers["Location"]
15+
code = redirect_url.split("code=")[1]
16+
17+
token_response = test_client.post("/token", json={"code": code})
18+
assert token_response.status_code == 200
19+
20+
21+
# === GET /profile Tests ===
22+
23+
24+
def test_get_profile_success(test_client: FlaskClient):
25+
"""
26+
logged-in user: '/profile' endpoint is requested (GET)
27+
-> correct data and 200 status
28+
"""
29+
login_as_student(test_client)
30+
31+
response = test_client.get("/profile")
32+
data = json.loads(response.data)
33+
34+
assert response.status_code == 200
35+
assert data["email"] == "[email protected]"
36+
assert data["first_name"] == "Test"
37+
assert data["last_name"] == "User"
38+
assert "departments" in data
39+
assert "majors" in data
40+
41+
42+
def test_get_profile_unauthorized(test_client: FlaskClient):
43+
"""
44+
no user is logged in: '/profile' endpoint is requested (GET)
45+
-> 401 Unauthorized status is returned.
46+
"""
47+
test_client.get("/logout")
48+
response = test_client.get("/profile")
49+
assert response.status_code == 401
50+
51+
52+
# === PUT /profile Tests ===
53+
54+
55+
def test_update_profile_success(test_client: FlaskClient):
56+
"""
57+
logged-in user: '/profile' endpoint is updated with new data (PUT)
58+
-> 200 status and database changed.
59+
"""
60+
login_as_student(test_client)
61+
62+
update_data = {
63+
"first_name": "UpdatedFirst",
64+
"last_name": "UpdatedLast",
65+
"preferred_name": "Pref",
66+
"class_year": 2025,
67+
"website": "https://new.example.com",
68+
"description": "This is an updated description.",
69+
"departments": ["CS"],
70+
"majors": ["CSCI", "MATH"],
71+
}
72+
73+
response = test_client.put("/profile", json=update_data)
74+
assert response.status_code == 200
75+
assert "Profile updated successfully" in json.loads(response.data)["msg"]
76+
77+
# Verify the changes in the database
78+
user = db.session.execute(
79+
db.select(User).where(User.email == "[email protected]")
80+
).scalar_one()
81+
assert user.first_name == "UpdatedFirst"
82+
assert user.website == "https://new.example.com"
83+
assert user.class_year == 2025
84+
85+
user_depts = (
86+
db.session.execute(
87+
db.select(UserDepartments.department_id).where(
88+
UserDepartments.user_id == user.id
89+
)
90+
)
91+
.scalars()
92+
.all()
93+
)
94+
assert set(user_depts) == {"CS"}
95+
96+
user_majors = (
97+
db.session.execute(
98+
db.select(UserMajors.major_code).where(UserMajors.user_id == user.id)
99+
)
100+
.scalars()
101+
.all()
102+
)
103+
assert set(user_majors) == {"CSCI", "MATH"}
104+
105+
106+
def test_update_profile_partial(test_client: FlaskClient):
107+
"""
108+
logged-in user: '/profile' endpoint is updated with partial data (PUT)
109+
-> check only provided fields updated.
110+
"""
111+
login_as_student(test_client)
112+
113+
update_data = {
114+
"website": "https://partial.update.com",
115+
"description": "Only this was updated.",
116+
}
117+
118+
response = test_client.put("/profile", json=update_data)
119+
assert response.status_code == 200
120+
121+
user = db.session.execute(
122+
db.select(User).where(User.email == "[email protected]")
123+
).scalar_one()
124+
assert user.website == "https://partial.update.com"
125+
assert user.description == "Only this was updated."
126+
assert user.last_name == "User"
127+
128+
129+
def test_update_profile_unauthorized(test_client: FlaskClient):
130+
"""
131+
no user is logged in: '/profile' endpoint is sent a PUT request
132+
-> 401 Unauthorized status.
133+
"""
134+
test_client.get("/logout")
135+
update_data = {"first_name": "ShouldFail"}
136+
response = test_client.put("/profile", json=update_data)
137+
assert response.status_code == 401

0 commit comments

Comments
 (0)