· 

Django: admin.py

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.