-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add admin action to delete inactive users #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,12 @@ | ||||||||||||||||||||||||
| from django.contrib import admin | ||||||||||||||||||||||||
| from django.contrib.auth.models import User | ||||||||||||||||||||||||
| from django.contrib.auth.admin import UserAdmin | ||||||||||||||||||||||||
| from django.utils.translation import gettext_lazy as _, ngettext | ||||||||||||||||||||||||
| from django.utils import timezone | ||||||||||||||||||||||||
| from django.contrib import messages | ||||||||||||||||||||||||
| from django.conf import settings | ||||||||||||||||||||||||
| from django.db import models | ||||||||||||||||||||||||
| from datetime import timedelta | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| from .models import * | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
@@ -20,3 +28,58 @@ | |||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # Register all the models in myModels to the admin site | ||||||||||||||||||||||||
| admin.site.register(myModels) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def delete_inactive_users(modeladmin, request, queryset): | ||||||||||||||||||||||||
| """Admin action to delete inactive users based on settings.""" | ||||||||||||||||||||||||
| years = getattr(settings, 'INACTIVE_USER_AUTODELETE_YEARS', 5) | ||||||||||||||||||||||||
| if years <= 0: | ||||||||||||||||||||||||
| modeladmin.message_user( | ||||||||||||||||||||||||
| request, | ||||||||||||||||||||||||
| _("Inactive user auto-deletion is disabled (INACTIVE_USER_AUTODELETE_YEARS is 0 or not set)."), | ||||||||||||||||||||||||
| messages.WARNING | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| return | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| threshold_date = timezone.now() - timedelta(days=years * 365) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # Find inactive users: exclude staff and superusers | ||||||||||||||||||||||||
| inactive_users = queryset.filter( | ||||||||||||||||||||||||
| is_staff=False, | ||||||||||||||||||||||||
| is_superuser=False | ||||||||||||||||||||||||
| ).filter( | ||||||||||||||||||||||||
| models.Q(last_login__lt=threshold_date) | | ||||||||||||||||||||||||
| models.Q(last_login__isnull=True, date_joined__lt=threshold_date) | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
it-amanuens marked this conversation as resolved.
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| count = inactive_users.count() | ||||||||||||||||||||||||
| if count == 0: | ||||||||||||||||||||||||
| modeladmin.message_user( | ||||||||||||||||||||||||
| request, | ||||||||||||||||||||||||
| _("No inactive users found to delete."), | ||||||||||||||||||||||||
| messages.INFO | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| return | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| inactive_users.delete() | ||||||||||||||||||||||||
| modeladmin.message_user( | ||||||||||||||||||||||||
| request, | ||||||||||||||||||||||||
| ngettext( | ||||||||||||||||||||||||
| "%d inactive user was successfully deleted.", | ||||||||||||||||||||||||
| "%d inactive users were successfully deleted.", | ||||||||||||||||||||||||
| count, | ||||||||||||||||||||||||
| ) % count, | ||||||||||||||||||||||||
| messages.SUCCESS, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| delete_inactive_users.short_description = _("Delete inactive users") | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| class CustomUserAdmin(UserAdmin): | ||||||||||||||||||||||||
| actions = [delete_inactive_users] + list(UserAdmin.actions) | ||||||||||||||||||||||||
|
Comment on lines
+76
to
+80
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n rodatraden/admin.py | head -100Repository: it-amanuens/rodatraden Length of output: 3019 🏁 Script executed: rg "allowed_permissions" --type py -iRepository: it-amanuens/rodatraden Length of output: 48 🏁 Script executed: fd -e txt -e txt -e cfg -e ini | grep -i reqRepository: it-amanuens/rodatraden Length of output: 83 🏁 Script executed: cat requirements.txtRepository: it-amanuens/rodatraden Length of output: 817 🌐 Web query:
💡 Result: In Django 6.0, the admin action function attribute you should use is action_func.allowed_permissions (not ModelAdmin.allowed_permissions). You set it via the Citations: Require delete permission for this destructive action The Suggested fixdelete_inactive_users.short_description = _("Delete inactive users")
+delete_inactive_users.allowed_permissions = ("delete",)
class CustomUserAdmin(UserAdmin):
actions = [delete_inactive_users] + list(UserAdmin.actions)📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.15.12)[warning] 80-80: Consider Replace with (RUF005) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # Unregister the default User admin and register our custom one | ||||||||||||||||||||||||
| admin.site.unregister(User) | ||||||||||||||||||||||||
| admin.site.register(User, CustomUserAdmin) | ||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,95 @@ | ||
| from django.test import TestCase | ||
| from django.test import TestCase, RequestFactory | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: git ls-files rodatraden/tests.pyRepository: it-amanuens/rodatraden Length of output: 86 🏁 Script executed: fd tests.pyRepository: it-amanuens/rodatraden Length of output: 86 🏁 Script executed: cat -n rodatraden/tests.py | head -100Repository: it-amanuens/rodatraden Length of output: 4355 Use Tests directly mutate Import Suggested fix-from django.test import TestCase, RequestFactory
+from django.test import TestCase, RequestFactory, override_settings
...
- def test_delete_inactive_users(self):
- # Set the threshold to 5 years
- original_setting = getattr(settings, 'INACTIVE_USER_AUTODELETE_YEARS', 5)
- settings.INACTIVE_USER_AUTODELETE_YEARS = 5
+ `@override_settings`(INACTIVE_USER_AUTODELETE_YEARS=5)
+ def test_delete_inactive_users(self):
...
- # Restore setting
- settings.INACTIVE_USER_AUTODELETE_YEARS = original_setting
- def test_delete_inactive_users_disabled(self):
- # Set to disabled
- original_setting = getattr(settings, 'INACTIVE_USER_AUTODELETE_YEARS', 5)
- settings.INACTIVE_USER_AUTODELETE_YEARS = 0
+ `@override_settings`(INACTIVE_USER_AUTODELETE_YEARS=0)
+ def test_delete_inactive_users_disabled(self):
...
- # Restore setting
- settings.INACTIVE_USER_AUTODELETE_YEARS = original_setting🤖 Prompt for AI Agents |
||
| from django.contrib.auth.models import User | ||
| from django.utils import timezone | ||
| from django.contrib.admin.sites import AdminSite | ||
| from django.contrib import messages | ||
| from unittest.mock import Mock | ||
| from datetime import timedelta | ||
| from django.conf import settings | ||
|
|
||
| # Create your tests here. | ||
| from .admin import CustomUserAdmin, delete_inactive_users | ||
|
|
||
|
|
||
| class InactiveUserDeletionTest(TestCase): | ||
| def setUp(self): | ||
| self.factory = RequestFactory() | ||
| self.admin_site = AdminSite() | ||
| self.user_admin = CustomUserAdmin(User, self.admin_site) | ||
|
|
||
| # Create test users | ||
| self.old_user = User.objects.create_user( | ||
| username='olduser', | ||
| email='old@example.com', | ||
| date_joined=timezone.now() - timedelta(days=6*365) # 6 years ago | ||
| ) | ||
| self.old_user.last_login = timezone.now() - timedelta(days=6*365) | ||
| self.old_user.save() | ||
|
|
||
| self.new_user = User.objects.create_user( | ||
| username='newuser', | ||
| email='new@example.com', | ||
| date_joined=timezone.now() - timedelta(days=1*365) # 1 year ago | ||
| ) | ||
| self.new_user.last_login = timezone.now() - timedelta(days=1*365) | ||
| self.new_user.save() | ||
|
|
||
| self.never_logged_in_old = User.objects.create_user( | ||
| username='neverold', | ||
| email='never@example.com', | ||
| date_joined=timezone.now() - timedelta(days=6*365) # 6 years ago | ||
| ) | ||
| # last_login remains None | ||
|
|
||
| self.staff_user = User.objects.create_user( | ||
| username='staff', | ||
| email='staff@example.com', | ||
| is_staff=True | ||
| ) | ||
|
|
||
| def test_delete_inactive_users(self): | ||
| # Set the threshold to 5 years | ||
| original_setting = getattr(settings, 'INACTIVE_USER_AUTODELETE_YEARS', 5) | ||
| settings.INACTIVE_USER_AUTODELETE_YEARS = 5 | ||
|
|
||
| request = self.factory.post('/') | ||
| request.user = self.staff_user | ||
|
|
||
| # Mock message_user | ||
| self.user_admin.message_user = Mock() | ||
|
|
||
| # Call the action | ||
| delete_inactive_users(self.user_admin, request, User.objects.all()) | ||
|
|
||
| # Check that old users were deleted | ||
| self.assertFalse(User.objects.filter(username='olduser').exists()) | ||
| self.assertFalse(User.objects.filter(username='neverold').exists()) | ||
|
|
||
| # Check that new user and staff were not deleted | ||
| self.assertTrue(User.objects.filter(username='newuser').exists()) | ||
| self.assertTrue(User.objects.filter(username='staff').exists()) | ||
|
|
||
| # Restore setting | ||
| settings.INACTIVE_USER_AUTODELETE_YEARS = original_setting | ||
|
|
||
| def test_delete_inactive_users_disabled(self): | ||
| # Set to disabled | ||
| original_setting = getattr(settings, 'INACTIVE_USER_AUTODELETE_YEARS', 5) | ||
| settings.INACTIVE_USER_AUTODELETE_YEARS = 0 | ||
|
|
||
| request = self.factory.post('/') | ||
| request.user = self.staff_user | ||
|
|
||
| # Mock message_user | ||
| self.user_admin.message_user = Mock() | ||
|
|
||
| # Call the action | ||
| delete_inactive_users(self.user_admin, request, User.objects.all()) | ||
|
|
||
| # No users should be deleted | ||
| self.assertTrue(User.objects.filter(username='olduser').exists()) | ||
| self.assertTrue(User.objects.filter(username='neverold').exists()) | ||
| self.assertTrue(User.objects.filter(username='newuser').exists()) | ||
| self.assertTrue(User.objects.filter(username='staff').exists()) | ||
|
|
||
| # Restore setting | ||
| settings.INACTIVE_USER_AUTODELETE_YEARS = original_setting | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix inaccurate disabled-state message
The text says “0 or not set”, but
getattr(settings, 'INACTIVE_USER_AUTODELETE_YEARS', 5)treats “not set” as enabled (5). Update the message to match the real condition (<= 0).🤖 Prompt for AI Agents