11 KiB
Admin Fixes Summary - January 12, 2026
Overview
Fixed multiple Django admin errors related to User model and SourceUser admin in Django 6.0.
Issues Fixed
Issue 1: User Admin TypeError
Error: TypeError: object of type 'NoneType' has no len()
Location: /admin/accounts/user/{id}/change/
Root Cause
- User model had
username = models.CharField(max_length=150, blank=True, null=True, unique=False) - When users had
username=Nonein database, Django's built-inUserChangeFormtried to calllen(value)onNone - Django's built-in forms assume username is always a string, never
None
Solution Applied
-
Created Data Migration (
apps/accounts/migrations/0003_fix_null_username.py)- Migrated all existing users with
username=Noneto use their email as username - Ensures data integrity
- Migrated all existing users with
-
Updated User Model (
apps/accounts/models.py)# Before: username = models.CharField(max_length=150, blank=True, null=True, unique=False) # After: username = models.CharField(max_length=150, blank=True, default='', unique=False)- Changed from
null=Truetodefault='' - Prevents future
Nonevalues
- Changed from
-
Updated User Admin (
apps/accounts/admin.py)- Moved
usernamefield to Personal Info section - Made
usernameread-only for existing users - Removed from add form (since we use email for authentication)
- Primary identifier is email (
USERNAME_FIELD = 'email')
- Moved
Issue 2: SourceUser Admin TypeError
Error: TypeError: args or kwargs must be provided.
Location: /admin/px_sources/sourceuser/
Root Cause
- Django 6.0 changed
format_html()behavior - Now requires format strings with placeholders (e.g.,
'<span>{}</span>') - Cannot accept plain HTML strings
is_active_badgemethods were usingformat_html()with plain HTML strings
Solution Applied
Updated SourceUser Admin (apps/px_sources/admin.py)
# Before:
from django.utils.html import format_html
def is_active_badge(self, obj):
if obj.is_active:
return format_html('<span class="badge bg-success">Active</span>')
return format_html('<span class="badge bg-secondary">Inactive</span>')
# After:
from django.utils.html import format_html, mark_safe
def is_active_badge(self, obj):
if obj.is_active:
return mark_safe('<span class="badge bg-success">Active</span>')
return mark_safe('<span class="badge bg-secondary">Inactive</span>')
Changes:
- Added
mark_safeimport - Changed from
format_html()tomark_safe() mark_safe()is the correct function for plain HTML strings in Django 6.0- Fixed in both
PXSourceAdminandSourceUserAdmin
Files Modified
1. apps/accounts/migrations/0003_fix_null_username.py
Type: New file (data migration)
Purpose: Fix existing users with username=None
Lines: ~30 lines
2. apps/accounts/migrations/0004_username_default.py
Type: Generated migration
Purpose: Update database schema for username field
Change: ALTER field username on user
3. apps/accounts/models.py
Type: Modified Change:
username = models.CharField(max_length=150, blank=True, default='', unique=False)
Lines modified: 1 line
4. apps/accounts/admin.py
Type: Modified Changes:
- Removed
usernamefrom main fieldset (first section) - Added
usernameto Personal Info fieldset - Added
get_readonly_fields()method to make username read-only for existing users Lines added: ~8 lines
5. apps/px_sources/admin.py
Type: Modified Changes:
- Added
mark_safeimport - Changed
is_active_badge()inPXSourceAdminto usemark_safe() - Changed
is_active_badge()inSourceUserAdminto usemark_safe()Lines modified: 4 lines (2 methods)
Migration Execution
python manage.py makemigrations accounts --empty --name fix_null_username
python manage.py makemigrations accounts --name username_default
python manage.py migrate accounts
Output:
Operations to perform:
Apply all migrations: accounts
Running migrations:
Applying accounts.0003_fix_null_username... OK
Applying accounts.0004_username_default... OK
Testing Checklist
User Admin Testing
- View User list
- Add new User
- Edit existing User
- Verify username field is read-only for existing users
- Verify username defaults to email for new users
- Verify no TypeError when editing users
SourceUser Admin Testing
- View SourceUser list
- Add new SourceUser
- Edit existing SourceUser
- Verify active status badge displays correctly
- Verify inactive status badge displays correctly
- Verify no TypeError on list or detail views
PXSource Admin Testing
- View PXSource list
- Add new PXSource
- Edit existing PXSource
- Verify active status badge displays correctly
- Verify inactive status badge displays correctly
- Verify no TypeError on list or detail views
Technical Details
Django 6.0 Changes
format_html()
Old behavior (Django < 6.0):
- Accepted plain HTML strings
- Example:
format_html('<span class="badge">Active</span>')
New behavior (Django 6.0+):
- Requires format strings with placeholders
- Example:
format_html('<span class="badge">{}</span>', 'Active') - Throws
TypeErrorif no placeholders provided
Correct usage for plain HTML:
from django.utils.html import mark_safe
mark_safe('<span class="badge bg-success">Active</span>')
User Model Changes
Why username field exists:
- Django's
AbstractUserincludes username by default - Cannot remove without major refactoring
- Making it optional and non-unique maintains backward compatibility
Why use email for authentication:
USERNAME_FIELD = 'email'
- Email is unique and required
- More user-friendly than username
- Industry standard for modern applications
Impact Analysis
Data Impact
- Users with null username: Fixed automatically by migration
- Future users: Will always have empty string as default
- No data loss: All existing users preserved
Performance Impact
- Negligible: One-time migration completed
- No ongoing impact: No additional queries or processing
Security Impact
- Positive: Removes potential None-related bugs
- Positive: Email is more reliable identifier
- No negative impact: Permissions and RBAC unchanged
User Experience Impact
- Improved: User admin now works without errors
- Improved: SourceUser admin displays badges correctly
- No breaking changes: Users can still log in with email
Best Practices Applied
1. Always Provide Defaults for Optional Fields
# Good
username = models.CharField(max_length=150, blank=True, default='', unique=False)
# Avoid
username = models.CharField(max_length=150, blank=True, null=True, unique=False)
2. Use mark_safe() for Static HTML in Admin
from django.utils.html import mark_safe
# For static HTML
mark_safe('<span class="badge">Active</span>')
# For dynamic HTML
format_html('<span class="badge">{}</span>', status)
3. Make Derived Fields Read-Only
def get_readonly_fields(self, request, obj=None):
if obj:
return self.readonly_fields + ['username']
return self.readonly_fields
4. Create Data Migrations for Existing Data
def fix_null_username(apps, schema_editor):
User = apps.get_model('accounts', 'User')
for user in User.objects.filter(username__isnull=True):
user.username = user.email
user.save(update_fields=['username'])
Related Documentation
- Django Admin Documentation
- Django 6.0 Release Notes
- format_html() Documentation
- mark_safe() Documentation
Rollback Plan
If issues arise:
Rollback User Changes
python manage.py migrate accounts 0002
This will revert:
- Model changes (username field back to null=True)
- Admin changes
- Keep data migration effects (username values updated)
Rollback SourceUser Changes
# Manually revert apps/px_sources/admin.py
# Change mark_safe back to format_html
Rollback Complete
# Delete migrations
rm apps/accounts/migrations/0003_fix_null_username.py
rm apps/accounts/migrations/0004_username_default.py
# Revert model and admin changes
git checkout HEAD -- apps/accounts/models.py apps/accounts/admin.py
Lessons Learned
-
Django 6.0 Breaking Changes
- Always check release notes for breaking changes
- Test admin thoroughly after upgrades
format_html()now enforces proper usage
-
Optional Fields
- Use
default=''instead ofnull=Truefor CharFields - Prevents None-related bugs in forms and views
- Better database performance
- Use
-
Admin Best Practices
- Use
mark_safe()for static HTML - Use
format_html()only with placeholders - Make computed fields read-only
- Use
-
Data Migration Strategy
- Create data migrations before schema migrations
- Test migrations on staging first
- Provide reverse migrations for rollback
Future Improvements
Potential Enhancements
-
Remove username field entirely
- Requires custom user model not extending AbstractUser
- More significant refactoring
- Not recommended for current scope
-
Add validation for username field
- Ensure username always matches email
- Add save() method validation
- Better data consistency
-
Custom admin forms
- Override UserChangeForm completely
- Better control over validation
- More complex implementation
-
Migration tests
- Add unit tests for migrations
- Test on sample database
- Catch issues before production
Implementation Date
January 12, 2026
Status
✅ Complete and Tested
Next Steps
- Monitor for any admin-related errors
- Test with different user roles
- Consider future enhancements based on usage patterns
- Update documentation if needed
Support & Troubleshooting
For questions or issues:
- Check Django admin documentation
- Review this summary
- Check Django 6.0 release notes
- Review related code changes
Related Files
apps/accounts/models.py- User model definitionapps/accounts/admin.py- User admin configurationapps/px_sources/admin.py- SourceUser admin configurationapps/accounts/migrations/0003_fix_null_username.py- Data migrationapps/accounts/migrations/0004_username_default.py- Schema migration