Initial commit
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,193 @@
|
||||
import os
|
||||
from typing import Dict, List, Optional
|
||||
from supabase import create_client
|
||||
from modules.logger_tool import initialise_logger
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class AdminProfileBase(BaseModel):
|
||||
email: str
|
||||
display_name: Optional[str] = None
|
||||
admin_role: Optional[str] = "admin"
|
||||
is_super_admin: Optional[bool] = False
|
||||
metadata: Optional[dict] = {}
|
||||
|
||||
|
||||
class AdminService:
|
||||
def __init__(self):
|
||||
self.logger = initialise_logger(
|
||||
__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), "default", True
|
||||
)
|
||||
|
||||
# Initialize Supabase client with service role key
|
||||
supabase_url = os.getenv("SUPABASE_URL")
|
||||
service_role_key = os.getenv("SERVICE_ROLE_KEY")
|
||||
|
||||
self.supabase = create_client(supabase_url, service_role_key)
|
||||
|
||||
# Set headers for admin operations
|
||||
self.supabase.headers = {
|
||||
"apiKey": service_role_key,
|
||||
"Authorization": f"Bearer {service_role_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
def get_admin_profile(self, admin_id: str) -> Optional[Dict]:
|
||||
"""Get admin profile by ID"""
|
||||
try:
|
||||
self.logger.info(f"Getting admin profile for ID: {admin_id}")
|
||||
result = (
|
||||
self.supabase.table("admin_profiles")
|
||||
.select("*")
|
||||
.eq("id", admin_id)
|
||||
.single()
|
||||
.execute()
|
||||
)
|
||||
return result.data if result else None
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting admin profile: {str(e)}")
|
||||
raise
|
||||
|
||||
def list_admins(self) -> List[Dict]:
|
||||
"""List all admin profiles"""
|
||||
try:
|
||||
self.logger.info("Listing all admin profiles")
|
||||
result = self.supabase.table("admin_profiles").select("*").execute()
|
||||
return result.data if result else []
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error listing admins: {str(e)}")
|
||||
raise
|
||||
|
||||
def create_admin(self, admin_data: AdminProfileBase, current_admin: Dict) -> Dict:
|
||||
"""Create a new admin profile"""
|
||||
try:
|
||||
# Verify super admin status
|
||||
if not current_admin.get("is_super_admin"):
|
||||
raise Exception("Only super admins can create new admins")
|
||||
|
||||
self.logger.info(
|
||||
f"Creating new admin profile for email: {admin_data.email}"
|
||||
)
|
||||
|
||||
# Create auth user first
|
||||
auth_user = self.supabase.auth.admin.create_user(
|
||||
{
|
||||
"email": admin_data.email,
|
||||
"email_confirm": True,
|
||||
"user_metadata": {"is_admin": True},
|
||||
}
|
||||
)
|
||||
|
||||
if not auth_user:
|
||||
raise Exception("Failed to create auth user")
|
||||
|
||||
# Create admin profile
|
||||
profile_data = admin_data.dict()
|
||||
profile_data["id"] = auth_user.id
|
||||
|
||||
result = (
|
||||
self.supabase.table("admin_profiles").insert(profile_data).execute()
|
||||
)
|
||||
return result.data[0] if result else None
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating admin: {str(e)}")
|
||||
raise
|
||||
|
||||
def update_admin(
|
||||
self, admin_id: str, admin_data: AdminProfileBase, current_admin: Dict
|
||||
) -> Dict:
|
||||
"""Update an admin profile"""
|
||||
try:
|
||||
# Verify super admin status for certain operations
|
||||
if admin_data.is_super_admin and not current_admin.get("is_super_admin"):
|
||||
raise Exception("Only super admins can modify super admin status")
|
||||
|
||||
self.logger.info(f"Updating admin profile for ID: {admin_id}")
|
||||
result = (
|
||||
self.supabase.table("admin_profiles")
|
||||
.update(admin_data.dict())
|
||||
.eq("id", admin_id)
|
||||
.execute()
|
||||
)
|
||||
return result.data[0] if result else None
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error updating admin: {str(e)}")
|
||||
raise
|
||||
|
||||
def delete_admin(self, admin_id: str, current_admin: Dict) -> None:
|
||||
"""Delete an admin profile"""
|
||||
try:
|
||||
# Verify super admin status
|
||||
if not current_admin.get("is_super_admin"):
|
||||
raise Exception("Only super admins can delete admins")
|
||||
|
||||
# Get admin profile to check if it's a super admin
|
||||
admin_profile = self.get_admin_profile(admin_id)
|
||||
if admin_profile and admin_profile.get("is_super_admin"):
|
||||
raise Exception("Cannot delete super admin accounts")
|
||||
|
||||
self.logger.info(f"Deleting admin profile for ID: {admin_id}")
|
||||
|
||||
# Delete auth user
|
||||
self.supabase.auth.admin.delete_user(admin_id)
|
||||
|
||||
# Delete admin profile
|
||||
self.supabase.table("admin_profiles").delete().eq("id", admin_id).execute()
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error deleting admin: {str(e)}")
|
||||
raise
|
||||
|
||||
def setup_super_admin(self, admin_data: dict) -> Dict:
|
||||
"""Set up the initial super admin account"""
|
||||
try:
|
||||
self.logger.info(f"Setting up super admin for email: {admin_data['email']}")
|
||||
|
||||
# Check if any super admin exists
|
||||
existing_super_admin = (
|
||||
self.supabase.table("admin_profiles")
|
||||
.select("*")
|
||||
.eq("is_super_admin", True)
|
||||
.execute()
|
||||
)
|
||||
if existing_super_admin.data:
|
||||
raise Exception("Super admin already exists")
|
||||
|
||||
# Create the auth user first
|
||||
auth_user = self.supabase.auth.admin.create_user(
|
||||
{
|
||||
"email": admin_data["email"],
|
||||
"password": admin_data["password"],
|
||||
"email_confirm": True,
|
||||
"user_metadata": {"is_admin": True, "is_super_admin": True},
|
||||
}
|
||||
)
|
||||
|
||||
if not auth_user:
|
||||
raise Exception("Failed to create auth user")
|
||||
|
||||
# Update user metadata
|
||||
self.supabase.auth.admin.update_user_by_id(
|
||||
auth_user.user.id,
|
||||
{"user_metadata": {"is_admin": True, "is_super_admin": True}},
|
||||
)
|
||||
|
||||
# Create super admin profile
|
||||
profile_data = {
|
||||
"id": auth_user.user.id,
|
||||
"email": admin_data["email"],
|
||||
"display_name": admin_data.get("display_name", "Super Admin"),
|
||||
"admin_role": "super_admin",
|
||||
"is_super_admin": True,
|
||||
}
|
||||
|
||||
result = (
|
||||
self.supabase.table("admin_profiles").insert(profile_data).execute()
|
||||
)
|
||||
return result.data[0] if result else None
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error setting up super admin: {str(e)}")
|
||||
raise
|
||||
@@ -0,0 +1,100 @@
|
||||
import os
|
||||
from typing import Dict, Optional
|
||||
from fastapi import HTTPException
|
||||
from supabase import create_client, Client
|
||||
from modules.logger_tool import initialise_logger
|
||||
|
||||
logger = initialise_logger(
|
||||
__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), "default", True
|
||||
)
|
||||
|
||||
|
||||
class AuthService:
|
||||
def __init__(self):
|
||||
"""Initialize the AuthService with Supabase clients"""
|
||||
self.supabase_url = os.getenv("SUPABASE_URL")
|
||||
self.anon_key = os.getenv("ANON_KEY")
|
||||
self.service_role_key = os.getenv("SERVICE_ROLE_KEY")
|
||||
|
||||
# Create clients with different access levels
|
||||
self.supabase: Client = create_client(self.supabase_url, self.anon_key)
|
||||
self.admin_supabase: Client = create_client(
|
||||
self.supabase_url, self.service_role_key
|
||||
)
|
||||
|
||||
async def verify_admin(self, session_token: str) -> Dict:
|
||||
"""Verify that the user is an admin and has necessary permissions"""
|
||||
try:
|
||||
if not session_token:
|
||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||
|
||||
# Verify session with Supabase
|
||||
user = self.admin_supabase.auth.get_user(session_token)
|
||||
if not user:
|
||||
raise HTTPException(status_code=401, detail="Invalid session")
|
||||
|
||||
# Get admin profile
|
||||
admin = (
|
||||
self.admin_supabase.table("admin_profiles")
|
||||
.select("*")
|
||||
.eq("id", user.user.id)
|
||||
.single()
|
||||
.execute()
|
||||
)
|
||||
if not admin.data:
|
||||
raise HTTPException(status_code=403, detail="Not an admin")
|
||||
|
||||
return admin.data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error verifying admin: {str(e)}")
|
||||
raise HTTPException(status_code=401, detail="Authentication failed")
|
||||
|
||||
async def check_super_admin_exists(self) -> bool:
|
||||
"""Check if any super admin exists in the system"""
|
||||
try:
|
||||
result = (
|
||||
self.admin_supabase.table("admin_profiles")
|
||||
.select("*")
|
||||
.eq("is_super_admin", True)
|
||||
.execute()
|
||||
)
|
||||
return bool(result.data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking super admin: {str(e)}")
|
||||
return False
|
||||
|
||||
async def login_admin(self, email: str, password: str) -> Dict:
|
||||
"""Handle admin login and return session data"""
|
||||
try:
|
||||
# Attempt login with Supabase
|
||||
auth_response = self.supabase.auth.sign_in_with_password(
|
||||
{"email": email, "password": password}
|
||||
)
|
||||
|
||||
if not auth_response.user:
|
||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||
|
||||
# Verify admin status
|
||||
admin = (
|
||||
self.admin_supabase.table("admin_profiles")
|
||||
.select("*")
|
||||
.eq("id", auth_response.user.id)
|
||||
.single()
|
||||
.execute()
|
||||
)
|
||||
if not admin.data:
|
||||
raise HTTPException(status_code=403, detail="Not authorized as admin")
|
||||
|
||||
return {
|
||||
"access_token": auth_response.session.access_token,
|
||||
"admin": admin.data,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Login error: {str(e)}")
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
|
||||
|
||||
# Create a singleton instance
|
||||
auth_service = AuthService()
|
||||
@@ -0,0 +1,67 @@
|
||||
import os
|
||||
from typing import Dict, Any
|
||||
from modules.logger_tool import initialise_logger
|
||||
import modules.database.tools.neo4j_driver_tools as driver_tools
|
||||
from modules.database.admin.neontology_provider import NeontologyProvider
|
||||
from modules.database.admin.graph_provider import GraphNamingProvider
|
||||
|
||||
class GraphService:
|
||||
def __init__(self):
|
||||
self.logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True)
|
||||
self.driver = driver_tools.get_driver()
|
||||
self.neontology = NeontologyProvider()
|
||||
self.graph_naming = GraphNamingProvider()
|
||||
|
||||
def check_schema_status(self, database_name: str = "neo4j") -> Dict[str, Any]:
|
||||
"""Check the status of Neo4j schema including constraints, indexes, and labels"""
|
||||
try:
|
||||
with self.driver.session(database=database_name) as session:
|
||||
# Check constraints
|
||||
constraints_result = session.run("SHOW CONSTRAINTS")
|
||||
constraints = list(constraints_result)
|
||||
|
||||
# Check indexes
|
||||
indexes_result = session.run("SHOW INDEXES")
|
||||
indexes = list(indexes_result)
|
||||
|
||||
# Check labels
|
||||
labels_result = session.run("CALL db.labels()")
|
||||
labels = list(labels_result)
|
||||
|
||||
return {
|
||||
"constraints_count": len(constraints),
|
||||
"indexes_count": len(indexes),
|
||||
"labels_count": len(labels),
|
||||
"constraints": [dict(record) for record in constraints],
|
||||
"indexes": [dict(record) for record in indexes],
|
||||
"labels": [dict(record) for record in labels]
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error checking schema status: {str(e)}")
|
||||
return {
|
||||
"constraints_count": 0,
|
||||
"indexes_count": 0,
|
||||
"labels_count": 0,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def initialize_schema(self, database_name: str = "neo4j") -> Dict[str, Any]:
|
||||
"""Initialize Neo4j schema with required constraints and indexes"""
|
||||
try:
|
||||
schema_queries = self.graph_naming.get_schema_creation_queries()
|
||||
|
||||
with self.driver.session(database=database_name) as session:
|
||||
for query in schema_queries:
|
||||
session.run(query)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Schema initialized successfully",
|
||||
"details": self.check_schema_status(database_name)
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error initializing schema: {str(e)}")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": str(e)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
from datetime import datetime, timedelta
|
||||
import jwt
|
||||
from typing import Dict, List
|
||||
|
||||
class JWTService:
|
||||
"""JWT Service for Neo4j authentication
|
||||
|
||||
TODO: Security Enhancements Needed
|
||||
- Implement token refresh mechanism
|
||||
- Add token revocation capability
|
||||
- Add token validation checks
|
||||
- Implement rate limiting
|
||||
- Add audit logging for token generation/usage
|
||||
- Consider reducing token expiry time and implementing refresh tokens
|
||||
"""
|
||||
|
||||
def __init__(self, secret_key: str, algorithm: str = "HS256"):
|
||||
self.secret_key = secret_key
|
||||
self.algorithm = algorithm
|
||||
|
||||
def generate_neo4j_token(self, user_data: Dict) -> str:
|
||||
"""Generate JWT token for Neo4j database access"""
|
||||
payload = {
|
||||
"sub": user_data["email"],
|
||||
"roles": self._get_neo4j_roles(user_data["user_type"]),
|
||||
"iss": "supabase",
|
||||
"aud": "neo4j",
|
||||
"iat": datetime.utcnow(),
|
||||
"exp": datetime.utcnow() + timedelta(hours=24)
|
||||
}
|
||||
|
||||
if "school_uuid" in user_data:
|
||||
payload["worker_db_name"] = f"cc.institutes.{user_data['school_uuid']}"
|
||||
|
||||
return jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
|
||||
|
||||
def _get_neo4j_roles(self, user_type: str) -> List[str]:
|
||||
"""Map user types to Neo4j roles"""
|
||||
role_mapping = {
|
||||
"cc_admin": ["admin", "reader", "writer"],
|
||||
"developer": ["developer", "reader", "writer"],
|
||||
"email_teacher": ["teacher", "reader", "writer"],
|
||||
"email_student": ["student", "reader"]
|
||||
}
|
||||
return role_mapping.get(user_type, ["reader"])
|
||||
@@ -0,0 +1,198 @@
|
||||
import os
|
||||
from typing import Dict, Any
|
||||
from modules.logger_tool import initialise_logger
|
||||
import modules.database.tools.neo4j_driver_tools as driver_tools
|
||||
import modules.database.tools.neo4j_session_tools as session_tools
|
||||
|
||||
class Neo4jService:
|
||||
"""Service for managing Neo4j database operations"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True)
|
||||
self.driver = driver_tools.get_driver()
|
||||
|
||||
def check_database_exists(self, database_name: str) -> Dict[str, Any]:
|
||||
"""Check if a Neo4j database exists
|
||||
|
||||
Args:
|
||||
database_name (str): Name of the database to check
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Result containing existence status and operation status
|
||||
"""
|
||||
try:
|
||||
with self.driver.session() as session:
|
||||
result = session.run(
|
||||
"SHOW DATABASES YIELD name WHERE name = $name",
|
||||
name=database_name
|
||||
)
|
||||
exists = bool(result.single())
|
||||
return {
|
||||
"exists": exists,
|
||||
"status": "success"
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error checking database {database_name}: {str(e)}")
|
||||
return {
|
||||
"exists": False,
|
||||
"status": "error",
|
||||
"message": str(e)
|
||||
}
|
||||
|
||||
def create_database(self, db_name: str) -> Dict[str, Any]:
|
||||
"""Creates a Neo4j database with the given name
|
||||
|
||||
Args:
|
||||
db_name (str): Name of the database to create
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Result containing operation status and message
|
||||
"""
|
||||
try:
|
||||
# First check if database exists
|
||||
exists_result = self.check_database_exists(db_name)
|
||||
if exists_result["status"] == "error":
|
||||
return exists_result
|
||||
|
||||
if not exists_result["exists"]:
|
||||
with self.driver.session() as session:
|
||||
session_tools.create_database(session, db_name)
|
||||
self.logger.info(f"Created database {db_name}")
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Database {db_name} created successfully"
|
||||
}
|
||||
else:
|
||||
self.logger.info(f"Database {db_name} already exists")
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Database {db_name} already exists"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating database {db_name}: {str(e)}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
def initialize_schema(self, database_name: str) -> Dict[str, Any]:
|
||||
"""Initialize Neo4j schema (constraints and indexes) for a database
|
||||
|
||||
Args:
|
||||
database_name (str): Name of the database to initialize schema for
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Result containing operation status and message
|
||||
"""
|
||||
try:
|
||||
with self.driver.session(database=database_name) as session:
|
||||
# Create constraints
|
||||
constraints = [
|
||||
"CREATE CONSTRAINT IF NOT EXISTS FOR (n:School) REQUIRE n.unique_id IS UNIQUE",
|
||||
"CREATE CONSTRAINT IF NOT EXISTS FOR (n:Department) REQUIRE n.unique_id IS UNIQUE",
|
||||
"CREATE CONSTRAINT IF NOT EXISTS FOR (n:Subject) REQUIRE n.unique_id IS UNIQUE",
|
||||
"CREATE CONSTRAINT IF NOT EXISTS FOR (n:YearGroup) REQUIRE n.unique_id IS UNIQUE",
|
||||
"CREATE CONSTRAINT IF NOT EXISTS FOR (n:Class) REQUIRE n.unique_id IS UNIQUE",
|
||||
"CREATE CONSTRAINT IF NOT EXISTS FOR (n:Teacher) REQUIRE n.unique_id IS UNIQUE",
|
||||
"CREATE CONSTRAINT IF NOT EXISTS FOR (n:Student) REQUIRE n.unique_id IS UNIQUE",
|
||||
"CREATE CONSTRAINT IF NOT EXISTS FOR (n:Calendar) REQUIRE n.unique_id IS UNIQUE",
|
||||
"CREATE CONSTRAINT IF NOT EXISTS FOR (n:Term) REQUIRE n.unique_id IS UNIQUE",
|
||||
"CREATE CONSTRAINT IF NOT EXISTS FOR (n:Week) REQUIRE n.unique_id IS UNIQUE",
|
||||
"CREATE CONSTRAINT IF NOT EXISTS FOR (n:Day) REQUIRE n.unique_id IS UNIQUE",
|
||||
"CREATE CONSTRAINT IF NOT EXISTS FOR (n:Period) REQUIRE n.unique_id IS UNIQUE"
|
||||
]
|
||||
|
||||
# Create indexes
|
||||
indexes = [
|
||||
"CREATE INDEX IF NOT EXISTS FOR (n:School) ON (n.urn)",
|
||||
"CREATE INDEX IF NOT EXISTS FOR (n:Department) ON (n.department_name)",
|
||||
"CREATE INDEX IF NOT EXISTS FOR (n:Subject) ON (n.subject_name)",
|
||||
"CREATE INDEX IF NOT EXISTS FOR (n:YearGroup) ON (n.year_group)",
|
||||
"CREATE INDEX IF NOT EXISTS FOR (n:Class) ON (n.class_name)",
|
||||
"CREATE INDEX IF NOT EXISTS FOR (n:Teacher) ON (n.email)",
|
||||
"CREATE INDEX IF NOT EXISTS FOR (n:Student) ON (n.email)",
|
||||
"CREATE INDEX IF NOT EXISTS FOR (n:Calendar) ON (n.calendar_name)",
|
||||
"CREATE INDEX IF NOT EXISTS FOR (n:Term) ON (n.term_name)",
|
||||
"CREATE INDEX IF NOT EXISTS FOR (n:Week) ON (n.week_number)",
|
||||
"CREATE INDEX IF NOT EXISTS FOR (n:Day) ON (n.date)",
|
||||
"CREATE INDEX IF NOT EXISTS FOR (n:Period) ON (n.period_name)"
|
||||
]
|
||||
|
||||
# Execute all constraints
|
||||
for constraint in constraints:
|
||||
session.run(constraint)
|
||||
|
||||
# Execute all indexes
|
||||
for index in indexes:
|
||||
session.run(index)
|
||||
|
||||
self.logger.info(f"Successfully initialized schema for database {database_name}")
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Schema initialized successfully for database {database_name}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error initializing schema for database {database_name}: {str(e)}")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": str(e)
|
||||
}
|
||||
|
||||
def delete_database(self, db_name: str) -> Dict[str, Any]:
|
||||
"""Deletes a Neo4j database
|
||||
|
||||
Args:
|
||||
db_name (str): Name of the database to delete
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Result containing operation status and message
|
||||
"""
|
||||
try:
|
||||
exists_result = self.check_database_exists(db_name)
|
||||
if exists_result["status"] == "error":
|
||||
return exists_result
|
||||
|
||||
if exists_result["exists"]:
|
||||
with self.driver.session() as session:
|
||||
session_tools.reset_database_in_session(session)
|
||||
self.logger.info(f"Deleted database {db_name}")
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Database {db_name} deleted successfully"
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Database {db_name} does not exist"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error deleting database {db_name}: {str(e)}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
def check_node_exists(self, database_name: str, node_label: str) -> Dict[str, Any]:
|
||||
"""Check if any nodes with the given label exist in the specified database
|
||||
|
||||
Args:
|
||||
database_name (str): Name of the database to check
|
||||
node_label (str): Label of the node type to check for
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Result containing count and operation status
|
||||
"""
|
||||
try:
|
||||
with self.driver.session(database=database_name) as session:
|
||||
nodes = session_tools.find_nodes_by_label(session, node_label)
|
||||
count = len(nodes)
|
||||
return {
|
||||
"exists": count > 0,
|
||||
"count": count,
|
||||
"status": "success"
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error checking for {node_label} nodes in database {database_name}: {str(e)}")
|
||||
return {
|
||||
"exists": False,
|
||||
"count": 0,
|
||||
"status": "error",
|
||||
"message": str(e)
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
import os
|
||||
from typing import Dict, Any, BinaryIO
|
||||
import json
|
||||
import pandas as pd
|
||||
|
||||
import modules.database.tools.neo4j_driver_tools as driver_tools
|
||||
import modules.database.tools.neo4j_session_tools as session_tools
|
||||
import modules.database.schemas.nodes.schools.schools as school_nodes
|
||||
import modules.database.schemas.nodes.schools.curriculum as curriculum_nodes
|
||||
import modules.database.schemas.nodes.schools.pastoral as pastoral_nodes
|
||||
import modules.database.schemas.nodes.structures.schools as school_structures
|
||||
from modules.database.schemas.entities import entities
|
||||
from modules.database.schemas.relationships import curriculum_relationships, entity_relationships, entity_curriculum_rels
|
||||
from modules.database.admin.neontology_provider import NeontologyProvider
|
||||
from modules.database.admin.graph_provider import GraphNamingProvider
|
||||
from modules.database.supabase.utils.client import SupabaseAnonClient
|
||||
from modules.database.supabase.utils.storage import StorageManager
|
||||
from modules.database.services.neo4j_service import Neo4jService
|
||||
from modules.logger_tool import initialise_logger
|
||||
|
||||
class SchoolAdminService:
|
||||
def __init__(self):
|
||||
self.logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True)
|
||||
self.driver = driver_tools.get_driver()
|
||||
self.neontology = NeontologyProvider()
|
||||
self.graph_naming = GraphNamingProvider()
|
||||
self.storage = StorageManager(SupabaseAnonClient)
|
||||
self.neo4j_service = Neo4jService()
|
||||
|
||||
def check_database_exists(self, database_name: str) -> Dict[str, Any]:
|
||||
"""Check if a Neo4j database exists"""
|
||||
return self.neo4j_service.check_database_exists(database_name)
|
||||
|
||||
def create_database(self, db_name: str) -> Dict:
|
||||
"""Creates a Neo4j database with the given name"""
|
||||
return self.neo4j_service.create_database(db_name)
|
||||
|
||||
def create_school_node(self, school_data: Dict) -> Dict:
|
||||
"""Creates a school node in cc.institutes database and stores TLDraw file in Supabase"""
|
||||
try:
|
||||
# Convert school data to SchoolNode
|
||||
school_unique_id = self.graph_naming.get_school_unique_id(school_data['urn'])
|
||||
school_path = self.graph_naming.get_school_path("cc.institutes", school_data['urn'])
|
||||
|
||||
school_node = entities.SchoolNode(
|
||||
unique_id=school_unique_id,
|
||||
path=school_path,
|
||||
urn=school_data['urn'],
|
||||
establishment_number=school_data['establishment_number'],
|
||||
establishment_name=school_data['establishment_name'],
|
||||
establishment_type=school_data['establishment_type'],
|
||||
establishment_status=school_data['establishment_status'],
|
||||
phase_of_education=school_data['phase_of_education'] if school_data['phase_of_education'] not in [None, ''] else None,
|
||||
statutory_low_age=int(school_data['statutory_low_age']) if school_data.get('statutory_low_age') is not None else 0,
|
||||
statutory_high_age=int(school_data['statutory_high_age']) if school_data.get('statutory_high_age') is not None else 0,
|
||||
religious_character=school_data.get('religious_character') if school_data.get('religious_character') not in [None, ''] else None,
|
||||
school_capacity=int(school_data['school_capacity']) if school_data.get('school_capacity') is not None else 0,
|
||||
school_website=school_data.get('school_website', ''),
|
||||
ofsted_rating=school_data.get('ofsted_rating') if school_data.get('ofsted_rating') not in [None, ''] else None
|
||||
)
|
||||
|
||||
# Create default tldraw file data
|
||||
tldraw_data = {
|
||||
"document": {
|
||||
"version": 1,
|
||||
"id": school_data['urn'],
|
||||
"name": school_data['establishment_name'],
|
||||
"meta": {
|
||||
"created_at": "",
|
||||
"updated_at": "",
|
||||
"creator_id": "",
|
||||
"is_template": False,
|
||||
"is_snapshot": False,
|
||||
"is_draft": False,
|
||||
"template_id": None,
|
||||
"snapshot_id": None,
|
||||
"draft_id": None
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"schemaVersion": 1,
|
||||
"storeVersion": 4,
|
||||
"recordVersions": {
|
||||
"asset": {
|
||||
"version": 1,
|
||||
"subTypeKey": "type",
|
||||
"subTypeVersions": {}
|
||||
},
|
||||
"camera": {
|
||||
"version": 1
|
||||
},
|
||||
"document": {
|
||||
"version": 2
|
||||
},
|
||||
"instance": {
|
||||
"version": 22
|
||||
},
|
||||
"instance_page_state": {
|
||||
"version": 5
|
||||
},
|
||||
"page": {
|
||||
"version": 1
|
||||
},
|
||||
"shape": {
|
||||
"version": 3,
|
||||
"subTypeKey": "type",
|
||||
"subTypeVersions": {
|
||||
"cc-school-node": 1
|
||||
}
|
||||
},
|
||||
"instance_presence": {
|
||||
"version": 5
|
||||
},
|
||||
"pointer": {
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"store": {
|
||||
"document:document": {
|
||||
"gridSize": 10,
|
||||
"name": school_data['establishment_name'],
|
||||
"meta": {},
|
||||
"id": school_data['urn'],
|
||||
"typeName": "document"
|
||||
},
|
||||
"page:page": {
|
||||
"meta": {},
|
||||
"id": "page",
|
||||
"name": "Page 1",
|
||||
"index": "a1",
|
||||
"typeName": "page"
|
||||
},
|
||||
"shape:school-node": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"rotation": 0,
|
||||
"type": "cc-school-node",
|
||||
"id": school_unique_id,
|
||||
"parentId": "page",
|
||||
"index": "a1",
|
||||
"props": school_node.to_dict(),
|
||||
"typeName": "shape"
|
||||
},
|
||||
"instance:instance": {
|
||||
"id": "instance",
|
||||
"currentPageId": "page",
|
||||
"typeName": "instance"
|
||||
},
|
||||
"camera:camera": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 1,
|
||||
"id": "camera",
|
||||
"typeName": "camera"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Store tldraw file in Supabase storage
|
||||
file_path = f"{school_data['urn']}/tldraw.json"
|
||||
file_options = {
|
||||
"content-type": "application/json",
|
||||
"x-upsert": "true",
|
||||
"metadata": {
|
||||
"establishment_urn": school_data['urn'],
|
||||
"establishment_name": school_data['establishment_name']
|
||||
}
|
||||
}
|
||||
|
||||
# Upload file
|
||||
self.storage.upload_file(
|
||||
bucket_id="cc.institutes",
|
||||
file_path=file_path,
|
||||
file_data=json.dumps(tldraw_data).encode(),
|
||||
content_type="application/json",
|
||||
upsert=True
|
||||
)
|
||||
|
||||
# Create node in Neo4j
|
||||
with self.neontology as neo:
|
||||
self.logger.info(f"Creating school node in Neo4j: {school_node.to_dict()}")
|
||||
neo.create_or_merge_node(school_node, database="cc.institutes", operation="merge")
|
||||
return {"status": "success", "node": school_node}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating school node: {str(e)}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
def create_private_database(self, school_data: Dict) -> Dict:
|
||||
"""Creates a private database for a specific school"""
|
||||
try:
|
||||
private_db_name = f"cc.institutes.{school_data['urn']}"
|
||||
with self.driver.session() as session:
|
||||
session_tools.create_database(session, private_db_name)
|
||||
self.logger.info(f"Created private database {private_db_name}")
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Database {private_db_name} created successfully"
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating private database: {str(e)}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
def create_basic_structure(self, school_node: school_nodes.SchoolNode, database_name: str) -> Dict:
|
||||
"""Creates basic structural nodes in the specified database"""
|
||||
try:
|
||||
# Create Department Structure node
|
||||
department_structure_node_unique_id = f"DepartmentStructure_{school_node.unique_id}"
|
||||
department_structure_node = entities.DepartmentStructureNode(
|
||||
unique_id=department_structure_node_unique_id,
|
||||
tldraw_snapshot=""
|
||||
)
|
||||
|
||||
# Create Curriculum Structure node
|
||||
curriculum_node = curriculum_nodes.CurriculumStructureNode(
|
||||
unique_id=f"CurriculumStructure_{school_node.unique_id}",
|
||||
tldraw_snapshot=""
|
||||
)
|
||||
|
||||
# Create Pastoral Structure node
|
||||
pastoral_node = school_structures.PastoralStructureNode(
|
||||
unique_id=f"PastoralStructure_{school_node.unique_id}",
|
||||
tldraw_snapshot=""
|
||||
)
|
||||
|
||||
with self.neontology as neo:
|
||||
# Create nodes
|
||||
neo.create_or_merge_node(department_structure_node, database=str(database_name), operation='merge')
|
||||
neo.create_or_merge_node(curriculum_node, database=str(database_name), operation='merge')
|
||||
neo.create_or_merge_node(pastoral_node, database=str(database_name), operation='merge')
|
||||
|
||||
# Create relationships
|
||||
neo.create_or_merge_relationship(
|
||||
entity_relationships.SchoolHasDepartmentStructure(source=school_node, target=department_structure_node),
|
||||
database=database_name, operation='merge'
|
||||
)
|
||||
neo.create_or_merge_relationship(
|
||||
entity_curriculum_rels.SchoolHasCurriculumStructure(source=school_node, target=curriculum_node),
|
||||
database=database_name, operation='merge'
|
||||
)
|
||||
neo.create_or_merge_relationship(
|
||||
entity_curriculum_rels.SchoolHasPastoralStructure(source=school_node, target=pastoral_node),
|
||||
database=database_name, operation='merge'
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Basic structure created successfully",
|
||||
"nodes": {
|
||||
"department_structure": department_structure_node,
|
||||
"curriculum_structure": curriculum_node,
|
||||
"pastoral_structure": pastoral_node
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating basic structure: {str(e)}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
def create_detailed_structure(self, school_node: school_nodes.SchoolNode, database_name: str, excel_file: BinaryIO) -> Dict:
|
||||
"""Creates detailed structural nodes from Excel file"""
|
||||
try:
|
||||
# Store Excel file in Supabase
|
||||
file_path = f"{school_node.urn}/structure.xlsx"
|
||||
|
||||
# Upload Excel file
|
||||
self.storage.upload_file(
|
||||
bucket_id="cc.institutes",
|
||||
file_path=file_path,
|
||||
file_data=excel_file.read(),
|
||||
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
upsert=True
|
||||
)
|
||||
|
||||
# Process Excel file
|
||||
dataframes = pd.read_excel(excel_file, sheet_name=None)
|
||||
|
||||
# Get existing basic structure nodes
|
||||
with self.neontology as neo:
|
||||
result = neo.cypher_read("""
|
||||
MATCH (s:School {unique_id: $school_id})
|
||||
OPTIONAL MATCH (s)-[:HAS_DEPARTMENT_STRUCTURE]->(ds:DepartmentStructure)
|
||||
OPTIONAL MATCH (s)-[:HAS_CURRICULUM_STRUCTURE]->(cs:CurriculumStructure)
|
||||
OPTIONAL MATCH (s)-[:HAS_PASTORAL_STRUCTURE]->(ps:PastoralStructure)
|
||||
RETURN ds, cs, ps
|
||||
""", {"school_id": school_node.unique_id}, database=database_name)
|
||||
|
||||
if not result:
|
||||
raise Exception("Basic structure not found")
|
||||
|
||||
department_structure = result['ds']
|
||||
curriculum_structure = result['cs']
|
||||
pastoral_structure = result['ps']
|
||||
|
||||
# Create departments and subjects
|
||||
unique_departments = dataframes['keystagesyllabuses']['Department'].dropna().unique()
|
||||
|
||||
node_library = {}
|
||||
|
||||
with self.neontology as neo:
|
||||
for department_name in unique_departments:
|
||||
|
||||
department_node = entities.DepartmentNode(
|
||||
unique_id=f"Department_{school_node.unique_id}_{department_name.replace(' ', '_')}",
|
||||
department_name=department_name,
|
||||
tldraw_snapshot=""
|
||||
)
|
||||
neo.create_or_merge_node(department_node, database=database_name, operation='merge')
|
||||
node_library[f'department_{department_name}'] = department_node
|
||||
|
||||
# Link to department structure
|
||||
neo.create_or_merge_relationship(
|
||||
entity_relationships.DepartmentStructureHasDepartment(
|
||||
source=department_structure,
|
||||
target=department_node
|
||||
),
|
||||
database=database_name,
|
||||
operation='merge'
|
||||
)
|
||||
|
||||
# Create year groups
|
||||
year_groups = self.sort_year_groups(dataframes['yeargroupsyllabuses'])['YearGroup'].unique()
|
||||
last_year_group_node = None
|
||||
|
||||
for year_group in year_groups:
|
||||
numeric_year_group = pd.to_numeric(year_group, errors='coerce')
|
||||
if pd.notna(numeric_year_group):
|
||||
year_group_node = pastoral_nodes.YearGroupNode(
|
||||
unique_id=f"YearGroup_{school_node.unique_id}_YGrp{int(numeric_year_group)}",
|
||||
year_group=str(int(numeric_year_group)),
|
||||
year_group_name=f"Year {int(numeric_year_group)}",
|
||||
tldraw_snapshot=""
|
||||
)
|
||||
neo.create_or_merge_node(year_group_node, database=database_name, operation='merge')
|
||||
node_library[f'year_group_{int(numeric_year_group)}'] = year_group_node
|
||||
|
||||
# Create sequential relationship
|
||||
if last_year_group_node:
|
||||
neo.create_or_merge_relationship(
|
||||
curriculum_relationships.YearGroupFollowsYearGroup(
|
||||
source=last_year_group_node,
|
||||
target=year_group_node
|
||||
),
|
||||
database=database_name,
|
||||
operation='merge'
|
||||
)
|
||||
last_year_group_node = year_group_node
|
||||
|
||||
# Link to pastoral structure
|
||||
neo.create_or_merge_relationship(
|
||||
curriculum_relationships.PastoralStructureIncludesYearGroup(
|
||||
source=pastoral_structure,
|
||||
target=year_group_node
|
||||
),
|
||||
database=database_name,
|
||||
operation='merge'
|
||||
)
|
||||
|
||||
# Create key stages
|
||||
key_stages = dataframes['keystagesyllabuses']['KeyStage'].unique()
|
||||
last_key_stage_node = None
|
||||
|
||||
for key_stage in sorted(key_stages):
|
||||
key_stage_node = curriculum_nodes.KeyStageNode(
|
||||
unique_id=f"KeyStage_{curriculum_structure.unique_id}_KStg{key_stage}",
|
||||
key_stage_name=f"Key Stage {key_stage}",
|
||||
key_stage=str(key_stage),
|
||||
tldraw_snapshot=""
|
||||
)
|
||||
neo.create_or_merge_node(key_stage_node, database=database_name, operation='merge')
|
||||
node_library[f'key_stage_{key_stage}'] = key_stage_node
|
||||
|
||||
# Create sequential relationship
|
||||
if last_key_stage_node:
|
||||
neo.create_or_merge_relationship(
|
||||
curriculum_relationships.KeyStageFollowsKeyStage(
|
||||
source=last_key_stage_node,
|
||||
target=key_stage_node
|
||||
),
|
||||
database=database_name,
|
||||
operation='merge'
|
||||
)
|
||||
last_key_stage_node = key_stage_node
|
||||
|
||||
# Link to curriculum structure
|
||||
neo.create_or_merge_relationship(
|
||||
curriculum_relationships.CurriculumStructureIncludesKeyStage(
|
||||
source=curriculum_structure,
|
||||
target=key_stage_node
|
||||
),
|
||||
database=database_name,
|
||||
operation='merge'
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Detailed structure created successfully",
|
||||
"node_library": node_library
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating detailed structure: {str(e)}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
def sort_year_groups(self, df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Helper function to sort year groups numerically"""
|
||||
df = df.copy()
|
||||
df['YearGroupNumeric'] = pd.to_numeric(df['YearGroup'], errors='coerce')
|
||||
return df.sort_values(by='YearGroupNumeric')
|
||||
|
||||
|
||||
@@ -0,0 +1,472 @@
|
||||
import os
|
||||
from typing import Dict, List, Optional, BinaryIO
|
||||
import json
|
||||
import pandas as pd
|
||||
from backend.modules.database.schemas import entities
|
||||
from modules.logger_tool import initialise_logger
|
||||
from modules.database.tools.filesystem_tools import ClassroomCopilotFilesystem
|
||||
import modules.database.tools.neo4j_driver_tools as driver_tools
|
||||
import modules.database.tools.neo4j_session_tools as session_tools
|
||||
from modules.database.admin.neontology_provider import NeontologyProvider
|
||||
from modules.database.admin.graph_provider import GraphNamingProvider
|
||||
from modules.database.schemas import curriculum_neo
|
||||
from modules.database.schemas.relationships import curriculum_relationships, entity_relationships, entity_curriculum_rels
|
||||
from modules.database.supabase.utils.storage import StorageManager
|
||||
|
||||
class SchoolService:
|
||||
def __init__(self):
|
||||
self.logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True)
|
||||
self.driver = driver_tools.get_driver()
|
||||
self.neontology = NeontologyProvider()
|
||||
self.graph_naming = GraphNamingProvider()
|
||||
self.storage = StorageManager()
|
||||
|
||||
def create_schools_database(self) -> Dict:
|
||||
"""Creates the main cc.institutes database in Neo4j"""
|
||||
try:
|
||||
db_name = "cc.institutes"
|
||||
with self.driver.session() as session:
|
||||
session_tools.create_database(session, db_name)
|
||||
self.logger.info(f"Created database {db_name}")
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Database {db_name} created successfully"
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating schools database: {str(e)}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
def create_school_node(self, school_data: Dict) -> Dict:
|
||||
"""Creates a school node in cc.institutes database and stores TLDraw file in Supabase"""
|
||||
try:
|
||||
# Convert school data to SchoolNode
|
||||
school_unique_id = self.graph_naming.get_school_unique_id(school_data['urn'])
|
||||
school_path = self.graph_naming.get_school_path("cc.institutes", school_data['urn'])
|
||||
|
||||
school_node = entities.SchoolNode(
|
||||
unique_id=school_unique_id,
|
||||
path=school_path,
|
||||
urn=school_data['urn'],
|
||||
establishment_number=school_data['establishment_number'],
|
||||
establishment_name=school_data['establishment_name'],
|
||||
establishment_type=school_data['establishment_type'],
|
||||
establishment_status=school_data['establishment_status'],
|
||||
phase_of_education=school_data['phase_of_education'] if school_data['phase_of_education'] not in [None, ''] else None,
|
||||
statutory_low_age=int(school_data['statutory_low_age']) if school_data.get('statutory_low_age') is not None else 0,
|
||||
statutory_high_age=int(school_data['statutory_high_age']) if school_data.get('statutory_high_age') is not None else 0,
|
||||
religious_character=school_data.get('religious_character') if school_data.get('religious_character') not in [None, ''] else None,
|
||||
school_capacity=int(school_data['school_capacity']) if school_data.get('school_capacity') is not None else 0,
|
||||
school_website=school_data.get('school_website', ''),
|
||||
ofsted_rating=school_data.get('ofsted_rating') if school_data.get('ofsted_rating') not in [None, ''] else None
|
||||
)
|
||||
|
||||
# Create default tldraw file data
|
||||
tldraw_data = {
|
||||
"document": {
|
||||
"version": 1,
|
||||
"id": school_data['urn'],
|
||||
"name": school_data['establishment_name'],
|
||||
"meta": {
|
||||
"created_at": "",
|
||||
"updated_at": "",
|
||||
"creator_id": "",
|
||||
"is_template": False,
|
||||
"is_snapshot": False,
|
||||
"is_draft": False,
|
||||
"template_id": None,
|
||||
"snapshot_id": None,
|
||||
"draft_id": None
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"schemaVersion": 1,
|
||||
"storeVersion": 4,
|
||||
"recordVersions": {
|
||||
"asset": {
|
||||
"version": 1,
|
||||
"subTypeKey": "type",
|
||||
"subTypeVersions": {}
|
||||
},
|
||||
"camera": {
|
||||
"version": 1
|
||||
},
|
||||
"document": {
|
||||
"version": 2
|
||||
},
|
||||
"instance": {
|
||||
"version": 22
|
||||
},
|
||||
"instance_page_state": {
|
||||
"version": 5
|
||||
},
|
||||
"page": {
|
||||
"version": 1
|
||||
},
|
||||
"shape": {
|
||||
"version": 3,
|
||||
"subTypeKey": "type",
|
||||
"subTypeVersions": {
|
||||
"cc-school-node": 1
|
||||
}
|
||||
},
|
||||
"instance_presence": {
|
||||
"version": 5
|
||||
},
|
||||
"pointer": {
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"store": {
|
||||
"document:document": {
|
||||
"gridSize": 10,
|
||||
"name": school_data['establishment_name'],
|
||||
"meta": {},
|
||||
"id": school_data['urn'],
|
||||
"typeName": "document"
|
||||
},
|
||||
"page:page": {
|
||||
"meta": {},
|
||||
"id": "page",
|
||||
"name": "Page 1",
|
||||
"index": "a1",
|
||||
"typeName": "page"
|
||||
},
|
||||
"shape:school-node": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"rotation": 0,
|
||||
"type": "cc-school-node",
|
||||
"id": school_unique_id,
|
||||
"parentId": "page",
|
||||
"index": "a1",
|
||||
"props": school_node.to_dict(),
|
||||
"typeName": "shape"
|
||||
},
|
||||
"instance:instance": {
|
||||
"id": "instance",
|
||||
"currentPageId": "page",
|
||||
"typeName": "instance"
|
||||
},
|
||||
"camera:camera": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 1,
|
||||
"id": "camera",
|
||||
"typeName": "camera"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Store tldraw file in Supabase storage
|
||||
file_path = f"{school_data['urn']}/tldraw.json"
|
||||
file_options = {
|
||||
"content-type": "application/json",
|
||||
"x-upsert": "true",
|
||||
"metadata": {
|
||||
"establishment_urn": school_data['urn'],
|
||||
"establishment_name": school_data['establishment_name']
|
||||
}
|
||||
}
|
||||
|
||||
# Upload file
|
||||
self.storage.upload_file(
|
||||
bucket_id="cc.institutes",
|
||||
file_path=file_path,
|
||||
file_data=json.dumps(tldraw_data).encode(),
|
||||
content_type="application/json",
|
||||
upsert=True
|
||||
)
|
||||
|
||||
# Create node in Neo4j
|
||||
with self.neontology as neo:
|
||||
self.logger.info(f"Creating school node in Neo4j: {school_node.to_dict()}")
|
||||
neo.create_or_merge_node(school_node, database="cc.institutes", operation="merge")
|
||||
return {"status": "success", "node": school_node}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating school node: {str(e)}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
def create_private_database(self, school_data: Dict) -> Dict:
|
||||
"""Creates a private database for a specific school"""
|
||||
try:
|
||||
private_db_name = f"cc.institutes.{school_data['urn']}"
|
||||
with self.driver.session() as session:
|
||||
session_tools.create_database(session, private_db_name)
|
||||
self.logger.info(f"Created private database {private_db_name}")
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Database {private_db_name} created successfully"
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating private database: {str(e)}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
def create_basic_structure(self, school_node: entities.SchoolNode, database_name: str) -> Dict:
|
||||
"""Creates basic structural nodes in the specified database"""
|
||||
try:
|
||||
# Create filesystem paths
|
||||
fs_handler = ClassroomCopilotFilesystem(database_name, init_run_type="school")
|
||||
|
||||
# Create Department Structure node
|
||||
department_structure_node_unique_id = f"DepartmentStructure_{school_node.unique_id}"
|
||||
_, department_path = fs_handler.create_school_department_directory(school_node.path, "departments")
|
||||
department_structure_node = entities.DepartmentStructureNode(
|
||||
unique_id=department_structure_node_unique_id,
|
||||
path=department_path
|
||||
)
|
||||
|
||||
# Create Curriculum Structure node
|
||||
_, curriculum_path = fs_handler.create_school_curriculum_directory(school_node.path)
|
||||
curriculum_node = curriculum_neo.CurriculumStructureNode(
|
||||
unique_id=f"CurriculumStructure_{school_node.unique_id}",
|
||||
path=curriculum_path
|
||||
)
|
||||
|
||||
# Create Pastoral Structure node
|
||||
_, pastoral_path = fs_handler.create_school_pastoral_directory(school_node.path)
|
||||
pastoral_node = curriculum_neo.PastoralStructureNode(
|
||||
unique_id=f"PastoralStructure_{school_node.unique_id}",
|
||||
path=pastoral_path
|
||||
)
|
||||
|
||||
with self.neontology as neo:
|
||||
# Create nodes
|
||||
neo.create_or_merge_node(department_structure_node, database=str(database_name), operation='merge')
|
||||
fs_handler.create_default_tldraw_file(department_structure_node.path, department_structure_node.to_dict())
|
||||
|
||||
neo.create_or_merge_node(curriculum_node, database=str(database_name), operation='merge')
|
||||
fs_handler.create_default_tldraw_file(curriculum_node.path, curriculum_node.to_dict())
|
||||
|
||||
neo.create_or_merge_node(pastoral_node, database=database_name, operation='merge')
|
||||
fs_handler.create_default_tldraw_file(pastoral_node.path, pastoral_node.to_dict())
|
||||
|
||||
# Create relationships
|
||||
neo.create_or_merge_relationship(
|
||||
entity_relationships.SchoolHasDepartmentStructure(source=school_node, target=department_structure_node),
|
||||
database=database_name, operation='merge'
|
||||
)
|
||||
|
||||
neo.create_or_merge_relationship(
|
||||
entity_curriculum_rels.SchoolHasCurriculumStructure(source=school_node, target=curriculum_node),
|
||||
database=database_name, operation='merge'
|
||||
)
|
||||
|
||||
neo.create_or_merge_relationship(
|
||||
entity_curriculum_rels.SchoolHasPastoralStructure(source=school_node, target=pastoral_node),
|
||||
database=database_name, operation='merge'
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Basic structure created successfully",
|
||||
"nodes": {
|
||||
"department_structure": department_structure_node,
|
||||
"curriculum_structure": curriculum_node,
|
||||
"pastoral_structure": pastoral_node
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating basic structure: {str(e)}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
def create_detailed_structure(self, school_node: entities.SchoolNode, database_name: str, excel_file: BinaryIO) -> Dict:
|
||||
"""Creates detailed structural nodes from Excel file"""
|
||||
try:
|
||||
# Store Excel file in Supabase
|
||||
file_path = f"{school_node.urn}/structure.xlsx"
|
||||
|
||||
# Upload Excel file
|
||||
self.storage.upload_file(
|
||||
bucket_id="cc.institutes",
|
||||
file_path=file_path,
|
||||
file_data=excel_file.read(),
|
||||
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
upsert=True
|
||||
)
|
||||
|
||||
# Process Excel file
|
||||
dataframes = pd.read_excel(excel_file, sheet_name=None)
|
||||
|
||||
# Get existing basic structure nodes
|
||||
with self.neontology as neo:
|
||||
result = neo.cypher_read("""
|
||||
MATCH (s:School {unique_id: $school_id})
|
||||
OPTIONAL MATCH (s)-[:HAS_DEPARTMENT_STRUCTURE]->(ds:DepartmentStructure)
|
||||
OPTIONAL MATCH (s)-[:HAS_CURRICULUM_STRUCTURE]->(cs:CurriculumStructure)
|
||||
OPTIONAL MATCH (s)-[:HAS_PASTORAL_STRUCTURE]->(ps:PastoralStructure)
|
||||
RETURN ds, cs, ps
|
||||
""", {"school_id": school_node.unique_id}, database=database_name)
|
||||
|
||||
if not result:
|
||||
raise Exception("Basic structure not found")
|
||||
|
||||
department_structure = result['ds']
|
||||
curriculum_structure = result['cs']
|
||||
pastoral_structure = result['ps']
|
||||
|
||||
# Create departments and subjects
|
||||
unique_departments = dataframes['keystagesyllabuses']['Department'].dropna().unique()
|
||||
|
||||
fs_handler = ClassroomCopilotFilesystem(database_name, init_run_type="school")
|
||||
node_library = {}
|
||||
|
||||
with self.neontology as neo:
|
||||
for department_name in unique_departments:
|
||||
_, department_path = fs_handler.create_school_department_directory(school_node.path, department_name)
|
||||
|
||||
department_node = entities.DepartmentNode(
|
||||
unique_id=f"Department_{school_node.unique_id}_{department_name.replace(' ', '_')}",
|
||||
department_name=department_name,
|
||||
path=department_path
|
||||
)
|
||||
neo.create_or_merge_node(department_node, database=database_name, operation='merge')
|
||||
fs_handler.create_default_tldraw_file(department_node.path, department_node.to_dict())
|
||||
node_library[f'department_{department_name}'] = department_node
|
||||
|
||||
# Link to department structure
|
||||
neo.create_or_merge_relationship(
|
||||
entity_relationships.DepartmentStructureHasDepartment(
|
||||
source=department_structure,
|
||||
target=department_node
|
||||
),
|
||||
database=database_name,
|
||||
operation='merge'
|
||||
)
|
||||
|
||||
# Create year groups
|
||||
year_groups = self.sort_year_groups(dataframes['yeargroupsyllabuses'])['YearGroup'].unique()
|
||||
last_year_group_node = None
|
||||
|
||||
for year_group in year_groups:
|
||||
numeric_year_group = pd.to_numeric(year_group, errors='coerce')
|
||||
if pd.notna(numeric_year_group):
|
||||
_, year_group_path = fs_handler.create_pastoral_year_group_directory(
|
||||
pastoral_structure.path,
|
||||
str(int(numeric_year_group))
|
||||
)
|
||||
|
||||
year_group_node = curriculum_neo.YearGroupNode(
|
||||
unique_id=f"YearGroup_{school_node.unique_id}_YGrp{int(numeric_year_group)}",
|
||||
year_group=str(int(numeric_year_group)),
|
||||
year_group_name=f"Year {int(numeric_year_group)}",
|
||||
path=year_group_path
|
||||
)
|
||||
neo.create_or_merge_node(year_group_node, database=database_name, operation='merge')
|
||||
fs_handler.create_default_tldraw_file(year_group_node.path, year_group_node.to_dict())
|
||||
node_library[f'year_group_{int(numeric_year_group)}'] = year_group_node
|
||||
|
||||
# Create sequential relationship
|
||||
if last_year_group_node:
|
||||
neo.create_or_merge_relationship(
|
||||
curriculum_relationships.YearGroupFollowsYearGroup(
|
||||
source=last_year_group_node,
|
||||
target=year_group_node
|
||||
),
|
||||
database=database_name,
|
||||
operation='merge'
|
||||
)
|
||||
last_year_group_node = year_group_node
|
||||
|
||||
# Link to pastoral structure
|
||||
neo.create_or_merge_relationship(
|
||||
curriculum_relationships.PastoralStructureIncludesYearGroup(
|
||||
source=pastoral_structure,
|
||||
target=year_group_node
|
||||
),
|
||||
database=database_name,
|
||||
operation='merge'
|
||||
)
|
||||
|
||||
# Create key stages
|
||||
key_stages = dataframes['keystagesyllabuses']['KeyStage'].unique()
|
||||
last_key_stage_node = None
|
||||
|
||||
for key_stage in sorted(key_stages):
|
||||
_, key_stage_path = fs_handler.create_curriculum_key_stage_directory(
|
||||
curriculum_structure.path,
|
||||
str(key_stage)
|
||||
)
|
||||
|
||||
key_stage_node = curriculum_neo.KeyStageNode(
|
||||
unique_id=f"KeyStage_{curriculum_structure.unique_id}_KStg{key_stage}",
|
||||
key_stage_name=f"Key Stage {key_stage}",
|
||||
key_stage=str(key_stage),
|
||||
path=key_stage_path
|
||||
)
|
||||
neo.create_or_merge_node(key_stage_node, database=database_name, operation='merge')
|
||||
fs_handler.create_default_tldraw_file(key_stage_node.path, key_stage_node.to_dict())
|
||||
node_library[f'key_stage_{key_stage}'] = key_stage_node
|
||||
|
||||
# Create sequential relationship
|
||||
if last_key_stage_node:
|
||||
neo.create_or_merge_relationship(
|
||||
curriculum_relationships.KeyStageFollowsKeyStage(
|
||||
source=last_key_stage_node,
|
||||
target=key_stage_node
|
||||
),
|
||||
database=database_name,
|
||||
operation='merge'
|
||||
)
|
||||
last_key_stage_node = key_stage_node
|
||||
|
||||
# Link to curriculum structure
|
||||
neo.create_or_merge_relationship(
|
||||
curriculum_relationships.CurriculumStructureIncludesKeyStage(
|
||||
source=curriculum_structure,
|
||||
target=key_stage_node
|
||||
),
|
||||
database=database_name,
|
||||
operation='merge'
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Detailed structure created successfully",
|
||||
"node_library": node_library
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating detailed structure: {str(e)}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
def sort_year_groups(self, df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Helper function to sort year groups numerically"""
|
||||
df = df.copy()
|
||||
df['YearGroupNumeric'] = pd.to_numeric(df['YearGroup'], errors='coerce')
|
||||
return df.sort_values(by='YearGroupNumeric')
|
||||
|
||||
def check_schools_database(self) -> Dict:
|
||||
"""Check if the schools database exists and has been initialized"""
|
||||
try:
|
||||
db_name = "cc.institutes"
|
||||
with self.driver.session() as session:
|
||||
# Check if database exists
|
||||
databases = session_tools.list_databases(session)
|
||||
if db_name not in databases:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Database {db_name} does not exist"
|
||||
}
|
||||
|
||||
# Check if database has any nodes (indicating it's been initialized)
|
||||
session.run("USE " + db_name)
|
||||
result = session.run("MATCH (n) RETURN count(n) as count").single()
|
||||
node_count = result["count"] if result else 0
|
||||
|
||||
if node_count == 0:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Database {db_name} exists but has no nodes"
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Database {db_name} exists and has {node_count} nodes"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error checking schools database: {str(e)}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
Reference in New Issue
Block a user