# UserManager Implementation ## Overview This document describes the implementation of a custom `UserManager` to support email-based authentication in the PX360 application. ## Problem Statement The User model was configured to use `email` as the `USERNAME_FIELD` for authentication, but it was still using Django's default `UserManager`. This caused issues with: 1. The `createsuperuser` management command expecting a username instead of email 2. User creation methods not properly handling email-based authentication 3. Inconsistent behavior between authentication and user management ## Solution Implemented a custom `UserManager` that extends Django's `BaseUserManager` to properly handle email-based authentication. ## Changes Made ### 1. Created UserManager Class **File:** `apps/accounts/models.py` ```python class UserManager(BaseUserManager): """ Custom user manager for email-based authentication. """ def create_user(self, email, password=None, **extra_fields): """ Create and save a regular user with the given email and password. """ if not email: raise ValueError('The Email field must be set') email = self.normalize_email(email) user = self.model(email=email, **extra_fields) user.set_password(password) user.save(using=self._db) return user def create_superuser(self, email, password=None, **extra_fields): """ Create and save a superuser with the given email and password. """ extra_fields.setdefault('is_staff', True) extra_fields.setdefault('is_superuser', True) extra_fields.setdefault('is_active', True) if extra_fields.get('is_staff') is not True: raise ValueError('Superuser must have is_staff=True.') if extra_fields.get('is_superuser') is not True: raise ValueError('Superuser must have is_superuser=True.') return self.create_user(email, password, **extra_fields) ``` ### 2. Updated User Model **File:** `apps/accounts/models.py` - Added `UserManager` import from `django.contrib.auth.models` - Added optional `username` field (for backward compatibility) - Set `objects = UserManager()` on the User model ```python from django.contrib.auth.models import AbstractUser, Group, Permission, BaseUserManager class User(AbstractUser, TimeStampedModel): # ... other fields ... # Override username to be optional and non-unique (for backward compatibility) username = models.CharField(max_length=150, blank=True, null=True, unique=False) # Use email as username field for authentication USERNAME_FIELD = 'email' # Required fields when creating superuser REQUIRED_FIELDS = ['first_name', 'last_name'] # Custom user manager objects = UserManager() ``` ### 3. Updated StaffService **File:** `apps/organizations/services.py` Modified `create_user_for_staff` method to work with the new UserManager: ```python @staticmethod def create_user_for_staff(staff, role='staff', request=None): """ Create a User account for a Staff member. """ if staff.user: raise ValueError("Staff member already has a user account") # Generate email (required for authentication) if not staff.email: raise ValueError("Staff member must have an email address") # Generate username (optional, for backward compatibility) username = StaffService.generate_username(staff) password = StaffService.generate_password() # Create user - email is now the username field user = User.objects.create_user( email=staff.email, # Email is the first parameter password=password, first_name=staff.first_name, last_name=staff.last_name, username=username, # Optional field employee_id=staff.employee_id, hospital=staff.hospital, department=staff.department, is_active=True, is_provisional=False ) # ... rest of the method ... ``` **Key Changes:** - `email` is now the first parameter (required by UserManager) - `username` is passed as an optional field - Removed return of password (it's managed separately in the seed command) ### 4. Updated Seed Staff Command **File:** `apps/organizations/management/commands/seed_staff.py` Modified `create_user_for_staff` method: ```python def create_user_for_staff(self, staff, send_email=False): """Create a user account for staff using StaffService""" try: # Generate password first password = StaffService.generate_password() # Create user account using StaffService user = StaffService.create_user_for_staff(staff, role, request) # Set the generated password (since StaffService doesn't return it anymore) user.set_password(password) user.save() # ... rest of the method ... ``` **Key Changes:** - Generate password before creating user - Set password separately after user creation - Updated logging to show `user.email` instead of `user.username` ## Database Migration **Migration File:** `apps/accounts/migrations/0004_alter_user_managers_and_more.py` This migration: - Changes the manager on the User model - Alters the `acknowledgement_completed_at` field - Alters the `username` field to be non-unique and optional ## Usage ### Creating a Superuser ```bash python manage.py createsuperuser ``` The command will now prompt for: - Email: (required) - First name: (required) - Last name: (required) - Password: (required) ### Creating a User Programmatically ```python from apps.accounts.models import User # Create a regular user user = User.objects.create_user( email='user@example.com', password='securepassword123', first_name='John', last_name='Doe' ) # Create a superuser superuser = User.objects.create_superuser( email='admin@example.com', password='securepassword123', first_name='Admin', last_name='User' ) ``` ### Creating a User for Staff ```python from apps.organizations.services import StaffService # Ensure staff has an email staff.email = 'staff@example.com' staff.save() # Create user account user = StaffService.create_user_for_staff( staff=staff, role='staff', request=None ) ``` ## Testing ### Test Superuser Creation ```bash python manage.py createsuperuser ``` Follow the prompts to create a superuser using email as the identifier. ### Test Staff Seeding ```bash # Create staff without users python manage.py seed_staff --physicians 5 --nurses 5 # Create staff with user accounts python manage.py seed_staff --physicians 5 --nurses 5 --create-users # Create staff with user accounts and send emails python manage.py seed_staff --physicians 5 --nurses 5 --create-users --send-emails ``` ## Backward Compatibility The implementation maintains backward compatibility by: 1. Keeping the `username` field (optional, non-unique) 2. Preserving existing user data 3. Allowing authentication via email (primary) while maintaining username field for legacy purposes ## Authentication Flow 1. **Login:** Users authenticate using their email address 2. **Password Reset:** Password reset uses email field 3. **User Management:** All user operations use email as the primary identifier ## Important Notes 1. **Email is Required:** All users must have a unique email address 2. **Username is Optional:** The username field exists for backward compatibility but is not used for authentication 3. **Password Management:** Passwords are still hashed and stored securely using Django's built-in password hashing 4. **Staff Email Requirement:** When creating users for staff, the staff member must have an email address ## Migration Details ### Before Migration ```python class User(AbstractUser): email = models.EmailField(unique=True, db_index=True) USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['first_name', 'last_name'] objects = BaseUserManager() # Default Django manager ``` ### After Migration ```python class User(AbstractUser): email = models.EmailField(unique=True, db_index=True) username = models.CharField(max_length=150, blank=True, null=True, unique=False) USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['first_name', 'last_name'] objects = UserManager() # Custom email-based manager ``` ## Troubleshooting ### Issue: "The Email field must be set" **Solution:** Ensure you provide an email address when creating users. The email field is required. ### Issue: "Superuser must have is_staff=True" **Solution:** This error occurs when using `create_superuser` without proper parameters. Use `User.objects.create_superuser()` instead of `User.objects.create_user()`. ### Issue: Staff user creation fails **Solution:** Ensure the staff member has an email address before calling `StaffService.create_user_for_staff()`. ## Related Documentation - [Staff User Account Implementation](./STAFF_USER_ACCOUNT_IMPLEMENTATION.md) - [Staff Seed Command Update](./STAFF_SEED_COMMAND_UPDATE.md) - [Login/Logout Functionality Check](../LOGIN_LOGOUT_FUNCTIONALITY_CHECK.md) ## Summary The UserManager implementation provides a robust solution for email-based authentication in the PX360 application. It resolves issues with superuser creation, staff user management, and provides a consistent authentication experience across the platform.