# 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`) ```python # 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., `'{}'`) - 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`) ```python # Before: from django.utils.html import format_html def is_active_badge(self, obj): if obj.is_active: return format_html('Active') return format_html('Inactive') # After: from django.utils.html import format_html, mark_safe def is_active_badge(self, obj): if obj.is_active: return mark_safe('Active') return mark_safe('Inactive') ``` **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:** ```python 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 ```bash 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 - [x] View User list - [x] Add new User - [x] Edit existing User - [x] Verify username field is read-only for existing users - [x] Verify username defaults to email for new users - [x] Verify no TypeError when editing users ### SourceUser Admin Testing - [x] View SourceUser list - [x] Add new SourceUser - [x] Edit existing SourceUser - [x] Verify active status badge displays correctly - [x] Verify inactive status badge displays correctly - [x] Verify no TypeError on list or detail views ### PXSource Admin Testing - [x] View PXSource list - [x] Add new PXSource - [x] Edit existing PXSource - [x] Verify active status badge displays correctly - [x] Verify inactive status badge displays correctly - [x] 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('Active')` **New behavior (Django 6.0+):** - Requires format strings with placeholders - Example: `format_html('{}', 'Active')` - Throws `TypeError` if no placeholders provided **Correct usage for plain HTML:** ```python from django.utils.html import mark_safe mark_safe('Active') ``` ### 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:** ```python 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 ```python # 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 ```python from django.utils.html import mark_safe # For static HTML mark_safe('Active') # For dynamic HTML format_html('{}', status) ``` ### 3. Make Derived Fields Read-Only ```python 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 ```python 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](https://docs.djangoproject.com/en/stable/ref/contrib/admin/) - [Django 6.0 Release Notes](https://docs.djangoproject.com/en/stable/releases/6.0.html) - [format_html() Documentation](https://docs.djangoproject.com/en/stable/ref/utils/#django.utils.html.format_html) - [mark_safe() Documentation](https://docs.djangoproject.com/en/stable/ref/utils/#django.utils.html.mark_safe) --- ## Rollback Plan If issues arise: ### Rollback User Changes ```bash 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 ```bash # Manually revert apps/px_sources/admin.py # Change mark_safe back to format_html ``` ### Rollback Complete ```bash # 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 ## Related Files - `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