1. Which is more efficient?
Which is more efficient? First, this is in superadmin_summary():
admins = (
CustomUser.objects.filter(role="admin")
.annotate(
num_clients=Count("clients", distinct=True),
num_docs=Count("clients__user__documents", distinct=True),
)
.order_by("id") # .order_by("username")
)
And the following is in superadmin_dashboard():
admins = CustomUser.objects.filter(role="admin").order_by("username") # sort by name # get all admins
client_count = ClientProfile.objects.count()
document_count = ClientDocument.objects.count()
The answer depends on what information you need. They are doing different jobs, so one isn't simply "better" than the other.
superadmin_dashboard()
admins = CustomUser.objects.filter(role="admin").order_by("username")
client_count = ClientProfile.objects.count()
document_count = ClientDocument.objects.count()
This results in approximately:
- Query 1: fetch all admins
- Query 2:
SELECT COUNT(*) FROM client_profile - Query 3:
SELECT COUNT(*) FROM client_document
The two .count() calls are very efficient because the database just returns a single integer. It doesn't load every client or document into Python.
superadmin_summary()
admins = (
CustomUser.objects.filter(role="admin")
.annotate(
num_clients=Count("clients", distinct=True),
num_docs=Count("clients__user__documents", distinct=True),
)
.order_by("id")
)
This produces one SQL query that joins several tables and calculates two counts for each admin.
Conceptually, it is similar to:
SELECT
admin.id,
COUNT(DISTINCT client.id),
COUNT(DISTINCT document.id)
FROM ...
GROUP BY admin.id;
This query is more complex, but it gives information like:
| Admin | Clients | Documents |
|---|---|---|
| Alice | 12 | 158 |
| Bob | 8 | 73 |
| Charlie | 20 | 412 |
That information cannot be obtained with simple .count() calls.
Which is faster?
If you only need totals:
client_count = ClientProfile.objects.count()
document_count = ClientDocument.objects.count()
is faster.
If you need counts per admin, then
.annotate(
num_clients=Count(...),
num_docs=Count(...),
)
is the right approach.
One thing I would improve
In superadmin_summary(), you currently have:
for admin in admins:
reset_download_if_needed(admin)
admin.storage_used_mb = get_admin_storage_usage(admin)
The expensive part of this view is probably not the annotate(). Instead, it's:
get_admin_storage_usage(admin)
If that function walks through every document belonging to an admin, then it executes extra work for every admin.
So the rough cost becomes:
annotate() inexpensive
+
storage calculation potentially expensive
+
download reset inexpensive
I suspect the storage calculation dominates the runtime.
My recommendation
I would keep both views as they are:
Dashboard
admins = ...
client_count = ClientProfile.objects.count()
document_count = ClientDocument.objects.count()
because you only need totals.
Summary
admins.annotate(
num_clients=...,
num_docs=...
)
because you actually display per-admin counts.
That separation is clean and efficient.
One optimization I would consider in the future is get_admin_storage_usage(). If you show me that function, I can tell you whether it can be made
significantly faster. In many Django applications, storage calculations can be moved from Python loops into a single database aggregation, which scales much better as the number of documents
grows.
1.(2) get_admin_storage_usage()
You said:
One optimization I would consider in the future is get_admin_storage_usage(). If you show me that function, I can tell you whether it can be made significantly faster. In many Django applications, storage calculations can be moved from Python loops into a single database aggregation, which scales much better as the number of documents grows.
So, here is the code. Can you improve this?
def get_admin_storage_usage(admin_user):
total = (
ClientDocument.objects.filter(Q(client__clientprofile__admin=admin_user) | Q(uploader=admin_user))
.distinct() # ensure each document counted once
.aggregate(total=Sum("file_size"))["total"]
)
return total or 0 # bytes
Your function is already quite good. It lets the database do the summation instead of loading all documents into Python, which is the right approach.
def get_admin_storage_usage(admin_user):
total = (
ClientDocument.objects.filter(
Q(client__clientprofile__admin=admin_user) |
Q(uploader=admin_user)
)
.distinct()
.aggregate(total=Sum("file_size"))["total"]
)
return total or 0
There are only a few possible improvements.
1. The biggest issue is not this function itself
The expensive part is that you call it once for every admin.
Suppose you have
- 500 admins
- 50,000 documents
Your dashboard executes something like
SELECT SUM(...)
500 separate times.
That is called the N+1 query problem.
So even though each query is efficient, there are many of them.
2. Remove .distinct() if possible
Ask yourself:
Can the same document satisfy both conditions?
client__clientprofile__admin = admin_user
OR
uploader = admin_user
For example
- Admin uploads a client's document.
Then
- uploader == admin
- client's admin == admin
The same document matches twice.
Without .distinct(), SQL would count it twice.
So in your application, .distinct() is actually necessary.
I would leave it.
3. Slightly cleaner version
Instead of
.aggregate(total=Sum("file_size"))["total"]
I usually write
.aggregate(total_size=Sum("file_size"))["total_size"] or 0
which is just a little clearer.
def get_admin_storage_usage(admin_user):
return (
ClientDocument.objects.filter(
Q(client__clientprofile__admin=admin_user) |
Q(uploader=admin_user)
)
.distinct()
.aggregate(total_size=Sum("file_size"))["total_size"]
or 0
)
No performance difference.
4. A much faster approach for hundreds of admins
If your SaaS grows, instead of
for admin in admins:
get_admin_storage_usage(admin)
you can calculate all admins' storage usage in one query.
For example, something like
from django.db.models import Sum
admins = (
CustomUser.objects
.filter(role="admin")
.annotate(
storage_used=Sum(
"clients__user__documents__file_size",
distinct=True
)
)
)
Now every admin already has
admin.storage_used
without calling another function.
Instead of
500 SQL queries
you get
1 SQL query
which is dramatically faster.
Whether this works exactly depends on your model relationships. I'd need to see your ClientDocument, ClientProfile, and related models to produce the correct annotation.
5. The fastest approach (recommended for commercial SaaS)
Many production SaaS applications don't calculate storage every time someone opens the dashboard.
Instead they store it.
For example:
AdminProfile.storage_used_bytes
Whenever
- a document is uploaded
storage_used += file_size
Whenever
- a document is deleted
storage_used -= file_size
Now your dashboard simply does
admin.admin_profile.storage_used_bytes
No aggregation.
No scanning documents.
Almost instant.
This is how systems like Dropbox, Google Drive, and many commercial SaaS products handle storage quotas.
What I would recommend for your project
Since I know you're building a commercial Django client portal, I'd recommend these stages:
- Current stage (good enough): Keep your current function. It's correct and easy to understand.
-
When you have a few hundred admins: Replace the per-admin function calls with a single
annotate()query if practical. -
When you launch publicly and expect significant usage: Add a
storage_used_bytesfield toAdminProfileand update it whenever documents are uploaded or deleted. This gives constant-time (O(1)) lookups and scales far better than recalculating totals on every dashboard request.
1.(3) Code explained
Can you explain the code in details? I don't understand what "Q" does.
def get_admin_storage_usage(admin_user):
total = (
ClientDocument.objects.filter(Q(client__clientprofile__admin=admin_user) | Q(uploader=admin_user))
.distinct() # ensure each document counted once
.aggregate(total=Sum("file_size"))["total"]
)
return total or 0 # bytes
Certainly. The Q object is one of the most useful features in Django's ORM, and understanding it will help you write much more flexible database
queries.
Let's go through your function line by line.
def get_admin_storage_usage(admin_user):
The function receives one argument:
admin_user
which is a CustomUser object representing one admin.
For example,
admin_user.username
might be
alice
Step 1
ClientDocument.objects
This means
Start a query on the
ClientDocumenttable.
Suppose the table contains
| id | file | client | uploader | file_size |
|---|---|---|---|---|
| 1 | a.pdf | Client A | alice | 1000 |
| 2 | b.pdf | Client B | alice | 2000 |
| 3 | c.pdf | Client C | bob | 500 |
| 4 | d.pdf | Client A | alice | 1500 |
Nothing has been retrieved yet.
Step 2
Now comes the interesting part.
.filter(
Q(client__clientprofile__admin=admin_user)
|
Q(uploader=admin_user)
)
Let's first ignore the |.
Without Q
Normally you write
.filter(username="alice")
or
.filter(email="[email protected]")
If you write
.filter(
username="alice",
email="[email protected]"
)
Django automatically uses AND.
It becomes
WHERE username='alice'
AND email='[email protected]'
There is no way to get OR like this.
What is Q?
A Q object represents one condition.
For example
Q(username="alice")
means
username = "alice"
Another one
Q(email="[email protected]")
means
email = "[email protected]"
Notice these are just objects.
They haven't queried the database yet.
Why use Q?
Because you can combine them.
Example
Q(username="alice") | Q(email="[email protected]")
means
username = "alice"
OR
email = "[email protected]"
The | means OR.
Likewise
Q(username="alice") & Q(is_active=True)
means
username="alice"
AND
is_active=True
The & means AND.
You can even negate one.
~Q(is_active=True)
means
NOT active
In your function
The first condition is
Q(client__clientprofile__admin=admin_user)
Let's read it slowly.
client
means
Follow the foreign key named client.
Then
clientprofile
means
Go to that user's ClientProfile.
Then
admin
means
Look at the admin assigned to that client.
Finally
= admin_user
means
Is that admin the admin we're looking for?
So this condition means
Find documents whose client belongs to this admin.
The second condition
Q(uploader=admin_user)
means
Find documents uploaded by this admin.
Then you combine them
Q(client__clientprofile__admin=admin_user)
|
Q(uploader=admin_user)
The vertical bar
|
means
OR
So Django generates something equivalent to
WHERE
client.admin = admin_user
OR
uploader = admin_user
Why is OR needed?
Suppose your data is
| Document | Client's Admin | Uploader |
|---|---|---|
| Doc A | Alice | Alice |
| Doc B | Alice | Client |
| Doc C | Bob | Bob |
| Doc D | Bob | Alice |
For Alice
Condition 1
client.admin = Alice
matches
- Doc A
- Doc B
Condition 2
uploader = Alice
matches
- Doc A
- Doc D
Using OR gives
- Doc A
- Doc B
- Doc D
Exactly what you want.
Step 3
.distinct()
Remember Doc A?
It satisfies both conditions.
Without distinct()
Condition 1
Doc A
Condition 2
Doc A
Doc A could be counted twice.
distinct() removes duplicates.
Result
Doc A
Doc B
Doc D
each appearing only once.
Step 4
.aggregate(total=Sum("file_size"))
This tells the database
Add up the values in the
file_sizecolumn.
Suppose the remaining rows are
| Document | Size |
|---|---|
| Doc A | 1000 |
| Doc B | 2500 |
| Doc D | 700 |
The database computes
1000
+2500
+700
------
4200
The result returned is
{
"total": 4200
}
Step 5
["total"]
extracts the value
4200
from the dictionary.
Step 6
return total or 0
If there are no matching documents,
Sum(...)
returns
None
instead of zero.
So
None or 0
becomes
0
If
total = 4200
then
4200 or 0
is
4200
So the function always returns an integer number of bytes.
One important thing about |
Earlier you asked about | versus ||.
In Python:
-
|is the bitwise OR operator, and Django overloads it forQobjects to mean SQL OR. -
||does not exist in Python. If you write it, you'll get a syntax error.
So in Django, whenever you see
Q(...) | Q(...)
you can mentally read it as:
Condition A OR Condition B
and
Q(...) & Q(...)
as:
Condition A AND Condition B
This is the most common use of Q objects, and you'll see this pattern frequently in Django projects.
2. superadmin_summary()
This is part of superadmin_summary():
for admin in page_obj:
admin_profile = getattr(admin, "admin_profile", None)
reset_download_if_needed(admin) # ✅ keep usage accurate. It is defined in utils.py
# Create custom attributes and attach usage and limit
admin.storage_used_mb = get_admin_storage_usage(admin) / (1024 * 1024)
admin.storage_limit_mb = admin_profile.storage_limit_mb if admin_profile else 0
admin.download_used_mb = admin.download_bytes_used / (1024 * 1024)
admin.download_limit_mb = admin.download_bytes_limit / (1024 * 1024)
Why does the line of code admin_profile = getattr(admin, "admin_profile", None) work? It seems there is no attribute of admin_profile in CustomUser. admin is a CustomUser, which is defined as:
(Code omitted)
This is one of Django's nicest features. It looks like admin has an admin_profile attribute, but it is
actually being created automatically by Django through a OneToOneField.
I suspect your AdminProfile model looks something like this:
class AdminProfile(models.Model):
user = models.OneToOneField(
CustomUser,
on_delete=models.CASCADE,
related_name="admin_profile",
)
storage_limit_mb = models.PositiveIntegerField(default=5000)
or perhaps
user = models.OneToOneField(CustomUser, on_delete=models.CASCADE)
How does admin.admin_profile exist?
Suppose you have these database rows.
CustomUser
| id | username |
|---|---|
| 5 | alice |
AdminProfile
| id | user_id | storage_limit_mb |
|---|---|---|
| 1 | 5 | 5000 |
Because AdminProfile.user is a OneToOneField, Django automatically creates a reverse relationship.
So after retrieving the user
admin = CustomUser.objects.get(id=5)
you can write
admin.admin_profile
and Django automatically performs something equivalent to
SELECT *
FROM admin_profile
WHERE user_id = 5;
and returns the corresponding AdminProfile object.
You never defined admin_profile inside CustomUser, but Django added it for you.
Why does getattr() work?
Your code is
admin_profile = getattr(admin, "admin_profile", None)
getattr() is simply Python's built-in function.
Normally you might write
admin_profile = admin.admin_profile
However, if the user doesn't have an AdminProfile, Django raises an exception:
RelatedObjectDoesNotExist
instead of returning None.
Using
getattr(admin, "admin_profile", None)
is an attempt to avoid an error if the attribute doesn't exist.
But there is a small catch
For Django reverse OneToOne relations,
getattr(admin, "admin_profile", None)
does not always return None.
When Django knows the relation exists but no row is present, it raises
AdminProfile.DoesNotExist
(or RelatedObjectDoesNotExist).
So if an admin somehow has no profile, this can still fail.
The safest pattern is
try:
admin_profile = admin.admin_profile
except AdminProfile.DoesNotExist:
admin_profile = None
Why hasn't your code crashed?
Most likely because every admin in your system has an AdminProfile.
For example, when you create an admin you probably do something like
admin = CustomUser.objects.create(...)
AdminProfile.objects.create(
user=admin,
storage_limit_mb=5000,
)
or perhaps you create it in a signal.
Therefore
admin.admin_profile
always exists, so getattr() simply returns it.
You can verify this
In the Django shell:
admin = CustomUser.objects.filter(role="admin").first()
print(admin.admin_profile)
If it prints something like
AdminProfile object (3)
then Django is following the OneToOne relationship automatically.
A performance improvement
Since every admin appears to have an AdminProfile, you can avoid one database query per admin by fetching the related profile up front:
admins = (
CustomUser.objects
.filter(role="admin")
.select_related("admin_profile")
)
Then later:
admin_profile = admin.admin_profile
will not hit the database again—it will already have been loaded. This is a good optimization for your superadmin_summary() view because it loops over many admins.
2.(2) More efficient version
The following code can be modified into the one below, right?
admins = (
CustomUser.objects.filter(role="admin")
.annotate(
num_clients=Count("clients", distinct=True),
num_docs=Count("clients__user__documents", distinct=True),
)
.order_by("id") # .order_by("username")
)
....
for admin in page_obj:
admin_profile = getattr(admin, "admin_profile", None)
....
[Modified version]
admins = (
CustomUser.objects.filter(role="admin")
.select_related("admin_profile")
.annotate(
num_clients=Count("clients", distinct=True),
num_docs=Count("clients__user__documents", distinct=True),
)
.order_by("id") # .order_by("username")
)
...
for admin in page_obj:
admin_profile = admin.admin_profile
...
And the modified one is more efficient, right?
Yes, that's the right direction, and it is more efficient. There are just two small points I'd change.
1. select_related("admin_profile") is definitely an improvement
This:
admins = (
CustomUser.objects.filter(role="admin")
.select_related("admin_profile")
.annotate(
num_clients=Count("clients", distinct=True),
num_docs=Count("clients__user__documents", distinct=True),
)
.order_by("id")
)
is better than before.
Previously, for every admin on the page Django would execute another query when you did
admin.admin_profile
so if you displayed 50 admins, you'd have roughly
- 1 query for the admins
- 50 more queries for their profiles
With select_related(), Django performs one SQL JOIN and retrieves everything in one query.
So this is a worthwhile optimization.
2. admin_profile = admin.admin_profile
Assuming every admin always has an AdminProfile (which appears to be true in your project), then
admin_profile = admin.admin_profile
is perfectly fine.
You can even simplify the next line.
Instead of
admin.storage_limit_mb = (
admin_profile.storage_limit_mb if admin_profile else 0
)
you can simply write
admin.storage_limit_mb = admin.admin_profile.storage_limit_mb
because you're assuming the profile always exists.
3. One thing I would change
You wrote
Q(id__icontains=search)
This is the only line I would change.
id is an integer field.
icontains is intended for text fields.
On many databases this either
- raises an error,
- performs poorly,
- or forces Django to cast the integer to text.
A better approach is
from django.db.models import Q
if search:
filters = (
Q(username__icontains=search) |
Q(email__icontains=search)
)
if search.isdigit():
filters |= Q(id=int(search))
admins = admins.filter(filters)
This gives you
- searching
"mk"→ username/email - searching
"18"→ id=18 or username/email containing"18"
This is both cleaner and faster.
4. Another tiny improvement
Your comment says
page_number = request.GET.get("page") # if the result is None, 1 is returned
Actually,
request.GET.get("page")
returns None if there is no page parameter.
It is
page_obj = paginator.get_page(page_number)
that interprets None as page 1.
So I'd change the comment to
page_number = request.GET.get("page") # None if not specified
page_obj = paginator.get_page(page_number) # None → page 1
My final version
I'd write it like this:
admins = (
CustomUser.objects
.filter(role="admin")
.select_related("admin_profile")
.annotate(
num_clients=Count("clients", distinct=True),
num_docs=Count("clients__user__documents", distinct=True),
)
.order_by("id")
)
if search:
filters = (
Q(username__icontains=search) |
Q(email__icontains=search)
)
if search.isdigit():
filters |= Q(id=int(search))
admins = admins.filter(filters)
Then inside the loop:
admin.storage_limit_mb = admin.admin_profile.storage_limit_mb
Overall, I think this version is cleaner and more efficient than your original one. Using select_related() is exactly the kind of optimization
that's appropriate for a page like superadmin_summary(), which displays many admins and accesses each admin's profile.
2.(3) Abandoned signup does not have a profile
What should I do if I can't assume the profile always exists?
That's a good question. It depends on how robust you want your application to be.
If an admin should always have an AdminProfile, then there are two approaches.
Option 1 (recommended): Treat a missing profile as a bug
If a missing AdminProfile indicates your database is in an inconsistent state, don't silently ignore it. Instead:
for admin in page_obj:
try:
admin_profile = admin.admin_profile
except AdminProfile.DoesNotExist:
logger.error(
f"AdminProfile missing for admin id={admin.id}, username={admin.username}"
)
continue # or raise an exception
This lets you discover and fix the underlying problem.
Option 2: Continue gracefully
If it's acceptable that some admins don't yet have a profile, then use a fallback:
for admin in page_obj:
try:
admin_profile = admin.admin_profile
storage_limit_mb = admin_profile.storage_limit_mb
except AdminProfile.DoesNotExist:
storage_limit_mb = 0
admin.storage_limit_mb = storage_limit_mb
The page still renders, and that admin simply shows a storage limit of 0.
Option 3: Use getattr() (what you currently have)
admin_profile = getattr(admin, "admin_profile", None)
However, this is not my favorite for a reverse OneToOneField.
The reason is that admin_profile isn't a normal Python attribute—it is a Django descriptor. Depending on the situation, accessing it can raise
RelatedObjectDoesNotExist (which subclasses AdminProfile.DoesNotExist) rather than simply returning
None.
An explicit try/except makes it clearer what you're handling.
Option 4: Ensure the profile always exists (my preferred solution)
Rather than handling the missing profile everywhere, make it impossible for one to be missing.
For example, when creating an admin:
admin = CustomUser.objects.create(...)
AdminProfile.objects.get_or_create(
user=admin,
defaults={
"storage_limit_mb": 5000,
},
)
or use a post_save signal:
@receiver(post_save, sender=CustomUser)
def create_admin_profile(sender, instance, created, **kwargs):
if created and instance.role == "admin":
AdminProfile.objects.get_or_create(user=instance)
Then your view can simply do:
admin_profile = admin.admin_profile
without any defensive code.
Which approach fits your project?
From everything we've discussed over the past several months, your application seems designed so that:
- every admin has exactly one
AdminProfile, - storage limits are stored there,
- download limits depend on it,
- many views rely on it.
In that architecture, I would enforce the existence of AdminProfile rather than repeatedly
checking for its absence in every view. It's simpler, faster, and if a profile is ever missing, that's something you want to know about and fix rather than quietly masking.
3. One aggregation and two aggregations
3.(2) One Q() and two Q()s
Is the syntax of the following code correct?
stats = CustomUser.objects.aggregate(
total_download_used_bytes=Sum("download_bytes_used"),
total_download_limit_bytes=Sum("download_bytes_limit"),
active_admin_count=Count("id", filter=Q(role="admin") & Q(is_active=True)),
# paid_admin_count=Count("id", filter=Q(has_paid=True)),
# cnclatPE_count=Count("id", filter=Q(cancel_at_period_end=True)),
)
Yes, the syntax is correct. Django supports filter= inside Count() (Django 2.0+), and combining
Q() objects with & is the proper way to express an AND condition.
Your code:
stats = CustomUser.objects.aggregate(
total_download_used_bytes=Sum("download_bytes_used"),
total_download_limit_bytes=Sum("download_bytes_limit"),
active_admin_count=Count(
"id",
filter=Q(role="admin") & Q(is_active=True),
),
# paid_admin_count=Count("id", filter=Q(has_paid=True)),
# cnclatPE_count=Count("id", filter=Q(cancel_at_period_end=True)),
)
will work.
However, I'd recommend making the other counts consistent.
For example:
stats = CustomUser.objects.aggregate(
total_download_used_bytes=Sum("download_bytes_used"),
total_download_limit_bytes=Sum("download_bytes_limit"),
active_admin_count=Count(
"id",
filter=Q(role="admin", is_active=True),
),
paid_admin_count=Count(
"id",
filter=Q(role="admin", has_paid=True),
),
cancel_at_period_end_count=Count(
"id",
filter=Q(role="admin", cancel_at_period_end=True),
),
)
Notice two small improvements:
-
One
Q()instead of two
Instead of:
Q(role="admin") & Q(is_active=True)
you can write:
Q(role="admin", is_active=True)
They're equivalent, but the latter is shorter and easier to read.
- Restrict counts to admins
If your database contains superadmins and clients (which it does), then
Count("id", filter=Q(has_paid=True))
would count everyone with has_paid=True, including clients if they ever have that flag set.
It's safer to write:
Count("id", filter=Q(role="admin", has_paid=True))
so you're explicitly counting only admins.
Overall, this is a nice use of aggregate(). You're calculating several statistics in a single query, which is both efficient and easy to maintain.
3.(3) Use .filter(role="admin")
Is it safer to add role="admin" as a filter to the lines of total_download_used_bytes... and total_download_limit_bytes...? Does that reduce the efficiency?
Yes, I would recommend filtering to role="admin" if those totals are intended to represent admins only.
However, don't use the filter= argument inside Sum(). Instead, filter the queryset before calling aggregate().
stats = (
CustomUser.objects
.filter(role="admin")
.aggregate(
total_download_used_bytes=Sum("download_bytes_used"),
total_download_limit_bytes=Sum("download_bytes_limit"),
active_admin_count=Count("id", filter=Q(is_active=True)),
paid_admin_count=Count("id", filter=Q(has_paid=True)),
cancel_at_period_end_count=Count("id", filter=Q(cancel_at_period_end=True)),
)
)
Why is this better?
The database only considers rows where role="admin".
Conceptually, it executes something like:
SELECT
SUM(download_bytes_used),
SUM(download_bytes_limit),
COUNT(...) ...
FROM custom_user
WHERE role = 'admin';
instead of
SELECT ...
FROM custom_user;
Does it reduce efficiency?
Quite the opposite.
- If you have only a few superadmins and many admins, the difference is tiny.
- If you eventually have 100,000 clients and 500 admins, filtering by
role="admin"means the database ignores almost all of the client rows.
So it is:
- ✅ more correct (you're computing admin statistics)
- ✅ usually more efficient
- ✅ simpler to read
One more improvement
Because you've already filtered to admins, you can simplify the Count() filters:
stats = (
CustomUser.objects
.filter(role="admin")
.aggregate(
total_download_used_bytes=Sum("download_bytes_used"),
total_download_limit_bytes=Sum("download_bytes_limit"),
active_admin_count=Count("id", filter=Q(is_active=True)),
paid_admin_count=Count("id", filter=Q(has_paid=True)),
cancel_at_period_end_count=Count("id", filter=Q(cancel_at_period_end=True)),
)
)
Notice there's no need to repeat role="admin" inside every Q(...).
For your superadmin dashboard, this is the approach I'd use. It keeps the code clean, avoids accidentally including clients or superadmins in the statistics, and will scale well as your SaaS grows.
