api/modules/database/tools/navigation/user_navigation.py
2025-11-14 14:47:19 +00:00

491 lines
18 KiB
Python

import os
from modules.logger_tool import initialise_logger
logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True)
import modules.database.tools.neo4j_driver_tools as driver_tools
import modules.database.tools.neo4j_session_tools as session_tools
from datetime import datetime, timedelta
from typing import List, Optional, Dict, Any
def get_static_nodes(context: str, db_name: str) -> List[Dict[str, Any]]:
"""Get static nodes for a specific context."""
if context == 'workers':
# For workers context, show teacher node first, then timetables and classes
query = """
MATCH (t:Teacher)
RETURN DISTINCT {
id: t.uuid_string,
path: t.node_storage_path,
label: t.teacher_name_formal,
type: 'Teacher',
isStatic: true,
order: 0,
section: 'Root'
} as node
UNION ALL
MATCH (t:UserTeacherTimetable)
RETURN DISTINCT {
id: t.uuid_string,
path: t.node_storage_path,
label: t.name,
type: 'UserTeacherTimetable',
isStatic: true,
order: 1,
section: 'Timetables'
} as node
UNION ALL
MATCH (t:UserTeacherTimetable)-[:HAS_CLASS]->(c:Class)
RETURN DISTINCT {
id: c.uuid_string,
path: c.node_storage_path,
label: c.name,
type: 'Class',
isStatic: true,
order: 2,
section: 'Classes'
} as node
"""
elif context == 'user':
# For user context, show the user node
query = """
MATCH (u:User)
RETURN DISTINCT {
id: u.uuid_string,
path: u.node_storage_path,
label: u.user_name,
type: 'User',
isStatic: true,
order: 0,
section: 'Root'
} as node
"""
else:
# For calendar context, show today's calendar node first, then other calendar nodes
today = datetime.now().strftime("%Y-%m-%d")
query = """
MATCH (n:Calendar)
WITH n,
CASE
WHEN date($today) >= date(n.start_date) AND date($today) <= date(n.end_date)
THEN 0
ELSE 1
END as nodeOrder
RETURN DISTINCT {
id: n.uuid_string,
path: n.node_storage_path,
label: n.name,
type: 'Calendar',
isStatic: true,
order: nodeOrder,
section: CASE nodeOrder
WHEN 0 THEN 'Today'
ELSE 'Calendar'
END
} as node
ORDER BY node.order, node.label
"""
try:
with driver_tools.get_session(database=db_name) as session:
result = session.run(query, today=datetime.now().strftime("%Y-%m-%d"))
return [record["node"] for record in result]
except Exception as e:
logger.error(f"Error getting static nodes: {str(e)}")
return []
def get_today_calendar_node(db_name: str) -> Optional[Dict[str, Any]]:
"""Get today's calendar node."""
today = datetime.now().strftime("%Y-%m-%d")
query = """
MATCH (n:Calendar)
WHERE date($today) >= date(n.start_date) AND date($today) <= date(n.end_date)
RETURN n.uuid_string as id, n.path as path, n.name as label,
'Calendar' as type
LIMIT 1
"""
try:
with driver_tools.get_session(database=db_name) as session:
result = session.run(query, today=today)
record = result.single()
return dict(record) if record else None
except Exception as e:
logger.error(f"Error getting today's calendar node: {str(e)}")
return None
def get_relative_calendar_node(day_offset: int, db_name: str) -> Optional[Dict[str, Any]]:
"""Get calendar node relative to today."""
target_date = (datetime.now() + timedelta(days=day_offset)).strftime("%Y-%m-%d")
query = """
MATCH (n:Calendar)
WHERE date($target_date) >= date(n.start_date) AND date($target_date) <= date(n.end_date)
RETURN n.uuid_string as id, n.node_storage_path as path, n.name as label,
'Calendar' as type
LIMIT 1
"""
try:
with driver_tools.get_session(database=db_name) as session:
result = session.run(query, target_date=target_date)
record = result.single()
return dict(record) if record else None
except Exception as e:
logger.error(f"Error getting relative calendar node: {str(e)}")
return None
def get_next_month_node(db_name: str) -> Optional[Dict[str, Any]]:
"""Get next month's calendar node."""
next_month_start = (datetime.now().replace(day=1) + timedelta(days=32)).replace(day=1).strftime("%Y-%m-%d")
query = """
MATCH (n:Calendar)
WHERE date($next_month_start) >= date(n.start_date) AND date($next_month_start) <= date(n.end_date)
RETURN n.uuid_string as id, n.node_storage_path as path, n.name as label,
'Calendar' as type
LIMIT 1
"""
try:
with driver_tools.get_session(database=db_name) as session:
result = session.run(query, next_month_start=next_month_start)
record = result.single()
return dict(record) if record else None
except Exception as e:
logger.error(f"Error getting next month node: {str(e)}")
return None
def get_previous_month_node(db_name: str) -> Optional[Dict[str, Any]]:
"""Get previous month's calendar node."""
prev_month_start = (datetime.now().replace(day=1) - timedelta(days=1)).replace(day=1).strftime("%Y-%m-%d")
query = """
MATCH (n:Calendar)
WHERE date($prev_month_start) >= date(n.start_date) AND date($prev_month_start) <= date(n.end_date)
RETURN n.uuid_string as id, n.node_storage_path as path, n.name as label,
'Calendar' as type
LIMIT 1
"""
try:
with driver_tools.get_session(database=db_name) as session:
result = session.run(query, prev_month_start=prev_month_start)
record = result.single()
return dict(record) if record else None
except Exception as e:
logger.error(f"Error getting previous month node: {str(e)}")
return None
def get_user_timetables(db_name: str) -> List[Dict[str, Any]]:
"""Get user's timetables."""
query = """
MATCH (t:UserTeacherTimetable)
RETURN t.uuid_string as id, t.node_storage_path as path, t.name as label,
'UserTeacherTimetable' as type
"""
try:
with driver_tools.get_session(database=db_name) as session:
result = session.run(query)
return [dict(record) for record in result]
except Exception as e:
logger.error(f"Error getting user timetables: {str(e)}")
return []
def get_timetable_classes(timetable_id: str, db_name: str) -> List[Dict[str, Any]]:
"""Get classes for a timetable."""
query = """
MATCH (t:UserTeacherTimetable {uuid_string: $timetable_id})-[:HAS_CLASS]->(c:Class)
RETURN c.uuid_string as id, c.node_storage_path as path, c.name as label,
'Class' as type
"""
try:
with driver_tools.get_session(database=db_name) as session:
result = session.run(query, timetable_id=timetable_id)
return [dict(record) for record in result]
except Exception as e:
logger.error(f"Error getting timetable classes: {str(e)}")
return []
def get_next_lesson(class_id: str, db_name: str) -> Optional[Dict[str, Any]]:
"""Get next lesson for a class."""
now = datetime.now().strftime("%Y-%m-%d %H:%M")
query = """
MATCH (c:Class {uuid_string: $class_id})-[:HAS_LESSON]->(l:Lesson)
WHERE l.start_time > $now
RETURN l.uuid_string as id, l.node_storage_path as path, l.name as label,
'Lesson' as type
ORDER BY l.start_time ASC
LIMIT 1
"""
try:
with driver_tools.get_session(database=db_name) as session:
result = session.run(query, class_id=class_id, now=now)
record = result.single()
return dict(record) if record else None
except Exception as e:
logger.error(f"Error getting next lesson: {str(e)}")
return None
def get_previous_lesson(class_id: str, db_name: str) -> Optional[Dict[str, Any]]:
"""Get previous lesson for a class."""
now = datetime.now().strftime("%Y-%m-%d %H:%M")
query = """
MATCH (c:Class {uuid_string: $class_id})-[:HAS_LESSON]->(l:Lesson)
WHERE l.start_time < $now
RETURN l.uuid_string as id, l.path as path, l.name as label,
'Lesson' as type
ORDER BY l.start_time DESC
LIMIT 1
"""
try:
with driver_tools.get_session(database=db_name) as session:
result = session.run(query, class_id=class_id, now=now)
record = result.single()
return dict(record) if record else None
except Exception as e:
logger.error(f"Error getting previous lesson: {str(e)}")
return None
def save_shared_snapshot(path: str, room_id: str, snapshot: Dict[str, Any]) -> bool:
"""Save snapshot to a shared room."""
try:
# Save the snapshot to the shared room's storage
session_tools.save_tldraw_node_file(path, room_id, snapshot)
return True
except Exception as e:
logger.error(f"Error saving shared snapshot: {str(e)}")
return False
def get_connected_nodes_for_workers(node_id: str, db_name: str) -> List[Dict[str, Any]]:
"""Get connected nodes specific to the workers context."""
query = """
MATCH (n {uuid_string: $node_id})
WITH n
CALL {
WITH n
MATCH (n:UserTeacherTimetable)-[:HAS_CLASS]->(c:Class)
RETURN c.uuid_string as id, c.node_storage_path as path, c.name as label,
'Class' as type
UNION
MATCH (n:Class)<-[:HAS_CLASS]-(t:UserTeacherTimetable)
RETURN t.uuid_string as id, t.node_storage_path as path, t.name as label,
'UserTeacherTimetable' as type
UNION
MATCH (n:Class)-[:HAS_LESSON]->(l:Lesson)
RETURN l.uuid_string as id, l.node_storage_path as path, l.name as label,
'Lesson' as type
}
RETURN DISTINCT id, path, label, type
"""
try:
with driver_tools.get_session(database=db_name) as session:
result = session.run(query, node_id=node_id)
return [dict(record) for record in result]
except Exception as e:
logger.error(f"Error getting connected nodes for workers: {str(e)}")
return []
def get_connected_nodes(node_id: str, db_name: str, context: str = None) -> List[Dict[str, Any]]:
"""Get connected nodes based on context."""
if context == 'workers':
return get_connected_nodes_for_workers(node_id, db_name)
# Default query for other contexts
query = """
MATCH (n {uuid_string: $node_id})-[r]-(connected)
RETURN DISTINCT connected.uuid_string as id, connected.path as path,
connected.name as label, labels(connected)[0] as type
"""
try:
with driver_tools.get_session(database=db_name) as session:
result = session.run(query, node_id=node_id)
return [dict(record) for record in result]
except Exception as e:
logger.error(f"Error getting connected nodes: {str(e)}")
return []
## Worker Navigation
def get_worker_structure(db_name: str) -> Dict[str, Any]:
"""Get the complete worker structure including schools, departments, timetables, classes, and lessons."""
try:
query = """
// Match all worker-related nodes
MATCH (s:School)
OPTIONAL MATCH (s)-[:HAS_DEPARTMENT]->(d:Department)
OPTIONAL MATCH (d)-[:HAS_TIMETABLE]->(t:UserTeacherTimetable)
OPTIONAL MATCH (t)-[:HAS_CLASS]->(c:Class)
OPTIONAL MATCH (c)-[:HAS_LESSON]->(l:TimetableLesson)
WITH s, d, t, c, l
ORDER BY s.school_name, d.department_code, t.name, c.class_code, l.start_time
// Collect all nodes
RETURN {
schools: collect(DISTINCT {
id: s.uuid_string,
path: s.node_storage_path,
name: s.school_name,
__primarylabel__: 'School'
}),
departments: collect(DISTINCT {
id: d.uuid_string,
path: d.node_storage_path,
code: d.department_code,
school_id: s.uuid_string,
__primarylabel__: 'Department'
}),
timetables: collect(DISTINCT {
id: t.uuid_string,
path: t.node_storage_path,
name: t.name,
department_id: d.uuid_string,
__primarylabel__: 'UserTeacherTimetable'
}),
classes: collect(DISTINCT {
id: c.uuid_string,
path: c.node_storage_path,
code: c.class_code,
timetable_id: t.uuid_string,
__primarylabel__: 'Class'
}),
lessons: collect(DISTINCT {
id: l.uuid_string,
path: l.node_storage_path,
start_time: l.start_time,
class_id: c.uuid_string,
__primarylabel__: 'TimetableLesson'
})
} as structure
"""
with driver_tools.get_session(database=db_name) as session:
result = session.run(query)
record = result.single()
if not record:
logger.error('No worker structure found')
return None
return {
"status": "success",
"structure": record["structure"]
}
except Exception as e:
logger.error(f"Error getting worker structure: {str(e)}")
return None
def get_school_node(school_id: str, db_name: str) -> Optional[Dict[str, Any]]:
"""Get a specific school node."""
query = """
MATCH (s:School {uuid_string: $school_id})
RETURN {
id: s.uuid_string,
path: s.node_storage_path,
name: s.school_name,
__primarylabel__: 'School'
} as node
"""
try:
with driver_tools.get_session(database=db_name) as session:
result = session.run(query, school_id=school_id)
record = result.single()
return record["node"] if record else None
except Exception as e:
logger.error(f"Error getting school node: {str(e)}")
return None
def get_department_node(dept_id: str, db_name: str) -> Optional[Dict[str, Any]]:
"""Get a specific department node."""
query = """
MATCH (d:Department {uuid_string: $dept_id})
RETURN {
id: d.uuid_string,
path: d.node_storage_path,
code: d.department_code,
__primarylabel__: 'Department'
} as node
"""
try:
with driver_tools.get_session(database=db_name) as session:
result = session.run(query, dept_id=dept_id)
record = result.single()
return record["node"] if record else None
except Exception as e:
logger.error(f"Error getting department node: {str(e)}")
return None
def get_timetable_node(timetable_id: str, db_name: str) -> Optional[Dict[str, Any]]:
"""Get a specific timetable node."""
query = """
MATCH (t:UserTeacherTimetable {uuid_string: $timetable_id})
RETURN {
id: t.uuid_string,
path: t.node_storage_path,
name: t.name,
__primarylabel__: 'UserTeacherTimetable'
} as node
"""
try:
with driver_tools.get_session(database=db_name) as session:
result = session.run(query, timetable_id=timetable_id)
record = result.single()
return record["node"] if record else None
except Exception as e:
logger.error(f"Error getting timetable node: {str(e)}")
return None
def get_class_node(class_id: str, db_name: str) -> Optional[Dict[str, Any]]:
"""Get a specific class node."""
query = """
MATCH (c:Class {uuid_string: $class_id})
RETURN {
id: c.uuid_string,
path: c.node_storage_path,
code: c.class_code,
__primarylabel__: 'Class'
} as node
"""
try:
with driver_tools.get_session(database=db_name) as session:
result = session.run(query, class_id=class_id)
record = result.single()
return record["node"] if record else None
except Exception as e:
logger.error(f"Error getting class node: {str(e)}")
return None
def get_lesson_node(lesson_id: str, db_name: str) -> Optional[Dict[str, Any]]:
"""Get a specific lesson node."""
query = """
MATCH (l:TimetableLesson {uuid_string: $lesson_id})
RETURN {
id: l.uuid_string,
path: l.node_storage_path,
start_time: l.start_time,
__primarylabel__: 'TimetableLesson'
} as node
"""
try:
with driver_tools.get_session(database=db_name) as session:
result = session.run(query, lesson_id=lesson_id)
record = result.single()
return record["node"] if record else None
except Exception as e:
logger.error(f"Error getting lesson node: {str(e)}")
return None
def get_current_lesson(db_name: str) -> Optional[Dict[str, Any]]:
"""Get the current or next upcoming lesson."""
now = datetime.now().strftime("%Y-%m-%d %H:%M")
query = """
MATCH (l:TimetableLesson)
WHERE l.start_time >= $now
RETURN {
id: l.uuid_string,
path: l.node_storage_path,
start_time: l.start_time,
__primarylabel__: 'TimetableLesson'
} as node
ORDER BY l.start_time ASC
LIMIT 1
"""
try:
with driver_tools.get_session(database=db_name) as session:
result = session.run(query, now=now)
record = result.single()
return record["node"] if record else None
except Exception as e:
logger.error(f"Error getting current lesson: {str(e)}")
return None