386 lines
11 KiB
Markdown
386 lines
11 KiB
Markdown
# 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., `'<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`)
|
|
```python
|
|
# 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:**
|
|
```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('<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:**
|
|
```python
|
|
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:**
|
|
```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('<span class="badge">Active</span>')
|
|
|
|
# For dynamic HTML
|
|
format_html('<span class="badge">{}</span>', 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 |