Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@ env/
*.db
.coverage
*.vscode
.env
.env

labconnect/bin
labconnect/include
labconnect/lib
labconnect/pyvenv.cfg
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ posted by professors, graduate students, or lab staff.</p>
```bash
$ python3 -m pytest --cov
```
or individual tests
```bash
$ python3 -m pytest -q tests/(file_name).py -q
```

## Development
* Run flask with python directly
Expand Down
56 changes: 56 additions & 0 deletions labconnect/main/opportunity_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,62 @@
from . import main_blueprint


def opportunity_to_dict(opportunity: Opportunities) -> dict:
"""Return a plain dict representation of an Opportunities model instance."""
if opportunity is None:
return {}

return {
"id": opportunity.id,
"name": opportunity.name,
"description": opportunity.description,
"recommended_experience": opportunity.recommended_experience,
"pay": opportunity.pay,
"one_credit": bool(opportunity.one_credit),
"two_credits": bool(opportunity.two_credits),
"three_credits": bool(opportunity.three_credits),
"four_credits": bool(opportunity.four_credits),
"semester": str(opportunity.semester)
if opportunity.semester is not None
else None,
"year": opportunity.year,
"active": bool(opportunity.active),
}


# Single opportunity endpoints used by the frontend/tests
@main_blueprint.get("/opportunity/<int:opportunity_id>")
def get_single_opportunity(opportunity_id: int):
"""Return a single opportunity by id. Returns 404 if not found"""
opp = db.session.get(Opportunities, opportunity_id)
if not opp:
abort(404)

return opportunity_to_dict(opp)


@main_blueprint.get("/opportunity")
def get_opportunity_via_json():
"""GET /opportunity expects a JSON payload with {"id": <int>}."""
data = request.get_json()
if not data:
abort(400)

if "id" not in data:
abort(400)

try:
opp_id = int(data["id"])
except ValueError:
abort(400)

opp = db.session.get(Opportunities, opp_id)
if not opp:
abort(404)

return opportunity_to_dict(opp)


@main_blueprint.get("/searchOpportunity/<string:query>")
def searchOpportunity(query: str):
# Perform a search
Expand Down
104 changes: 104 additions & 0 deletions labconnect/main/profile_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from flask import Response, jsonify, make_response, request
from flask_jwt_extended import get_jwt_identity, jwt_required

from labconnect import db
from labconnect.models import Majors, RPIDepartments, User, UserDepartments, UserMajors

from . import main_blueprint


def user_to_dict(user: User) -> dict:
"""Helper function to serialize User object data."""
user_departments = (
db.session.execute(
db.select(UserDepartments.department_id).where(
UserDepartments.user_id == user.id
)
)
.scalars()
.all()
)

user_majors = (
db.session.execute(
db.select(UserMajors.major_code).where(UserMajors.user_id == user.id)
)
.scalars()
.all()
)

return {
"id": user.id,
"email": user.email,
"first_name": user.first_name,
"last_name": user.last_name,
"preferred_name": user.preferred_name,
"class_year": user.class_year,
"profile_picture": user.profile_picture,
"website": user.website,
"description": user.description,
"departments": user_departments,
"majors": user_majors,
}


@main_blueprint.route("/profile", methods=["GET"])
@jwt_required()
def get_profile() -> Response:
"""GET /profile: current user profile"""
user_email = get_jwt_identity()
user = db.session.execute(
db.select(User).where(User.email == user_email)
).scalar_one_or_none()

if not user:
return make_response(jsonify({"msg": "User not found"}), 404)

return jsonify(user_to_dict(user))


@main_blueprint.route("/profile", methods=["PUT"])
@jwt_required()
def update_profile() -> Response:
"""PUT /profile: Updates current user profile"""
user_email = get_jwt_identity()
user = db.session.execute(
db.select(User).where(User.email == user_email)
).scalar_one_or_none()

if not user:
return make_response(jsonify({"msg": "User not found"}), 404)

json_data = request.get_json()
if not json_data:
return make_response(jsonify({"msg": "Missing JSON in request"}), 400)

# Update basic User fields
user.first_name = json_data.get("first_name", user.first_name)
user.last_name = json_data.get("last_name", user.last_name)
user.preferred_name = json_data.get("preferred_name", user.preferred_name)
user.class_year = json_data.get("class_year", user.class_year)
user.website = json_data.get("website", user.website)
user.description = json_data.get("description", user.description)

if "departments" in json_data:
db.session.query(UserDepartments).filter(
UserDepartments.user_id == user.id
).delete()
for dept_id in json_data["departments"]:
if db.session.get(
RPIDepartments, dept_id
): # Ensure department exists before adding
new_user_dept = UserDepartments(user_id=user.id, department_id=dept_id)
db.session.add(new_user_dept)

if "majors" in json_data:
db.session.query(UserMajors).filter(UserMajors.user_id == user.id).delete()
for major_code in json_data["majors"]:
if db.session.get(Majors, major_code): # Ensure major exists before adding
new_user_major = UserMajors(user_id=user.id, major_code=major_code)
db.session.add(new_user_major)

db.session.commit()

return jsonify({"msg": "Profile updated successfully"})
137 changes: 137 additions & 0 deletions tests/test_profile_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import json

from flask.testing import FlaskClient

from labconnect import db
from labconnect.models import User, UserDepartments, UserMajors


def login_as_student(test_client: FlaskClient):
"""Helper function to log in a user and handle the auth flow."""
response = test_client.get("/login")
assert response.status_code == 302

redirect_url = response.headers["Location"]
code = redirect_url.split("code=")[1]

token_response = test_client.post("/token", json={"code": code})
assert token_response.status_code == 200


# === GET /profile Tests ===


def test_get_profile_success(test_client: FlaskClient):
"""
logged-in user: '/profile' endpoint is requested (GET)
-> correct data and 200 status
"""
login_as_student(test_client)

response = test_client.get("/profile")
data = json.loads(response.data)

assert response.status_code == 200
assert data["email"] == "[email protected]"
assert data["first_name"] == "Test"
assert data["last_name"] == "User"
assert "departments" in data
assert "majors" in data


def test_get_profile_unauthorized(test_client: FlaskClient):
"""
no user is logged in: '/profile' endpoint is requested (GET)
-> 401 Unauthorized status is returned.
"""
test_client.get("/logout")
response = test_client.get("/profile")
assert response.status_code == 401


# === PUT /profile Tests ===


def test_update_profile_success(test_client: FlaskClient):
"""
logged-in user: '/profile' endpoint is updated with new data (PUT)
-> 200 status and database changed.
"""
login_as_student(test_client)

update_data = {
"first_name": "UpdatedFirst",
"last_name": "UpdatedLast",
"preferred_name": "Pref",
"class_year": 2025,
"website": "https://new.example.com",
"description": "This is an updated description.",
"departments": ["CS"],
"majors": ["CSCI", "MATH"],
}

response = test_client.put("/profile", json=update_data)
assert response.status_code == 200
assert "Profile updated successfully" in json.loads(response.data)["msg"]

# Verify the changes in the database
user = db.session.execute(
db.select(User).where(User.email == "[email protected]")
).scalar_one()
assert user.first_name == "UpdatedFirst"
assert user.website == "https://new.example.com"
assert user.class_year == 2025

user_depts = (
db.session.execute(
db.select(UserDepartments.department_id).where(
UserDepartments.user_id == user.id
)
)
.scalars()
.all()
)
assert set(user_depts) == {"CS"}

user_majors = (
db.session.execute(
db.select(UserMajors.major_code).where(UserMajors.user_id == user.id)
)
.scalars()
.all()
)
assert set(user_majors) == {"CSCI", "MATH"}


def test_update_profile_partial(test_client: FlaskClient):
"""
logged-in user: '/profile' endpoint is updated with partial data (PUT)
-> check only provided fields updated.
"""
login_as_student(test_client)

update_data = {
"website": "https://partial.update.com",
"description": "Only this was updated.",
}

response = test_client.put("/profile", json=update_data)
assert response.status_code == 200

user = db.session.execute(
db.select(User).where(User.email == "[email protected]")
).scalar_one()
assert user.website == "https://partial.update.com"
assert user.description == "Only this was updated."
assert user.last_name == "User"


def test_update_profile_unauthorized(test_client: FlaskClient):
"""
no user is logged in: '/profile' endpoint is sent a PUT request
-> 401 Unauthorized status.
"""
test_client.get("/logout")
update_data = {"first_name": "ShouldFail"}
response = test_client.put("/profile", json=update_data)
assert response.status_code == 401
Loading
Loading