From 82bb5c37f2765f0c39e9ddab2141500f36f24dcb Mon Sep 17 00:00:00 2001 From: Ajey95 Date: Thu, 1 Jan 2026 17:54:05 +0530 Subject: [PATCH 1/2] Add locator and faculty credential features --- app/routers/locator.py | 290 +++++++++++++++++++++++++++++++ app/routers/users.py | 190 ++++++++++++++++---- app/scripts/set_faculty_creds.py | 86 +++++++++ debug.py | 153 ++++++++++++++++ 4 files changed, 688 insertions(+), 31 deletions(-) create mode 100644 app/routers/locator.py create mode 100644 app/scripts/set_faculty_creds.py create mode 100644 debug.py diff --git a/app/routers/locator.py b/app/routers/locator.py new file mode 100644 index 0000000..723e6c5 --- /dev/null +++ b/app/routers/locator.py @@ -0,0 +1,290 @@ +from fastapi import APIRouter, HTTPException, Depends +from pydantic import BaseModel +from app.core.database import db +from datetime import datetime +import json +import uuid + +router = APIRouter() + +# --- MODELS --- +class StatusUpdate(BaseModel): + status: str # "Available", "Busy", "In Class", "Away" + source: str # "Manual", "Student-QR" (Crowdsourcing) + +class FutureCheck(BaseModel): + datetime: str # ISO Format: "2024-03-25T14:30:00" + +# --- MOCK TIMETABLE (Fallback) --- +MOCK_TIMETABLE = [ + { "day": 'Monday', "start": '09:00', "end": '10:00', "activity": 'Class (CS302)' }, + { "day": 'Monday', "start": '14:00', "end": '16:00', "activity": 'Lab (CS304)' }, + { "day": 'Tuesday', "start": '11:00', "end": '12:00', "activity": 'Office Hours' }, + { "day": 'Wednesday', "start": '09:00', "end": '10:00', "activity": 'Class (CS302)' }, + { "day": 'Friday', "start": '10:00', "end": '11:00', "activity": 'Dept Meeting' }, +] + +# --- NOTIFICATION HELPER --- +def create_system_notification(tx, user_id, message, type="ALERT"): + """ + Writes a notification directly to the user's node in Neo4j. + """ + query = """ + MATCH (u:User {user_id: $uid}) + CREATE (n:Notification { + id: $nid, + message: $message, + type: $type, + is_read: false, + created_at: datetime(), + trigger_role: 'System' + }) + CREATE (n)-[:NOTIFIES]->(u) + """ + tx.run(query, uid=user_id, nid=str(uuid.uuid4()), message=message, type=type) + +# ========================================== +# 1. SEEDING +# ========================================== +@router.post("/seed") +def seed_locator_data(): + """ + Migrates the provided SQL/JSON map data into Neo4j. + Creates (:Cabin) nodes and links (:Faculty) to them. + """ + session = db.get_session() + try: + # 1. Cabin Data + cabins = [ + {'code': 'AP 9', 'block': 'Block A', 'coords': '{"top": 85, "left": 70}', 'dir': 'Block A (South).'}, + {'code': 'AP 12', 'block': 'Block A', 'coords': '{"top": 85, "left": 65}', 'dir': 'Block A (South).'}, + {'code': 'AP 8', 'block': 'Block A', 'coords': '{"top": 85, "left": 75}', 'dir': 'Block A (South).'}, + {'code': 'AP 13', 'block': 'Block A', 'coords': '{"top": 82, "left": 80}', 'dir': 'Block A.'}, + {'code': 'AP 14', 'block': 'Block A', 'coords': '{"top": 82, "left": 75}', 'dir': 'Block A.'}, + {'code': 'AP 15', 'block': 'Block A', 'coords': '{"top": 82, "left": 70}', 'dir': 'Block A.'}, + {'code': 'AP 1', 'block': 'Block A', 'coords': '{"top": 85, "left": 80}', 'dir': 'Block A (South).'}, + {'code': 'AP 19', 'block': 'Block A', 'coords': '{"top": 80, "left": 65}', 'dir': 'Block A.'}, + {'code': 'AP 20', 'block': 'Block A', 'coords': '{"top": 80, "left": 60}', 'dir': 'Block A.'}, + {'code': 'AP 18', 'block': 'Block A', 'coords': '{"top": 80, "left": 70}', 'dir': 'Block A.'}, + {'code': 'AP 17', 'block': 'Block A', 'coords': '{"top": 80, "left": 75}', 'dir': 'Block A.'}, + {'code': 'AP 16', 'block': 'Block A', 'coords': '{"top": 80, "left": 80}', 'dir': 'Block A.'}, + {'code': 'P 2', 'block': 'Center Block', 'coords': '{"top": 50, "left": 45}', 'dir': 'Center Block.'}, + {'code': 'P 3', 'block': 'Center Block', 'coords': '{"top": 50, "left": 40}', 'dir': 'Center Block.'}, + {'code': 'P 4', 'block': 'Center Block', 'coords': '{"top": 50, "left": 55}', 'dir': 'Center Block.'}, + {'code': 'P 5', 'block': 'Center Block', 'coords': '{"top": 50, "left": 60}', 'dir': 'Center Block.'}, + {'code': 'P 1', 'block': 'Center Block', 'coords': '{"top": 50, "left": 50}', 'dir': 'Center Block.'}, + {'code': 'VICE CHAIR', 'block': 'Admin', 'coords': '{"top": 90, "left": 45}', 'dir': 'Vice Chairperson Office.'}, + {'code': 'PRINCIPAL', 'block': 'Admin', 'coords': '{"top": 90, "left": 50}', 'dir': 'Principal Office.'}, + ] + + # 2. Faculty Assignments + faculty_assignments = [ + {'name': 'Dr. Bagavathi C', 'cabin': 'AP 9', 'status': 'Available'}, + {'name': 'Deepika T', 'cabin': 'AP 12', 'status': 'Busy'}, + {'name': 'Dr. Dhanya M Dhanyalakshmy', 'cabin': 'AP 8', 'status': 'Available'}, + {'name': 'Dr. Vandana S', 'cabin': 'AP 13', 'status': 'In Class'}, + {'name': 'Sujee R', 'cabin': 'AP 14', 'status': 'Available'}, + {'name': 'Dr. J Uma', 'cabin': 'AP 15', 'status': 'Busy'}, + {'name': 'Arjun PK', 'cabin': 'AP 1', 'status': 'Available'}, + {'name': 'Prajna Dora', 'cabin': 'AP 19', 'status': 'Away'}, + {'name': 'Govindarajan J', 'cabin': 'AP 20', 'status': 'Available'}, + {'name': 'Malathi P', 'cabin': 'AP 18', 'status': 'Available'}, + {'name': 'Prathilothamai M', 'cabin': 'AP 17', 'status': 'Available'}, + {'name': 'Bharati D', 'cabin': 'AP 16', 'status': 'Busy'}, + {'name': 'Thangavelu S', 'cabin': 'P 2', 'status': 'Available'}, + {'name': 'Padmavathi S', 'cabin': 'P 3', 'status': 'In Class'}, + {'name': 'Rajathilagam B', 'cabin': 'P 4', 'status': 'Available'}, + {'name': 'Radhika N', 'cabin': 'P 5', 'status': 'Busy'}, + {'name': 'Gireesh Kumar T', 'cabin': 'P 1', 'status': 'Available'}, + {'name': 'Vice Chairperson', 'cabin': 'VICE CHAIR', 'status': 'Available'}, + {'name': 'Principal', 'cabin': 'PRINCIPAL', 'status': 'Busy'}, + ] + + # Query 1: Create Cabins + cabin_query = """ + UNWIND $cabins AS c + MERGE (n:Cabin {code: c.code}) + SET n.block = c.block, + n.coordinates = c.coords, + n.directions = c.dir + """ + session.run(cabin_query, cabins=cabins) + + # Query 2: Link Faculty to Cabins + assign_query = """ + UNWIND $assignments AS a + MATCH (f:Faculty {name: a.name}) + MATCH (c:Cabin {code: a.cabin}) + MERGE (f)-[:LOCATED_AT]->(c) + SET f.current_status = a.status, + f.status_source = 'Initial Seed', + f.last_status_updated = datetime() + """ + session.run(assign_query, assignments=faculty_assignments) + + return {"message": "Map data seeded successfully! šŸš€"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + finally: + session.close() + + +# ========================================== +# 2. LOCATOR & MAP ENDPOINTS +# ========================================== + +@router.get("/faculty/{faculty_id}/location") +def get_faculty_location(faculty_id: str): + """ + Returns data for the Red Dot Locator: coordinates, directions, and status. + """ + session = db.get_session() + try: + query = """ + MATCH (f:Faculty {user_id: $fid}) + OPTIONAL MATCH (f)-[:LOCATED_AT]->(c:Cabin) + RETURN f.name as name, + f.current_status as status, + f.status_source as source, + f.last_status_updated as last_updated, + c.code as cabin_code, + c.block as block, + c.coordinates as coords, + c.directions as directions + """ + result = session.run(query, fid=faculty_id).single() + + if not result: + raise HTTPException(status_code=404, detail="Faculty not found") + + # Parse coordinates JSON string back to object + coords = json.loads(result['coords']) if result['coords'] else None + + return { + "name": result['name'], + "status": { + "current": result['status'] or "Available", + "source": result['source'] or "Manual", + "last_updated": result['last_updated'].isoformat() if result['last_updated'] else None + }, + "location": { + "cabin_code": result['cabin_code'], + "block": result['block'], + "directions": result['directions'], + "coordinates": coords + } + } + finally: + session.close() + + +# ========================================== +# 3. AVAILABILITY & CROWDSOURCING +# ========================================== + +@router.put("/faculty/{faculty_id}/status") +def update_status(faculty_id: str, update: StatusUpdate): + """ + Updates status. + Source can be 'Manual' (Professor) or 'Student-QR' (Crowdsourced "I'm at the cabin"). + """ + session = db.get_session() + try: + query = """ + MATCH (f:Faculty {user_id: $fid}) + SET f.current_status = $status, + f.status_source = $source, + f.last_status_updated = datetime() + RETURN f.name as name + """ + result = session.run(query, fid=faculty_id, status=update.status, source=update.source).single() + + if not result: + raise HTTPException(status_code=404, detail="Faculty not found") + + return {"message": f"Status updated to {update.status} via {update.source}"} + finally: + session.close() + +@router.post("/faculty/{faculty_id}/request-update") +def request_update(faculty_id: str): + """ + Spam Protection Logic: + Increments a counter. Notification is only sent if count reaches 3. + """ + session = db.get_session() + try: + # 1. Increment Count + query = """ + MATCH (f:Faculty {user_id: $fid}) + SET f.request_count = coalesce(f.request_count, 0) + 1 + RETURN f.request_count as count, f.name as name, f.user_id as uid + """ + result = session.run(query, fid=faculty_id).single() + + if not result: + raise HTTPException(status_code=404, detail="Faculty not found") + + count = result['count'] + name = result['name'] + uid = result['uid'] + + response_msg = "Request counted. Waiting for more students." + + # 2. Check Threshold (3 Requests) + if count >= 3: + # --- REAL NOTIFICATION LOGIC --- + print(f"🚨 ALERT: 3 students are looking for Prof. {name}! Sending notification...") + + # Send the notification to Neo4j + session.write_transaction( + create_system_notification, + uid, + "āš ļø 3+ Students are requesting your status update.", + "ALERT" + ) + + response_msg = f"Notification sent to Prof. {name}!" + + # 3. Reset Count + session.run("MATCH (f:Faculty {user_id: $fid}) SET f.request_count = 0", fid=faculty_id) + count = 0 + + return {"message": response_msg, "current_requests": count} + finally: + session.close() + + +# ========================================== +# 4. FUTURE AVAILABILITY CHECKER +# ========================================== + +@router.post("/faculty/{faculty_id}/future") +def check_future_availability(faculty_id: str, check: FutureCheck): + """ + Checks the MOCK_TIMETABLE for conflicts at a specific future date/time. + """ + try: + # 1. Parse Input Date + dt = datetime.fromisoformat(check.datetime) + day_name = dt.strftime("%A") # e.g., 'Monday' + time_str = dt.strftime("%H:%M") # e.g., '14:30' + + status = "Available" + message = "Free according to timetable" + + # 2. Check against Mock Timetable + for slot in MOCK_TIMETABLE: + if slot['day'] == day_name: + if slot['start'] <= time_str < slot['end']: + status = "Busy" + message = slot['activity'] # e.g., "Class (CS302)" + break + + return { + "query_time": f"{day_name}, {time_str}", + "status": status, + "message": message + } + except ValueError: + raise HTTPException(status_code=400, detail="Invalid Date Format. Use ISO (YYYY-MM-DDTHH:MM:SS)") \ No newline at end of file diff --git a/app/routers/users.py b/app/routers/users.py index 8c61309..94b4468 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -1,15 +1,84 @@ from fastapi import APIRouter, HTTPException, Depends, UploadFile, File -# We import models from the file you renamed to 'user.py' (singular) -from app.models.user import StudentProfileUpdate, FacultyProfileUpdate +from pydantic import BaseModel +from typing import List, Optional from app.core.database import db from app.core.security import get_current_user import shutil import uuid import os +import json from datetime import datetime router = APIRouter() +# ========================================== +# 1. MODELS +# ========================================== + +# --- Shared Models --- +class WorkItem(BaseModel): + title: str + type: str + year: str + outcome: Optional[str] = None + collaborators: Optional[str] = None + +class ProjectCreate(BaseModel): + title: str + description: str + duration: Optional[str] = "" + from_date: Optional[str] = "" + to_date: Optional[str] = "" + tools: List[str] = [] + +class PublicationItem(BaseModel): + title: str + year: str + publisher: Optional[str] = "" + link: Optional[str] = "" + +# --- Student Profile Model --- +class StudentProfileUpdate(BaseModel): + name: Optional[str] = None + phone: Optional[str] = None + department: Optional[str] = None + batch: Optional[str] = None + bio: Optional[str] = None + profile_picture: Optional[str] = None + + skills: List[str] = [] + interests: List[str] = [] + projects: List[ProjectCreate] = [] + publications: List[PublicationItem] = [] + +# --- Faculty Profile Model --- +class FacultyProfileUpdate(BaseModel): + name: Optional[str] = None + profile_picture: Optional[str] = None + email: Optional[str] = None + phone: Optional[str] = None + designation: Optional[str] = None + department: Optional[str] = None + office_hours: Optional[str] = None + + cabin_block: Optional[str] = None + cabin_floor: Optional[str] = None + cabin_number: Optional[str] = None + + ug_details: List[str] = [] + pg_details: List[str] = [] + phd_details: List[str] = [] + + domain_interests: List[str] = [] + previous_work: List[WorkItem] = [] + + current_status: Optional[str] = None + status_source: Optional[str] = None + +# ========================================== +# 2. ROUTES +# ========================================== + # --- A. Upload Picture --- @router.post("/upload-profile-picture") async def upload_profile_picture(file: UploadFile = File(...)): @@ -20,8 +89,8 @@ async def upload_profile_picture(file: UploadFile = File(...)): file_path = f"uploads/{unique_filename}" with open(file_path, "wb") as buffer: shutil.copyfileobj(file.file, buffer) - # Update this IP if your network changes - return {"url": f"http://10.169.201.42:8000/uploads/{unique_filename}"} + # Ensure this matches your actual server URL or localhost + return {"url": f"http://127.0.0.1:8000/uploads/{unique_filename}"} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @@ -30,6 +99,7 @@ async def upload_profile_picture(file: UploadFile = File(...)): def get_student_profile(user_id: str, current_user: dict = Depends(get_current_user)): session = db.get_session() try: + # Fetch Basic Info query = """ MATCH (u:User {user_id: $uid}) OPTIONAL MATCH (u)-[:HAS_SKILL]->(s:Concept) @@ -48,9 +118,8 @@ def get_student_profile(user_id: str, current_user: dict = Depends(get_current_u # Fetch Projects proj_query = """ MATCH (u:User {user_id: $uid})-[:WORKED_ON]->(w:Work {type: 'Student Project'}) - RETURN w.title as title, w.description as description, - w.duration as duration, w.from_date as from_date, w.to_date as to_date, - w.tools as tools + RETURN w.title as title, w.description as description, w.from_date as from_date, + w.to_date as to_date, w.tools as tools, w.duration as duration """ projects = [dict(record) for record in session.run(proj_query, uid=user_id)] @@ -84,6 +153,7 @@ def update_student_profile( query = """ MATCH (u:User {user_id: $user_id}) + // 1. Update Basic Info SET u.name = COALESCE($name, u.name), u.profile_picture = $profile_picture, u.phone = $phone, @@ -91,6 +161,7 @@ def update_student_profile( u.batch = $batch, u.bio = $bio + // 2. Clean Old Relations WITH u OPTIONAL MATCH (u)-[r:HAS_SKILL|INTERESTED_IN]->() DELETE r WITH u @@ -98,42 +169,30 @@ def update_student_profile( WITH u OPTIONAL MATCH (u)-[:PUBLISHED]->(oldPub:Work {type: 'Publication'}) DETACH DELETE oldPub + // 3. Add Skills & Interests WITH u - FOREACH (skill IN $skills | - MERGE (s:Concept {name: toLower(skill)}) - MERGE (u)-[:HAS_SKILL]->(s) - ) - FOREACH (interest IN $interests | - MERGE (i:Concept {name: toLower(interest)}) - MERGE (u)-[:INTERESTED_IN]->(i) - ) + FOREACH (skill IN $skills | MERGE (s:Concept {name: toLower(skill)}) MERGE (u)-[:HAS_SKILL]->(s)) + FOREACH (interest IN $interests | MERGE (i:Concept {name: toLower(interest)}) MERGE (u)-[:INTERESTED_IN]->(i)) + // 4. Add Projects WITH u FOREACH (proj IN $projects | CREATE (w:Work { id: randomUUID(), - title: proj.title, - description: proj.description, - duration: proj.duration, - from_date: proj.from_date, - to_date: proj.to_date, - tools: proj.tools, - type: "Student Project", - created_at: datetime() + title: proj.title, from_date: proj.from_date, to_date: proj.to_date, + description: proj.description, tools: proj.tools, duration: proj.duration, + type: "Student Project", created_at: datetime() }) CREATE (u)-[:WORKED_ON]->(w) ) + // 5. Add Publications WITH u FOREACH (pub IN $publications | CREATE (w:Work { id: randomUUID(), - title: pub.title, - year: pub.year, - publisher: pub.publisher, - link: pub.link, - type: "Publication", - created_at: datetime() + title: pub.title, year: pub.year, publisher: pub.publisher, link: pub.link, + type: "Publication", created_at: datetime() }) CREATE (u)-[:PUBLISHED]->(w) ) @@ -157,12 +216,58 @@ def update_student_profile( ) return {"message": "Profile updated successfully"} except Exception as e: - print(f"Student Update Error: {e}") + print(f"Update Error: {e}") raise HTTPException(status_code=500, detail=str(e)) finally: session.close() -# --- D. UPDATE Faculty Profile --- +# --- ADDED: Get All Faculty List (Supports Map & Status) --- +@router.get("/faculty") +def get_all_faculty(search: Optional[str] = None, department: Optional[str] = None): + session = db.get_session() + try: + # Fetch Faculty Nodes with Map Status + query = """ + MATCH (f:Faculty) + OPTIONAL MATCH (f)-[:LOCATED_AT]->(c:Cabin) + WHERE ($search IS NULL OR toLower(f.name) CONTAINS toLower($search)) + AND ($dept IS NULL OR f.department = $dept) + + RETURN f.user_id as id, + f.name as name, + f.department as department, + f.designation as designation, + f.profile_picture as profile_picture, + f.current_status as status, + f.status_source as status_source, + c.code as cabin_number, + c.coordinates as coordinates + """ + + result = session.run(query, search=search, dept=department) + faculty_list = [] + + for record in result: + data = dict(record) + # Safe JSON parse for coords + if data['coordinates']: + try: + data['coordinates'] = json.loads(data['coordinates']) + except: + data['coordinates'] = None + + # Default fallback for status + if not data['status']: + data['status'] = 'Available' + data['status_source'] = 'System' + + faculty_list.append(data) + + return faculty_list + finally: + session.close() + +# --- D. Faculty Update (FIXED WITH MAP LOGIC) --- @router.put("/faculty/profile") def update_faculty_profile( data: FacultyProfileUpdate, @@ -177,6 +282,7 @@ def update_faculty_profile( try: work_data = [w.dict() for w in data.previous_work] + # 1. Update Basic Text & Details query = """ MATCH (f:User {user_id: $user_id}) @@ -194,17 +300,22 @@ def update_faculty_profile( f.pg_details = $pg_details, f.phd_details = $phd_details + // Clear and Recreate Domain Interests WITH f OPTIONAL MATCH (f)-[r:INTERESTED_IN]->() DELETE r + + // Clear and Recreate Work WITH f OPTIONAL MATCH (f)-[:WORKED_ON]->(w:Work) DETACH DELETE w + // Add Domains WITH f FOREACH (dom IN $domain_interests | MERGE (c:Concept {name: toLower(dom)}) MERGE (f)-[:INTERESTED_IN]->(c) ) + // Add Work WITH f FOREACH (item IN $previous_work | CREATE (w:Work { @@ -242,6 +353,23 @@ def update_faculty_profile( previous_work=work_data ) + # 2. CRITICAL: Update Map Relationship (The "Req") + # This moves the pin on the map when you save the profile + if data.cabin_number: + map_query = """ + MATCH (f:Faculty {user_id: $uid}) + + // Remove old map link + OPTIONAL MATCH (f)-[r:LOCATED_AT]->(:Cabin) + DELETE r + + // Find new cabin by code and link it + WITH f + MATCH (c:Cabin {code: $code}) + MERGE (f)-[:LOCATED_AT]->(c) + """ + session.run(map_query, uid=user_id, code=data.cabin_number) + return {"message": "Faculty profile updated successfully"} except Exception as e: diff --git a/app/scripts/set_faculty_creds.py b/app/scripts/set_faculty_creds.py new file mode 100644 index 0000000..9fb0e01 --- /dev/null +++ b/app/scripts/set_faculty_creds.py @@ -0,0 +1,86 @@ +import sys +import os +from passlib.context import CryptContext + +# Setup path to import from 'app' +sys.path.append(os.path.join(os.path.dirname(__file__), "../..")) + +from app.core.database import db + +# Setup Password Hashing +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +def set_credentials(): + session = db.get_session() + try: + print("šŸ”„ Fetching Faculty...") + # Get all faculty nodes + result = session.run("MATCH (f:Faculty) RETURN f.user_id as uid, f.name as name") + + faculty_list = [{"uid": r["uid"], "name": r["name"]} for r in result] + + print(f"found {len(faculty_list)} faculty members.\n") + print(f"{'NAME':<30} | {'EMAIL (Login)':<35} | {'PASSWORD'}") + print("-" * 80) + + for fac in faculty_list: + name = fac['name'] + uid = fac['uid'] + + # 1. Generate Cleaner Email (Fixing the leading dot issue) + # Remove Dr. and extra spaces first + temp_name = name.lower().replace("dr.", "").replace("dr ", "") + temp_name = temp_name.strip() # Remove leading/trailing spaces + + # Replace remaining spaces with dots + clean_name = temp_name.replace(" ", ".") + + # Remove double dots if any + while ".." in clean_name: + clean_name = clean_name.replace("..", ".") + + base_email = f"{clean_name}@gurusetu.edu" + email = base_email + + # 2. Check for collisions (Is this email taken by SOMEONE ELSE?) + counter = 1 + while True: + check_query = """ + MATCH (u:User {email: $email}) + WHERE u.user_id <> $uid + RETURN count(u) as exists + """ + exists = session.run(check_query, email=email, uid=uid).single()["exists"] + + if exists == 0: + break # Email is free for this user! + + # Email taken, increment counter + counter += 1 + email = f"{clean_name}{counter}@gurusetu.edu" + + # 3. Hash Password + plain_password = "123456" + hashed_password = pwd_context.hash(plain_password) + + # 4. Update the DB + session.run(""" + MATCH (f:Faculty {user_id: $uid}) + SET f.email = $email, + f.password = $password + """, uid=uid, email=email, password=hashed_password) + + print(f"{name:<30} | {email:<35} | {plain_password}") + + print("-" * 80) + print("\nāœ… SUCCESS! Credentials updated correctly.") + + except Exception as e: + print(f"āŒ Error: {e}") + finally: + session.close() + +if __name__ == "__main__": + db.connect() + set_credentials() + db.close() \ No newline at end of file diff --git a/debug.py b/debug.py new file mode 100644 index 0000000..8b70174 --- /dev/null +++ b/debug.py @@ -0,0 +1,153 @@ +# # import requests +# # import json + +# # # URL of your backend login +# # url = "http://127.0.0.1:8000/auth/login" + +# # # Test Credentials (Faculty) +# # payload = { +# # "email": "deepika.t@gurusetu.edu", +# # "password": "123456", +# # "role": "faculty" # We are sending lowercase 'faculty' +# # } + +# # print(f"šŸ”„ Attempting Login for: {payload['email']}...") + +# # try: +# # response = requests.post(url, json=payload) + +# # print(f"\nšŸ”¢ Status Code: {response.status_code}") +# # print(f"šŸ“„ Response Body: {response.text}") + +# # if response.status_code == 200: +# # print("\nāœ… LOGIN SUCCESS! The backend is working fine.") +# # print(" If you still can't login from the browser, the Frontend is sending the wrong data.") +# # else: +# # print("\nāŒ LOGIN FAILED.") +# # print(" Look at the 'Response Body' above to see the missing field or error.") + +# # except Exception as e: +# # print(f"\nšŸ’„ Connection Error: {e}") +# # print(" Make sure your backend (uvicorn) is running!") + +# import sys +# import os +# from passlib.context import CryptContext + +# # Setup path +# sys.path.append(os.path.join(os.path.dirname(__file__), ".")) + +# from app.core.database import db + +# # Setup the same hasher +# pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +# def check_new_user(): +# session = db.get_session() + +# # --- ENTER YOUR NEW EMAIL HERE --- +# email = "j.uma@gurusetu.edu" +# # --------------------------------- + +# try: +# print(f"šŸ” Searching for: {email}...") + +# # 1. Check if node exists and get labels/properties +# query = "MATCH (u:User {email: $email}) RETURN u, labels(u) as labels" +# result = session.run(query, email=email).single() + +# if not result: +# print("āŒ ERROR: User NOT FOUND in Database!") +# print(" Did the signup actually succeed? Check the 'User' table.") +# return + +# node = result["u"] +# labels = result["labels"] + +# print("\nāœ… USER FOUND!") +# print(f" Labels: {labels} <-- Should contain 'User' AND 'Faculty'") +# print(f" Name: {node.get('name')}") +# print(f" Role: {node.get('role')} <-- Should be 'faculty' (lowercase)") +# print(f" Stored Password: {node.get('password')}") + +# # 2. Check Role Logic +# if "Faculty" not in labels and node.get('role') == 'faculty': +# print("āš ļø WARNING: Node has 'faculty' role but missing :Faculty label.") +# print(" This might prevent you from appearing in the directory.") + +# if node.get('role') == 'Faculty': +# print("āš ļø WARNING: Role is 'Faculty' (Capitalized). Login might expect 'faculty'.") + +# except Exception as e: +# print(f"šŸ’„ Error: {e}") +# finally: +# session.close() + +# if __name__ == "__main__": +# db.connect() +# check_new_user() +# db.close() +import requests +import sys +import os +from passlib.context import CryptContext + +# Setup path +sys.path.append(os.path.join(os.path.dirname(__file__), ".")) +from app.core.database import db + +# Setup Password Hashing +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +URL = "http://127.0.0.1:8000/auth/login" + +def fix_and_test(): + session = db.get_session() + email = "j.uma@gurusetu.edu" + password = "123456" + + try: + print(f"šŸ”§ RESETTING password for {email}...") + + # 1. Force Reset Password in DB + hashed_pw = pwd_context.hash(password) + query = """ + MATCH (u:User {email: $email}) + SET u.password = $pw, u.role = 'faculty' + RETURN u.name, u.role + """ + # Note: We also force role to lowercase 'faculty' to match your output + session.run(query, email=email, pw=hashed_pw) + print("āœ… Password reset in Neo4j.") + + # 2. Test Login Endpoint + print(f"\nšŸš€ TESTING Login Endpoint: {URL}") + + payload = { + "email": email, + "password": password, + "role": "faculty" # Trying lowercase first + } + + print(f" Sending: {payload}") + response = requests.post(URL, json=payload) + + print(f"\n Status: {response.status_code}") + print(f" Response: {response.text}") + + if response.status_code == 200: + print("\nšŸŽ‰ SUCCESS! Login is working.") + print(" Go to your browser and use these exact credentials.") + else: + print("\nāŒ FAILED via Python too.") + print(" This means the bug is inside your 'app/routers/auth.py' file.") + print(" Please paste your 'auth.py' code here so I can fix the logic.") + + except Exception as e: + print(f"šŸ’„ Error: {e}") + finally: + session.close() + +if __name__ == "__main__": + db.connect() + fix_and_test() + db.close() \ No newline at end of file From 891b2cf964c3279fa4be27fcf2462cafa3514979 Mon Sep 17 00:00:00 2001 From: Ajey95 Date: Thu, 1 Jan 2026 19:23:52 +0530 Subject: [PATCH 2/2] Add locator and faculty credential features --- app/main.py | 6 +- app/routers/dashboard.py | 153 ++++++++++++++++++++++++++++++++++++--- app/routers/locator.py | 141 +++++++++++++++++++----------------- 3 files changed, 222 insertions(+), 78 deletions(-) diff --git a/app/main.py b/app/main.py index 2dc0da7..400e383 100644 --- a/app/main.py +++ b/app/main.py @@ -16,7 +16,8 @@ faculty_projects, dashboard, applications, - notifications + notifications, + locator ) @asynccontextmanager @@ -63,4 +64,5 @@ def read_root(): app.include_router(faculty_projects.router, prefix="/faculty-projects", tags=["Faculty Research"]) app.include_router(dashboard.router, prefix="/dashboard", tags=["Dashboard"]) app.include_router(applications.router, prefix="/applications", tags=["Applications"]) # <--- NEW REGISTER -app.include_router(notifications.router, prefix="/notifications", tags=["Notifications"]) # <--- 2. REGISTER IT \ No newline at end of file +app.include_router(notifications.router, prefix="/notifications", tags=["Notifications"]) # <--- 2. REGISTER IT +app.include_router(locator.router, prefix="/locator", tags=["Locator"]) \ No newline at end of file diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index 8110db1..37bbbdf 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -3,14 +3,46 @@ from app.core.security import get_current_user from app.core.database import db import uuid +import json from datetime import datetime, date from pydantic import BaseModel router = APIRouter(tags=["Dashboard"]) +# ========================================================= +# 0. MODELS +# ========================================================= + class ShortlistRequest(BaseModel): opening_id: str +class WorkItem(BaseModel): + title: str + type: str + year: str + outcome: Optional[str] = None + collaborators: Optional[str] = None + +# --- NEW: Model for Profile Updates --- +class FacultyProfileUpdate(BaseModel): + name: Optional[str] = None + profile_picture: Optional[str] = None + email: Optional[str] = None + phone: Optional[str] = None + designation: Optional[str] = None + department: Optional[str] = None + office_hours: Optional[str] = None + cabin_block: Optional[str] = None + cabin_floor: Optional[str] = None + cabin_number: Optional[str] = None + ug_details: List[str] = [] + pg_details: List[str] = [] + phd_details: List[str] = [] + domain_interests: List[str] = [] + previous_work: List[WorkItem] = [] + current_status: Optional[str] = None + status_source: Optional[str] = None + # ========================================================= # HELPER FUNCTIONS # ========================================================= @@ -20,7 +52,6 @@ def safe_date(date_obj): if not date_obj: return "N/A" try: - # If it's a Neo4j DateTime/Date or Python datetime/date if hasattr(date_obj, 'isoformat'): return date_obj.isoformat().split('T')[0] return str(date_obj) @@ -183,7 +214,7 @@ def get_student_dashboard(current_user: dict = Depends(get_current_user)): "faculty_pic": r["fpic"], "match_score": f"{min(99, 60 + (r['match_count'] * 10))}%", "skills_required": r["skills_required"][:3], - "deadline": safe_date(r["deadline"]) # <--- FIXED DATE + "deadline": safe_date(r["deadline"]) }) # C. All Openings List @@ -209,7 +240,7 @@ def get_student_dashboard(current_user: dict = Depends(get_current_user)): "description": r["desc"], "faculty_pic": r["fpic"], "skills_required": r["skills"], - "deadline": safe_date(r["deadline"]) # <--- FIXED DATE + "deadline": safe_date(r["deadline"]) }) return { @@ -379,6 +410,9 @@ def get_all_students(search: Optional[str] = None, department: Optional[str] = N finally: session.close() +# --------------------------------------------------------------------- +# CRITICAL UPDATE: ALL FACULTY LIST WITH MAP DATA & REAL STATUS +# --------------------------------------------------------------------- @router.get("/student/all-faculty") def get_all_faculty(search: Optional[str] = None, department: Optional[str] = None, domain: Optional[str] = None, current_user: dict = Depends(get_current_user)): if current_user["role"].lower() != "student": @@ -392,16 +426,26 @@ def get_all_faculty(search: Optional[str] = None, department: Optional[str] = No if department: query += " AND f.department = $dept" + # 1. Fetch Domains query += """ OPTIONAL MATCH (f)-[:INTERESTED_IN]->(c:Concept) - WITH f, collect(c.name) as domains """ + + # 2. Fetch Cabin & Map Data + query += """ + OPTIONAL MATCH (f)-[:LOCATED_AT]->(cab:Cabin) + WITH f, cab, collect(c.name) as domains + """ + if domain: query += " WHERE $domain IN domains" + # 3. Return EVERYTHING needed for the Frontend (ID, Map, Status) query += """ RETURN f.user_id as id, f.name as name, f.department as dept, f.profile_picture as pic, f.designation as designation, + f.current_status as status, f.status_source as status_source, + cab.code as cabin_number, cab.coordinates as coordinates, domains ORDER BY f.name ASC """ @@ -409,14 +453,26 @@ def get_all_faculty(search: Optional[str] = None, department: Optional[str] = No res = session.run(query, search=search, dept=department, domain=domain) results = [] for r in res: + # Parse Coordinates safely + coords = None + if r["coordinates"]: + try: + coords = json.loads(r["coordinates"]) + except: + coords = None + results.append({ - "faculty_id": r["id"], + "id": r["id"], + "faculty_id": r["id"], "name": r["name"], "department": r["dept"], "designation": r.get("designation", "Professor"), "profile_picture": r["pic"], "domains": r["domains"][:3], - "status": "Available" + "status": r["status"] or "Available", + "status_source": r["status_source"] or "Manual", + "cabin_number": r["cabin_number"], + "coordinates": coords }) return results finally: @@ -452,7 +508,7 @@ def get_student_applications(current_user: dict = Depends(get_current_user)): "department": row["dept"] or "General", "faculty_pic": row["pic"], "status": row["status"] or "Pending", - "applied_date": safe_date(row["applied_date"]) # <--- FIXED DATE + "applied_date": safe_date(row["applied_date"]) }) return applications finally: @@ -530,7 +586,8 @@ def get_faculty_public_profile(faculty_id: str, current_user: dict = Depends(get f.cabin_block as block, f.cabin_floor as floor, f.cabin_number as cabin_no, f.office_hours as office_hours, f.ug_details as ug, f.pg_details as pg, f.phd_details as phd, - collect(DISTINCT c.name) as interests + collect(DISTINCT c.name) as interests, + f.current_status as status """ profile = session.run(profile_query, fid=faculty_id).single() @@ -552,7 +609,7 @@ def get_faculty_public_profile(faculty_id: str, current_user: dict = Depends(get "pg_details": profile["pg"] or [], "phd_details": profile["phd"] or [], "interests": profile["interests"], - "availability_status": "Available Now" + "availability_status": profile["status"] or "Available" }, "schedule": profile["office_hours"] or "Mon-Fri 9AM-5PM", "openings": [], @@ -597,6 +654,80 @@ def get_faculty_public_profile(faculty_id: str, current_user: dict = Depends(get # 5. ACTIONS & NOTIFICATIONS # ========================================================= +# --- NEW: FACULTY PROFILE UPDATE ENDPOINT (THE FIX) --- +@router.put("/faculty/profile") +def update_faculty_profile(update: FacultyProfileUpdate, current_user: dict = Depends(get_current_user)): + if current_user["role"].lower() != "faculty": + raise HTTPException(status_code=403, detail="Access denied") + + session = db.get_session() + user_id = current_user["user_id"] + + try: + work_data = [w.dict() for w in update.previous_work] + + # 1. Update Basic Text Properties + basic_query = """ + MATCH (f:Faculty {user_id: $uid}) + SET f.name = coalesce($name, f.name), + f.designation = coalesce($desig, f.designation), + f.department = coalesce($dept, f.department), + f.email = coalesce($email, f.email), + f.phone = coalesce($phone, f.phone), + f.profile_picture = coalesce($pic, f.profile_picture), + f.office_hours = coalesce($hours, f.office_hours), + f.cabin_block = coalesce($block, f.cabin_block), + f.cabin_floor = coalesce($floor, f.cabin_floor), + f.cabin_number = coalesce($num, f.cabin_number), + f.ug_details = $ug, + f.pg_details = $pg, + f.phd_details = $phd + """ + session.run(basic_query, + uid=user_id, + name=update.name, desig=update.designation, dept=update.department, + email=update.email, phone=update.phone, pic=update.profile_picture, + hours=update.office_hours, block=update.cabin_block, floor=update.cabin_floor, + num=update.cabin_number, ug=update.ug_details, pg=update.pg_details, phd=update.phd_details + ) + + # 2. Update Domain Interests + if update.domain_interests is not None: + session.run("MATCH (f:Faculty {user_id: $uid})-[r:INTERESTED_IN]->() DELETE r", uid=user_id) + for domain in update.domain_interests: + session.run("MATCH (f:Faculty {user_id: $uid}) MERGE (c:Concept {name: $domain}) MERGE (f)-[:INTERESTED_IN]->(c)", uid=user_id, domain=domain) + + # 3. Update Previous Work + if update.previous_work: + session.run("MATCH (f:Faculty {user_id: $uid})-[r:WORKED_ON]->() DELETE r", uid=user_id) + for item in work_data: + session.run(""" + MATCH (f:Faculty {user_id: $uid}) + CREATE (w:Work {id: randomUUID(), title: $title, type: $type, year: $year, outcome: $outcome, collaborators: $collab, created_at: datetime()}) + CREATE (f)-[:WORKED_ON]->(w) + """, uid=user_id, title=item['title'], type=item['type'], year=item['year'], outcome=item['outcome'], collab=item['collaborators']) + + # 4. CRITICAL: Update Map Connection if Cabin Number changed + # This fixes the issue where map pins wouldn't move! + if update.cabin_number: + map_query = """ + MATCH (f:Faculty {user_id: $uid}) + OPTIONAL MATCH (f)-[r:LOCATED_AT]->(:Cabin) + DELETE r + WITH f + MATCH (c:Cabin {code: $code}) + MERGE (f)-[:LOCATED_AT]->(c) + """ + session.run(map_query, uid=user_id, code=update.cabin_number) + + return {"message": "Profile updated successfully"} + + except Exception as e: + print(f"Update Error: {str(e)}") + raise HTTPException(status_code=500, detail="Failed to update profile") + finally: + session.close() + @router.post("/shortlist/{student_id}") def shortlist_student( student_id: str, @@ -670,7 +801,7 @@ def get_notifications(current_user: dict = Depends(get_current_user)): "message": r["message"], "type": r["type"], "is_read": r["is_read"], - "date": safe_date(r["date"]), # <--- FIXED DATE + "date": safe_date(r["date"]), "trigger_id": r["trigger_id"], "trigger_role": r["trigger_role"] }) @@ -725,7 +856,7 @@ def get_faculty_projects(current_user: dict = Depends(get_current_user)): "title": r["title"], "status": "Active", "domain": "Research", - "posted_date": safe_date(r["date"]), # <--- FIXED DATE + "posted_date": safe_date(r["date"]), "applicant_count": r["applicant_count"], "shortlisted_count": r["shortlisted_count"] }) diff --git a/app/routers/locator.py b/app/routers/locator.py index 723e6c5..14b8348 100644 --- a/app/routers/locator.py +++ b/app/routers/locator.py @@ -1,21 +1,26 @@ from fastapi import APIRouter, HTTPException, Depends from pydantic import BaseModel +from typing import Optional, List from app.core.database import db -from datetime import datetime -import json +from app.core.security import get_current_user import uuid +import json +from datetime import datetime -router = APIRouter() +router = APIRouter(tags=["Locator"]) + +# ========================================== +# 0. MODELS & DATA +# ========================================== -# --- MODELS --- class StatusUpdate(BaseModel): status: str # "Available", "Busy", "In Class", "Away" - source: str # "Manual", "Student-QR" (Crowdsourcing) + source: str # "Manual", "Student-QR" class FutureCheck(BaseModel): - datetime: str # ISO Format: "2024-03-25T14:30:00" + datetime: str # ISO Format: "2024-03-25T14:30:00.000Z" -# --- MOCK TIMETABLE (Fallback) --- +# Mock Timetable (Fallback when DB is empty) MOCK_TIMETABLE = [ { "day": 'Monday', "start": '09:00', "end": '10:00', "activity": 'Class (CS302)' }, { "day": 'Monday', "start": '14:00', "end": '16:00', "activity": 'Lab (CS304)' }, @@ -24,11 +29,8 @@ class FutureCheck(BaseModel): { "day": 'Friday', "start": '10:00', "end": '11:00', "activity": 'Dept Meeting' }, ] -# --- NOTIFICATION HELPER --- -def create_system_notification(tx, user_id, message, type="ALERT"): - """ - Writes a notification directly to the user's node in Neo4j. - """ +# --- Helper: Notification --- +def create_system_notification(tx, target_uid, message, type="ALERT"): query = """ MATCH (u:User {user_id: $uid}) CREATE (n:Notification { @@ -41,20 +43,19 @@ def create_system_notification(tx, user_id, message, type="ALERT"): }) CREATE (n)-[:NOTIFIES]->(u) """ - tx.run(query, uid=user_id, nid=str(uuid.uuid4()), message=message, type=type) + tx.run(query, uid=target_uid, nid=str(uuid.uuid4()), message=message, type=type) # ========================================== -# 1. SEEDING +# 1. SEEDING (Run once to setup Map) # ========================================== @router.post("/seed") def seed_locator_data(): - """ - Migrates the provided SQL/JSON map data into Neo4j. - Creates (:Cabin) nodes and links (:Faculty) to them. - """ session = db.get_session() try: - # 1. Cabin Data + # 1. Clean up old connections to prevent duplicates + session.run("MATCH (f:Faculty)-[r:LOCATED_AT]->() DELETE r") + + # 2. Define Data cabins = [ {'code': 'AP 9', 'block': 'Block A', 'coords': '{"top": 85, "left": 70}', 'dir': 'Block A (South).'}, {'code': 'AP 12', 'block': 'Block A', 'coords': '{"top": 85, "left": 65}', 'dir': 'Block A (South).'}, @@ -77,7 +78,6 @@ def seed_locator_data(): {'code': 'PRINCIPAL', 'block': 'Admin', 'coords': '{"top": 90, "left": 50}', 'dir': 'Principal Office.'}, ] - # 2. Faculty Assignments faculty_assignments = [ {'name': 'Dr. Bagavathi C', 'cabin': 'AP 9', 'status': 'Available'}, {'name': 'Deepika T', 'cabin': 'AP 12', 'status': 'Busy'}, @@ -100,18 +100,15 @@ def seed_locator_data(): {'name': 'Principal', 'cabin': 'PRINCIPAL', 'status': 'Busy'}, ] - # Query 1: Create Cabins - cabin_query = """ + # 3. Create Nodes + session.run(""" UNWIND $cabins AS c MERGE (n:Cabin {code: c.code}) - SET n.block = c.block, - n.coordinates = c.coords, - n.directions = c.dir - """ - session.run(cabin_query, cabins=cabins) + SET n.block = c.block, n.coordinates = c.coords, n.directions = c.dir + """, cabins=cabins) - # Query 2: Link Faculty to Cabins - assign_query = """ + # 4. Link & Update Status + session.run(""" UNWIND $assignments AS a MATCH (f:Faculty {name: a.name}) MATCH (c:Cabin {code: a.cabin}) @@ -119,8 +116,7 @@ def seed_locator_data(): SET f.current_status = a.status, f.status_source = 'Initial Seed', f.last_status_updated = datetime() - """ - session.run(assign_query, assignments=faculty_assignments) + """, assignments=faculty_assignments) return {"message": "Map data seeded successfully! šŸš€"} except Exception as e: @@ -128,16 +124,12 @@ def seed_locator_data(): finally: session.close() - # ========================================== # 2. LOCATOR & MAP ENDPOINTS # ========================================== @router.get("/faculty/{faculty_id}/location") def get_faculty_location(faculty_id: str): - """ - Returns data for the Red Dot Locator: coordinates, directions, and status. - """ session = db.get_session() try: query = """ @@ -157,8 +149,14 @@ def get_faculty_location(faculty_id: str): if not result: raise HTTPException(status_code=404, detail="Faculty not found") - # Parse coordinates JSON string back to object - coords = json.loads(result['coords']) if result['coords'] else None + # Edge Case: Handle Missing/Bad JSON Coordinates + coords = None + if result['coords']: + try: + coords = json.loads(result['coords']) + except json.JSONDecodeError: + print(f"āŒ Error decoding coords for {result['name']}") + coords = None return { "name": result['name'], @@ -177,19 +175,18 @@ def get_faculty_location(faculty_id: str): finally: session.close() - # ========================================== # 3. AVAILABILITY & CROWDSOURCING # ========================================== @router.put("/faculty/{faculty_id}/status") -def update_status(faculty_id: str, update: StatusUpdate): - """ - Updates status. - Source can be 'Manual' (Professor) or 'Student-QR' (Crowdsourced "I'm at the cabin"). - """ +def update_status(faculty_id: str, update: StatusUpdate, current_user: dict = Depends(get_current_user)): session = db.get_session() try: + # Check permissions handled by `get_current_user` mostly, but logic: + # If Manual -> Must be Faculty themselves + # If Student-QR -> Must be Student at location + query = """ MATCH (f:Faculty {user_id: $fid}) SET f.current_status = $status, @@ -202,15 +199,14 @@ def update_status(faculty_id: str, update: StatusUpdate): if not result: raise HTTPException(status_code=404, detail="Faculty not found") - return {"message": f"Status updated to {update.status} via {update.source}"} + return {"message": f"Status updated to {update.status}"} finally: session.close() @router.post("/faculty/{faculty_id}/request-update") -def request_update(faculty_id: str): +def request_update(faculty_id: str, current_user: dict = Depends(get_current_user)): """ - Spam Protection Logic: - Increments a counter. Notification is only sent if count reaches 3. + Fixed: Now requires Login (current_user) so random people can't spam. """ session = db.get_session() try: @@ -227,26 +223,25 @@ def request_update(faculty_id: str): count = result['count'] name = result['name'] - uid = result['uid'] + target_uid = result['uid'] response_msg = "Request counted. Waiting for more students." # 2. Check Threshold (3 Requests) if count >= 3: - # --- REAL NOTIFICATION LOGIC --- - print(f"🚨 ALERT: 3 students are looking for Prof. {name}! Sending notification...") + print(f"🚨 ALERT: 3 students are looking for Prof. {name}!") - # Send the notification to Neo4j + # Send notification session.write_transaction( create_system_notification, - uid, - "āš ļø 3+ Students are requesting your status update.", + target_uid, + f"āš ļø 3+ Students are at your cabin requesting an update.", "ALERT" ) response_msg = f"Notification sent to Prof. {name}!" - # 3. Reset Count + # Reset Count session.run("MATCH (f:Faculty {user_id: $fid}) SET f.request_count = 0", fid=faculty_id) count = 0 @@ -254,37 +249,53 @@ def request_update(faculty_id: str): finally: session.close() - # ========================================== # 4. FUTURE AVAILABILITY CHECKER # ========================================== @router.post("/faculty/{faculty_id}/future") def check_future_availability(faculty_id: str, check: FutureCheck): - """ - Checks the MOCK_TIMETABLE for conflicts at a specific future date/time. - """ try: - # 1. Parse Input Date - dt = datetime.fromisoformat(check.datetime) - day_name = dt.strftime("%A") # e.g., 'Monday' + # 1. Edge Case Fix: Remove 'Z' (UTC marker) to prevent Python crash + clean_date = check.datetime.replace("Z", "") + + dt = datetime.fromisoformat(clean_date) + day_name = dt.strftime("%A") # e.g., 'Monday' time_str = dt.strftime("%H:%M") # e.g., '14:30' status = "Available" message = "Free according to timetable" + found_conflict = False - # 2. Check against Mock Timetable + # 2. Priority 1: Check against Mock Timetable (Specific Events) for slot in MOCK_TIMETABLE: if slot['day'] == day_name: + # String comparison works for ISO times: "09:00" <= "14:30" < "16:00" if slot['start'] <= time_str < slot['end']: - status = "Busy" - message = slot['activity'] # e.g., "Class (CS302)" + status = "In Class" # Or "Busy" + message = slot['activity'] + found_conflict = True break + # 3. Priority 2: General Availability (Only if NO Class was found) + # If they are free from class, check if they are actually at home (Weekend/After Hours) + if not found_conflict: + # Fix: Use 'in list' instead of 'or string' + if day_name in ['Saturday', 'Sunday']: + status = "Busy" + message = "At Home (Weekend)" + + # Fix: Check office hours (e.g., 9 AM to 4 PM) + elif time_str < "09:00" or time_str > "16:00": + status = "Busy" + message = "At Home (After Hours)" + return { "query_time": f"{day_name}, {time_str}", "status": status, "message": message } - except ValueError: + + except ValueError as e: + print(f"Date Error: {e}") raise HTTPException(status_code=400, detail="Invalid Date Format. Use ISO (YYYY-MM-DDTHH:MM:SS)") \ No newline at end of file