From 8d1d848e8e13f5533a1d22681251d14e518b6d9a Mon Sep 17 00:00:00 2001 From: Rafael Cenzano <32753063+RafaelCenzano@users.noreply.github.com> Date: Tue, 25 Mar 2025 17:03:29 -0400 Subject: [PATCH 1/9] cleanup --- labconnect/main/routes.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/labconnect/main/routes.py b/labconnect/main/routes.py index 7b595322..b16dab68 100644 --- a/labconnect/main/routes.py +++ b/labconnect/main/routes.py @@ -1,5 +1,3 @@ -# from typing import Any - from typing import NoReturn from flask import abort, request from flask_jwt_extended import get_jwt_identity, jwt_required From 059bf22e1c388dd6717a783863c5d64864e83035 Mon Sep 17 00:00:00 2001 From: Rafael Cenzano <32753063+RafaelCenzano@users.noreply.github.com> Date: Fri, 28 Mar 2025 16:36:26 -0400 Subject: [PATCH 2/9] Fix logo --- misc/LabConnect_Logo.png | Bin 16193 -> 19807 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/misc/LabConnect_Logo.png b/misc/LabConnect_Logo.png index 19fbf655fa173889f8cf85dbe7d40de61998921d..3c3120db725d905146ca037377914dda19e842d1 100644 GIT binary patch literal 19807 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 Hu0mjfTJWSw literal 16193 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 Date: Fri, 4 Apr 2025 23:52:06 -0400 Subject: [PATCH 3/9] update credits function --- labconnect/helpers.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/labconnect/helpers.py b/labconnect/helpers.py index 688daf96..647fb836 100644 --- a/labconnect/helpers.py +++ b/labconnect/helpers.py @@ -87,16 +87,20 @@ def format_credits(credit_1, credit_2, credit_3, credit_4): credits_output.append("4") # Handle different cases - if len(credits_output) == 0: - return None + if len(credits_output) == 4: + return "1-4 Credits" elif len(credits_output) == 1: return ( f"{credits_output[0]} Credit" if credit_1 else f"{credits_output[0]} Credits" ) - elif len(credits_output) == 4: - return "1-4 Credits" + elif credits_output == ["1", "2", "3"]: + return "1-3 Credits" + elif credits_output == ["2", "3", "4"]: + return "2-4 Credits" + elif len(credits_output) == 0: + return None else: return f"{','.join(credits_output)} Credits" From 138d1f85b8ffdae9d5907422ad1c903f90688db7 Mon Sep 17 00:00:00 2001 From: Rafael Cenzano <32753063+RafaelCenzano@users.noreply.github.com> Date: Tue, 8 Apr 2025 19:17:38 -0400 Subject: [PATCH 4/9] add more fields for opportunities --- labconnect/serializers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/labconnect/serializers.py b/labconnect/serializers.py index fb0dcda4..a659b7b9 100644 --- a/labconnect/serializers.py +++ b/labconnect/serializers.py @@ -6,7 +6,9 @@ def serialize_course(course: Courses) -> str: return f"{course.code} {course.name}" -def serialize_opportunity(opportunity: Opportunities) -> dict: +def serialize_opportunity( + opportunity: Opportunities, professor: str = "", saved: bool = False +) -> dict: return { "id": opportunity.id, "name": opportunity.name, @@ -25,4 +27,6 @@ def serialize_opportunity(opportunity: Opportunities) -> dict: "active": opportunity.active, "last_updated": opportunity.last_updated, "location": opportunity.location, + "professor": professor, + "saved": saved, } From 858a7618e5ec5cf64e4cc07abe0b4c474552fc19 Mon Sep 17 00:00:00 2001 From: Rafael Cenzano <32753063+RafaelCenzano@users.noreply.github.com> Date: Tue, 8 Apr 2025 19:17:55 -0400 Subject: [PATCH 5/9] fix filtering function remove uneeded function --- labconnect/main/opportunity_routes.py | 102 +++++++++++--------------- 1 file changed, 43 insertions(+), 59 deletions(-) diff --git a/labconnect/main/opportunity_routes.py b/labconnect/main/opportunity_routes.py index 1dbfe145..a1e4aa77 100644 --- a/labconnect/main/opportunity_routes.py +++ b/labconnect/main/opportunity_routes.py @@ -143,31 +143,6 @@ def packageIndividualOpportunity(opportunityInfo): return data -def packageOpportunityCard(opportunity): - # get professor and department by getting Leads and LabManager - query = db.session.execute( - db.select(Leads, LabManager, User.first_name, User.last_name) - .where(Leads.opportunity_id == opportunity.id) - .join(LabManager, Leads.lab_manager_id == LabManager.id) - .join(User, LabManager.id == User.lab_manager_id) - ) - - data = query.all() - - professorInfo = ", ".join(item[1].getName() for item in data) - - card = { - "id": opportunity.id, - "title": opportunity.name, - "professor": professorInfo, - "season": opportunity.semester, - "location": "TBA", - "year": opportunity.year, - } - - return card - - @main_blueprint.get("/getOpportunity/") def getOpportunity(opp_id: int): # query database for opportunity and recommended class years @@ -214,19 +189,28 @@ def getOpportunities(): @main_blueprint.post("/opportunity/filter") +@jwt_required() def filterOpportunities(): # Handle POST requests for filtering opportunities - json_request_data = request.get_json() - - if not json_request_data: - abort(400) - - filters = json_request_data.get("filters", None) + filters = request.get_json() + user_id = get_jwt_identity() + print(filters) data = None - if filters is None: - data = db.session.execute(db.select(Opportunities).limit(20)).scalars() + if filters is None or filters == {}: + data = db.session.execute( + db.select( + Opportunities, User.preferred_name, User.first_name, User.last_name + ) + .where(Opportunities.active) + .join(Leads, Opportunities.id == Leads.opportunity_id) + .join(LabManager, Leads.lab_manager_id == LabManager.id) + .join(User, LabManager.id == User.lab_manager_id) + .limit(20) + .order_by(Opportunities.last_updated) + .distinct() + ).all() elif not isinstance(filters, list): abort(400) @@ -234,8 +218,13 @@ def filterOpportunities(): else: where_conditions = [] query = ( - db.select(Opportunities) + db.select( + Opportunities, User.preferred_name, User.first_name, User.last_name + ) .where(Opportunities.active) + .join(Leads, Opportunities.id == Leads.opportunity_id) + .join(LabManager, Leads.lab_manager_id == LabManager.id) + .join(User, LabManager.id == User.lab_manager_id) .limit(20) .order_by(Opportunities.last_updated) .distinct() @@ -255,7 +244,7 @@ def filterOpportunities(): where_conditions.append(Opportunities.location != "REMOTE") # Class year filter - elif field == "class_year": + elif field == "years": if not isinstance(value, list): abort(400) query = query.join( @@ -307,16 +296,10 @@ def filterOpportunities(): ) # Pay filter - elif field == "pay": - if not isinstance(value, dict): + elif field == "hourlyPay": + if not isinstance(value, float) or not isinstance(value, int): abort(400) - min_pay = value.get("min") - max_pay = value.get("max") - if min_pay is None: - min_pay = 0 - if max_pay is None: - max_pay = float("inf") - where_conditions.append(Opportunities.pay.between(min_pay, max_pay)) + where_conditions.append(Opportunities.pay.gte(value)) # Other fields else: @@ -328,27 +311,28 @@ def filterOpportunities(): abort(400) query = query.where(*where_conditions) - data = db.session.execute(query).scalars() + data = db.session.execute(query).all() if not data: abort(404) - result = [serialize_opportunity(opportunity) for opportunity in data] - - return result - - -# Jobs page -@main_blueprint.get("/getOpportunityCards") -def getOpportunityCards(): - # query database for opportunity - query = db.session.execute(db.select(Opportunities).where(Opportunities.active)) + saved_opportunity = db.session.execute( + db.select(UserSavedOpportunities.opportunity_id).where( + UserSavedOpportunities.user_id == user_id + ) + ) - data = query.fetchall() - # return data in the below format if opportunity is found - cards = {"data": [packageOpportunityCard(opportunity[0]) for opportunity in data]} + result = [ + serialize_opportunity( + opportunity[0], + professor=f"{opportunity[1] or opportunity[2]} {opportunity[3]}", + saved=opportunity[0].id in saved_opportunity, + ) + for opportunity in data + ] + print("Results: ", result) - return cards + return result @main_blueprint.get("/staff/opportunities/") From 5b796f2c23f0bf69a901778fdcd455ee15992617 Mon Sep 17 00:00:00 2001 From: Rafael Cenzano <32753063+RafaelCenzano@users.noreply.github.com> Date: Mon, 14 Apr 2025 23:53:37 -0400 Subject: [PATCH 6/9] update to lab managers instead of professors --- labconnect/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/labconnect/serializers.py b/labconnect/serializers.py index a659b7b9..c404463a 100644 --- a/labconnect/serializers.py +++ b/labconnect/serializers.py @@ -7,7 +7,7 @@ def serialize_course(course: Courses) -> str: def serialize_opportunity( - opportunity: Opportunities, professor: str = "", saved: bool = False + opportunity: Opportunities, lab_managers: str = "", saved: bool = False ) -> dict: return { "id": opportunity.id, @@ -27,6 +27,6 @@ def serialize_opportunity( "active": opportunity.active, "last_updated": opportunity.last_updated, "location": opportunity.location, - "professor": professor, + "lab_managers": lab_managers, "saved": saved, } From 5ae71f5074d3c51140e59dc91d5d83426e2f9607 Mon Sep 17 00:00:00 2001 From: Rafael Cenzano <32753063+RafaelCenzano@users.noreply.github.com> Date: Mon, 14 Apr 2025 23:54:05 -0400 Subject: [PATCH 7/9] correct filtering and joining to retrieve desired data with as much work done via sql --- labconnect/main/opportunity_routes.py | 121 +++++++++++++++----------- 1 file changed, 68 insertions(+), 53 deletions(-) diff --git a/labconnect/main/opportunity_routes.py b/labconnect/main/opportunity_routes.py index 7f6cdcbf..7a2d2041 100644 --- a/labconnect/main/opportunity_routes.py +++ b/labconnect/main/opportunity_routes.py @@ -175,82 +175,89 @@ def getOpportunity(opp_id: int): @main_blueprint.get("/opportunity/filter") -def getOpportunities(): - # Handle GET requests for fetching default active opportunities - data = db.session.execute( - db.select(Opportunities) - .where(Opportunities.active) - .limit(20) - .order_by(Opportunities.last_updated.desc()) - .distinct() - ).scalars() - result = [serialize_opportunity(opportunity) for opportunity in data] - return result - - -@main_blueprint.post("/opportunity/filter") @jwt_required() def filterOpportunities(): - # Handle POST requests for filtering opportunities - filters = request.get_json() + # Handle GET requests for filtering opportunities using query parameters + filters = request.args.to_dict(flat=False) user_id = get_jwt_identity() - - print(filters) data = None if filters is None or filters == {}: data = db.session.execute( db.select( - Opportunities, User.preferred_name, User.first_name, User.last_name + Opportunities, + func.json_agg( + func.json_build_object( + "first_name", + User.first_name, + "last_name", + User.last_name, + "preferred_name", + User.preferred_name, + ) + ).label("lab_managers"), ) - .where(Opportunities.active) .join(Leads, Opportunities.id == Leads.opportunity_id) .join(LabManager, Leads.lab_manager_id == LabManager.id) .join(User, LabManager.id == User.lab_manager_id) - .limit(20) + .where(Opportunities.active) + .group_by(Opportunities.id) .order_by(Opportunities.last_updated) - .distinct() + .limit(20) ).all() - elif not isinstance(filters, list): - abort(400) - else: where_conditions = [] query = ( db.select( - Opportunities, User.preferred_name, User.first_name, User.last_name + Opportunities, + func.json_agg( + func.json_build_object( + "first_name", + User.first_name, + "last_name", + User.last_name, + "preferred_name", + User.preferred_name, + ) + ).label("lab_managers"), ) - .where(Opportunities.active) .join(Leads, Opportunities.id == Leads.opportunity_id) .join(LabManager, Leads.lab_manager_id == LabManager.id) .join(User, LabManager.id == User.lab_manager_id) - .limit(20) + .outerjoin( + RecommendsMajors, Opportunities.id == RecommendsMajors.opportunity_id + ) + .where(Opportunities.active) + .group_by(Opportunities.id) .order_by(Opportunities.last_updated) - .distinct() + .limit(20) ) - for given_filter in filters: - field = given_filter.get("field", None) - value = given_filter.get("value", None) - + for field, value in filters.items(): if field and value: field = field.lower() + value = value[0].split(",") # Location filter + # not in use yet if field == "location": - if value.lower() == "remote": - where_conditions.append(Opportunities.location == "REMOTE") - else: - where_conditions.append(Opportunities.location != "REMOTE") + for location in value: + if location.lower() == "remote": + where_conditions.append(Opportunities.location == "REMOTE") + else: + where_conditions.append(Opportunities.location != "REMOTE") # Class year filter elif field == "years": if not isinstance(value, list): abort(400) + years = list(map(int, filter(str.isdigit, value))) + if len(years) == 0: + abort(400) query = query.join( RecommendsClassYears, Opportunities.id == RecommendsClassYears.opportunity_id, - ).where(RecommendsClassYears.class_year.in_(value)) + ).where(RecommendsClassYears.class_year.in_(years)) # Credits filter elif field == "credits": @@ -258,17 +265,17 @@ def filterOpportunities(): abort(400) credit_conditions = [] for credit in value: - if credit == 1: + if credit == "1": credit_conditions.append(Opportunities.one_credit.is_(True)) - elif credit == 2: + elif credit == "2": credit_conditions.append( Opportunities.two_credits.is_(True) ) - elif credit == 3: + elif credit == "3": credit_conditions.append( Opportunities.three_credits.is_(True) ) - elif credit == 4: + elif credit == "4": credit_conditions.append( Opportunities.four_credits.is_(True) ) @@ -280,12 +287,10 @@ def filterOpportunities(): elif field == "majors": if not isinstance(value, list): abort(400) - query = query.join( - RecommendsMajors, - Opportunities.id == RecommendsMajors.opportunity_id, - ).where(RecommendsMajors.major_code.in_(value)) + where_conditions.append(RecommendsMajors.major_code.in_(value)) # Departments filter + # not currently in use elif field == "departments": if not isinstance(value, list): abort(400) @@ -296,10 +301,11 @@ def filterOpportunities(): ) # Pay filter - elif field == "hourlyPay": - if not isinstance(value, float) or not isinstance(value, int): + elif field == "hourlypay": + pay = value[0] + if not pay.isnumeric(): abort(400) - where_conditions.append(Opportunities.pay.gte(value)) + where_conditions.append(Opportunities.pay >= float(pay)) # Other fields else: @@ -317,20 +323,28 @@ def filterOpportunities(): abort(404) saved_opportunity = db.session.execute( - db.select(UserSavedOpportunities.opportunity_id).where( - UserSavedOpportunities.user_id == user_id + db.select(UserSavedOpportunities.opportunity_id) + .where(UserSavedOpportunities.user_id == user_id) + .where( + UserSavedOpportunities.opportunity_id.in_( + [opportunity[0].id for opportunity in data] + ) ) ) result = [ serialize_opportunity( opportunity[0], - professor=f"{opportunity[1] or opportunity[2]} {opportunity[3]}", + lab_managers=", ".join( + [ + f"{name.get('preferred_name', None) or name.get('first_name')} {name.get('last_name')}" + for name in opportunity[1] + ] + ), saved=opportunity[0].id in saved_opportunity, ) for opportunity in data ] - print("Results: ", result) return result @@ -372,6 +386,7 @@ def getLabManagerOpportunityCards(rcs_id: str) -> list[dict[str, str]]: @main_blueprint.get("/profile/opportunities/") +@jwt_required() def getProfileOpportunities(rcs_id: str) -> list[dict[str, str]]: query = ( db.select( From bd062045d959b0e2356a336a6edfb1f89efbf60d Mon Sep 17 00:00:00 2001 From: Rafael Cenzano <32753063+RafaelCenzano@users.noreply.github.com> Date: Tue, 15 Apr 2025 00:03:31 -0400 Subject: [PATCH 8/9] optimize querying and code --- labconnect/main/opportunity_routes.py | 105 +++++++++++--------------- 1 file changed, 42 insertions(+), 63 deletions(-) diff --git a/labconnect/main/opportunity_routes.py b/labconnect/main/opportunity_routes.py index 7a2d2041..04e796f1 100644 --- a/labconnect/main/opportunity_routes.py +++ b/labconnect/main/opportunity_routes.py @@ -2,7 +2,7 @@ from flask import abort, request from flask_jwt_extended import get_jwt_identity, jwt_required -from sqlalchemy import func +from sqlalchemy import func, case from labconnect import db from labconnect.helpers import ( @@ -182,57 +182,44 @@ def filterOpportunities(): user_id = get_jwt_identity() data = None - if filters is None or filters == {}: - data = db.session.execute( - db.select( - Opportunities, - func.json_agg( - func.json_build_object( - "first_name", - User.first_name, - "last_name", - User.last_name, - "preferred_name", - User.preferred_name, - ) - ).label("lab_managers"), - ) - .join(Leads, Opportunities.id == Leads.opportunity_id) - .join(LabManager, Leads.lab_manager_id == LabManager.id) - .join(User, LabManager.id == User.lab_manager_id) - .where(Opportunities.active) - .group_by(Opportunities.id) - .order_by(Opportunities.last_updated) - .limit(20) - ).all() - - else: - where_conditions = [] - query = ( - db.select( - Opportunities, - func.json_agg( - func.json_build_object( - "first_name", - User.first_name, - "last_name", - User.last_name, - "preferred_name", - User.preferred_name, - ) - ).label("lab_managers"), - ) - .join(Leads, Opportunities.id == Leads.opportunity_id) - .join(LabManager, Leads.lab_manager_id == LabManager.id) - .join(User, LabManager.id == User.lab_manager_id) - .outerjoin( - RecommendsMajors, Opportunities.id == RecommendsMajors.opportunity_id - ) - .where(Opportunities.active) - .group_by(Opportunities.id) - .order_by(Opportunities.last_updated) - .limit(20) + query = ( + db.select( + Opportunities, + func.json_agg( + func.json_build_object( + "first_name", + User.first_name, + "last_name", + User.last_name, + "preferred_name", + User.preferred_name, + ) + ).label("lab_managers"), + case((UserSavedOpportunities.user_id.isnot(None), True), else_=False).label( + "is_saved" + ), + ) + .join(Leads, Opportunities.id == Leads.opportunity_id) + .join(LabManager, Leads.lab_manager_id == LabManager.id) + .join(User, LabManager.id == User.lab_manager_id) + .outerjoin( + RecommendsMajors, Opportunities.id == RecommendsMajors.opportunity_id ) + .outerjoin( + UserSavedOpportunities, + db.and_( + Opportunities.id == UserSavedOpportunities.opportunity_id, + UserSavedOpportunities.user_id == user_id, # filter for current user + ), + ) + .where(Opportunities.active) + .group_by(Opportunities.id, UserSavedOpportunities.user_id) + .order_by(Opportunities.last_updated) + .limit(20) + ) + + if filters is not None or filters != {}: + where_conditions = [] for field, value in filters.items(): if field and value: field = field.lower() @@ -317,21 +304,13 @@ def filterOpportunities(): abort(400) query = query.where(*where_conditions) - data = db.session.execute(query).all() + # data = db.session.execute(query).all() + + data = db.session.execute(query).all() if not data: abort(404) - saved_opportunity = db.session.execute( - db.select(UserSavedOpportunities.opportunity_id) - .where(UserSavedOpportunities.user_id == user_id) - .where( - UserSavedOpportunities.opportunity_id.in_( - [opportunity[0].id for opportunity in data] - ) - ) - ) - result = [ serialize_opportunity( opportunity[0], @@ -341,7 +320,7 @@ def filterOpportunities(): for name in opportunity[1] ] ), - saved=opportunity[0].id in saved_opportunity, + saved=opportunity[2], ) for opportunity in data ] From 66eb25dbba1e01d27dd5d4a48b6a6a6f66423b05 Mon Sep 17 00:00:00 2001 From: Rafael Cenzano <32753063+RafaelCenzano@users.noreply.github.com> Date: Tue, 15 Apr 2025 00:08:35 -0400 Subject: [PATCH 9/9] remove commented code --- labconnect/main/opportunity_routes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/labconnect/main/opportunity_routes.py b/labconnect/main/opportunity_routes.py index 04e796f1..adfda2bc 100644 --- a/labconnect/main/opportunity_routes.py +++ b/labconnect/main/opportunity_routes.py @@ -304,7 +304,6 @@ def filterOpportunities(): abort(400) query = query.where(*where_conditions) - # data = db.session.execute(query).all() data = db.session.execute(query).all()