-
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
Merged
jaswanthDuddu
merged 24 commits into
main
from
Single-Opportunity-and-User-Profile-Management
Nov 4, 2025
Merged
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
2328150
API Endpoint: Update opportunity_routes.py
jaswanthDuddu 474d3cf
API Endpoint: Update test_opportunity.py
jaswanthDuddu ce80f77
Single Opportunity: Update opportunity_routes.py
jaswanthDuddu 0995fce
Single Opportunity: Update test_opportunity.py
jaswanthDuddu 2a3cf8e
Update README.md
jaswanthDuddu 071ee70
Merge branch 'LabConnect-RCOS:main' into main
jaswanthDuddu 557cb0d
Fixes and Improvements for Single and All Opportunity Endpoints
jaswanthDuddu f9b2025
User Profile Management
jaswanthDuddu 5e30938
User Profile Management Update
jaswanthDuddu 87ff46e
User Profile Management Update Majors, Departments
jaswanthDuddu 24b0590
Tests: User Profile Management endpoints
jaswanthDuddu 75852a5
Merge branch 'LabConnect-RCOS:main' into main
jaswanthDuddu dcc07ff
Fixing Ruff Linter Errors
jaswanthDuddu 62dab3c
Add profile management and single opportunity endpoints (#340)
WBroadwell 80d5afc
Add profile_routes to main blueprint imports
jaswanthDuddu 646b909
Update README.md
jaswanthDuddu 58b4693
Refactor opportunity retrieval error handling
jaswanthDuddu 2ddea05
Refactor user profile update responses and queries
jaswanthDuddu 69c9dbf
Refactor SQLAlchemy queries to use select and delete
jaswanthDuddu 6fd93ef
Merge branch 'main' into Single-Opportunity-and-User-Profile-Management
jaswanthDuddu c59099f
Refactor opportunity_to_dict tests for dynamic import
jaswanthDuddu 4acbb9b
Revert "Refactor opportunity_to_dict tests for dynamic import"
jaswanthDuddu 847487a
Refactor sqlalchemy functions
jaswanthDuddu 2499177
Refactor test to fix Cyclic import
jaswanthDuddu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| from flask import Response, jsonify, 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 {"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 {"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) | ||
| 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( | ||
| db.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( | ||
| db.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( | ||
| db.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( | ||
| db.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"} | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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