-
Notifications
You must be signed in to change notification settings - Fork 5
Single opportunity and User Profile Management #342
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 22 commits
2328150
474d3cf
ce80f77
0995fce
2a3cf8e
071ee70
557cb0d
f9b2025
5e30938
87ff46e
24b0590
75852a5
dcc07ff
62dab3c
80d5afc
646b909
58b4693
2ddea05
69c9dbf
6fd93ef
c59099f
4acbb9b
847487a
2499177
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,4 +6,9 @@ env/ | |
| *.db | ||
| .coverage | ||
| *.vscode | ||
| .env | ||
| .env | ||
|
|
||
| labconnect/bin | ||
| labconnect/include | ||
| labconnect/lib | ||
| labconnect/pyvenv.cfg | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| from flask import Response, jsonify, request | ||
| from flask_jwt_extended import get_jwt_identity, jwt_required | ||
|
|
||
| from sqlalchemy import select, delete | ||
|
|
||
| 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( | ||
| select(UserDepartments.department_id).where( | ||
| UserDepartments.user_id == user.id | ||
| ) | ||
| ) | ||
| .scalars() | ||
| .all() | ||
| ) | ||
|
|
||
| user_majors = ( | ||
| db.session.execute( | ||
| 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( | ||
| select(User).where(User.email == user_email) | ||
| ).scalar_one_or_none() | ||
|
|
||
| if not user: | ||
| return {"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( | ||
| select(User).where(User.email == user_email) | ||
| ).scalar_one_or_none() | ||
|
|
||
| if not user: | ||
| return {"msg": "User not found"}, 404 | ||
|
|
||
| json_data = request.get_json() | ||
| if not json_data: | ||
| return {"msg": "Missing JSON in request"}, 400 | ||
|
|
||
| # Update basic User fields | ||
| user.first_name = json_data.get("first_name", user.first_name) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @WBroadwell not going to hold up this PR but would be good to make a ticket to validate and sanitze inputs on the frontend and backend. Sanitization can be like this PR I have on another projects: https://github.com/alpha-phi-omega-ez/backend/pull/51/files#diff-b678c90e066bcc3eaff460004c1d8c094e9abc66acafe9226cbff14a85354882R24-R28 And validation just checkings like length limits like whats set in the database schema |
||
| 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.execute( | ||
| delete(UserDepartments).where(UserDepartments.user_id == user.id) | ||
| ) | ||
|
|
||
| req_dept_ids = set(json_data["departments"]) | ||
| if req_dept_ids: # Only query if list is not empty | ||
| valid_dept_ids = db.session.execute( | ||
| select(RPIDepartments.id).where( | ||
| RPIDepartments.id.in_(req_dept_ids) | ||
| ) | ||
| ).scalars().all() | ||
|
|
||
| for dept_id in valid_dept_ids: # Add only the valid ones | ||
| 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.execute( | ||
| delete(UserMajors).where(UserMajors.user_id == user.id) | ||
| ) | ||
|
|
||
| req_major_codes = set(json_data["majors"]) | ||
| if req_major_codes: # Only query if list is not empty | ||
| valid_major_codes = db.session.execute( | ||
| select(Majors.code).where(Majors.code.in_(req_major_codes)) | ||
| ).scalars().all() | ||
|
|
||
| for major_code in valid_major_codes: # Add only the valid ones | ||
| new_user_major = UserMajors(user_id=user.id, major_code=major_code) | ||
| db.session.add(new_user_major) | ||
|
|
||
| db.session.commit() | ||
|
|
||
| return {"msg": "Profile updated successfully"} | ||
| 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 |
Uh oh!
There was an error while loading. Please reload this page.