import os from datetime import timedelta, datetime from abc import ABC, abstractmethod from typing import Dict, Optional, Any, Union from modules.database.services.neo4j_service import Neo4jService import modules.database.schemas.nodes.users as user_nodes import modules.database.schemas.nodes.workers.workers as worker_nodes import modules.database.init.init_calendar as init_calendar import modules.database.schemas.relationships.entity_relationships as entity_relationships import modules.database.tools.neontology_tools as neon import modules.database.tools.supabase_storage_tools as storage_tools from modules.logger_tool import initialise_logger logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) def create_and_check_db(db_name): neo4j_service = Neo4jService() neo4j_service.create_database(db_name) database_status = neo4j_service.check_database_exists(db_name) if not database_status['exists']: raise ValueError(f"Database {db_name} not found") return database_status class UserCreator(ABC): def __init__( self, user_id, cc_users_db_name, user_type, worker_type, user_email, worker_email, cc_username, user_name, worker_name, calendar_start_date, calendar_end_date, storage_tools=None, user_db_name: Optional[str] = None, worker_db_name: Optional[str] = None, cc_schools_db_name: str = "cc.institutes", ): self.cc_users_db_name = cc_users_db_name self.user_db_name = user_db_name or f"{cc_users_db_name}.{user_type}.{cc_username}" self.worker_db_name = worker_db_name or f"{cc_schools_db_name}.{worker_type}.{cc_username}" self.user_type = user_type self.worker_type = worker_type self.cc_username = cc_username self.user_email = user_email self.worker_email = worker_email self.user_name = user_name self.worker_name = worker_name self.user_id = user_id self.storage_tools = storage_tools # Store the storage tools instance self.user_nodes: Dict[str, Optional[Any]] = { 'default_user_node': None, 'private_user_node': None, 'worker_node': None, 'calendar_node': None } if calendar_start_date and calendar_end_date: self.calendar_start_date = calendar_start_date self.calendar_end_date = calendar_end_date else: logger.warning("No calendar start and end date provided, using default values") self.calendar_start_date = datetime.now().date() self.calendar_end_date = (datetime.now() + timedelta(days=5)).date() def _derive_user_storage_path(self) -> str: return os.path.join( "users", self.user_id, "databases", self.user_db_name, self.user_id, ).replace('\\', '/') def _derive_internal_worker_path(self, worker_kind: str) -> str: return os.path.join( "users", self.user_id, "databases", self.user_db_name, worker_kind, self.user_id, ).replace('\\', '/') @abstractmethod def create_user(self): pass def create_user_node(self, db_name: str): logger.info(f"Module is creating {self.cc_users_db_name} user node for {self.user_type} user {self.cc_username}") try: user_node = self._create_user_node(db_name) logger.debug(f"User node creation completed for {self.cc_users_db_name} user node for {self.user_type} user {self.cc_username}: {user_node.to_dict()}") return user_node except Exception as e: logger.error(f"Error creating user node: {e}") raise def _create_user_node(self, db_name: str): # Ensure Neontology is initialized neon.init_neontology_connection() # Generate storage path for user node using Supabase Storage if self.storage_tools: user_dir_created, node_storage_path = self.storage_tools.create_user_storage_path(self.user_id) self.user_path = node_storage_path # Store for later use else: node_storage_path = self._derive_user_storage_path() self.user_path = None user_node = user_nodes.UserNode( uuid_string=f"{self.user_id}", node_storage_path=node_storage_path, cc_username=f"{self.cc_username}", user_email=f"{self.user_email}", user_name=f"{self.user_name}", user_db_name=f"{self.user_db_name}", user_type=f"{self.user_type}", ) logger.debug(f"User node template created: {user_node.to_dict()}. Writing to database {db_name}") logger.debug(f"About to call create_or_merge_neontology_node with node class: {user_node.__class__.__name__}") logger.debug(f"Node primary label: {user_node.__primarylabel__}") logger.debug(f"Node primary property: {user_node.__primaryproperty__}") try: neon.create_or_merge_neontology_node(node=user_node, database=db_name, operation='merge') logger.info(f"User node created successfully: {user_node.to_dict()}") except Exception as e: logger.error(f"Failed to create user node: {e}") raise return user_node def create_storage_bucket(self, bucket_id: str, bucket_name: str, access_token: Optional[str] = None) -> bool: """Create storage buckets for the user - DEPRECATED: Use centralized bucket initialization instead""" logger.warning(f"Individual user bucket creation is deprecated. Use centralized bucket initialization instead.") logger.info(f"User {self.cc_username} will use centralized storage buckets.") return True # Return success to avoid breaking existing code class SchoolUserCreator(UserCreator): def __init__( self, user_id, cc_users_db_name, user_type, worker_type, user_email, worker_email, cc_username, user_name, worker_name, calendar_start_date, calendar_end_date, school_node, worker_node=None, storage_tools=None, user_db_name: Optional[str] = None, worker_db_name: Optional[str] = None, ): super().__init__( user_id, cc_users_db_name, user_type, worker_type, user_email, worker_email, cc_username, user_name, worker_name, calendar_start_date, calendar_end_date, storage_tools, user_db_name=user_db_name, worker_db_name=worker_db_name or (school_node.private_database_name if hasattr(school_node, "private_database_name") else None), cc_schools_db_name="cc.institutes", ) self.school_node = school_node self.worker_node = worker_node def _derive_school_worker_path(self, worker_kind: str) -> str: school_identifier = getattr(self.school_node, 'uuid_string', None) if self.school_node else None if not school_identifier and self.worker_db_name: school_identifier = self.worker_db_name.split('.')[-1] school_identifier = school_identifier or self.user_id db_name_segment = self.worker_db_name or f"cc.institutes.{school_identifier}" return os.path.join( "schools", school_identifier, "databases", db_name_segment, worker_kind, self.user_id, ).replace('\\', '/') def create_user(self): # Ensure Neontology is initialized logger.debug(f"Initializing Neontology connection. Closing any existing connection") neon.close_neontology_connection() logger.debug(f"Neontology connection closed. Initializing new connection") neon.init_neontology_connection() if self.user_type in ['email_teacher', 'ms_teacher']: worker_node = self.create_teacher_node() elif self.user_type in ['email_student', 'ms_student']: worker_node = self.create_student_node() else: raise ValueError(f"User type {self.user_type} not supported") self.user_nodes[f'worker_node'] = worker_node user_node = self.create_user_node(self.user_db_name) logger.info(f"User node created: {user_node}") self.user_nodes['default_user_node'] = user_node self.create_user_worker_relationship(user_node, worker_node) self.create_worker_school_relationship(worker_node, self.school_node) logger.info(f"Worker school relationship created between {worker_node} and {self.school_node}") return self.user_nodes def create_teacher_node(self): logger.debug(f"Teacher node will be created for school: {self.school_node}") try: return self._create_teacher_node() except KeyError as ke: raise ValueError(f"Missing required key in worker_data: {ke}") from ke except Exception as e: raise ValueError(f"Error creating teacher node: {e}") from e def _create_teacher_node(self): # Generate storage path for teacher node using Supabase Storage if self.storage_tools: teacher_dir_created, node_storage_path = self.storage_tools.create_teacher_storage_path(self.user_id) else: node_storage_path = self._derive_school_worker_path(self.worker_type) teacher_node = worker_nodes.TeacherNode( uuid_string=f"{self.user_id}", node_storage_path=node_storage_path, worker_name=self.worker_name, worker_email=self.worker_email, worker_db_name=self.worker_db_name, worker_type=self.worker_type ) # Use the school's private database name if available school_db = self.worker_db_name or ( self.school_node.private_database_name if hasattr(self.school_node, 'private_database_name') else f"cc.institutes.{self.school_node.school_type}.{getattr(self.school_node, 'id', self.school_node.uuid_string)}" ) logger.info(f"Teacher node template created: {teacher_node}... setting school db to {school_db}") neon.create_or_merge_neontology_node(node=teacher_node, database=school_db, operation='merge') logger.info(f"Teacher node merged into database {school_db}: {teacher_node}") return teacher_node def create_student_node(self): # Generate storage path for student node using Supabase Storage if self.storage_tools: student_dir_created, node_storage_path = self.storage_tools.create_student_storage_path(f"Student_{self.user_id}") else: node_storage_path = self._derive_school_worker_path(self.worker_type) student_node = worker_nodes.StudentNode( uuid_string=f"Student_{self.user_id}", worker_name=self.worker_name, worker_email=self.worker_email, worker_db_name=self.worker_db_name, worker_type=self.worker_type, node_storage_path=node_storage_path ) # Use the school's private database name if available school_db = self.worker_db_name or ( self.school_node.private_database_name if hasattr(self.school_node, 'private_database_name') else f"cc.institutes.{self.school_node.school_type}.{getattr(self.school_node, 'id', self.school_node.uuid_string)}" ) logger.info(f"Student node template created: {student_node}... setting school db to {school_db}") neon.create_or_merge_neontology_node(node=student_node, database=school_db, operation='merge') logger.info(f"Student node merged into database {school_db}: {student_node}") return student_node def create_user_worker_relationship(self, user_node, worker_node): user_role_rel = entity_relationships.UserIsSchoolWorker(source=user_node, target=worker_node) # Use the school's private database name if available school_db = self.worker_db_name or ( self.school_node.private_database_name if hasattr(self.school_node, 'private_database_name') else f"cc.institutes.{self.school_node.school_type}.{getattr(self.school_node, 'id', self.school_node.uuid_string)}" ) neon.create_or_merge_neontology_relationship(user_role_rel, database=school_db, operation='merge') logger.info(f"Relationship created between user and worker in database {school_db}") def create_worker_school_relationship(self, worker_node, school_node): worker_school_rel = entity_relationships.EntityBelongsToSchool(source=worker_node, target=school_node) # Use the school's private database name if available school_db = self.worker_db_name or ( school_node.private_database_name if hasattr(school_node, 'private_database_name') else f"cc.institutes.{school_node.school_type}.{getattr(school_node, 'id', school_node.uuid_string)}" ) neon.create_or_merge_neontology_relationship(worker_school_rel, database=school_db, operation='merge') logger.info(f"Relationship created between worker and school in database {school_db}") class NonSchoolUserCreator(UserCreator): def __init__( self, user_id, cc_users_db_name, user_type, worker_type, user_email, worker_email, cc_username, user_name, worker_name, calendar_start_date, calendar_end_date, developer_role: str = "developer", storage_tools=None, user_db_name: Optional[str] = None, worker_db_name: Optional[str] = None, ): super().__init__( user_id, cc_users_db_name, user_type, worker_type, user_email, worker_email, cc_username, user_name, worker_name, calendar_start_date, calendar_end_date, storage_tools, user_db_name=user_db_name, worker_db_name=worker_db_name, ) self.developer_role = developer_role def create_user(self, access_token: Optional[str] = None): logger.debug(f"Creating user node for {self.user_type} user {self.cc_username} in database {self.cc_users_db_name}") # Create storage buckets for the user user_bucket_id = self.user_db_name user_bucket_name = f"{self.user_type.title()} User Files - {self.user_name}" if not self.create_storage_bucket(user_bucket_id, user_bucket_name, access_token=access_token): logger.error(f"Failed to create storage bucket for user {self.cc_username}") raise ValueError(f"Failed to create storage bucket for user {self.cc_username}") # Create default user node first default_user_node = self.create_user_node(self.cc_users_db_name) logger.debug(f"Default user node created: {default_user_node}") # Verify the return value of create_user_node if default_user_node is None: logger.error("Failed to create default user node. It is None.") raise ValueError("Failed to create default user node. It is None.") self.user_nodes[f'default_user_node'] = default_user_node # Create the appropriate user db based on user_type if self.user_type == 'admin': logger.debug(f"Creating super admin db for {self.user_type} user {self.cc_username} in database {self.user_db_name}") self.create_super_admin_db() elif self.user_type == 'developer': logger.debug(f"Creating developer db for {self.user_type} user {self.cc_username} in database {self.user_db_name}") self.create_developer_db() else: logger.warning(f"User type {self.user_type} not explicitly supported; defaulting to developer workspace") self.create_developer_db() logger.debug(f"User nodes after creation: {self.user_nodes}") return self.user_nodes def create_super_admin_db(self): logger.debug(f"Creating super admin db for {self.user_type} user {self.cc_username} in database {self.user_db_name}") neon.init_neontology_connection() # Create the user db self.user_db_name create_and_check_db(self.user_db_name) try: # Create the user node again for the user db logger.debug(f"Creating super admin user node for {self.user_type} user {self.cc_username} in database {self.user_db_name}") private_user_node = self.create_user_node(self.user_db_name) # Generate storage path for super admin node using Supabase Storage if self.storage_tools: admin_dir_created, node_storage_path = self.storage_tools.create_super_admin_storage_path(self.user_id) else: node_storage_path = self._derive_internal_worker_path(self.worker_type) super_admin_node = worker_nodes.SuperAdminNode( uuid_string=self.user_id, worker_email=self.worker_email, node_storage_path=node_storage_path, worker_name=self.worker_name, worker_db_name=self.worker_db_name, worker_type=self.worker_type ) logger.debug(f"Super admin node template created: {super_admin_node}. Writing to database {self.user_db_name}") neon.create_or_merge_neontology_node(node=super_admin_node, database=self.user_db_name, operation='merge') logger.info(f"Super admin node created: {super_admin_node}") logger.debug(f"Creating relationship between user node: {private_user_node} and worker node: {super_admin_node}") self.create_user_specific_relationship(user_node=private_user_node, worker_node=super_admin_node) logger.debug(f"Creating calendar for {self.user_type} user {self.cc_username} in database {self.user_db_name}") calendar_nodes = self.create_calendar(user_node=private_user_node) logger.info(f"Super admin calendar created.") self.user_nodes['private_user_node'] = private_user_node self.user_nodes['worker_node'] = super_admin_node logger.info(f"Returning user nodes: {self.user_nodes}") return self.user_nodes except Exception as e: logger.error(f"Error creating super admin node: {e}") raise ValueError(f"Error creating super admin node: {e}") from e def create_developer_db(self): neon.init_neontology_connection() # Create the user db self.user_db_name create_and_check_db(self.user_db_name) try: # Create the user node again for the user db private_user_node = self.create_user_node(self.user_db_name) # Generate storage path for developer node using Supabase Storage if self.storage_tools: dev_dir_created, node_storage_path = self.storage_tools.create_developer_storage_path(self.user_id) else: node_storage_path = self._derive_internal_worker_path(self.worker_type) developer_node = worker_nodes.DeveloperNode( uuid_string=self.user_id, worker_name=self.worker_name, worker_email=self.worker_email, node_storage_path=node_storage_path, worker_db_name=self.worker_db_name, worker_type=self.worker_type, developer_role=self.developer_role ) neon.create_or_merge_neontology_node(developer_node, database=self.user_db_name, operation='merge') logger.info(f"Developer node created: {developer_node}") self.user_nodes['private_user_node'] = private_user_node self.user_nodes['worker_node'] = developer_node self.create_user_specific_relationship(user_node=private_user_node, worker_node=developer_node) return self.user_nodes except Exception as e: raise ValueError(f"Error creating developer node: {e}") from e def create_user_specific_relationship(self, user_node: user_nodes.UserNode, worker_node: Union[worker_nodes.SuperAdminNode, worker_nodes.DeveloperNode]): if user_node is None or worker_node is None: logger.error("User node or worker node is None. Cannot create relationship.") raise ValueError("User node or worker node is None. Cannot create relationship.") logger.info(f"Creating relationship between user node: {user_node} and worker node: {worker_node}") # Log the state of user_node and worker_node logger.debug(f"user_node: {user_node}") logger.debug(f"worker_node: {worker_node}") if worker_node.worker_type == 'developer': specific_user_rel = entity_relationships.UserIsSystemWorker(source=user_node, target=worker_node) elif worker_node.worker_type == 'superadmin': specific_user_rel = entity_relationships.UserIsSystemWorker(source=user_node, target=worker_node) else: raise ValueError(f"User type {worker_node.worker_type} not supported") neon.create_or_merge_neontology_relationship(specific_user_rel, database=self.user_db_name, operation='merge') logger.info("Relationship created between user and specific node") def create_calendar(self, user_node: user_nodes.UserNode): calendar_nodes = init_calendar.create_calendar(self.user_db_name, self.calendar_start_date, self.calendar_end_date, attach_to_calendar_node=True, owner_node=user_node, filesystem=self.filesystem) logger.info(f"Calendar nodes created.") return calendar_nodes def _default_date_range(): today = datetime.now().date() return today, (datetime.now() + timedelta(days=365)).date() def create_user( *, user_id: str, user_type: str, username: str, user_email: str, user_name: Optional[str] = None, worker_name: Optional[str] = None, worker_type: Optional[str] = None, worker_email: Optional[str] = None, cc_users_db_name: str = "cc.users", user_db_name: Optional[str] = None, worker_db_name: Optional[str] = None, calendar_start_date: Optional[datetime.date] = None, calendar_end_date: Optional[datetime.date] = None, school_node: Optional[Any] = None, storage_tools=None, ) -> Dict[str, Optional[Any]]: """Create a user graph structure in Neo4j. Args: user_id: Identifier used as UUID for graph nodes. user_type: Application-level user type (e.g. email_teacher, developer). username: Canonical username/slug. user_email: Contact email for the user node. user_name: Friendly display name (defaults to username). worker_name: Friendly name for worker node (defaults to user_name). worker_type: Worker role (teacher, student, developer, etc.). worker_email: Email for worker node (defaults to user_email). cc_users_db_name: Root namespace for user databases (defaults to cc.users). user_db_name: Fully-qualified target database for the user graph. worker_db_name: Database for worker entities (usually school private DB). calendar_start_date/calendar_end_date: Date range for initial calendar seeding. school_node: Optional school context; if provided a SchoolUserCreator is used. storage_tools: Optional Supabase storage tools for generating storage paths. Returns: Dict describing created nodes keyed by semantic role. """ start_date, end_date = calendar_start_date, calendar_end_date if not start_date or not end_date: start_date, end_date = _default_date_range() worker_email = worker_email or user_email user_name = user_name or username worker_name = worker_name or user_name if school_node is not None: creator = SchoolUserCreator( user_id=user_id, cc_users_db_name=cc_users_db_name, user_type=user_type, worker_type=worker_type or "teacher", user_email=user_email, worker_email=worker_email, cc_username=username, user_name=user_name, worker_name=worker_name, calendar_start_date=start_date, calendar_end_date=end_date, school_node=school_node, worker_node=None, storage_tools=storage_tools, user_db_name=user_db_name, worker_db_name=worker_db_name, ) else: creator = NonSchoolUserCreator( user_id=user_id, cc_users_db_name=cc_users_db_name, user_type=user_type, worker_type=worker_type or user_type, user_email=user_email, worker_email=worker_email, cc_username=username, user_name=user_name, worker_name=worker_name, calendar_start_date=start_date, calendar_end_date=end_date, storage_tools=storage_tools, user_db_name=user_db_name, worker_db_name=worker_db_name, ) return creator.create_user()