1. Django Admin page
This is part of accounts/admin.py. Will it work?
@admin.register(CronJobLog)
class CronJobLogAdmin(admin.ModelAdmin):
# for the edit user page
fieldsets = ( ("Custom Fields", {"fields": ( "job_name", "started_at", "finished_at", "status", "message",)}), )
# for the create new user page
add_fieldsets = ( ("Custom Fields", {"fields": ( "job_name", "started_at", "finished_at", "status", "message",)}), )
# define which columns you see in the user list
list_display = ("job_name", "started_at", "finished_at", "status", "message",)
search_fields = ("job_name", "started_at", "finished_at", "status", "message",)
# define which filters appear
list_filter = ("job_name", "started_at", "finished_at", "status",)
#This subclass restricts the width of each column.
class Media: css = { "all": ("accounts/admin_custom.css",) }
It depends on what CronJobLog inherits from.
If your model is:
class CronJobLog(models.Model):
...
then no, this admin class should be simplified. The fieldsets and
especially add_fieldsets pattern comes from registering a custom user model, not a normal model.
For a regular model, I'd write something like:
@admin.register(CronJobLog)
class CronJobLogAdmin(admin.ModelAdmin):
list_display = (
"job_name",
"started_at",
"finished_at",
"status",
)
search_fields = (
"job_name",
"message",
)
list_filter = (
"job_name",
"status",
"started_at",
)
ordering = ("-started_at",)
readonly_fields = (
"started_at",
"finished_at",
)
fieldsets = (
(
None,
{
"fields": (
"job_name",
"started_at",
"finished_at",
"status",
"message",
)
},
),
)
class Media:
css = {
"all": ("accounts/admin_custom.css",)
}
Why remove add_fieldsets?
add_fieldsets is used by Django's UserAdmin because users have a special "Add User" page that is
different from the "Change User" page.
For example:
class CustomUserAdmin(UserAdmin):
add_fieldsets = (...)
works because UserAdmin knows what add_fieldsets means.
Your CronJobLogAdmin inherits from:
admin.ModelAdmin
and ModelAdmin doesn't use add_fieldsets. So it
won't have any effect.
search_fields
I would also change:
search_fields = (
"job_name",
"started_at",
"finished_at",
"status",
"message",
)
to:
search_fields = (
"job_name",
"message",
)
Searching a DateTimeField is not very useful. Django can do it, but users rarely type a full timestamp into the search box.
If you want to filter by date, list_filter is the better tool.
message in list_display
One thing I would change is removing message from list_display.
Imagine a message like:
Deleted 23 accounts.
Sent 15 emails.
Removed 87 orphan documents.
Finished in 5.81 seconds.
Your admin list becomes extremely wide.
Instead:
list_display = (
"job_name",
"started_at",
"finished_at",
"status",
)
When you click a row, you'll still see the full message on the detail page.
If you do want a preview, you can add a custom method:
@admin.display(description="Message")
def short_message(self, obj):
return obj.message[:60]
and then:
list_display = (
"job_name",
"started_at",
"finished_at",
"status",
"short_message",
)
That keeps the list much cleaner.
Overall, you're very close. The main thing I'd remove is add_fieldsets, because it's specific to UserAdmin and doesn't apply to a normal ModelAdmin.
1.(2) Why None?
Why Custom Field has to be replaced with None?
@admin.register(CronJobLog)
class CronJobLogAdmin(admin.ModelAdmin):
# for the edit user page
fieldsets = ( ("Custom Fields", {"fields": ( "job_name", "started_at", "finished_at", "status", "message",)}),
It doesn't have to be replaced with None. Both are valid, but they produce different appearances in the
Django admin.
Your version
fieldsets = (
(
"Custom Fields",
{
"fields": (
"job_name",
"started_at",
"finished_at",
"status",
"message",
)
},
),
)
On the edit page, you'll see a heading:
------------------------------
Custom Fields
------------------------------
Job name
Started at
Finished at
Status
Message
Using None
fieldsets = (
(
None,
{
"fields": (
"job_name",
"started_at",
"finished_at",
"status",
"message",
)
},
),
)
The page looks like:
Job name
Started at
Finished at
Status
Message
There is no heading.
Why did I suggest None?
For CronJobLog, there is only one logical group of fields:
- job_name
- started_at
- finished_at
- status
- message
Adding a heading like "Custom Fields" doesn't tell the administrator anything new.
By contrast, your CustomUser model has many different categories of information. There, headings are very helpful. For example:
fieldsets = (
("Personal Info", {...}),
("Permissions", {...}),
("Stripe", {...}),
("Subscription", {...}),
("Trial", {...}),
("Deletion", {...}),
)
Each heading separates a distinct section.
When a heading is useful
Suppose your CronJobLog later grows to:
job_name
started_at
finished_at
status
message
server_name
ip_address
duration
exit_code
Then you might organize it as:
fieldsets = (
(
"Execution",
{
"fields": (
"job_name",
"status",
"started_at",
"finished_at",
)
},
),
(
"Details",
{
"fields": (
"message",
"duration",
"exit_code",
)
},
),
)
Now the headings are meaningful.
My recommendation
For your current model, I would choose one of these:
Option A (cleanest)
fieldsets = (
(
None,
{
"fields": (
"job_name",
"started_at",
"finished_at",
"status",
"message",
)
},
),
)
Option B (also perfectly fine)
fieldsets = (
(
"Cron Job",
{
"fields": (
"job_name",
"started_at",
"finished_at",
"status",
"message",
)
},
),
)
I would avoid "Custom Fields", because that label is usually used when extending another model (like CustomUser) with additional fields. For a standalone CronJobLog model, a title like "Cron
Job", "Execution", or no title at all is clearer.
2. Acount Deletion log
How about this one? Is this okay?
@admin.register(AccountDeletionLog)
class AccountDeletionLogAdmin(admin.ModelAdmin):
# for the edit user page
fieldsets = (
("Acount Deletion", {"fields": (
"email", "username", "stripe_customer_id", "deleted_at", "reason",)}),
)
# define which columns you see in the user list
list_display = ("email", "username", "stripe_customer_id", "deleted_at", "reason",)
search_fields = (("email", "username", "stripe_customer_id", "deleted_at", "reason",,)
# define which filters appear
list_filter = (" deleted_at", "reason",)
ordering = ("-deleted_at",)
#This subclass restricts the width of each column.
class Media:
css = {
"all": ("accounts/admin_custom.css",)
}
Yes, that's very close. I noticed a few small issues that should be fixed.
1. search_fields has a syntax error
You have:
search_fields = (("email", "username", "stripe_customer_id", "deleted_at", "reason",,)
There are:
- an extra
( - an extra comma
- a missing closing
)
It should be:
search_fields = (
"email",
"username",
"stripe_customer_id",
"reason",
)
I would not include deleted_at in search_fields. It's
much easier to filter dates than to search them.
2. list_filter
You have:
list_filter = (" deleted_at", "reason",)
Notice the leading space before " deleted_at".
It should be:
list_filter = (
"deleted_at",
"reason",
)
3. list_display
This is perfectly fine:
list_display = (
"email",
"username",
"stripe_customer_id",
"deleted_at",
"reason",
)
4. fieldsets
This is also fine:
fieldsets = (
(
"Account Deletion",
{
"fields": (
"email",
"username",
"stripe_customer_id",
"deleted_at",
"reason",
)
},
),
)
I corrected the spelling from "Acount" to "Account".
5. ordering
Good:
ordering = ("-deleted_at",)
The newest deletions appear first.
One suggestion
If these records are only an audit trail and should never be edited manually, I'd make them read-only in the Django admin.
For example:
readonly_fields = (
"email",
"username",
"stripe_customer_id",
"deleted_at",
"reason",
)
You can even prevent adding and deleting records through the admin:
def has_add_permission(self, request):
return False
def has_delete_permission(self, request, obj=None):
return False
Since your deletion commands will create these logs automatically, administrators usually shouldn't create or modify them manually.
For an audit log such as AccountDeletionLog, making the records read-only helps preserve the integrity of the history while still allowing
administrators to view and search it.
3. Custom Filter
Modifying the code below, I want to have a filter for "processed" with the options of all, 0, <0 (more than 0). Is it possible? How can I do it, if it is possible?
@admin.register(CronJobLog)
class CronJobLogAdmin(admin.ModelAdmin):
# for the edit user page
fieldsets = (
("Cron Job", {"fields": (
"job_name", "processed", "started_at", "finished_at", "status", "message",)}),
)
# define which columns you see in the user list
list_display = ("job_name", "processed", "started_at", "finished_at", "status",)
search_fields = ("job_name", "processed", "message",)
# define which filters appear
list_filter = ("job_name", "processed", "started_at", "status",)
ordering = ("-started_at",)
#This subclass restricts the width of each column.
class Media:
css = {
"all": ("accounts/admin_custom.css",)
}
Yes, it's possible, but not by simply putting "processed" in list_filter.
For numeric fields, Django's default list_filter doesn't provide the options you want. Instead, you create a custom filter using SimpleListFilter.
Step 1: Import admin (you probably already have this)
from django.contrib import admin
Step 2: Create a custom filter
Put this above your CronJobLogAdmin class:
class ProcessedFilter(admin.SimpleListFilter):
title = "Processed"
parameter_name = "processed"
def lookups(self, request, model_admin):
return (
("0", "0"),
("gt0", "> 0"),
)
def queryset(self, request, queryset):
if self.value() == "0":
return queryset.filter(processed=0)
if self.value() == "gt0":
return queryset.filter(processed__gt=0)
return queryset
Notice that:
- If no option is selected, Django automatically shows All.
- You only define the additional choices.
Step 3: Use it in list_filter
Replace
list_filter = ("job_name", "processed", "started_at", "status",)
with
list_filter = (
"job_name",
ProcessedFilter,
"started_at",
"status",
)
The result
In the Django admin sidebar you'll see something like:
Processed
All
0
> 0
Selecting:
- All → all cron jobs
- 0 → jobs that processed nothing
- > 0 → jobs that actually processed something
I have one suggestion
For cron jobs, I think 0 and > 0 are useful, but I'd label them a little more clearly:
def lookups(self, request, model_admin):
return (
("0", "Nothing processed"),
("gt0", "Processed something"),
)
or
def lookups(self, request, model_admin):
return (
("0", "0"),
("gt0", "1 or more"),
)
Those labels are a bit more descriptive while keeping the filter simple.
For your project, I think this custom filter is the right solution because it makes it easy to quickly answer questions like "Which scheduled jobs actually did work today?" without scrolling through every log entry.
