HH/ADMIN_FIXES_SUMMARY.md
2026-01-15 15:02:42 +03:00

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=None in database, Django's built-in UserChangeForm tried to call len(value) on None
  • Django's built-in forms assume username is always a string, never None

Solution Applied

  1. Created Data Migration (apps/accounts/migrations/0003_fix_null_username.py)

    • Migrated all existing users with username=None to use their email as username
    • Ensures data integrity
  2. 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=True to default=''
    • Prevents future None values
  3. Updated User Admin (apps/accounts/admin.py)

    • Moved username field to Personal Info section
    • Made username read-only for existing users
    • Removed from add form (since we use email for authentication)
    • Primary identifier is email (USERNAME_FIELD = 'email')

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_badge methods were using format_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_safe import
  • Changed from format_html() to mark_safe()
  • mark_safe() is the correct function for plain HTML strings in Django 6.0
  • Fixed in both PXSourceAdmin and SourceUserAdmin

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 username from main fieldset (first section)
  • Added username to 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_safe import
  • Changed is_active_badge() in PXSourceAdmin to use mark_safe()
  • Changed is_active_badge() in SourceUserAdmin to use mark_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 TypeError if 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 AbstractUser includes 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'])


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

  1. Django 6.0 Breaking Changes

    • Always check release notes for breaking changes
    • Test admin thoroughly after upgrades
    • format_html() now enforces proper usage
  2. Optional Fields

    • Use default='' instead of null=True for CharFields
    • Prevents None-related bugs in forms and views
    • Better database performance
  3. Admin Best Practices

    • Use mark_safe() for static HTML
    • Use format_html() only with placeholders
    • Make computed fields read-only
  4. Data Migration Strategy

    • Create data migrations before schema migrations
    • Test migrations on staging first
    • Provide reverse migrations for rollback

Future Improvements

Potential Enhancements

  1. Remove username field entirely

    • Requires custom user model not extending AbstractUser
    • More significant refactoring
    • Not recommended for current scope
  2. Add validation for username field

    • Ensure username always matches email
    • Add save() method validation
    • Better data consistency
  3. Custom admin forms

    • Override UserChangeForm completely
    • Better control over validation
    • More complex implementation
  4. 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

  1. Monitor for any admin-related errors
  2. Test with different user roles
  3. Consider future enhancements based on usage patterns
  4. Update documentation if needed

Support & Troubleshooting

For questions or issues:

  1. Check Django admin documentation
  2. Review this summary
  3. Check Django 6.0 release notes
  4. Review related code changes
  • apps/accounts/models.py - User model definition
  • apps/accounts/admin.py - User admin configuration
  • apps/px_sources/admin.py - SourceUser admin configuration
  • apps/accounts/migrations/0003_fix_null_username.py - Data migration
  • apps/accounts/migrations/0004_username_default.py - Schema migration