9.2 KiB
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:
- The
createsuperusermanagement command expecting a username instead of email - User creation methods not properly handling email-based authentication
- 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
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
UserManagerimport fromdjango.contrib.auth.models - Added optional
usernamefield (for backward compatibility) - Set
objects = UserManager()on the User model
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:
@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:
emailis now the first parameter (required by UserManager)usernameis 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:
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.emailinstead ofuser.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_atfield - Alters the
usernamefield to be non-unique and optional
Usage
Creating a Superuser
python manage.py createsuperuser
The command will now prompt for:
- Email: (required)
- First name: (required)
- Last name: (required)
- Password: (required)
Creating a User Programmatically
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
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
python manage.py createsuperuser
Follow the prompts to create a superuser using email as the identifier.
Test Staff Seeding
# 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:
- Keeping the
usernamefield (optional, non-unique) - Preserving existing user data
- Allowing authentication via email (primary) while maintaining username field for legacy purposes
Authentication Flow
- Login: Users authenticate using their email address
- Password Reset: Password reset uses email field
- User Management: All user operations use email as the primary identifier
Important Notes
- Email is Required: All users must have a unique email address
- Username is Optional: The username field exists for backward compatibility but is not used for authentication
- Password Management: Passwords are still hashed and stored securely using Django's built-in password hashing
- Staff Email Requirement: When creating users for staff, the staff member must have an email address
Migration Details
Before Migration
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
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
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.