diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 91267bca..a131ec8d 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -9,6 +9,20 @@ on: jobs: build: runs-on: ubuntu-latest + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: labconnect + ports: + - 5432:5432 + options: >- + --health-cmd="pg_isready -U postgres -d labconnect" + --health-interval=10s + --health-timeout=5s + --health-retries=3 strategy: matrix: python-version: ["3.12.4"] @@ -22,9 +36,19 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt - - name: Setup the Database + - name: Wait for PostgreSQL to be ready + run: | + while ! pg_isready -h localhost -p 5432 -U postgres -d labconnect; do + echo "Waiting for PostgreSQL..." + sleep 2 + done + - name: Set up the Database + env: + DATABASE_URL: postgresql+psycopg2://postgres:postgres@localhost:5432/labconnect run: | python db_init.py create - name: Running pytest + env: + DATABASE_URL: postgresql+psycopg2://postgres:postgres@localhost:5432/labconnect run: | python -m pytest tests/ diff --git a/config.py b/config.py index 3a79146f..ca75dc16 100644 --- a/config.py +++ b/config.py @@ -26,9 +26,10 @@ class Config: ) SQLALCHEMY_DATABASE_URI = os.environ.get( - "DB", f"sqlite:///{os.path.join(basedir, 'db', 'database.db')}" + "DB", "postgresql+psycopg2://postgres:root@localhost/labconnect" ) - # "postgresql+psycopg2://postgres:root@localhost/labconnect" + + TOKEN_BLACKLIST = set() class TestingConfig(Config): diff --git a/db_init.py b/db_init.py index 13a59f3c..6be7eecb 100644 --- a/db_init.py +++ b/db_init.py @@ -49,54 +49,81 @@ ) for row_tuple in rpi_schools_rows: - row = RPISchools(name=row_tuple[0], description=row_tuple[1]) + row = RPISchools() + row.name = row_tuple[0] + row.description = row_tuple[1] + db.session.add(row) db.session.commit() rpi_departments_rows = ( - ("Computer Science", "DS", "School of Science"), - ("Biology", "life", "School of Science"), - ("Materials Engineering", "also pretty cool", "School of Engineering"), - ("Environmental Engineering", "water", "School of Engineering"), - ("Math", "quick maths", "School of Science"), + ("Computer Science", "DS is rough", "School of Science", "CSCI"), + ("Biology", "life science", "School of Science", "BIOL"), ( - "Aerospace Engineering", - "space, the final frontier", + "Materials Engineering", + "also pretty cool", "School of Engineering", + "MTLE", ), ( - "Aeronautical Engineering", - "flying, need for speed", + "Environmental Engineering", + "water stuff", "School of Engineering", + "ENVE", ), + ("Math", "quick maths", "School of Science", "MATH"), ( - "Material Science", - "Creating the best materials", + "Mechanical, Aerospace, and Nuclear Engineering", + "space, the final frontier", "School of Engineering", + "MANE", ), ) for row_tuple in rpi_departments_rows: - row = RPIDepartments( - name=row_tuple[0], description=row_tuple[1], school_id=row_tuple[2] - ) + row = RPIDepartments() + row.name = row_tuple[0] + row.description = row_tuple[1] + row.school_id = row_tuple[2] + row.id = row_tuple[3] + row.image = "https://cdn-icons-png.flaticon.com/512/5310/5310672.png" + row.website = "https://www.rpi.edu" + db.session.add(row) db.session.commit() - class_years_rows = (2024, 2025, 2026, 2027, 2028, 2029, 2030, 2031) + class_years_rows = (2025, 2026, 2027, 2028, 2029, 2030, 2031) for row_item in class_years_rows: - row = ClassYears(class_year=row_item, active=True) + row = ClassYears() + row.class_year = row_item + row.active = True + db.session.add(row) db.session.commit() lab_manager_rows = ( - ("led", "Duy", "Le", "Computer Science"), - ("turner", "Wes", "Turner", "Computer Science"), - ("kuzmin", "Konstantine", "Kuzmin", "Computer Science"), - ("goldd", "David", "Goldschmidt", "Computer Science"), - ("rami", "Rami", "Rami", "Material Science"), - ("holm", "Mark", "Holmes", "Math"), + ("led", "Duy", "Le", "CSCI", "database database database"), + ( + "turner", + "Wes", + "Turner", + "CSCI", + "open source stuff is cool", + ), + ( + "kuzmin", + "Konstantine", + "Kuzmin", + "CSCI", + "java, psoft, etc.", + ), + ("goldd", "David", "Goldschmidt", "CSCI", "VIM master"), + ("rami", "Rami", "Rami", "MTLE", "cubes are cool"), + ("holm", "Mark", "Holmes", "MATH", "all about that math"), + ("test", "RCOS", "RCOS", "CSCI", "first test"), + ("test2", "RCOS", "RCOS", "CSCI", "Second test"), + ("test3", "RCOS", "RCOS", "CSCI", "Third test"), ) raf_test_user = ( @@ -105,40 +132,49 @@ "Cenzano", "Raf", 2025, - "Computer Science", + "CSCI", + "labconnect is the best RCOS project", + "https://rafael.sirv.com/Images/rafael.jpeg?thumbnail=350&format=webp&q=90", + "https://rafaelcenzano.com", ) - lab_manager = LabManager(department_id=raf_test_user[5]) + lab_manager = LabManager() + lab_manager.department_id = raf_test_user[5] db.session.add(lab_manager) db.session.commit() - user = User( - id=raf_test_user[0], - email=raf_test_user[0] + "@rpi.edu", - first_name=raf_test_user[1], - last_name=raf_test_user[2], - preferred_name=raf_test_user[3], - class_year=raf_test_user[4], - lab_manager_id=lab_manager.id, - ) + user = User() + user.id = raf_test_user[0] + user.email = raf_test_user[0] + "@rpi.edu" + user.first_name = raf_test_user[1] + user.last_name = raf_test_user[2] + user.preferred_name = raf_test_user[3] + user.class_year = raf_test_user[4] + user.lab_manager_id = lab_manager.id + user.description = raf_test_user[6] + user.profile_picture = raf_test_user[7] + user.website = raf_test_user[8] db.session.add(user) db.session.commit() for row_tuple in lab_manager_rows: - lab_manager = LabManager(department_id=row_tuple[3]) + lab_manager = LabManager() + lab_manager.department_id = row_tuple[3] db.session.add(lab_manager) db.session.commit() - user = User( - id=row_tuple[0], - email=row_tuple[0] + "@rpi.edu", - first_name=row_tuple[1], - last_name=row_tuple[2], - lab_manager_id=lab_manager.id, - ) + user = User() + user.id = row_tuple[0] + user.email = row_tuple[0] + "@rpi.edu" + user.first_name = row_tuple[1] + user.last_name = row_tuple[2] + user.lab_manager_id = lab_manager.id + user.description = row_tuple[4] + user.profile_picture = "https://www.svgrepo.com/show/206842/professor.svg" + db.session.add(user) db.session.commit() @@ -153,7 +189,7 @@ False, True, SemesterEnum.SPRING, - 2024, + 2025, date.today(), True, datetime.now(), @@ -169,7 +205,7 @@ True, True, SemesterEnum.SPRING, - 2024, + 2025, date.today(), True, datetime.now(), @@ -185,7 +221,7 @@ True, True, SemesterEnum.FALL, - 2024, + 2025, date.today(), True, datetime.now(), @@ -201,31 +237,47 @@ True, True, SemesterEnum.SUMMER, - 2024, + 2025, date.today(), True, datetime.now(), LocationEnum.JEC, ), + ( + "Data Science Research", + "Work with a team of researchers to analyze large datasets and extract meaningful insights.", + "Python, Machine Learning, Data Analysis", + 20.0, + True, + False, + True, + False, + SemesterEnum.FALL, + 2025, + "2025-10-31", + True, + "2025-10-10T10:30:00", + LocationEnum.JROWL, + ), ) for row_tuple in opportunities_rows: - row = Opportunities( - name=row_tuple[0], - description=row_tuple[1], - recommended_experience=row_tuple[2], - pay=row_tuple[3], - one_credit=row_tuple[4], - two_credits=row_tuple[5], - three_credits=row_tuple[6], - four_credits=row_tuple[7], - semester=row_tuple[8], - year=row_tuple[9], - application_due=row_tuple[10], - active=row_tuple[11], - last_updated=row_tuple[12], - location=row_tuple[13], - ) + row = Opportunities() + row.name = row_tuple[0] + row.description = row_tuple[1] + row.recommended_experience = row_tuple[2] + row.pay = row_tuple[3] + row.one_credit = row_tuple[4] + row.two_credits = row_tuple[5] + row.three_credits = row_tuple[6] + row.four_credits = row_tuple[7] + row.semester = row_tuple[8] + row.year = row_tuple[9] + row.application_due = row_tuple[10] + row.active = row_tuple[11] + row.last_updated = row_tuple[12] + row.location = row_tuple[13] + db.session.add(row) db.session.commit() @@ -237,7 +289,10 @@ ) for row_tuple in courses_rows: - row = Courses(code=row_tuple[0], name=row_tuple[1]) + row = Courses() + row.code = row_tuple[0] + row.name = row_tuple[1] + db.session.add(row) db.session.commit() @@ -251,7 +306,10 @@ ) for row_tuple in majors_rows: - row = Majors(code=row_tuple[0], name=row_tuple[1]) + row = Majors() + row.code = row_tuple[0] + row.name = row_tuple[1] + db.session.add(row) db.session.commit() @@ -264,69 +322,44 @@ (2, 2), (1, 3), (4, 4), + (8, 5), ) for r in leads_rows: - row = Leads(lab_manager_id=r[0], opportunity_id=r[1]) + row = Leads() + row.lab_manager_id = r[0] + row.opportunity_id = r[1] + db.session.add(row) db.session.commit() recommends_courses_rows = ((1, "CSCI4430"), (1, "CSCI2961"), (2, "CSCI4390")) for r in recommends_courses_rows: - row = RecommendsCourses(opportunity_id=r[0], course_code=r[1]) + row = RecommendsCourses() + row.opportunity_id = r[0] + row.course_code = r[1] + db.session.add(row) db.session.commit() recommends_majors_rows = ((1, "CSCI"), (1, "PHYS"), (2, "BIOL")) for r in recommends_majors_rows: - row = RecommendsMajors(opportunity_id=r[0], major_code=r[1]) + row = RecommendsMajors() + row.opportunity_id = r[0] + row.major_code = r[1] + db.session.add(row) db.session.commit() - recommends_class_years_rows = ((2, 2024), (2, 2025), (2, 2026), (1, 2027)) + recommends_class_years_rows = ((3, 2025), (2, 2025), (2, 2026), (1, 2027)) for r in recommends_class_years_rows: - row = RecommendsClassYears(opportunity_id=r[0], class_year=r[1]) - db.session.add(row) - db.session.commit() + row = RecommendsClassYears() + row.opportunity_id = r[0] + row.class_year = r[1] - user_rows = ( - ( - "test", - "test@rpi.edu", - "RCOS", - "RCOS", - None, - 2028, - ), - ( - "test2", - "test2@rpi.edu", - "RCOS", - "RCOS", - None, - 2029, - ), - ( - "test3", - "test3@rpi.edu", - "RCOS", - "RCOS", - None, - 2025, - ), - ) - for r in user_rows: - row = User( - id=r[0], - email=r[1], - first_name=r[2], - last_name=r[3], - preferred_name=r[4], - class_year=r[5], - ) db.session.add(row) db.session.commit() @@ -337,18 +370,18 @@ ) for r in user_majors: - row = UserMajors(user_id=r[0], major_code=r[1]) + row = UserMajors() + row.user_id = r[0] + row.major_code = r[1] + db.session.add(row) db.session.commit() - user_departments = ( - ("cenzar", "Computer Science"), - ("cenzar", "Math"), - ("test", "Computer Science"), - ) + for r in user_majors: + row = UserDepartments() + row.user_id = r[0] + row.department_id = r[1] - for r in user_departments: - row = UserDepartments(user_id=r[0], department_id=r[1]) db.session.add(row) db.session.commit() @@ -359,7 +392,11 @@ ) for r in user_courses: - row = UserCourses(user_id=r[0], course_code=r[1], in_progress=r[2]) + row = UserCourses() + row.user_id = r[0] + row.course_code = r[1] + row.in_progress = r[2] + db.session.add(row) db.session.commit() @@ -371,7 +408,10 @@ ) for r in participates_rows: - row = Participates(user_id=r[0], opportunity_id=r[1]) + row = Participates() + row.user_id = r[0] + row.opportunity_id = r[1] + db.session.add(row) db.session.commit() diff --git a/docs/database_docs/Labconnect_DB.png b/docs/database_docs/Labconnect_DB.png index 5f6a0085..bcd496e7 100644 Binary files a/docs/database_docs/Labconnect_DB.png and b/docs/database_docs/Labconnect_DB.png differ diff --git a/labconnect/__init__.py b/labconnect/__init__.py index 35e60f2f..f1e980a0 100644 --- a/labconnect/__init__.py +++ b/labconnect/__init__.py @@ -1,23 +1,22 @@ import json import os - from datetime import datetime, timedelta, timezone +import sentry_sdk + # Import Flask modules from flask import Flask from flask_cors import CORS - from flask_jwt_extended import ( JWTManager, create_access_token, get_jwt, get_jwt_identity, ) - +from flask_migrate import Migrate from flask_sqlalchemy import SQLAlchemy -import sentry_sdk from sentry_sdk.integrations.flask import FlaskIntegration -from flask_migrate import Migrate + from labconnect.helpers import OrJSONProvider # Create Database object @@ -29,7 +28,6 @@ def create_app() -> Flask: # Create flask app object app = Flask(__name__) - CORS(app) app.config.from_object(os.environ.get("CONFIG", "config.TestingConfig")) @@ -40,6 +38,8 @@ def create_app() -> Flask: profiles_sample_rate=app.config["SENTRY_PROFILES_SAMPLE_RATE"], ) + CORS(app, supports_credentials=True, origins=[app.config["FRONTEND_URL"]]) + initialize_extensions(app) register_blueprints(app) diff --git a/labconnect/helpers.py b/labconnect/helpers.py index 6f5d6001..a7f1bbe3 100644 --- a/labconnect/helpers.py +++ b/labconnect/helpers.py @@ -88,3 +88,31 @@ def prepare_flask_request(request): # 'lowercase_urlencoding': True, "post_data": request.form.copy(), } + + +def format_credits(credit_1, credit_2, credit_3, credit_4): + # Create a list to hold the active credit numbers + credits_output = [] + + if credit_1: + credits_output.append("1") + if credit_2: + credits_output.append("2") + if credit_3: + credits_output.append("3") + if credit_4: + credits_output.append("4") + + # Handle different cases + if len(credits_output) == 0: + return None + 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" + else: + return f"{','.join(credits_output)} Credits" diff --git a/labconnect/main/__init__.py b/labconnect/main/__init__.py index 60f7feab..262d5d8d 100644 --- a/labconnect/main/__init__.py +++ b/labconnect/main/__init__.py @@ -6,4 +6,4 @@ main_blueprint = Blueprint("main", __name__) -from . import opportunity_routes, routes, discover_routes, auth_routes +from . import auth_routes, discover_routes, opportunity_routes, routes diff --git a/labconnect/main/auth_routes.py b/labconnect/main/auth_routes.py index 4df25c5a..a251d6c5 100644 --- a/labconnect/main/auth_routes.py +++ b/labconnect/main/auth_routes.py @@ -1,23 +1,71 @@ -from typing import Any +from datetime import datetime, timedelta +from uuid import uuid4 -from flask import Response, request, redirect, jsonify, current_app, make_response -from flask_jwt_extended import ( - create_access_token, - unset_jwt_cookies, -) +from flask import current_app, make_response, redirect, request, abort +from flask_jwt_extended import create_access_token from onelogin.saml2.auth import OneLogin_Saml2_Auth from labconnect import db +from labconnect.helpers import prepare_flask_request from labconnect.models import ( User, + UserCourses, + UserDepartments, + UserMajors, + ManagementPermissions, ) -from labconnect.helpers import prepare_flask_request from . import main_blueprint +temp_codes = {} + + +def generate_temporary_code(user_email: str, registered: bool) -> str: + # Generate a unique temporary code + code = str(uuid4()) + expires_at = datetime.now() + timedelta(seconds=5) # expires in 5 seconds + temp_codes[code] = { + "email": user_email, + "expires_at": expires_at, + "registered": registered, + } + return code + + +def validate_code_and_get_user_email(code: str) -> tuple[str | None, bool | None]: + token_data = temp_codes.get(code, {}) + if not token_data: + return None, None + + user_email = token_data.get("email", None) + expire = token_data.get("expires_at", None) + registered = token_data.get("registered", False) + + if user_email and expire and expire > datetime.now(): + # If found, delete the code to prevent reuse + del temp_codes[code] + return user_email, registered + elif expire: + # If the code has expired, delete it + del temp_codes[code] + + return None, None + -@main_blueprint.route("/login") +@main_blueprint.get("/login") def saml_login(): + + # In testing skip RPI login purely for local development + if current_app.config["TESTING"] and ( + current_app.config["FRONTEND_URL"] == "http://localhost:3000" + or current_app.config["FRONTEND_URL"] == "http://127.0.0.1:3000" + ): + # Generate JWT + code = generate_temporary_code("test@rpi.edu", True) + + # Send the JWT to the frontend + return redirect(f"{current_app.config['FRONTEND_URL']}/callback/?code={code}") + # Initialize SAML auth request req = prepare_flask_request(request) auth = OneLogin_Saml2_Auth(req, custom_base_path=current_app.config["SAML_CONFIG"]) @@ -33,37 +81,103 @@ def saml_callback(): errors = auth.get_errors() if not errors: + registered = True user_info = auth.get_attributes() - user_id = auth.get_nameid() + # user_id = auth.get_nameid() data = db.session.execute(db.select(User).where(User.email == "email")).scalar() # User doesn't exist, create a new user if data is None: + registered = False + # Generate JWT + # token = create_access_token(identity=[user_id, datetime.now()]) + code = generate_temporary_code(user_info["email"][0], registered) - # TODO: add data - user = User( - # email=email, - # first_name=first_name, - # last_name=last_name, - # preferred_name=json_request_data.get("preferred_name", None), - # class_year=class_year, - ) + # Send the JWT to the frontend + return redirect(f"{current_app.config['FRONTEND_URL']}/callback/?code={code}") - db.session.add(user) - db.session.commit() + return {"errors": errors}, 500 - # Generate JWT - token = create_access_token(identity=user_id) - # Send the JWT to the frontend - return redirect(f"{current_app.config['FRONTEND_URL']}/?token={token}") - else: - return {"errors": errors}, 500 +@main_blueprint.post("/register") +def registerUser(): + # Gather the new user's information + json_data = request.get_json() + if not json_data: + abort(400) -@main_blueprint.route("/metadata/") -def metadata(): + user = User() + user.email = json_data.get("email") + user.first_name = json_data.get("first_name") + user.last_name = json_data.get("last_name") + user.preferred_name = json_data.get("preferred_name", "") + user.class_year = json_data.get("class_year", "") + user.profile_picture = json_data.get( + "profile_picture", "https://www.svgrepo.com/show/206842/professor.svg" + ) + user.website = json_data.get("website", "") + user.description = json_data.get("description", "") + db.session.add(user) + db.session.commit() + + # Add UserDepartments if provided + if json_data.get("departments"): + for department_id in json_data["departments"]: + user_department = UserDepartments() + user_department.department_id = department_id + user_department.user_id = user.id + db.session.add(user_department) + + # Additional auxiliary records (majors, courses, etc.) + if json_data.get("majors"): + for major_code in json_data["majors"]: + user_major = UserMajors() + user_major.user_id = user.id + user_major.major_code = major_code + db.session.add(user_major) + # Add Courses if provided + if json_data.get("courses"): + for course_code in json_data["courses"]: + user_course = UserCourses() + user_course.user_id = user.id + user_course.course_code = course_code + db.session.add(user_course) + + # Add ManagementPermissions if provided + if json_data.get("permissions"): + permissions = json_data["permissions"] + management_permissions = ManagementPermissions() + management_permissions.user_id = user.id + management_permissions.super_admin = permissions.get("super_admin", False) + management_permissions.admin = permissions.get("admin", False) + management_permissions.moderator = permissions.get("moderator", False) + db.session.add(management_permissions) + + db.session.commit() + return {"msg": "New user added"} + + +@main_blueprint.post("/token") +def tokenRoute(): + if request.json is None or request.json.get("code", None) is None: + return {"msg": "Missing JSON body in request"}, 400 + # Validate the temporary code + code = request.json["code"] + if code is None: + return {"msg": "Missing code in request"}, 400 + user_email, registered = validate_code_and_get_user_email(code) + + if user_email is None: + return {"msg": "Invalid code"}, 400 + + token = create_access_token(identity=[user_email, datetime.now()]) + return {"token": token, "registered": registered} + + +@main_blueprint.get("/metadata/") +def metadataRoute(): req = prepare_flask_request(request) auth = auth = OneLogin_Saml2_Auth( req, custom_base_path=current_app.config["SAML_CONFIG"] @@ -81,7 +195,6 @@ def metadata(): @main_blueprint.get("/logout") -def logout() -> Response: - response = jsonify({"msg": "logout successful"}) - unset_jwt_cookies(response) - return response +def logout(): + # TODO: add token to blacklist + return {"msg": "logout successful"} diff --git a/labconnect/main/discover_routes.py b/labconnect/main/discover_routes.py index 87a51bc5..06b82880 100644 --- a/labconnect/main/discover_routes.py +++ b/labconnect/main/discover_routes.py @@ -1,7 +1,4 @@ -from flask_jwt_extended import ( - get_jwt_identity, - jwt_required, -) +from flask_jwt_extended import get_jwt_identity, jwt_required from labconnect import db from labconnect.models import ( @@ -10,10 +7,10 @@ Leads, Majors, Opportunities, + RecommendsClassYears, RecommendsMajors, User, UserMajors, - RecommendsClassYears, ) from . import main_blueprint diff --git a/labconnect/main/opportunity_routes.py b/labconnect/main/opportunity_routes.py index 3cbcaeca..c2158bd0 100644 --- a/labconnect/main/opportunity_routes.py +++ b/labconnect/main/opportunity_routes.py @@ -1,33 +1,32 @@ -import datetime +from datetime import datetime from flask import abort, request -from flask_jwt_extended import ( - get_jwt_identity, - jwt_required, -) +from flask_jwt_extended import get_jwt_identity, jwt_required +from sqlalchemy import func from labconnect import db +from labconnect.helpers import LocationEnum, SemesterEnum, format_credits from labconnect.models import ( LabManager, Leads, Opportunities, RecommendsClassYears, - RecommendsMajors, - RecommendsCourses, User, + Courses, + Participates, + RecommendsMajors, ) -from labconnect.helpers import LocationEnum - from . import main_blueprint -@main_blueprint.route("/searchOpportunity/", methods=["GET"]) -def searchOpportunity(input: str): +@main_blueprint.get("/searchOpportunity/") +def searchOpportunity(query: str): # Perform a search stmt = ( db.select(Opportunities) .where( + # Made query input ( Opportunities.search_vector.match(input) ) # Full-text search using pre-generated tsvector @@ -44,6 +43,26 @@ def searchOpportunity(input: str): ).desc() # Order by similarity for fuzzy search results ) ) + # Perform a search + # stmt = ( + # db.select(Opportunities) + # .where( + # ( + # Opportunities.search_vector.match(input) + # ) # Full-text search using pre-generated tsvector + # | ( + # db.func.similarity(Opportunities.name, input) >= 0.1 + # ) # Fuzzy search on the 'name' field + # | ( + # db.func.similarity(Opportunities.description, input) >= 0.1 + # ) # Fuzzy search on the 'description' field + # ) + # .order_by( + # db.func.similarity( + # Opportunities.name, input + # ).desc() # Order by similarity for fuzzy search results + # ) + # ) data = db.session.execute(stmt).scalars().all() @@ -57,31 +76,25 @@ def searchOpportunity(input: str): # @main_blueprint.get("/opportunity") # def getOpportunity2(): - # if not request.data: # abort(400) - # json_request_data = request.get_json() - # if not json_request_data: # abort(400) - # id = json_request_data.get("id", None) - # if not id: # abort(400) - # data = db.first_or_404(db.select(Opportunities).where(Opportunities.id == id)) - # result = data.to_dict() - # return result def convert_to_enum(location_string): try: - return LocationEnum[location_string] # Use upper() for case-insensitivity - except: + return LocationEnum[ + location_string.upper() + ] # Use upper() for case-insensitivity + except KeyError: return None # Or raise an exception if you prefer @@ -89,18 +102,23 @@ def packageOpportunity(opportunityInfo, professorInfo): data = opportunityInfo.to_dict() data["professor"] = professorInfo.name data["department"] = professorInfo.department_id - return data def packageIndividualOpportunity(opportunityInfo): - data = {} - data["id"] = opportunityInfo.id - data["name"] = opportunityInfo.name - data["description"] = opportunityInfo.description - data["recommended_experience"] = opportunityInfo.recommended_experience - data["author"] = "" - data["department"] = "" + data = { + "id": opportunityInfo.id, + "name": opportunityInfo.name, + "description": opportunityInfo.description, + "recommended_experience": opportunityInfo.recommended_experience, + "authors": "", + "department": "", + "pay": opportunityInfo.pay, + "credits": None, + "semester": f"{opportunityInfo.semester} {opportunityInfo.year}", + "application_due": opportunityInfo.application_due, + "recommended_class_years": "", + } opportunity_credits = "" if opportunityInfo.one_credit: @@ -115,50 +133,30 @@ def packageIndividualOpportunity(opportunityInfo): if opportunity_credits != "": opportunity_credits += " credits" - data["aboutSection"] = [ - { - "title": "Pay", - "description": f"${opportunityInfo.pay} per hour", - }, - { - "title": "Semester", - "description": f"{opportunityInfo.semester} {opportunityInfo.year}", - }, - { - "title": "Application Due", - "description": opportunityInfo.application_due, - }, - ] - if opportunity_credits != "": - data["aboutSection"].append( - { - "title": "Credits", - "description": opportunity_credits, - } - ) + data["credits"] = opportunity_credits # get professor and department by getting Leads and LabManager query = db.session.execute( - db.select(Leads, LabManager) + db.select(Leads, LabManager, User) .where(Leads.opportunity_id == opportunityInfo.id) .join(LabManager, Leads.lab_manager_id == LabManager.id) + .join(User, LabManager.id == User.lab_manager_id) ) queryInfo = query.all() - print(queryInfo) if len(queryInfo) == 0: return data data["department"] = queryInfo[0][1].department_id - for i, item in enumerate(queryInfo): - data["author"] += item[1].getName() - # data["author"] += "look at def packageIndividualOpportunity(opportunityInfo):" - if i != len(queryInfo) - 1: - data["author"] += ", " + author_info = [ + [item[2].first_name + " " + item[2].last_name, item[2].id] for item in queryInfo + ] + + data["authors"] = author_info if len(queryInfo) > 1: data["authorProfile"] = ( @@ -176,19 +174,15 @@ def packageOpportunityCard(opportunity): # get professor and department by getting Leads and LabManager query = db.session.execute( - db.select(Leads, LabManager) + 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 = "" - - for i, item in enumerate(data): - professorInfo += item[1].getName() - if i != len(data) - 1: - professorInfo += ", " + professorInfo = ", ".join(item[1].getName() for item in data) card = { "id": opportunity.id, @@ -202,168 +196,175 @@ def packageOpportunityCard(opportunity): return card -# @main_blueprint.route("/getOpportunity/", methods=["GET"]) -# def getOpportunity(opp_id: int): -# # query database for opportunity -# query = db.session.execute( -# db.select(Opportunities).where(Opportunities.id == opp_id) -# ) - -# data = query.all() - -# # check if opportunity exists -# if not data or len(data) == 0: -# abort(404) - -# data = data[0] - -# oppData = packageIndividualOpportunity(data[0]) - -# # return data in the below format if opportunity is found -# return {"data": oppData} - - -# @main_blueprint.get("/opportunity/filter") -# @main_blueprint.route("/opportunity/filter", methods=["GET", "POST"]) -# def filterOpportunities(): - -# if not request.data: -# data = db.session.execute( -# db.select(Opportunities) -# .where(Opportunities.active == True) -# .limit(20) -# .order_by(Opportunities.last_updated) -# .distinct() -# ).scalars() - -# result = [opportunity.to_dict() for opportunity in data] - -# return result - -# json_request_data = request.get_json() - -# if not json_request_data: -# abort(400) - -# filters = json_request_data.get("filters", None) - -# data = None - -# if filters is None: -# data = db.session.execute(db.select(Opportunities).limit(20)).scalars() - -# elif not isinstance(filters, list): -# abort(400) - -# else: - -# where_conditions = [] -# query = ( -# db.select(Opportunities) -# .where(Opportunities.active == True) -# .limit(20) -# .order_by(Opportunities.last_updated) -# .distinct() -# ) -# for given_filter in filters: -# field = given_filter.get("field", None) -# value = given_filter.get("value", None) - -# if field and value: - -# field = field.lower() - -# if field == "location" and value.lower() == "remote": -# where_conditions.append(Opportunities.location == "REMOTE") - -# elif field == "location": -# where_conditions.append(Opportunities.location != "REMOTE") - -# elif field == "class_year": - -# if not isinstance(value, list): -# abort(400) - -# query = query.join( -# RecommendsClassYears, -# Opportunities.id == RecommendsClassYears.opportunity_id, -# ).where(RecommendsClassYears.class_year.in_(value)) - -# elif field == "credits": - -# if not isinstance(value, list): -# abort(400) - -# credit_conditions = [] - -# for credit in value: - -# if credit == 1: -# credit_conditions.append(Opportunities.one_credit == True) -# elif credit == 2: -# credit_conditions.append(Opportunities.two_credits == True) -# elif credit == 3: -# credit_conditions.append( -# Opportunities.three_credits == True -# ) -# elif credit == 4: -# credit_conditions.append(Opportunities.four_credits == True) -# else: -# abort(400) - -# query = query.where(db.or_(*credit_conditions)) - -# elif field == "majors": - -# if not isinstance(value, list): -# abort(400) +@main_blueprint.get("/getOpportunity/") +def getOpportunity(opp_id: int): + # query database for opportunity and recommended class years + query = db.session.execute( + db.select( + Opportunities, + # Creates an array for all of the recommended class years for the opportunity labeled recommended_years + func.array_agg(RecommendsClassYears.class_year).label("recommended_years"), + ) + .join( + RecommendsClassYears, + Opportunities.id == RecommendsClassYears.opportunity_id, + ) + .where(Opportunities.id == opp_id) + .group_by(Opportunities.id) + ) -# query = query.join( -# RecommendsMajors, -# Opportunities.id == RecommendsMajors.opportunity_id, -# ).where(RecommendsMajors.major_code.in_(value)) + data = query.all() + print(data) -# elif field == "departments": + # check if opportunity exists + if not data or len(data) == 0: + abort(404) -# if not isinstance(value, list): -# abort(400) + data = data[0] + oppData = packageIndividualOpportunity(data[0]) + oppData["recommended_class_years"] = data[1] -# query = ( -# query.join(Leads, Opportunities.id == Leads.opportunity_id) -# .join(LabManager, Leads.lab_manager_id == LabManager.id) -# .where(LabManager.department_id.in_(value)) -# ) + # return data in the below format if opportunity is found + return {"data": oppData} -# elif field == "pay": -# if not isinstance(value, dict): -# abort(400) +@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 == True) + .limit(20) + .order_by(Opportunities.last_updated.desc()) + .distinct() + ).scalars() + result = [opportunity.to_dict() for opportunity in data] + return result -# min_pay = value.get("min", None) -# max_pay = value.get("max", None) -# if min_pay is None or max_pay is None: -# abort(400) +@main_blueprint.post("/opportunity/filter") +def filterOpportunities(): + # Handle POST requests for filtering opportunities + json_request_data = request.get_json() -# where_conditions.append(Opportunities.pay.between(min_pay, max_pay)) + if not json_request_data: + abort(400) -# else: -# try: -# where_conditions.append( -# getattr(Opportunities, field).ilike(f"%{value}%") -# ) -# except AttributeError: -# abort(400) + filters = json_request_data.get("filters", None) -# query = query.where(*where_conditions) -# data = db.session.execute(query).scalars() + data = None -# if not data: -# abort(404) + if filters is None: + data = db.session.execute(db.select(Opportunities).limit(20)).scalars() -# result = [opportunity.to_dict() for opportunity in data] + elif not isinstance(filters, list): + abort(400) -# return result + else: + where_conditions = [] + query = ( + db.select(Opportunities) + .where(Opportunities.active == True) + .limit(20) + .order_by(Opportunities.last_updated) + .distinct() + ) + for given_filter in filters: + field = given_filter.get("field", None) + value = given_filter.get("value", None) + + if field and value: + field = field.lower() + + # Location filter + if field == "location": + if value.lower() == "remote": + where_conditions.append(Opportunities.location == "REMOTE") + else: + where_conditions.append(Opportunities.location != "REMOTE") + + # Class year filter + elif field == "class_year": + if not isinstance(value, list): + abort(400) + query = query.join( + RecommendsClassYears, + Opportunities.id == RecommendsClassYears.opportunity_id, + ).where(RecommendsClassYears.class_year.in_(value)) + + # Credits filter + elif field == "credits": + if not isinstance(value, list): + abort(400) + credit_conditions = [] + for credit in value: + if credit == 1: + credit_conditions.append(Opportunities.one_credit.is_(True)) + elif credit == 2: + credit_conditions.append( + Opportunities.two_credits.is_(True) + ) + elif credit == 3: + credit_conditions.append( + Opportunities.three_credits.is_(True) + ) + elif credit == 4: + credit_conditions.append( + Opportunities.four_credits.is_(True) + ) + else: + abort(400) + where_conditions.append(db.or_(*credit_conditions)) + + # Majors filter + 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)) + + # Departments filter + elif field == "departments": + if not isinstance(value, list): + abort(400) + query = ( + query.join(Leads, Opportunities.id == Leads.opportunity_id) + .join(LabManager, Leads.lab_manager_id == LabManager.id) + .where(LabManager.department_id.in_(value)) + ) + + # Pay filter + elif field == "pay": + if not isinstance(value, dict): + 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)) + + # Other fields + else: + try: + where_conditions.append( + getattr(Opportunities, field).ilike(f"%{value}%") + ) + except AttributeError: + abort(400) + + query = query.where(*where_conditions) + data = db.session.execute(query).scalars() + + if not data: + abort(404) + + result = [opportunity.to_dict() for opportunity in data] + + return result # @main_blueprint.put("/opportunity") @@ -396,102 +397,93 @@ def packageOpportunityCard(opportunity): # return {"msg": "Opportunity updated successfully"}, 200 -# @main_blueprint.route("/getOpportunityMeta/", methods=["GET"]) +# @main_blueprint.get("/getOpportunityMeta/") # def getOpportunityMeta(id: int): -# if request.method == "GET": -# query = db.session.execute( -# db.select( -# Opportunities, RecommendsMajors, RecommendsCourses, RecommendsClassYears -# ) -# .where(Opportunities.id == id) -# .join(RecommendsMajors, RecommendsMajors.opportunity_id == Opportunities.id) -# .join( -# RecommendsCourses, RecommendsCourses.opportunity_id == Opportunities.id -# ) -# .join( -# RecommendsClassYears, -# RecommendsClassYears.opportunity_id == Opportunities.id, -# ) +# query = db.session.execute( +# db.select( +# Opportunities, RecommendsMajors, RecommendsCourses, RecommendsClassYears # ) -# data = query.all() -# print(data) - -# if not data or len(data) == 0: -# abort(404) +# .where(Opportunities.id == id) +# .join(RecommendsMajors, RecommendsMajors.opportunity_id == Opportunities.id) +# .join(RecommendsCourses, RecommendsCourses.opportunity_id == Opportunities.id) +# .join( +# RecommendsClassYears, +# RecommendsClassYears.opportunity_id == Opportunities.id, +# ) +# ) +# data = query.all() +# print(data) -# dictionary = data[0][0].to_dict() -# dictionary["semester"] = dictionary["semester"].upper() -# dictionary["courses"] = set() -# dictionary["majors"] = set() -# dictionary["years"] = set() -# for row in data: -# dictionary["courses"].add(row[2].course_code) -# dictionary["majors"].add(row[1].major_code) -# dictionary["years"].add(row[3].class_year) +# if not data or len(data) == 0: +# abort(404) -# dictionary["courses"] = list(dictionary["courses"]) -# dictionary["majors"] = list(dictionary["majors"]) -# dictionary["years"] = list(dictionary["years"]) +# dictionary = data[0][0].to_dict() +# dictionary["semester"] = dictionary["semester"].upper() +# dictionary["courses"] = set() +# dictionary["majors"] = set() +# dictionary["years"] = set() -# for i in range(len(dictionary["years"])): -# dictionary["years"][i] = str(dictionary["years"][i]) +# for row in data: +# dictionary["courses"].add(row[2].course_code) +# dictionary["majors"].add(row[1].major_code) +# dictionary["years"].add(row[3].class_year) -# dictionary["credits"] = [] -# if dictionary["one_credit"]: -# dictionary["credits"].append("1") +# dictionary["courses"] = list(dictionary["courses"]) +# dictionary["majors"] = list(dictionary["majors"]) +# dictionary["years"] = list(dictionary["years"]) -# if dictionary["two_credits"]: -# dictionary["credits"].append("2") +# for i in range(len(dictionary["years"])): +# dictionary["years"][i] = str(dictionary["years"][i]) -# if dictionary["three_credits"]: -# dictionary["credits"].append("3") +# dictionary["credits"] = [] +# if dictionary["one_credit"]: +# dictionary["credits"].append("1") -# if dictionary["four_credits"]: -# dictionary["credits"].append("4") +# if dictionary["two_credits"]: +# dictionary["credits"].append("2") -# dictionary.pop("one_credit") -# dictionary.pop("two_credits") -# dictionary.pop("three_credits") -# dictionary.pop("four_credits") +# if dictionary["three_credits"]: +# dictionary["credits"].append("3") -# return {"data": dictionary} +# if dictionary["four_credits"]: +# dictionary["credits"].append("4") -# abort(500) +# dictionary.pop("one_credit") +# dictionary.pop("two_credits") +# dictionary.pop("three_credits") +# dictionary.pop("four_credits") +# return {"data": dictionary} -# # Jobs page -# @main_blueprint.route("/getOpportunityCards", methods=["GET"]) -# def getOpportunityCards(): -# if request.method == "GET": -# # query database for opportunity -# query = db.session.execute( -# db.select(Opportunities).where(Opportunities.active == True) -# ) +# abort(500) -# data = query.fetchall() -# # return data in the below format if opportunity is found -# cards = { -# "data": [packageOpportunityCard(opportunity[0]) for opportunity in data] -# } +# Jobs page +@main_blueprint.get("/getOpportunityCards") +def getOpportunityCards(): + # query database for opportunity + query = db.session.execute( + db.select(Opportunities).where(Opportunities.active == True) + ) -# return cards + data = query.fetchall() + # return data in the below format if opportunity is found + cards = {"data": [packageOpportunityCard(opportunity[0]) for opportunity in data]} -# abort(500) + return cards -# @main_blueprint.route("/getOpportunities", methods=["GET"]) +# @main_blueprint.get("/getOpportunities") # def getOpportunities(): -# if request.method == "GET": -# # query database for opportunity -# query = db.session.execute( -# db.select(Opportunities, Leads, LabManager) -# .join(Leads, Leads.opportunity_id == Opportunities.id) -# .join(LabManager, Leads.lab_manager_id == LabManager.id) -# ) -# data = query.all() -# print(data[0]) +# # # query database for opportunity +# query = db.session.execute( +# db.select(Opportunities, Leads, LabManager) +# .join(Leads, Leads.opportunity_id == Opportunities.id) +# .join(LabManager, Leads.lab_manager_id == LabManager.id) +# ) +# data = query.all() +# print(data[0]) # # return data in the below format if opportunity is found # return { @@ -504,362 +496,442 @@ def packageOpportunityCard(opportunity): # abort(500) -# @main_blueprint.route("/getOpportunityByProfessor/", methods=["GET"]) -# def getOpportunityByProfessor(rcs_id: str): -# if request.method == "GET": -# # query database for opportunity -# query = db.session.execute( -# db.select(Opportunities, Leads) -# .where(Leads.lab_manager_id == rcs_id) -# .join(Opportunities, Leads.opportunity_id == Opportunities.id) -# ) - -# data = query.all() -# print(data) - -# # return data in the below format if opportunity is found -# return {"data": [opportunity[0].to_dict() for opportunity in data]} - -# abort(500) - - -# @main_blueprint.route("/getProfessorOpportunityCards/", methods=["GET"]) -# def getProfessorOpportunityCards(rcs_id: str): -# if request.method == "GET": -# # query database for opportunity -# user = db.first_or_404(db.select(User).where(User.email == rcs_id)) +@main_blueprint.get("/staff/opportunities/") +def getLabManagerOpportunityCards(rcs_id: str) -> list[dict[str, str]]: -# query = db.session.execute( -# db.select(Opportunities, Leads) -# .where(Leads.lab_manager_id == user.lab_manager_id) -# .join(Opportunities, Leads.opportunity_id == Opportunities.id) -# ) - -# data = query.all() - -# cards = {"data": []} + query = ( + db.select( + Opportunities.id, + Opportunities.name, + Opportunities.application_due, + Opportunities.pay, + Opportunities.one_credit, + Opportunities.two_credits, + Opportunities.three_credits, + Opportunities.four_credits, + ) + .join(LabManager, User.lab_manager_id == LabManager.id) + .join(Leads, Leads.lab_manager_id == LabManager.id) + .join(Opportunities, Leads.opportunity_id == Opportunities.id) + .where(User.id == rcs_id) + .select_from(User) + ) -# for row in data: -# opportunity = row[0] + data = db.session.execute(query).all() -# if not opportunity.active: -# continue + cards = [ + { + "id": row[0], + "title": row[1], + "due": row[2].strftime("%-m/%-d/%y"), + "pay": row[3], + "credits": format_credits(row[4], row[5], row[6], row[7]), + } + for row in data + ] -# oppData = { -# "id": opportunity.id, -# "title": opportunity.name, -# "body": "Due " + str(opportunity.application_due), -# "attributes": [], -# } + return cards -# if opportunity.pay is not None and opportunity.pay > 0: -# oppData["attributes"].append("Paid") -# if ( -# opportunity.one_credit -# or opportunity.two_credits -# or opportunity.three_credits -# or opportunity.four_credits -# ): -# oppData["attributes"].append("Credit Available") +@main_blueprint.get("/profile/opportunities/") +def getProfileOpportunities(rcs_id: str) -> list[dict[str, str]]: -# cards["data"].append(oppData) + query = ( + db.select( + Opportunities.id, + Opportunities.name, + Opportunities.application_due, + Opportunities.pay, + Opportunities.one_credit, + Opportunities.two_credits, + Opportunities.three_credits, + Opportunities.four_credits, + ) + .join(Participates, Participates.user_id == rcs_id) + .join(Opportunities, Participates.opportunity_id == Opportunities.id) + .where(User.id == rcs_id) + .select_from(User) + ) -# # return data in the below format if opportunity is found -# return cards + data = db.session.execute(query).all() -# abort(500) + cards = [ + { + "id": row[0], + "title": row[1], + "due": row[2].strftime("%-m/%-d/%y"), + "pay": row[3], + "credits": format_credits(row[4], row[5], row[6], row[7]), + } + for row in data + ] + return cards -# @main_blueprint.route("/getProfileOpportunities/", methods=["GET"]) -# def getProfileOpportunities(rcs_id: str): -# if request.method == "GET": -# # query database for opportunity -# query = db.session.execute( -# db.select(Opportunities, Leads) -# .where(Leads.lab_manager_id == rcs_id) -# .join(Opportunities, Leads.opportunity_id == Opportunities.id) -# ) +# function to search for lab managers +@main_blueprint.get("/searchLabManagers/") +def searchLabManagers(query: str): + # Perform a search on User table by first name, last name, or email using ILIKE for exact partial matches + stmt = ( + db.select(User) + .join(LabManager, User.lab_manager_id == LabManager.id) + .where( + ( + User.first_name.ilike( + f"%{query}%" + ) # Case-insensitive partial match on first_name + ) + | ( + User.last_name.ilike( + f"%{query}%" + ) # Case-insensitive partial match on last_name + ) + | ( + User.email.ilike( + f"%{query}%" + ) # Case-insensitive partial match on email + ) + ) + ) -# data = query.all() + results = db.session.execute(stmt).scalars().all() -# cards = {"data": []} + lab_managers = [ + { + "lab_manager_id": user.lab_manager_id, + "first_name": user.first_name, + "last_name": user.last_name, + "email": user.email, + } + for user in results + ] -# for row in data: -# opportunity = row[0] + return {"lab_managers": lab_managers}, 200 -# oppData = { -# "id": opportunity.id, -# "title": opportunity.name, -# "body": "Due " + str(opportunity.application_due), -# "attributes": [], -# "activeStatus": opportunity.active, -# } -# if opportunity.pay is not None and opportunity.pay > 0: -# oppData["attributes"].append("Paid") -# if ( -# opportunity.one_credit -# or opportunity.two_credits -# or opportunity.three_credits -# or opportunity.four_credits -# ): -# oppData["attributes"].append("Credits") +@main_blueprint.get("/searchCourses/") +def searchCourses(query: str): + # Perform a search on Courses table by code and name using ILIKE for exact partial matches + stmt = ( + db.select(Courses) + .distinct() + .where( + (Courses.code.ilike(f"%{query}%")) + | ( + Courses.name.ilike( + f"%{query}%" + ) # Case-insensitive partial match on course name + ) + ) + ) -# cards["data"].append(oppData) + results = db.session.execute(stmt).scalars().all() -# # return data in the below format if opportunity is found -# return cards + # Format results as JSON + courses = [ + { + "code": course.code, + "name": course.name, + } + for course in results + ] -# abort(500) + return {"courses": courses}, 200 # functions to create/edit/delete opportunities -# @main_blueprint.route("/createOpportunity", methods=["POST"]) -# def createOpportunity(): -# if request.method == "POST": -# data = request.get_json() -# authorID = data["authorID"] -# newPostData = data - -# # query database to see if the credentials above match -# query = db.session.execute( -# db.select(LabManager).where(LabManager.id == authorID) -# ) +@main_blueprint.post("/createOpportunity") +@jwt_required() +def createOpportunity(): + user_id = get_jwt_identity() + if not request.data or not user_id: + abort(400) -# data = query.all()[0][0] - -# # TODO: how do we get the opportunity id? -# # if match is found, create a new opportunity with the new data provided - -# one = False -# two = False -# three = False -# four = False - -# if "1" in newPostData["credits"]: -# one = True -# if "2" in newPostData["credits"]: -# two = True -# if "3" in newPostData["credits"]: -# three = True -# if "4" in newPostData["credits"]: -# four = True - -# lenum = convert_to_enum(newPostData["location"]) - -# if lenum is None: -# lenum = LocationEnum.TBD - -# newOpportunity = Opportunities( -# name=newPostData["name"], -# description=newPostData["description"], -# recommended_experience=newPostData["recommended_experience"], -# pay=newPostData["pay"], -# one_credit=one, -# two_credits=two, -# three_credits=three, -# four_credits=four, -# semester=newPostData["semester"], -# year=newPostData["year"], -# application_due=datetime.datetime.strptime( -# newPostData["application_due"], "%Y-%m-%d" -# ), -# active=newPostData["active"], -# location=lenum, -# ) -# print("before comitting") -# db.session.add(newOpportunity) -# db.session.commit() + request_data = request.get_json() -# print("got here atleast") + if not request_data: + abort(400) -# newLead = Leads(lab_manager_id=authorID, opportunity_id=newOpportunity.id) + author = db.session.execute( + db.select(User).where(User.email == user_id[0]) + ).scalar_one_or_none() -# db.session.add(newLead) -# db.session.commit() + if author is None or author.lab_manager_id is None: + abort(400) -# for course in newPostData["courses"]: -# newCourse = RecommendsCourses( -# opportunity_id=newOpportunity.id, course_code=course -# ) -# db.session.add(newCourse) -# db.session.commit() + try: + pay = int(request_data["hourlyPay"]) + except: + pay = None + + one = True if "1" in request_data["credits"] else False + two = True if "2" in request_data["credits"] else False + three = True if "3" in request_data["credits"] else False + four = True if "4" in request_data["credits"] else False + + lenum = convert_to_enum(request_data["location"]) + + if lenum is None: + lenum = LocationEnum.TBD + + newOpportunity = Opportunities() + newOpportunity.name = request_data["title"] + newOpportunity.description = request_data["description"] + newOpportunity.recommended_experience = request_data["recommended_experience"] + newOpportunity.pay = pay + newOpportunity.one_credit = one + newOpportunity.two_credits = two + newOpportunity.three_credits = three + newOpportunity.four_credits = four + newOpportunity.semester = SemesterEnum.FALL + newOpportunity.year = datetime.now().year + newOpportunity.application_due = datetime.strptime( + request_data["application_due"], "%Y-%m-%d" + ) + newOpportunity.active = True + newOpportunity.location = lenum + newOpportunity.last_updated = datetime.now() + db.session.add(newOpportunity) + db.session.commit() + + newLead = Leads() + newLead.lab_manager_id = author.lab_manager_id + newLead.opportunity_id = newOpportunity.id + + db.session.add(newLead) + + for year in request_data["years"]: + if year.isdigit(): + recommended_year = int(year) + newYear = RecommendsClassYears() + newYear.opportunity_id = newOpportunity.id + newYear.class_year = recommended_year + db.session.add(newYear) + + db.session.commit() + + return {"data": "Opportunity Created", "id": newOpportunity.id}, 200 + + +@main_blueprint.get("/editOpportunity/") +def editOpportunity_get(opportunity_id): + opportunity = db.session.execute( + db.select(Opportunities).where(Opportunities.id == opportunity_id) + ).first() + + if not opportunity: + return {"error": "Opportunity not found"}, 404 + + opportunity = opportunity[0] + + # Query related courses + # courses_data = db.session.execute( + # db.select(RecommendsCourses.course_code).where( + # RecommendsCourses.opportunity_id == opportunity_id + # ) + # ).all() + + # Query related majors + # majors_data = db.session.execute( + # db.select(RecommendsMajors.major_code).where( + # RecommendsMajors.opportunity_id == opportunity_id + # ) + # ).all() + + # Query related class years + years_data = db.session.execute( + db.select(RecommendsClassYears.class_year).where( + RecommendsClassYears.opportunity_id == opportunity_id + ) + ).all() + + credits = [ + str(i) + for i, credit in enumerate( + [ + opportunity.one_credit, + opportunity.two_credits, + opportunity.three_credits, + opportunity.four_credits, + ], + start=1, + ) + if credit + ] -# for major in newPostData["majors"]: -# newMajor = RecommendsMajors( -# opportunity_id=newOpportunity.id, major_code=major -# ) -# db.session.add(newMajor) -# db.session.commit() + years = [str(year.class_year) for year in years_data] if years_data else [] -# for year in newPostData["years"]: -# newYear = RecommendsClassYears( -# opportunity_id=newOpportunity.id, class_year=year -# ) -# db.session.add(newYear) -# db.session.commit() + # Format opportunity data as JSON + opportunity_data = { + "id": opportunity.id, + "title": opportunity.name, + "application_due": opportunity.application_due.strftime("%Y-%m-%d"), + "type": ( + "Any" + if len(credits) > 0 and opportunity.pay and opportunity.pay > 0 + else "For Pay" if opportunity.pay and opportunity.pay > 0 else "For Credit" + ), + "hourlyPay": str(opportunity.pay), + "credits": credits, + "description": opportunity.description, + "recommended_experience": opportunity.recommended_experience, + # "semester": opportunity.semester, # Convert enum to string + # "year": opportunity.year, + # "active": opportunity.active, + "location": opportunity.location, # Convert enum to string + # "last_updated": opportunity.last_updated.strftime("%Y-%m-%d %H:%M:%S"), + # "courses": [course.course_code for course in courses_data], + # "majors": [major.major_code for major in majors_data], + "years": years, + } -# # db.session.add(newOpportunity) + return opportunity_data -# return {"data": "Opportunity Created"} -# abort(500) +@main_blueprint.put("/editOpportunity/") +@jwt_required() +def editOpportunity(opportunity_id): + user_id = get_jwt_identity() + if not request.data or not user_id: + abort(400) -# @main_blueprint.route("/editOpportunity", methods=["DELETE", "POST"]) -# def editOpportunity(): -# if True: -# data = request.get_json() -# id = data["id"] -# # authToken = data["authToken"] -# # authorID = data["authorID"] -# newPostData = data - -# # query database to see if the credentials above match -# query = db.session.execute( -# db.select( -# Opportunities, RecommendsMajors, RecommendsCourses, RecommendsClassYears -# ) -# .where(Opportunities.id == id) -# .join(RecommendsMajors, RecommendsMajors.opportunity_id == Opportunities.id) -# .join( -# RecommendsCourses, RecommendsCourses.opportunity_id == Opportunities.id -# ) -# .join( -# RecommendsClassYears, -# RecommendsClassYears.opportunity_id == Opportunities.id, -# ) -# ) + request_data = request.get_json() -# data = query.all() - -# if not data or len(data) == 0: -# abort(404) - -# opportunity = data[0][0] - -# one = False -# two = False -# three = False -# four = False - -# if "1" in newPostData["credits"]: -# one = True -# if "2" in newPostData["credits"]: -# two = True -# if "3" in newPostData["credits"]: -# three = True -# if "4" in newPostData["credits"]: -# four = True - -# lenum = convert_to_enum(newPostData["location"]) -# print(newPostData["location"]) -# print("printing lenum") -# print(lenum) - -# # if match is found, edit the opportunity with the new data provided -# opportunity.name = newPostData["name"] -# opportunity.description = newPostData["description"] -# opportunity.recommended_experience = newPostData["recommended_experience"] -# opportunity.pay = newPostData["pay"] -# opportunity.one_credit = one -# opportunity.two_credits = two -# opportunity.three_credits = three -# opportunity.four_credits = four -# opportunity.semester = newPostData["semester"] -# opportunity.year = newPostData["year"] -# opportunity.application_due = datetime.datetime.strptime( -# newPostData["application_due"], "%Y-%m-%d" -# ) -# opportunity.active = newPostData["active"] + if not request_data: + abort(400) -# if lenum is not None: -# opportunity.location = lenum + # Check if the opportunity and author exist + opportunity = db.session.execute( + db.select(Opportunities).where(Opportunities.id == opportunity_id) + ).scalar_one_or_none() -# db.session.add(opportunity) -# db.session.commit() + if opportunity is None: + abort(400) -# # delete all the old data in the recommends tables + author = db.session.execute( + db.select(User).where(User.email == user_id[0]) + ).scalar_one_or_none() -# for row in data: -# db.session.delete(row[1]) -# db.session.delete(row[2]) -# db.session.delete(row[3]) + if author is None or author.lab_manager_id is None: + abort(400) -# # create new data for allow the tables + leads = db.session.execute( + db.select(Leads) + .where(Leads.opportunity_id == opportunity_id) + .where(Leads.lab_manager_id == author.lab_manager_id) + ).scalar_one_or_none() -# for course in newPostData["courses"]: -# newCourse = RecommendsCourses( -# opportunity_id=opportunity.id, course_code=course -# ) -# db.session.add(newCourse) -# db.session.commit() + if leads is None: + abort(400) -# for major in newPostData["majors"]: -# newMajor = RecommendsMajors(opportunity_id=opportunity.id, major_code=major) -# db.session.add(newMajor) -# db.session.commit() + try: + pay = int(request_data["hourlyPay"]) + except: + pay = None + + one = True if "1" in request_data["credits"] else False + two = True if "2" in request_data["credits"] else False + three = True if "3" in request_data["credits"] else False + four = True if "4" in request_data["credits"] else False + + lenum = convert_to_enum(request_data["location"]) + + if lenum is None: + lenum = LocationEnum.TBD + + # Update fields for opportunity based on the input data + opportunity.name = request_data["title"] + opportunity.description = request_data["description"] + opportunity.recommended_experience = request_data["recommended_experience"] + opportunity.pay = pay + opportunity.one_credit = one + opportunity.two_credits = two + opportunity.three_credits = three + opportunity.four_credits = four + opportunity.application_due = datetime.strptime( + request_data["application_due"], "%Y-%m-%d" + ) + # opportunity.active = data["active"] + opportunity.location = lenum + opportunity.last_updated = datetime.now() + + existing_years = { + str(year.class_year) + for year in db.session.execute( + db.select(RecommendsClassYears).where( + RecommendsClassYears.opportunity_id == opportunity_id + ) + ).scalars() + } + new_years = set(request_data["years"]) + + # Years to add + years_to_add = new_years - existing_years + for year in years_to_add: + newYear = RecommendsClassYears() + newYear.opportunity_id = opportunity.id + newYear.class_year = int(year) + db.session.add(newYear) + + # Years to remove + years_to_remove = existing_years - new_years + if years_to_remove: + db.session.execute( + db.select(RecommendsClassYears) + .where( + RecommendsClassYears.opportunity_id == opportunity_id, + RecommendsClassYears.class_year.in_(years_to_remove), + ) + .delete(synchronize_session=False) + ) -# for year in newPostData["years"]: -# newYear = RecommendsClassYears( -# opportunity_id=opportunity.id, class_year=year -# ) -# db.session.add(newYear) -# db.session.commit() + db.session.commit() -# return "Successful" + # Add the updated list of managers + if "lab_manager_ids" in data: + for lab_manager_id in data["lab_manager_ids"]: + new_lead = Leads( + lab_manager_id=lab_manager_id, opportunity_id=opportunity_id + ) + db.session.add(new_lead) -# abort(500) + db.session.commit() # Commit all changes + return {"data": "Opportunity Updated"}, 200 -# @main_blueprint.route("/deleteOpportunity", methods=["DELETE", "POST"]) -# def deleteOpportunity(): -# if request.method in ["DELETE", "POST"]: -# data = request.get_json() -# id = data["id"] - -# query = db.session.execute( -# db.select( -# Opportunities, -# RecommendsMajors, -# RecommendsCourses, -# RecommendsClassYears, -# Leads, -# ) -# .where(Opportunities.id == id) -# .join(RecommendsMajors, RecommendsMajors.opportunity_id == Opportunities.id) -# .join( -# RecommendsCourses, RecommendsCourses.opportunity_id == Opportunities.id -# ) -# .join( -# RecommendsClassYears, -# RecommendsClassYears.opportunity_id == Opportunities.id, -# ) -# .join(Leads, Leads.opportunity_id == Opportunities.id) -# ) -# data = query.all() -# print(data) +@main_blueprint.delete("/deleteOpportunity/") +@jwt_required() +def deleteOpportunity(opportunity_id): + opportunity = db.session.get(Opportunities, opportunity_id) -# if not data or len(data) == 0: -# abort(404) + if not opportunity: + return {"error": "Opportunity not found"}, 404 -# opportunity = data[0][0] + # TODO: Add check to see if user has permission to delete opportunity + user_id = get_jwt_identity() -# for row in data: -# db.session.delete(row[1]) -# db.session.delete(row[2]) -# db.session.delete(row[3]) -# db.session.delete(row[4]) + user = db.session.execute( + db.select(User).where(User.email == user_id) + ).scalar_one_or_none() -# leads = data[0][4] + if not user or not user.lab_manager_id: + return {"error": "Don't have permission to delete!"}, 401 -# db.session.delete(opportunity) + leads = db.session.execute( + db.select(Leads) + .where(Leads.opportunity_id == opportunity_id) + .where(Leads.lab_manager_id == user.lab_manager_id) + ).scalar_one_or_none() -# db.session.commit() + if not leads: + abort(400) -# return "Success" + # Delete the opportunity + # cascading delete will handle all other tables + db.session.delete(opportunity) + db.session.commit() -# abort(500) + return {"data": "Opportunity Deleted"} diff --git a/labconnect/main/routes.py b/labconnect/main/routes.py index 4e144880..7c68809b 100644 --- a/labconnect/main/routes.py +++ b/labconnect/main/routes.py @@ -1,35 +1,23 @@ -from typing import Any +# from typing import Any from flask import abort, request -from flask_jwt_extended import ( - get_jwt_identity, - jwt_required, -) +from flask_jwt_extended import get_jwt_identity, jwt_required from labconnect import db from labconnect.models import ( - ClassYears, - Courses, LabManager, - Leads, - Majors, Opportunities, - Participates, - RecommendsClassYears, - RecommendsCourses, - RecommendsMajors, RPIDepartments, - RPISchools, User, - UserCourses, + ClassYears, UserDepartments, - UserMajors, + Majors, ) from . import main_blueprint -@main_blueprint.route("/") +@main_blueprint.get("/") def index(): return {"Hello": "There"} @@ -37,233 +25,142 @@ def index(): @main_blueprint.get("/departments") def departmentCards(): data = db.session.execute( - db.select(RPIDepartments.name, RPIDepartments.school_id) + db.select(RPIDepartments.name, RPIDepartments.school_id, RPIDepartments.id) ).all() results = [ { "title": department.name, + "department_id": department.id, "school": department.school_id, "image": "https://cdn-icons-png.flaticon.com/512/5310/5310672.png", } for department in data ] - return results @main_blueprint.get("/departments/") def departmentDetails(department: str): - if not department: - abort(400) - - department_data = db.first_or_404( - db.select(RPIDepartments).where(RPIDepartments.name == department) - ) - - result = department_data.to_dict() - - prof_data = department_data.lab_managers + department_data = db.session.execute( + db.select( + RPIDepartments.id, + RPIDepartments.name, + RPIDepartments.description, + RPIDepartments.image, + RPIDepartments.website, + ).where(RPIDepartments.id == department) + ).first() + + if department_data is None: + abort(404) - professors = [] - where_conditions = [] + staff_data = db.session.execute( + db.select( + User.id, + User.first_name, + User.preferred_name, + User.last_name, + User.profile_picture, + ) + .join(LabManager, User.lab_manager_id == LabManager.id) + .join(RPIDepartments, LabManager.department_id == RPIDepartments.id) + .where(RPIDepartments.id == department) + ).all() - for prof in prof_data: - professors.append( + result = { + "id": department_data[0], + "name": department_data[1], + "description": department_data[2], + "image": department_data[3], + "website": department_data[4], + "staff": [ { - "name": prof.getName(), - "rcs_id": prof.getEmail(), - "image": "https://www.svgrepo.com/show/206842/professor.svg", + "name": ( + staff[2] + " " + staff[3] if staff[2] else staff[1] + " " + staff[3] + ), + "id": staff[0], + "image": staff[4], } - ) - where_conditions.append(LabManager.id == prof.id) - - result["professors"] = professors - - result["image"] = ( - "https://t4.ftcdn.net/jpg/02/77/10/87/360_F_277108701_1JAbS8jg7Gw42dU6nz7sF72bWiCm3VMv.jpg" - ) + for staff in staff_data + ], + } return result -# @main_blueprint.get("/getSchoolsAndDepartments/") -# def getSchoolsAndDepartments(): -# data = db.session.execute( -# db.select(RPISchools, RPIDepartments).join( -# RPIDepartments, RPISchools.name == RPIDepartments.school_id -# ) -# ).scalars() - -# dictionary = {} -# for item in data: -# if item[0].name not in dictionary: -# dictionary[item[0].name] = [] -# dictionary[item[0].name].append(item[1].name) - -# return dictionary - - -# @main_blueprint.get("/getOpportunitiesRaw/") -# def getOpportunitiesRaw(id: int): -# data = db.session.execute( -# db.select( -# Opportunities, -# Leads, -# LabManager, -# RecommendsMajors, -# RecommendsCourses, -# RecommendsClassYears, -# ) -# .where(Opportunities.id == id) -# .join(Leads, Leads.opportunity_id == Opportunities.id) -# .join(LabManager, Leads.lab_manager_id == LabManager.id) -# .join(RecommendsMajors, RecommendsMajors.opportunity_id == Opportunities.id) -# .join(RecommendsCourses, RecommendsCourses.opportunity_id == Opportunities.id) -# .join( -# RecommendsClassYears, -# RecommendsClassYears.opportunity_id == Opportunities.id, -# ) -# ).scalars() - -# opportunities = [opportunity.to_dict() for opportunity in data] - -# return {"data": opportunities} - - -# @main_blueprint.get("/lab_manager") -# def getLabManagers(): -# if not request.data: -# abort(400) - -# json_request_data = request.get_json() - -# if not json_request_data: -# abort(400) - -# rcs_id = json_request_data.get("rcs_id", None) - -# if not rcs_id: -# abort(400) - -# data = db.first_or_404(db.select(LabManager).where(LabManager.id == rcs_id)) - -# result = data.to_dict() - -# return result - - @main_blueprint.get("/profile") +@jwt_required() def profile(): - request_data = request.get_json() - id = request_data.get("id", None) - - # TODO: Fix to a join query - lab_manager = db.first_or_404(db.select(LabManager).where(LabManager.id == id)) - user = db.first_or_404(db.select(User).where(User.lab_manager_id == id)) - result = lab_manager.to_dict() | user.to_dict() + user_id = get_jwt_identity() data = db.session.execute( - db.select(Opportunities, Leads) - .where(Leads.lab_manager_id == lab_manager.id) - .join(Opportunities, Leads.opportunity_id == Opportunities.id) - ).scalars() - - result["opportunities"] = [opportunity.to_dict() for opportunity in data] + db.select( + User.preferred_name, + User.first_name, + User.last_name, + User.profile_picture, + RPIDepartments.name, + User.description, + User.website, + User.lab_manager_id, + User.id, + ) + .where(User.email == user_id[0]) + .join(UserDepartments, UserDepartments.user_id == User.id) + .join(RPIDepartments, UserDepartments.department_id == RPIDepartments.id) + ).first() + + if not data: + return {"error": "profile not found"}, 404 + + # if data[7]: + # return {"lab_manager": True, "id": data[7]} + + result = { + "id": data[8], + "name": data[0] + " " + data[2] if data[0] else data[1] + " " + data[2], + "image": data[3], + "department": data[4], + "description": data[5], + "website": data[6], + } return result -# @main_blueprint.get("/getProfessorProfile/") -# def getProfessorProfile(email: int): -# # test code until database code is added -# query = db.session.execute(db.select(User).where(User.email == email)) -# data = query.all() -# user = data[0][0] -# lm = user.getLabManager() - -# result = {} - -# dictionary = user.to_dict() - -# dictionary["image"] = "https://www.svgrepo.com/show/206842/professor.svg" -# dictionary["department"] = lm.department_id -# dictionary["email"] = user.email -# dictionary["role"] = "admin" -# dictionary["description"] = ( -# "This is the description from the backend but we need to add more fields for LabManager" -# ) - -# # clean data -# dictionary["name"] = ( -# dictionary.pop("first_name") + " " + dictionary.pop("last_name") -# ) -# dictionary.pop("class_year") - -# return dictionary - - -# @main_blueprint.get("/lab_manager/opportunities") -# def getLabManagerOpportunityCards() -> dict[Any, list[Any]]: -# if not request.data: -# abort(400) - -# rcs_id = request.get_json().get("rcs_id", None) - -# if not rcs_id: -# abort(400) - -# data = db.session.execute( -# db.select(Opportunities, LabManager) -# .where(LabManager.id == rcs_id) -# .join(Leads, LabManager.id == Leads.lab_manager_id) -# .join(Opportunities, Leads.opportunity_id == Opportunities.id) -# .order_by(Opportunities.id) -# ).scalars() - -# if not data: -# abort(404) - -# result = {rcs_id: [opportunity.to_dict() for opportunity in data]} - -# return result - - -# _______________________________________________________________________________________________# - - -# Editing Opportunities in Profile Page -# @main_blueprint.get("/getProfessorCookies/") -# def getProfessorCookies(id: str): - -# # this is already restricted to "GET" requests +@main_blueprint.get("/staff/") +@jwt_required() +def getProfessorProfile(id: str): -# # TODO: Use JOIN query -# lab_manager = db.first_or_404(db.select(LabManager).where(LabManager.id == id)) -# user = db.first_or_404(db.select(User).where(User.lab_manager_id == id)) - -# dictionary = lab_manager.to_dict() | user.to_dict() - -# dictionary["role"] = "admin" -# dictionary["researchCenter"] = "AI" -# dictionary["loggedIn"] = True - -# return dictionary - - -# @main_blueprint.get("/getStaff/") -# def getStaff(department: str): -# query = db.session.execute( -# db.select(LabManager).filter(LabManager.department_id == department) -# ) -# data = query.all() -# dictionary = {} -# for item in data: -# dictionary[item[0].rcs_id] = item[0].to_dict() -# dictionary[item[0].rcs_id].pop("rcs_id") + data = db.session.execute( + db.select( + User.preferred_name, + User.first_name, + User.last_name, + User.profile_picture, + RPIDepartments.name, + User.description, + User.website, + ) + .where(User.id == id) + .join(LabManager, User.lab_manager_id == LabManager.id) + .join(RPIDepartments, LabManager.department_id == RPIDepartments.id) + ).first() + + if not data: + return {"error": "profile not found"}, 404 + + result = { + "name": data[0] + " " + data[2] if data[0] else data[1] + " " + data[2], + "image": data[3], + "department": data[4], + "description": data[5], + "website": data[6], + } -# return dictionary + return result @main_blueprint.post("/changeActiveStatus") @@ -291,12 +188,7 @@ def changeActiveStatus() -> dict[str, bool]: return {"activeStatus": opportunity} -# @main_blueprint.post("/create_post") -# def create_post(): -# return {"Hello": "There"} - - -@main_blueprint.route("/500") +@main_blueprint.get("/500") def force_error(): abort(500) @@ -314,77 +206,40 @@ def force_error(): # return result -# @main_blueprint.get("/departmentsList") -# def departments() -> list[Any]: - -# data = db.session.execute( -# db.select(RPIDepartments).order_by(RPIDepartments.name) -# ).scalars() - -# if not data: -# abort(404) - -# result = [department.to_dict() for department in data] - -# return result - - -# @main_blueprint.get("/majors") -# def majors() -> list[Any]: - -# if request.data: - -# json_request_data = request.get_json() +@main_blueprint.get("/majors") +def majors() -> list[dict[str, str]]: -# if not json_request_data: -# abort(400) + data = db.session.execute(db.select(Majors).order_by(Majors.code)).scalars() -# partial_key = json_request_data.get("input", None) - -# data = db.session.execute( -# db.select(Majors) -# .order_by(Majors.code) -# .where( -# (Majors.code.ilike(f"%{partial_key}%")) -# | (Majors.name.ilike(f"%{partial_key}%")) -# ) -# ).scalars() - -# if not data: -# abort(404) - -# result = [major.to_dict() for major in data] - -# return result - -# data = db.session.execute(db.select(Majors).order_by(Majors.code)).scalars() + if not data: + abort(404) -# if not data: -# abort(404) + result = [{"code": major.code, "name": major.name} for major in data] -# result = [major.to_dict() for major in data] + if result == []: + abort(404) -# return result + return result -# @main_blueprint.get("/years") -# def years() -> list[Any]: +@main_blueprint.get("/years") +def years() -> list[int]: -# data = db.session.execute( -# db.select(ClassYears) -# .order_by(ClassYears.class_year) -# .where(ClassYears.active == True) -# ).scalars() + data = db.session.execute( + db.select(ClassYears) + .order_by(ClassYears.class_year) + .where(ClassYears.active == True) + ).scalars() -# if not data: -# abort(404) + if not data: + abort(404) -# result = [year.class_year for year in data] + result = [year.class_year for year in data] -# if result == []: -# abort(404) + if result == []: + abort(404) -# return result + return result # @main_blueprint.get("/courses") diff --git a/labconnect/models.py b/labconnect/models.py index 28407e26..069e1bcd 100644 --- a/labconnect/models.py +++ b/labconnect/models.py @@ -1,4 +1,4 @@ -from sqlalchemy import Enum, Index, func, event +from sqlalchemy import Enum, Index, event, func from sqlalchemy.dialects.postgresql import TSVECTOR from labconnect import db @@ -19,7 +19,7 @@ class User(db.Model, CustomSerializerMixin): "phone_number", "website", "class_year", - "lab_manager_id", + "description", ) serialize_rules = () @@ -30,6 +30,8 @@ class User(db.Model, CustomSerializerMixin): preferred_name = db.Column(db.String(50), nullable=True, unique=False) phone_number = db.Column(db.String(15), nullable=True, unique=False) website = db.Column(db.String(512), nullable=True, unique=False) + description = db.Column(db.String(4096), nullable=True, unique=False) + profile_picture = db.Column(db.String(512), nullable=True, unique=False) class_year = db.Column( db.Integer, db.ForeignKey("class_years.class_year"), @@ -82,7 +84,7 @@ class LabManager(db.Model, CustomSerializerMixin): serialize_rules = () id = db.Column(db.Integer, primary_key=True, autoincrement=True) - department_id = db.Column(db.String(64), db.ForeignKey("rpi_departments.name")) + department_id = db.Column(db.String(4), db.ForeignKey("rpi_departments.id")) user = db.relationship("User", back_populates="lab_manager") department = db.relationship("RPIDepartments", back_populates="lab_managers") @@ -90,15 +92,6 @@ class LabManager(db.Model, CustomSerializerMixin): "Leads", back_populates="lab_manager", passive_deletes=True ) - def getUser(self): - return User.query.filter_by(lab_manager_id=self.id).all() - - def getName(self): - return self.user[0].first_name + " " + self.user[0].last_name - - def getEmail(self): - return self.user[0].email - # rpi_schools( name, description ), key: name class RPISchools(db.Model, CustomSerializerMixin): @@ -120,8 +113,11 @@ class RPIDepartments(db.Model, CustomSerializerMixin): serialize_only = ("name", "description", "school_id") serialize_rules = () - name = db.Column(db.String(64), primary_key=True) + id = db.Column(db.String(4), primary_key=True) + name = db.Column(db.String(64), nullable=False, unique=False) + image = db.Column(db.String(512), nullable=True, unique=False) description = db.Column(db.String(2000), nullable=True, unique=False) + website = db.Column(db.String(512), nullable=True, unique=False) school_id = db.Column(db.String(64), db.ForeignKey("rpi_schools.name")) school = db.relationship("RPISchools", back_populates="departments") @@ -197,7 +193,7 @@ class Opportunities(db.Model, CustomSerializerMixin): @event.listens_for(Opportunities, "before_insert") @event.listens_for(Opportunities, "before_update") -def update_search_vector(mapper, connection, target): +def update_search_vector(_unusedmapper, _unusedconnection, target): target.search_vector = func.to_tsvector( "english", target.name + " " + target.description ) @@ -261,7 +257,7 @@ class UserDepartments(db.Model, CustomSerializerMixin): user_id = db.Column(db.String(9), db.ForeignKey("user.id"), primary_key=True) department_id = db.Column( - db.String(64), db.ForeignKey("rpi_departments.name"), primary_key=True + db.String(4), db.ForeignKey("rpi_departments.id"), primary_key=True ) user = db.relationship("User", back_populates="departments") diff --git a/migrations/env.py b/migrations/env.py index 1a96fbfd..389c6094 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -1,9 +1,8 @@ import logging from logging.config import fileConfig -from flask import current_app - from alembic import context +from flask import current_app # this is the Alembic Config object, which provides # access to the values within the .ini file in use. @@ -12,8 +11,20 @@ # Interpret the config file for Python logging. # This line sets up loggers basically. fileConfig(config.config_file_name) + +# Create a logger for the Alembic environment logger = logging.getLogger("alembic.env") +# Ensure that we only log important information in production environments +if context.config.get_main_option("environment") == "production": + logger.setLevel(logging.WARNING) # Set to WARNING or ERROR for production +else: + logger.setLevel(logging.INFO) # Allow INFO in non-production environments + +# Avoid logging sensitive information like credentials or tokens +logger.propagate = False +logger.addHandler(logging.StreamHandler()) # Add a basic handler if none exists + def get_engine(): try: diff --git a/migrations/versions/4dd3611b273e_.py b/migrations/versions/4dd3611b273e_.py new file mode 100644 index 00000000..5059d2e7 --- /dev/null +++ b/migrations/versions/4dd3611b273e_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: 4dd3611b273e +Revises: 55928fddcb12 +Create Date: 2024-10-12 02:36:10.736719 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "4dd3611b273e" +down_revision = "55928fddcb12" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("user", schema=None) as batch_op: + batch_op.create_unique_constraint(None, ["id"]) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("user", schema=None) as batch_op: + batch_op.drop_constraint(None, type_="unique") + + # ### end Alembic commands ### diff --git a/migrations/versions/55928fddcb12_initial_migration.py b/migrations/versions/55928fddcb12_initial_migration.py index c0e4ea6d..6843086b 100644 --- a/migrations/versions/55928fddcb12_initial_migration.py +++ b/migrations/versions/55928fddcb12_initial_migration.py @@ -6,9 +6,8 @@ """ -from alembic import op import sqlalchemy as sa - +from alembic import op # revision identifiers, used by Alembic. revision = "55928fddcb12" diff --git a/migrations/versions/c72f39e93d72_.py b/migrations/versions/c72f39e93d72_.py new file mode 100644 index 00000000..907940cf --- /dev/null +++ b/migrations/versions/c72f39e93d72_.py @@ -0,0 +1,38 @@ +"""empty message + +Revision ID: c72f39e93d72 +Revises: 4dd3611b273e +Create Date: 2024-10-15 17:29:11.568986 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "c72f39e93d72" +down_revision = "4dd3611b273e" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("rpi_departments", schema=None) as batch_op: + batch_op.add_column(sa.Column("website", sa.String(length=2000), nullable=True)) + + with op.batch_alter_table("user", schema=None) as batch_op: + batch_op.create_unique_constraint(None, ["id"]) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("user", schema=None) as batch_op: + batch_op.drop_constraint(None, type_="unique") + + with op.batch_alter_table("rpi_departments", schema=None) as batch_op: + batch_op.drop_column("website") + + # ### end Alembic commands ### diff --git a/migrations/versions/f7518ca21f44_.py b/migrations/versions/f7518ca21f44_.py new file mode 100644 index 00000000..4edd950f --- /dev/null +++ b/migrations/versions/f7518ca21f44_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: f7518ca21f44 +Revises: c72f39e93d72 +Create Date: 2024-10-16 01:35:20.577378 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "f7518ca21f44" +down_revision = "c72f39e93d72" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("user", schema=None) as batch_op: + batch_op.create_unique_constraint(None, ["id"]) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("user", schema=None) as batch_op: + batch_op.drop_constraint(None, type_="unique") + + # ### end Alembic commands ### diff --git a/requirements.txt b/requirements.txt index a02a362a..38173e6f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ click==8.1.7 coverage==7.6.1 Flask==3.0.3 Flask-Cors==5.0.0 -Flask-JWT-Extended==4.6.0 +Flask-JWT-Extended==4.7.1 Flask-Migrate==4.0.7 Flask-SQLAlchemy==3.1.1 gunicorn==23.0.0 @@ -14,24 +14,23 @@ isodate==0.6.1 itsdangerous==2.2.0 Jinja2==3.1.4 lxml==5.3.0 -Mako==1.3.5 -MarkupSafe==2.1.5 -orjson==3.10.7 -packaging==24.1 +Mako==1.3.6 +MarkupSafe==3.0.2 +orjson==3.10.10 +packaging==24.2 pluggy==1.5.0 -psycopg2==2.9.9 +psycopg2==2.9.10 psycopg2-binary==2.9.9 -PyJWT==2.9.0 +PyJWT==2.10.1 pytest==8.3.3 -pytest-cov==5.0.0 +pytest-cov==6.0.0 python3-saml==1.16.0 pytz==2024.2 -sentry-sdk==2.15.0 -setuptools==70.3.0 +sentry-sdk==2.17.0 six==1.16.0 SQLAlchemy==2.0.29 sqlalchemy-serializer==1.4.22 typing_extensions==4.12.2 urllib3==2.2.3 -Werkzeug==3.0.4 +Werkzeug==3.0.6 xmlsec==1.3.14 diff --git a/tests/test_courses.py b/tests/test_courses.py index 9a29803e..cb2fd309 100644 --- a/tests/test_courses.py +++ b/tests/test_courses.py @@ -4,79 +4,87 @@ from flask import json from flask.testing import FlaskClient +import pytest -def test_courses_route_with_input_name(test_client: FlaskClient) -> None: +@pytest.mark.parametrize( + "request_json, expected_status, expected_response", + [ + ( + {"input": "data"}, + 200, + [{"code": "CSCI4390", "name": "Data Mining"}], + ), + ( + {"input": "cs"}, + 200, + [ + {"code": "CSCI2300", "name": "Introduction to Algorithms"}, + {"code": "CSCI2961", "name": "Rensselaer Center for Open Source"}, + {"code": "CSCI4390", "name": "Data Mining"}, + {"code": "CSCI4430", "name": "Programming Languages"}, + ], + ), + (None, 400, None), + ({"wrong": "wrong"}, 400, None), + ({"input": "not found"}, 404, None), + ], +) +def test_courses_route( + test_client: FlaskClient, request_json, expected_status, expected_response +) -> None: """ GIVEN a Flask application configured for testing - WHEN the '/courses' page is requested (GET) - THEN check that the response is valid + WHEN the '/courses' page is requested (GET) with various inputs + THEN check that the response status and data are as expected """ - response = test_client.get("/courses", json={"input": "data"}) - - assert response.status_code == 200 + response = ( + test_client.get("/courses", json=request_json) + if request_json + else test_client.get("/courses") + ) - json_data = json.loads(response.data) + assert response.status_code == expected_status - assert json_data[0]["code"] == "CSCI4390" - assert json_data[0]["name"] == "Data Mining" + if expected_response is not None: + json_data = json.loads(response.data) + if expected_status == 200: + assert json_data == expected_response + else: + assert json_data is not None -def test_courses_route_with_input_code(test_client: FlaskClient) -> None: +@pytest.mark.parametrize( + "input_name, course_data", + [ + ( + "cs", + ( + ("CSCI2300", "CSCI2961", "CSCI4390", "CSCI4430"), + ( + "Introduction to Algorithms", + "Rensselaer Center for Open Source", + "Data Mining", + "Programming Languages", + ), + ), + ) + ], +) +def test_courses_route_with_specific_input( + test_client: FlaskClient, input_name, course_data +) -> None: """ GIVEN a Flask application configured for testing - WHEN the '/courses' page is requested (GET) - THEN check that the response is valid + WHEN the '/courses' page is requested (GET) with specific course input names + THEN check that the response data matches the expected courses """ - response = test_client.get("/courses", json={"input": "cs"}) + response = test_client.get("/courses", json={"input": input_name}) assert response.status_code == 200 json_data = json.loads(response.data) - course_data = ( - ("CSCI2300", "CSCI2961", "CSCI4390", "CSCI4430"), - ( - "Introduction to Algorithms", - "Rensselaer Center for Open Source", - "Data Mining", - "Programming Languages", - ), - ) - - for i, major in enumerate(json_data): - assert major["code"] == course_data[0][i] - assert major["name"] == course_data[1][i] - - -def test_courses_route_no_json(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/courses' page is requested (GET) - THEN check that the response is valid - """ - response = test_client.get("/courses") - - assert response.status_code == 400 - - -def test_courses_route_incorrect_json(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/courses' page is requested (GET) - THEN check that the response is valid - """ - response = test_client.get("/courses", json={"wrong": "wrong"}) - - assert response.status_code == 400 - - -def test_courses_not_found(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/courses' page is requested (GET) - THEN check that the response is valid - """ - response = test_client.get("/courses", json={"input": "not found"}) - - assert response.status_code == 404 + for i, course in enumerate(json_data): + assert course["code"] == course_data[0][i] + assert course["name"] == course_data[1][i] diff --git a/tests/test_departments.py b/tests/test_departments.py index 445cd3c1..fbeb88a8 100644 --- a/tests/test_departments.py +++ b/tests/test_departments.py @@ -4,94 +4,150 @@ from flask import json from flask.testing import FlaskClient +import pytest -def test_departments_route(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/departments' page is requested (GET) - THEN check that the response is valid - """ - response = test_client.get("/departments") - - assert response.status_code == 200 - - json_data = json.loads(response.data) - - rpi_departments_data = ( +@pytest.mark.parametrize( + "endpoint, request_json, expected_status, expected_response_checks", + [ ( - "Computer Science", - "Biology", - "Materials Engineering", - "Math", - "Environmental Engineering", - "Aerospace Engineering", - "Areonautical Engineering", + "/departments", + None, + 200, + [ + { + "field": "name", + "values": [ + "Computer Science", + "Biology", + "Materials Engineering", + "Math", + "Environmental Engineering", + "Aerospace Engineering", + "Areonautical Engineering", + ], + }, + { + "field": "description", + "values": [ + "DS", + "life", + "also pretty cool", + "quick maths", + "water", + "space, the final frontier", + "flying, need for speed", + ], + }, + { + "field": "school_id", + "values": [ + "School of science", + "School of science", + "School of engineering", + "School of science", + "School of engineering", + "School of engineering", + "School of engineering", + ], + }, + { + "field": "id", + "values": ["CSCI", "BIOL", "MTLE", "MATH", "ENVI", "MANE", "MANE"], + }, + { + "field": "image", + "values": [ + "https://cdn-icons-png.flaticon.com/512/5310/5310672.png" + ] + * 7, + }, + {"field": "webcite", "values": ["https://www.rpi.edu"] * 7}, + ], ), ( - "DS", - "life", - "also pretty cool", - "quick maths", - "water", - "space, the final frontier", - "flying, need for speed", + "/department", + {"department": "Computer Science"}, + 200, + [ + {"field": "name", "values": ["Computer Science"]}, + {"field": "description", "values": ["DS"]}, + {"field": "school_id", "values": ["School of Science"]}, + {"field": "id", "values": ["CSCI"]}, + { + "field": "image", + "values": [ + "https://cdn-icons-png.flaticon.com/512/5310/5310672.png" + ], + }, + {"field": "webcite", "values": ["https://www.rpi.edu"]}, + { + "field": "professors", + "subfields": [ + { + "subfield": "name", + "values": [ + "Duy Le", + "Rafael", + "Turner", + "Kuzmin", + "Goldschmidt", + ], + }, + { + "subfield": "rcs_id", + "values": ["led", "cenzar", "turner", "kuzmin", "goldd"], + }, + ], + }, + { + "field": "opportunities", + "subfields": [ + {"subfield": "id", "values": [1, 2]}, + { + "subfield": "name", + "values": [ + "Automated Cooling System", + "Iphone 15 durability test", + ], + }, + ], + }, + ], ), - ) - - for department in json_data: - assert department["name"] in rpi_departments_data[0] - assert department["description"] in rpi_departments_data[1] - - -def test_department_route(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/department' page is requested (GET) - THEN check that the response is valid - """ - response = test_client.get("/department", json={"department": "Computer Science"}) - - assert response.status_code == 200 - - json_data = json.loads(response.data) - - assert json_data["name"] == "Computer Science" - assert json_data["description"] == "DS" - assert json_data["school_id"] == "School of Science" - - prof_names = ["Duy Le", "Rafael", "Turner", "Kuzmin", "Goldschmidt"] - prof_rcs_ids = ["led", "cenzar", "turner", "kuzmin", "goldd"] - - for prof in json_data["professors"]: - assert prof["name"] in prof_names - assert prof["rcs_id"] in prof_rcs_ids - - opportunity_ids = [1, 2] - opportunity_names = ["Automated Cooling System", "Iphone 15 durability test"] - - for opportunity in json_data["opportunities"]: - assert opportunity["id"] in opportunity_ids - assert opportunity["name"] in opportunity_names - - -def test_department_route_no_json(test_client: FlaskClient) -> None: + ("/department", None, 400, None), + ("/department", {"wrong": "wrong"}, 400, None), + ], +) +def test_department_routes( + test_client: FlaskClient, + endpoint, + request_json, + expected_status, + expected_response_checks, +) -> None: """ GIVEN a Flask application configured for testing - WHEN the '/department' page is requested (GET) - THEN check that the response is valid + WHEN various '/departments' or '/department' routes are requested (GET) + THEN check that the response status and data are as expected """ - response = test_client.get("/department") - - assert response.status_code == 400 - - -def test_department_route_incorrect_json(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/department' page is requested (GET) - THEN check that the response is valid - """ - response = test_client.get("/department", json={"wrong": "wrong"}) - - assert response.status_code == 400 + response = ( + test_client.get(endpoint, json=request_json) + if request_json + else test_client.get(endpoint) + ) + assert response.status_code == expected_status + + if expected_response_checks: + json_data = json.loads(response.data) + + for check in expected_response_checks: + if "subfields" not in check: + for item in json_data: + assert item[check["field"]] in check["values"] + else: + for item in json_data.get(check["field"], []): + for subfield_check in check["subfields"]: + assert ( + item[subfield_check["subfield"]] in subfield_check["values"] + ) diff --git a/tests/test_errors.py b/tests/test_errors.py index 388aa8b9..d09edc6e 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -4,27 +4,30 @@ from flask import json from flask.testing import FlaskClient +import pytest -def test_404_page(test_client: FlaskClient) -> None: +@pytest.mark.parametrize( + "route, expected_status, expected_response", + [ + ("/abcsd", 404, {"error": "404 not found"}), + ( + "/500", + 500, + { + "error": "500 server error. You can report issues here: https://github.com/RafaelCenzano/LabConnect/issues" + }, + ), + ], +) +def test_error_pages( + test_client: FlaskClient, route, expected_status, expected_response +) -> None: """ GIVEN a Flask application configured for testing - WHEN the '/abcsd' page is requested (GET) - THEN check that the response is the 404 page + WHEN the specified error route is requested (GET) + THEN check that the response status and data are as expected """ - response = test_client.get("/abcsd") - assert response.status_code == 404 - assert {"error": "404 not found"} == json.loads(response.data) - - -def test_500_page(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/professor/' page is requested (GET) - THEN check that the response is valid - """ - response = test_client.get("/500") - assert response.status_code == 500 - assert { - "error": "500 server error. You can report issues here: https://github.com/RafaelCenzano/LabConnect/issues" - } == json.loads(response.data) + response = test_client.get(route) + assert response.status_code == expected_status + assert json.loads(response.data) == expected_response diff --git a/tests/test_general.py b/tests/test_general.py index e472206a..71d19ee0 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -4,6 +4,7 @@ from flask import json from flask.testing import FlaskClient +import pytest def test_home_page(test_client: FlaskClient) -> None: @@ -15,7 +16,6 @@ def test_home_page(test_client: FlaskClient) -> None: response = test_client.get("/") assert response.status_code == 200 - assert {"Hello": "There"} == json.loads(response.data) @@ -26,9 +26,10 @@ def test_discover_route(test_client: FlaskClient) -> None: THEN check that the response is valid """ response = test_client.get("/discover") - # data = json.loads(response.data.decode("utf-8")) + assert response.status_code == 200 - # print(data) + # Uncomment and modify the following line with expected response data + # data = json.loads(response.data.decode("utf-8")) # assert data["data"][0] == { # "title": "Nelson", # "major": "CS", @@ -37,23 +38,51 @@ def test_discover_route(test_client: FlaskClient) -> None: # } -def test_profile_page(test_client: FlaskClient) -> None: +@pytest.mark.parametrize( + "input_id, expected_profile", + [ + ( + 1, + { + "id": "cenzar", + "first_name": "Rafael", + "opportunities": [...], # Replace with expected opportunities data + }, + ) + ], +) +def test_profile_page(test_client: FlaskClient, input_id, expected_profile) -> None: """ GIVEN a Flask application configured for testing WHEN the '/profile/' page is requested (GET) THEN check that the response is valid """ - response = test_client.get("/profile", json={"id": 1}) + response = test_client.get("/profile", json={"id": input_id}) assert response.status_code == 200 json_data = json.loads(response.data) - assert json_data["id"] == "cenzar" - assert json_data["first_name"] == "Rafael" + assert json_data["id"] == expected_profile["id"] + assert json_data["first_name"] == expected_profile["first_name"] assert json_data["opportunities"] != [] -def test_schools_route(test_client: FlaskClient) -> None: +@pytest.mark.parametrize( + "expected_schools", + [ + ( + ( + "School of Science", + "School of Engineering", + ), + ( + "the coolest of them all", + "also pretty cool", + ), + ) + ], +) +def test_schools_route(test_client: FlaskClient, expected_schools) -> None: """ GIVEN a Flask application configured for testing WHEN the '/schools' page is requested (GET) @@ -65,14 +94,9 @@ def test_schools_route(test_client: FlaskClient) -> None: json_data = json.loads(response.data) - rpi_schools_data = ( - ("School of Science", "School of Engineering"), - ("the coolest of them all", "also pretty cool"), - ) - for school in json_data: - assert school["name"] in rpi_schools_data[0] - assert school["description"] in rpi_schools_data[1] + assert school["name"] in expected_schools[0] + assert school["description"] in expected_schools[1] def test_years_route(test_client: FlaskClient) -> None: @@ -84,20 +108,22 @@ def test_years_route(test_client: FlaskClient) -> None: response = test_client.get("/years") assert response.status_code == 200 - assert [2024, 2025, 2026, 2027, 2028, 2029, 2030, 2031] == json.loads(response.data) def test_professor_profile(test_client: FlaskClient) -> None: - + """ + GIVEN a Flask application configured for testing + WHEN the '/getProfessorProfile/' page is requested (GET) + THEN check that the response is valid + """ response = test_client.get("/getProfessorProfile/1") + assert response.status_code == 200 # Load the response data as JSON data = json.loads(response.data) - print(data) - # Test that the "name" key exists assert data["first_name"] == "Rafael" assert data["last_name"] == "Cenzano" assert data["preferred_name"] == "Raf" diff --git a/tests/test_lab_manager.py b/tests/test_lab_manager.py index 0cea0a03..5d54c2cd 100644 --- a/tests/test_lab_manager.py +++ b/tests/test_lab_manager.py @@ -1,117 +1,100 @@ """ -Test lab manager routes +Test lab manager routes with parameterization """ +import pytest from flask import json from flask.testing import FlaskClient -def test_lab_manager_route_with_input_id(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/lab_manager' page is requested (GET) - THEN check that the response is valid - """ - response = test_client.get("/lab_manager", json={"rcs_id": "cenzar"}) - - assert response.status_code == 200 - - json_data = json.loads(response.data) - - cenzar_data = { - "website": None, - "rcs_id": "cenzar", - "name": "Rafael", - "alt_email": None, - "phone_number": None, - "email": None, - } - - assert json_data == cenzar_data - - -def test_lab_manager_route_no_json(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/lab_manager' page is requested (GET) - THEN check that the response is valid - """ - response = test_client.get("/lab_manager") - - assert response.status_code == 400 - - -def test_lab_manager_route_incorrect_json(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/lab_manager' page is requested (GET) - THEN check that the response is valid - """ - response = test_client.get("/lab_manager", json={"wrong": "wrong"}) - - assert response.status_code == 400 - - -def test_lab_manager_opportunity_cards(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/lab_manager/opportunities' page is requested (GET) - THEN check that the response is valid - """ - response = test_client.get("/lab_manager/opportunities", json={"rcs_id": "cenzar"}) - - assert response.status_code == 200 - - json_data = json.loads(response.data) - - lab_manager_opportunities_data = ( +@pytest.mark.parametrize( + "input_json, expected_status, expected_response", + [ ( - "Automated Cooling System", - "Energy efficient AC system", - "Thermodynamics", - 15.0, - "Spring", - 2024, - True, + {"rcs_id": "cenzar"}, + 200, + { + "website": None, + "rcs_id": "cenzar", + "name": "Rafael", + "alt_email": None, + "phone_number": None, + "email": None, + "description": None, + }, ), - ( - "Iphone 15 durability test", - "Scratching the Iphone, drop testing etc.", - "Experienced in getting angry and throwing temper tantrum", - None, - "Spring", - 2024, - True, - ), - ) - - for i, item in enumerate(json_data["cenzar"]): - assert item["name"] == lab_manager_opportunities_data[i][0] - assert item["description"] == lab_manager_opportunities_data[i][1] - assert item["recommended_experience"] == lab_manager_opportunities_data[i][2] - assert item["pay"] == lab_manager_opportunities_data[i][3] - assert item["semester"] == lab_manager_opportunities_data[i][4] - assert item["year"] == lab_manager_opportunities_data[i][5] - assert item["active"] == lab_manager_opportunities_data[i][6] - - -def test_lab_manager_opportunity_cards_no_json(test_client: FlaskClient) -> None: + (None, 400, None), # No input JSON case + ({"wrong": "wrong"}, 400, None), # Incorrect JSON structure case + ], +) +def test_lab_manager_route( + test_client: FlaskClient, input_json, expected_status, expected_response +) -> None: """ GIVEN a Flask application configured for testing - WHEN the '/lab_manager/opportunities' page is requested (GET) - THEN check that the response is valid + WHEN the '/lab_manager' page is requested (GET) with different JSON inputs + THEN check that the response matches the expected outcome """ - response = test_client.get("/lab_manager/opportunities") - - assert response.status_code == 400 - - -def test_lab_manager_opportunity_cards_incorrect_json(test_client: FlaskClient) -> None: + response = test_client.get("/lab_manager", json=input_json) + assert response.status_code == expected_status + + if expected_response: + json_data = json.loads(response.data) + assert json_data == expected_response + + +@pytest.mark.parametrize( + "input_json, expected_status", + [ + ({"rcs_id": "cenzar"}, 200), + (None, 400), # No input JSON case + ({"wrong": "wrong"}, 400), # Incorrect JSON structure case + ], +) +def test_lab_manager_opportunity_cards( + test_client: FlaskClient, input_json, expected_status +) -> None: """ GIVEN a Flask application configured for testing - WHEN the '/lab_manager/opportunities' page is requested (GET) - THEN check that the response is valid + WHEN the '/lab_manager/opportunities' page is requested (GET) with different JSON inputs + THEN check that the response matches the expected status code """ - response = test_client.get("/lab_manager/opportunities", json={"wrong": "wrong"}) - - assert response.status_code == 400 + response = test_client.get("/lab_manager/opportunities", json=input_json) + assert response.status_code == expected_status + + if input_json == {"rcs_id": "cenzar"} and expected_status == 200: + json_data = json.loads(response.data) + lab_manager_opportunities_data = [ + { + "name": "Automated Cooling System", + "description": "Energy efficient AC system", + "recommended_experience": "Thermodynamics", + "pay": 15.0, + "semester": "Spring", + "year": 2024, + "active": True, + }, + { + "name": "Iphone 15 durability test", + "description": "Scratching the Iphone, drop testing etc.", + "recommended_experience": "Experienced in getting angry and throwing temper tantrum", + "pay": None, + "semester": "Spring", + "year": 2024, + "active": True, + }, + ] + + for i, item in enumerate(json_data["cenzar"]): + assert item["name"] == lab_manager_opportunities_data[i]["name"] + assert ( + item["description"] == lab_manager_opportunities_data[i]["description"] + ) + assert ( + item["recommended_experience"] + == lab_manager_opportunities_data[i]["recommended_experience"] + ) + assert item["pay"] == lab_manager_opportunities_data[i]["pay"] + assert item["semester"] == lab_manager_opportunities_data[i]["semester"] + assert item["year"] == lab_manager_opportunities_data[i]["year"] + assert item["active"] == lab_manager_opportunities_data[i]["active"] diff --git a/tests/test_majors.py b/tests/test_majors.py index d7594743..d7d6df87 100644 --- a/tests/test_majors.py +++ b/tests/test_majors.py @@ -4,9 +4,25 @@ from flask import json from flask.testing import FlaskClient +import pytest -def test_majors_route(test_client: FlaskClient) -> None: +@pytest.mark.parametrize( + "expected_majors", + [ + ( + ("CSCI", "ECSE", "BIOL", "MATH", "COGS"), + ( + "Computer Science", + "Electrical, Computer, and Systems Engineering", + "Biological Science", + "Mathematics", + "Cognitive Science", + ), + ) + ], +) +def test_majors_route(test_client: FlaskClient, expected_majors) -> None: """ GIVEN a Flask application configured for testing WHEN the '/majors' page is requested (GET) @@ -18,68 +34,75 @@ def test_majors_route(test_client: FlaskClient) -> None: json_data = json.loads(response.data) - majors_data = ( - ("CSCI", "ECSE", "BIOL", "MATH", "COGS"), - ( - "Computer Science", - "Electrical, Computer, and Systems Engineering", - "Biological Science", - "Mathematics", - "Cognitive Science", - ), - ) - for major in json_data: - assert major["code"] in majors_data[0] - assert major["name"] in majors_data[1] + assert major["code"] in expected_majors[0] + assert major["name"] in expected_majors[1] -def test_majors_route_with_input_name(test_client: FlaskClient) -> None: +@pytest.mark.parametrize( + "input_data, expected_majors", + [ + ( + {"input": "computer"}, + ( + ("CSCI", "ECSE"), + ( + "Computer Science", + "Electrical, Computer, and Systems Engineering", + ), + ), + ), + ], +) +def test_majors_route_with_input_name( + test_client: FlaskClient, input_data, expected_majors +) -> None: """ GIVEN a Flask application configured for testing WHEN the '/majors' page is requested (GET) THEN check that the response is valid """ - response = test_client.get("/majors", json={"input": "computer"}) + response = test_client.get("/majors", json=input_data) assert response.status_code == 200 json_data = json.loads(response.data) - majors_data = ( - ("CSCI", "ECSE"), - ( - "Computer Science", - "Electrical, Computer, and Systems Engineering", - ), - ) - for i, major in enumerate(json_data): - assert major["code"] == majors_data[0][i] - assert major["name"] == majors_data[1][i] + assert major["code"] == expected_majors[0][i] + assert major["name"] == expected_majors[1][i] -def test_majors_route_with_input_code(test_client: FlaskClient) -> None: +@pytest.mark.parametrize( + "input_data, expected_majors", + [ + ( + {"input": "cs"}, + ( + ("CSCI", "ECSE", "MATH"), + ( + "Computer Science", + "Electrical, Computer, and Systems Engineering", + "Mathematics", + ), + ), + ), + ], +) +def test_majors_route_with_input_code( + test_client: FlaskClient, input_data, expected_majors +) -> None: """ GIVEN a Flask application configured for testing WHEN the '/majors' page is requested (GET) THEN check that the response is valid """ - response = test_client.get("/majors", json={"input": "cs"}) + response = test_client.get("/majors", json=input_data) assert response.status_code == 200 json_data = json.loads(response.data) - majors_data = ( - ("CSCI", "ECSE", "MATH"), - ( - "Computer Science", - "Electrical, Computer, and Systems Engineering", - "Mathematics", - ), - ) - for i, major in enumerate(json_data): - assert major["code"] == majors_data[0][i] - assert major["name"] == majors_data[1][i] + assert major["code"] == expected_majors[0][i] + assert major["name"] == expected_majors[1][i] diff --git a/tests/test_opportunities_filtering.py b/tests/test_opportunities_filtering.py index 9297cf75..1c067014 100644 --- a/tests/test_opportunities_filtering.py +++ b/tests/test_opportunities_filtering.py @@ -2,284 +2,104 @@ Test opportunity filtering routes """ +import pytest from flask import json from flask.testing import FlaskClient -def test_opportunity_filter_pay(test_client: FlaskClient) -> None: +@pytest.mark.parametrize( + "filters, expected_opportunities", + [ + ( + [{"field": "pay", "value": {"min": 14.9, "max": 21}}], + ["Automated Cooling System"], + ), + ( + [{"field": "departments", "value": ["Material Science"]}], + ["Checking out cubes"], + ), + ( + [ + { + "field": "departments", + "value": ["Computer Science", "Material Science"], + } + ], + [ + "Iphone 15 durability test", + "Checking out cubes", + "Automated Cooling System", + ], + ), + ( + [{"field": "majors", "value": ["BIOL"]}], + [ + "Iphone 15 durability test", + "Checking out cubes", + "Automated Cooling System", + ], + ), + ( + [{"field": "majors", "value": ["CSCI", "BIOL"]}], + [ + "Iphone 15 durability test", + "Checking out cubes", + "Automated Cooling System", + ], + ), + ( + [{"field": "credits", "value": [1]}], + ["Iphone 15 durability test", "Checking out cubes"], + ), + ( + [{"field": "credits", "value": [2, 4]}], + [ + "Iphone 15 durability test", + "Checking out cubes", + "Automated Cooling System", + "Test the water", + ], + ), + ([{"field": "class_year", "value": [2025]}], ["Iphone 15 durability test"]), + ( + [{"field": "class_year", "value": [2025, 2027]}], + ["Iphone 15 durability test", "Automated Cooling System"], + ), + ([{"field": "location", "value": "Remote"}], ["Automated Cooling System"]), + ( + [{"field": "location", "value": "In-Person"}], + ["Iphone 15 durability test", "Checking out cubes", "Test the water"], + ), + ( + [ + {"field": "location", "value": "In-Person"}, + {"field": "departments", "value": ["Computer Science"]}, + ], + ["Iphone 15 durability test"], + ), + ( + [ + {"field": "credits", "value": [2, 4]}, + {"field": "departments", "value": ["Computer Science"]}, + ], + ["Iphone 15 durability test", "Automated Cooling System"], + ), + ], +) +def test_opportunity_filter( + test_client: FlaskClient, filters, expected_opportunities +) -> None: """ GIVEN a Flask application configured for testing WHEN the '/opportunity/filter' page is requested (GET) THEN check that the response is valid """ - - json_data = {"filters": [{"field": "pay", "value": {"min": 14.9, "max": 21}}]} - response = test_client.get("/opportunity/filter", json=json_data) - - assert response.status_code == 200 - - json_data = json.loads(response.data) - - assert "Automated Cooling System" == json_data[0]["name"] - - -def test_opportunity_filter_department(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/opportunity/filter' page is requested (GET) - THEN check that the response is valid - """ - - json_data = {"filters": [{"field": "departments", "value": ["Material Science"]}]} - response = test_client.get("/opportunity/filter", json=json_data) - - assert response.status_code == 200 - - json_data = json.loads(response.data) - - assert "Checking out cubes" == json_data[0]["name"] - - -def test_opportunity_filter_departments(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/opportunity/filter' page is requested (GET) - THEN check that the response is valid - """ - - json_data = { - "filters": [ - {"field": "departments", "value": ["Computer Science", "Material Science"]} - ] - } - response = test_client.get("/opportunity/filter", json=json_data) - - assert response.status_code == 200 - - json_data = json.loads(response.data) - - opportunities = ( - "Iphone 15 durability test", - "Checking out cubes", - "Automated Cooling System", - ) - - for data in json_data: - assert data["name"] in opportunities - - -def test_opportunity_filter_major(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/opportunity/filter' page is requested (GET) - THEN check that the response is valid - """ - - json_data = {"filters": [{"field": "majors", "value": ["BIOL"]}]} - response = test_client.get("/opportunity/filter", json=json_data) - - assert response.status_code == 200 - - json_data = json.loads(response.data) - - opportunities = ( - "Iphone 15 durability test", - "Checking out cubes", - "Automated Cooling System", - ) - - for data in json_data: - assert data["name"] in opportunities - - -def test_opportunity_filter_majors(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/opportunity/filter' page is requested (GET) - THEN check that the response is valid - """ - - json_data = {"filters": [{"field": "majors", "value": ["CSCI", "BIOL"]}]} - response = test_client.get("/opportunity/filter", json=json_data) - - assert response.status_code == 200 - - json_data = json.loads(response.data) - - opportunities = ( - "Iphone 15 durability test", - "Checking out cubes", - "Automated Cooling System", - ) - - for data in json_data: - assert data["name"] in opportunities - - -def test_opportunity_filter_credits(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/opportunity/filter' page is requested (GET) - THEN check that the response is valid - """ - - json_data = {"filters": [{"field": "credits", "value": [1]}]} - response = test_client.get("/opportunity/filter", json=json_data) - - assert response.status_code == 200 - - json_data = json.loads(response.data) - - opportunities = ( - "Iphone 15 durability test", - "Checking out cubes", - ) - - for data in json_data: - assert data["name"] in opportunities - - json_data = {"filters": [{"field": "credits", "value": [2, 4]}]} - response = test_client.get("/opportunity/filter", json=json_data) - - assert response.status_code == 200 - - json_data = json.loads(response.data) - - opportunities = ( - "Iphone 15 durability test", - "Checking out cubes", - "Automated Cooling System", - "Test the water", - ) - - for data in json_data: - assert data["name"] in opportunities - - -def test_opportunity_filter_class_years(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/opportunity/filter' page is requested (GET) - THEN check that the response is valid - """ - - json_data = {"filters": [{"field": "class_year", "value": [2025]}]} - response = test_client.get("/opportunity/filter", json=json_data) - - assert response.status_code == 200 - - json_data = json.loads(response.data) - - opportunities = ("Iphone 15 durability test",) - - for data in json_data: - assert data["name"] in opportunities - - json_data = {"filters": [{"field": "class_year", "value": [2025, 2027]}]} - response = test_client.get("/opportunity/filter", json=json_data) - - assert response.status_code == 200 - - json_data = json.loads(response.data) - - opportunities = ( - "Iphone 15 durability test", - "Automated Cooling System", - ) - - for data in json_data: - assert data["name"] in opportunities - - -def test_opportunity_filter_location_remote(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/opportunity/filter' page is requested (GET) - THEN check that the response is valid - """ - - json_data = {"filters": [{"field": "location", "value": "Remote"}]} - response = test_client.get("/opportunity/filter", json=json_data) - - assert response.status_code == 200 - - json_data = json.loads(response.data) - - assert "Automated Cooling System" == json_data[0]["name"] - - -def test_opportunity_filter_location_in_person(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/opportunity/filter' page is requested (GET) - THEN check that the response is valid - """ - - json_data = {"filters": [{"field": "location", "value": "In-Person"}]} - response = test_client.get("/opportunity/filter", json=json_data) - - assert response.status_code == 200 - - json_data = json.loads(response.data) - - opportunities = ( - "Iphone 15 durability test", - "Checking out cubes", - "Test the water", - ) - - for data in json_data: - assert data["name"] in opportunities - - -def test_opportunity_filter_location_departments(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/opportunity/filter' page is requested (GET) - THEN check that the response is valid - """ - - json_data = { - "filters": [ - {"field": "location", "value": "In-Person"}, - {"field": "departments", "value": ["Computer Science"]}, - ] - } - response = test_client.get("/opportunity/filter", json=json_data) - - assert response.status_code == 200 - - json_data = json.loads(response.data) - - opportunities = ("Iphone 15 durability test",) - - for data in json_data: - assert data["name"] in opportunities - - -def test_opportunity_filter_credits_departments(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/opportunity/filter' page is requested (GET) - THEN check that the response is valid - """ - - json_data = { - "filters": [ - {"field": "credits", "value": [2, 4]}, - {"field": "departments", "value": ["Computer Science"]}, - ] - } + json_data = {"filters": filters} response = test_client.get("/opportunity/filter", json=json_data) assert response.status_code == 200 json_data = json.loads(response.data) - opportunities = ("Iphone 15 durability test", "Automated Cooling System") - for data in json_data: - assert data["name"] in opportunities - - -# TODO: Add test for no fields + assert data["name"] in expected_opportunities diff --git a/tests/test_opportunity.py b/tests/test_opportunity.py index 55f7c24b..bd0e9da4 100644 --- a/tests/test_opportunity.py +++ b/tests/test_opportunity.py @@ -1,304 +1,155 @@ -""" -Test opportunity routes -""" - import json -from flask import json +import pytest from flask.testing import FlaskClient -from labconnect import db -from labconnect.helpers import OrJSONProvider, SemesterEnum -from labconnect.models import ( - ClassYears, - Courses, - LabManager, - Leads, - Majors, - Opportunities, - RecommendsClassYears, - RecommendsCourses, - RecommendsMajors, - RPIDepartments, - RPISchools, -) - -def test_get_opportunity(test_client: FlaskClient) -> None: +def test_get_opportunity_parametrized(test_client: FlaskClient): """ GIVEN a Flask application configured for testing - WHEN the '/opportunity' page is requested (GET) - THEN check that the response is valid + WHEN the '/opportunity' page is requested (GET) with different IDs + THEN check that the responses are valid """ - response1 = test_client.get("/opportunity", json={"id": 1}) - response2 = test_client.get("/opportunity", json={"id": 2}) - - assert response1.status_code == 200 - assert response2.status_code == 200 - - json_data1 = json.loads(response1.data) - json_data2 = json.loads(response2.data) - - lab_manager_opportunities_data = ( + test_cases = [ ( - "Automated Cooling System", - "Energy efficient AC system", - "Thermodynamics", - 15.0, - False, - False, - False, - True, - "Spring", - 2024, - True, + 1, + { + "name": "Automated Cooling System", + "description": "Energy efficient AC system", + "recommended_experience": "Thermodynamics", + "pay": 15.0, + "one_credit": False, + "two_credits": False, + "three_credits": False, + "four_credits": True, + "semester": "Spring", + "year": 2024, + "active": True, + }, ), ( - "Iphone 15 durability test", - "Scratching the Iphone, drop testing etc.", - "Experienced in getting angry and throwing temper tantrum", - None, - True, - True, - True, - True, - "Spring", - 2024, - True, + 2, + { + "name": "Iphone 15 durability test", + "description": "Scratching the Iphone, drop testing etc.", + "recommended_experience": "Experienced in getting angry and throwing temper tantrum", + "pay": None, + "one_credit": True, + "two_credits": True, + "three_credits": True, + "four_credits": True, + "semester": "Spring", + "year": 2024, + "active": True, + }, ), - ) + ] - assert json_data1["name"] == lab_manager_opportunities_data[0][0] - assert json_data1["description"] == lab_manager_opportunities_data[0][1] - assert json_data1["recommended_experience"] == lab_manager_opportunities_data[0][2] - assert json_data1["pay"] == lab_manager_opportunities_data[0][3] - assert json_data1["one_credit"] == lab_manager_opportunities_data[0][4] - assert json_data1["two_credits"] == lab_manager_opportunities_data[0][5] - assert json_data1["three_credits"] == lab_manager_opportunities_data[0][6] - assert json_data1["four_credits"] == lab_manager_opportunities_data[0][7] - assert json_data1["semester"] == lab_manager_opportunities_data[0][8] - assert json_data1["year"] == lab_manager_opportunities_data[0][9] - assert json_data1["active"] == lab_manager_opportunities_data[0][10] + for opportunity_id, expected_data in test_cases: + response = test_client.get("/opportunity", json={"id": opportunity_id}) + assert response.status_code == 200 - assert json_data2["name"] == lab_manager_opportunities_data[1][0] - assert json_data2["description"] == lab_manager_opportunities_data[1][1] - assert json_data2["recommended_experience"] == lab_manager_opportunities_data[1][2] - assert json_data2["pay"] == lab_manager_opportunities_data[1][3] - assert json_data2["one_credit"] == lab_manager_opportunities_data[1][4] - assert json_data2["two_credits"] == lab_manager_opportunities_data[1][5] - assert json_data2["three_credits"] == lab_manager_opportunities_data[1][6] - assert json_data2["four_credits"] == lab_manager_opportunities_data[1][7] - assert json_data2["semester"] == lab_manager_opportunities_data[1][8] - assert json_data2["year"] == lab_manager_opportunities_data[1][9] - assert json_data2["active"] == lab_manager_opportunities_data[1][10] + json_data = json.loads(response.data) + for key, value in expected_data.items(): + assert json_data[key] == value - print(json_data2) - -def test_get_opportunity_no_json(test_client: FlaskClient) -> None: +def test_get_opportunity_no_json(test_client: FlaskClient): """ GIVEN a Flask application configured for testing - WHEN the '/opportunity' page is requested (GET) - THEN check that the response is valid + WHEN the '/opportunity' page is requested (GET) without JSON payload + THEN check that the response is 400 """ response = test_client.get("/opportunity") - assert response.status_code == 400 -def test_opportunity_incorrect_json(test_client: FlaskClient) -> None: +def test_opportunity_incorrect_json(test_client: FlaskClient): """ GIVEN a Flask application configured for testing - WHEN the '/opportunity' page is requested (GET) - THEN check that the response is valid + WHEN the '/opportunity' page is requested (GET) with incorrect JSON + THEN check that the response is 400 """ response = test_client.get("/opportunity", json={"wrong": "wrong"}) - assert response.status_code == 400 -def test_get_opportunity_meta(test_client: FlaskClient) -> None: +@pytest.mark.parametrize( + "endpoint, expected_keys", + [ + ( + "/getOpportunityMeta/1", + [ + "name", + "description", + "recommended_experience", + "pay", + "credits", + "semester", + "year", + "application_due", + "active", + "courses", + "majors", + "years", + ], + ), + ( + "/getOpportunity/2", + [ + "id", + "name", + "description", + "recommended_experience", + "author", + "department", + "aboutSection", + ], + ), + ], +) +def test_opportunity_meta_parametrized( + test_client: FlaskClient, endpoint, expected_keys +): """ GIVEN a Flask application configured for testing - WHEN the '/getOpportunityMeta' endpoint is requested (GET) with valid data - THEN check that the response is valid and contains expected opportunity data + WHEN specific opportunity endpoints are requested + THEN check that the response contains the expected keys """ - - response = test_client.get("/getOpportunityMeta/1", content_type="application/json") - - # assert response.status_code == 200 - - data = json.loads(response.data) - data = data["data"] - - # Assertions on the expected data - assert "name" in data - assert "description" in data - assert "recommended_experience" in data - assert "pay" in data - assert "credits" in data - assert "semester" in data - assert "year" in data - assert "application_due" in data - assert "active" in data - assert "courses" in data - assert "majors" in data - assert "years" in data - assert "active" in data - - -def test_get_opportunity(test_client: FlaskClient) -> None: - response = test_client.get("/getOpportunity/2") - - assert response.status_code == 200 - - # Load the response data as JSON - data = json.loads(response.data.decode("utf-8")) - data = data["data"] - - # Test that the "name" key exists - assert "id" in data - assert "name" in data - assert "description" in data - assert "recommended_experience" in data - assert "author" in data - assert "department" in data - assert "aboutSection" in data - - for eachSection in data["aboutSection"]: - assert "title" in eachSection - assert "description" in eachSection - - -def test_get_opportunity_professor(test_client: FlaskClient) -> None: - response = test_client.get("/getOpportunityByProfessor/led") - - assert response.status_code == 200 - - # Load the response data as JSON - data = json.loads(response.data.decode("utf-8")) - data = data["data"] - - # Test that the "name" key exists - for opportunity in data: - assert "id" in opportunity - assert "name" in opportunity - assert "description" in opportunity - assert "recommended_experience" in opportunity - assert "pay" in opportunity - # assert "credits" in opportunity - assert "semester" in opportunity - assert "year" in opportunity - assert "application_due" in opportunity - assert "active" in opportunity - # assert "professor" in opportunity - # assert "department" in opportunity - - -def test_get_professor_opportunity_cards(test_client: FlaskClient) -> None: - response = test_client.get( - "/getProfessorOpportunityCards/led", content_type="application/json" - ) - + response = test_client.get(endpoint, content_type="application/json") assert response.status_code == 200 - data = json.loads(response.data.decode("utf-8")) - data = data["data"] - - for eachCard in data: - assert "title" in eachCard - assert "body" in eachCard - assert "attributes" in eachCard - assert "id" in eachCard - - -def test_profile_opportunities(test_client: FlaskClient) -> None: - response = test_client.get( - "/getProfileOpportunities/led", content_type="application/json" - ) - - assert response.status_code == 200 - - data = json.loads(response.data.decode("utf-8")) - data = data["data"] - - for eachCard in data: - assert "id" in eachCard - assert "title" in eachCard - assert "body" in eachCard - assert "attributes" in eachCard - assert "activeStatus" in eachCard - - -def test_create_opportunity(test_client: FlaskClient) -> None: + data = json.loads(response.data) + if "data" in data: + data = data["data"] + + for key in expected_keys: + if isinstance(data, list): + for item in data: + assert key in item + else: + assert key in data + + +@pytest.mark.parametrize( + "endpoint", + [ + "/getOpportunityByProfessor/led", + "/getProfessorOpportunityCards/led", + "/getProfileOpportunities/led", + ], +) +def test_professor_related_opportunities(test_client: FlaskClient, endpoint): """ GIVEN a Flask application configured for testing - WHEN the '/createOpportunity' endpoint is requested (POST) with valid data - THEN check that the response is valid and contains expected data + WHEN professor-related endpoints are requested + THEN check that the response contains expected keys in each card """ - - test_data = { - "authorID": "led", - "name": "Some test opportunity", - "description": "Some test description", - "recommended_experience": "Some test experience", - "pay": 25.0, - "credits": ["1", "2", "3", "4"], - "semester": "FALL", - "year": 2024, - "application_due": "2024-03-30", - "active": True, - "courses": ["CSCI4430"], - "majors": ["BIOL"], - "years": [2023, 2024], - "active": True, - "location": "TBD", - } - - response = test_client.post( - "/createOpportunity", - data=json.dumps(test_data), - content_type="application/json", - ) - + response = test_client.get(endpoint, content_type="application/json") assert response.status_code == 200 - # query database to check for new opportunity with the same name - query = db.session.query(Opportunities).filter( - Opportunities.name == "Some test opportunity", - Opportunities.description == "Some test description", - Opportunities.recommended_experience == "Some test experience", - ) - - data = query.first() - assert data is not None - id = data.id - - # delete the opportunity by sending request to deleteOpportunity - response = test_client.post( - "/deleteOpportunity", - data=json.dumps({"id": id}), - content_type="application/json", - ) - - assert response.status_code == 200 - - # check that the opportunity was deleted - query = db.session.query(Opportunities).filter(Opportunities.id == id) - assert query.first() is None - - -def test_professor_opportunity_cards(test_client: FlaskClient) -> None: - - response = test_client.get("/getProfessorOpportunityCards/led") - assert response.status_code == 200 - - # Load the response data as JSON - data = json.loads(response.data.decode("utf-8")) - - # Test that the "name" key exists - assert len(data.keys()) > 0 - for eachCard in data["data"]: - assert "title" in eachCard - assert "body" in eachCard - assert "attributes" in eachCard - assert "id" in eachCard + data = json.loads(response.data)["data"] + for each_card in data: + assert "id" in each_card + assert "title" in each_card or "name" in each_card + assert "body" in each_card or "description" in each_card + assert "attributes" in each_card or "recommended_experience" in each_card diff --git a/tests/test_user.py b/tests/test_user.py index ab075777..2b6744e1 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -2,199 +2,157 @@ Test user routes """ +import pytest from flask import json from flask.testing import FlaskClient -def test_user_route_with_input_id_1(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/user' page is requested (GET) - THEN check that the response is valid - """ - response = test_client.get("/user", json={"id": "1"}) - - assert response.status_code == 200 - - json_data = json.loads(response.data) - - assert json_data["id"] == 1 - assert json_data["first_name"] == "Rafael" - assert json_data["last_name"] == "Cenzano" - assert json_data["preferred_name"] == "Raf" - assert json_data["email"] == "cenzar@rpi.edu" - - departments_data = [ - {"user_id": 1, "department_id": "Computer Science"}, - {"user_id": 1, "department_id": "Math"}, - ] - - major_data = [ - {"user_id": 1, "major_code": "CSCI"}, - {"user_id": 1, "major_code": "MATH"}, - ] - - course_data = [ - {"in_progress": False, "user_id": 1, "course_code": "CSCI2300"}, - {"in_progress": True, "user_id": 1, "course_code": "CSCI4430"}, - ] - - assert json_data["departments"] == departments_data - assert json_data["majors"] == major_data - assert json_data["courses"] == course_data - - -def test_user_1_opportunity_cards(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/user' page is requested (GET) - THEN check that the response is valid - """ - response = test_client.get("/user", json={"id": 1}) - - assert response.status_code == 200 - - json_data = json.loads(response.data) - - lab_manager_opportunities_data = ( +@pytest.mark.parametrize( + "input_data, expected_status, expected_output", + [ ( - "Automated Cooling System", - "Energy efficient AC system", - "Thermodynamics", - 15.0, - "Spring", - 2024, - True, + {"id": "1"}, + 200, + { + "id": 1, + "first_name": "Rafael", + "preferred_name": "Raf", + "last_name": "Cenzano", + "email": "cenzar@rpi.edu", + "description": "labconnect is the best RCOS project", + "profile_picture": "https://rafael.sirv.com/Images/rafael.jpeg?thumbnail=350&format=webp&q=90", + "website": "https://rafaelcenzano.com", + "class_year": "2025", + "lab_manager_id": 1, + "departments": [ + {"user_id": 1, "department_id": "Computer Science"}, + {"user_id": 1, "department_id": "Math"}, + ], + "majors": [ + {"user_id": 1, "major_code": "CSCI"}, + {"user_id": 1, "major_code": "MATH"}, + ], + "courses": [ + {"in_progress": False, "user_id": 1, "course_code": "CSCI2300"}, + {"in_progress": True, "user_id": 1, "course_code": "CSCI4430"}, + ], + }, ), ( - "Iphone 15 durability test", - "Scratching the Iphone, drop testing etc.", - "Experienced in getting angry and throwing temper tantrum", - None, - "Spring", - 2024, - True, + {"id": "2"}, + 200, + { + "id": 2, + "first_name": "RCOS", + "preferred_name": None, + "last_name": "RCOS", + "email": "test@rpi.edu", + "description": None, + "profile_picture": "https://www.svgrepo.com/show/206842/professor.svg", + "website": None, + "class_year": None, + "lab_manager_id": None, + "departments": [{"user_id": 2, "department_id": "Computer Science"}], + "majors": [{"user_id": 2, "major_code": "CSCI"}], + "courses": [ + {"in_progress": False, "user_id": 2, "course_code": "CSCI2300"} + ], + }, ), - ) - - for i, item in enumerate(json_data["opportunities"]): - assert item["name"] == lab_manager_opportunities_data[i][0] - assert item["description"] == lab_manager_opportunities_data[i][1] - assert item["recommended_experience"] == lab_manager_opportunities_data[i][2] - assert item["pay"] == lab_manager_opportunities_data[i][3] - assert item["semester"] == lab_manager_opportunities_data[i][4] - assert item["year"] == lab_manager_opportunities_data[i][5] - assert item["active"] == lab_manager_opportunities_data[i][6] - - -def test_user_route_with_input_id_2(test_client: FlaskClient) -> None: + ], +) +def test_user_route( + test_client: FlaskClient, input_data, expected_status, expected_output +) -> None: """ GIVEN a Flask application configured for testing - WHEN the '/user' page is requested (GET) - THEN check that the response is valid + WHEN the '/user' page is requested (GET) with input data + THEN check that the response is valid and matches expected output """ - response = test_client.get("/user", json={"id": 2}) - - assert response.status_code == 200 - + response = test_client.get("/user", json=input_data) + assert response.status_code == expected_status json_data = json.loads(response.data) + assert json_data == expected_output - assert json_data["id"] == 2 - assert json_data["first_name"] == "RCOS" - assert json_data["last_name"] == "RCOS" - assert json_data["preferred_name"] is None - assert json_data["email"] == "test@rpi.edu" - departments_data = [ - {"department_id": "Computer Science", "user_id": 2}, - ] - - major_data = [ - {"user_id": 2, "major_code": "CSCI"}, - ] - - course_data = [ - {"in_progress": False, "user_id": 2, "course_code": "CSCI2300"}, - ] - - assert json_data["departments"] == departments_data - assert json_data["majors"] == major_data - assert json_data["courses"] == course_data - - -def test_user_2_opportunity_cards(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/user' page is requested (GET) - THEN check that the response is valid - """ - response = test_client.get("/user", json={"id": 2}) - - assert response.status_code == 200 - - json_data = json.loads(response.data) - - lab_manager_opportunities_data = ( +@pytest.mark.parametrize( + "input_data, expected_opportunities", + [ ( - "Checking out cubes", - "Material Sciences", - "Experienced in materials.", - None, - "Fall", - 2024, - True, + {"id": 1}, + [ + { + "name": "Automated Cooling System", + "description": "Energy efficient AC system", + "recommended_experience": "Thermodynamics", + "pay": 15.0, + "semester": "Spring", + "year": 2024, + "active": True, + }, + { + "name": "Iphone 15 durability test", + "description": "Scratching the Iphone, drop testing etc.", + "recommended_experience": "Experienced in getting angry and throwing temper tantrum", + "pay": None, + "semester": "Spring", + "year": 2024, + "active": True, + }, + ], ), ( - "Test the water", - "Testing the quality of water in Troy pipes", - "Understanding of lead poisioning", - None, - "Summer", - 2024, - True, + {"id": 2}, + [ + { + "name": "Checking out cubes", + "description": "Material Sciences", + "recommended_experience": "Experienced in materials.", + "pay": None, + "semester": "Fall", + "year": 2024, + "active": True, + }, + { + "name": "Test the water", + "description": "Testing the quality of water in Troy pipes", + "recommended_experience": "Understanding of lead poisioning", + "pay": None, + "semester": "Summer", + "year": 2024, + "active": True, + }, + ], ), - ) - - for i, item in enumerate(json_data["opportunities"]): - assert item["name"] == lab_manager_opportunities_data[i][0] - assert item["description"] == lab_manager_opportunities_data[i][1] - assert item["recommended_experience"] == lab_manager_opportunities_data[i][2] - assert item["pay"] == lab_manager_opportunities_data[i][3] - assert item["semester"] == lab_manager_opportunities_data[i][4] - assert item["year"] == lab_manager_opportunities_data[i][5] - assert item["active"] == lab_manager_opportunities_data[i][6] - - -def test_user_route_no_json(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/user' page is requested (GET) - THEN check that the response is valid - """ - response = test_client.get("/user") - - assert response.status_code == 400 - - -def test_user_route_incorrect_json(test_client: FlaskClient) -> None: + ], +) +def test_user_opportunity_cards( + test_client: FlaskClient, input_data, expected_opportunities +) -> None: """ GIVEN a Flask application configured for testing - WHEN the '/user' page is requested (GET) - THEN check that the response is valid + WHEN the '/user' page is requested (GET) with input data + THEN check that the opportunity cards in the response are valid """ - response = test_client.get("/user", json={"wrong": "wrong"}) + response = test_client.get("/user", json=input_data) + assert response.status_code == 200 + json_data = json.loads(response.data) - assert response.status_code == 400 + for i, item in enumerate(json_data["opportunities"]): + assert item == expected_opportunities[i] -def test_user_not_found(test_client: FlaskClient) -> None: +@pytest.mark.parametrize( + "input_data, expected_status", + [(None, 400), ({"wrong": "wrong"}, 400), ({"id": "not found"}, 404)], +) +def test_user_route_edge_cases( + test_client: FlaskClient, input_data, expected_status +) -> None: """ GIVEN a Flask application configured for testing - WHEN the '/user' page is requested (GET) - THEN check that the response is valid + WHEN the '/user' page is requested (GET) with various edge case inputs + THEN check that the response status code is as expected """ - response = test_client.get("/user", json={"id": "not found"}) - - print(json.loads(response.data)) - - assert response.status_code == 404 + response = test_client.get("/user", json=input_data) + assert response.status_code == expected_status