installation-activity: Add export version of chart.

Adds a boolean export parameter to the installation activity view.

When true, the initial links column is removed from the chart, a
blank column is removed before human message counts anda column
with all organization owner and admin emails is added.

Updates the links at the top of the installation activity page to
have an option to toggle between the export and non-export versions
of the chart.
This commit is contained in:
Lauryn Menard 2024-09-23 21:11:09 +02:00 committed by Tim Abbott
parent c2abb5b4a7
commit 55d7ec843e
2 changed files with 67 additions and 12 deletions

View File

@ -1,4 +1,5 @@
from collections import defaultdict from collections import defaultdict
from typing import Any
from django.conf import settings from django.conf import settings
from django.db import connection from django.db import connection
@ -8,6 +9,7 @@ from django.template import loader
from django.utils.timezone import now as timezone_now from django.utils.timezone import now as timezone_now
from markupsafe import Markup from markupsafe import Markup
from psycopg2.sql import SQL from psycopg2.sql import SQL
from pydantic import Json
from analytics.lib.counts import COUNT_STATS from analytics.lib.counts import COUNT_STATS
from corporate.lib.activity import ( from corporate.lib.activity import (
@ -26,10 +28,11 @@ from corporate.lib.activity import (
from corporate.lib.stripe import cents_to_dollar_string from corporate.lib.stripe import cents_to_dollar_string
from corporate.views.support import get_plan_type_string from corporate.views.support import get_plan_type_string
from zerver.decorator import require_server_admin from zerver.decorator import require_server_admin
from zerver.lib.typed_endpoint import typed_endpoint_without_parameters from zerver.lib.typed_endpoint import typed_endpoint
from zerver.models import Realm from zerver.models import Realm
from zerver.models.realm_audit_logs import AuditLogEventType from zerver.models.realm_audit_logs import AuditLogEventType
from zerver.models.realms import get_org_type_display_name from zerver.models.realms import get_org_type_display_name
from zerver.models.users import UserProfile
def get_realm_day_counts() -> dict[str, dict[str, Markup]]: def get_realm_day_counts() -> dict[str, dict[str, Markup]]:
@ -88,7 +91,7 @@ def get_realm_day_counts() -> dict[str, dict[str, Markup]]:
return result return result
def realm_summary_table() -> str: def realm_summary_table(export: bool) -> str:
now = timezone_now() now = timezone_now()
query = SQL( query = SQL(
@ -103,7 +106,8 @@ def realm_summary_table() -> str:
coalesce(user_count_table.value, 0) user_profile_count, coalesce(user_count_table.value, 0) user_profile_count,
coalesce(bot_count_table.value, 0) bot_count, coalesce(bot_count_table.value, 0) bot_count,
coalesce(realm_audit_log_table.how_realm_creator_found_zulip, '') how_realm_creator_found_zulip, coalesce(realm_audit_log_table.how_realm_creator_found_zulip, '') how_realm_creator_found_zulip,
coalesce(realm_audit_log_table.how_realm_creator_found_zulip_extra_context, '') how_realm_creator_found_zulip_extra_context coalesce(realm_audit_log_table.how_realm_creator_found_zulip_extra_context, '') how_realm_creator_found_zulip_extra_context,
realm_admin_user.delivery_email admin_email
FROM FROM
zerver_realm as realm zerver_realm as realm
LEFT OUTER JOIN ( LEFT OUTER JOIN (
@ -168,6 +172,17 @@ def realm_summary_table() -> str:
WHERE WHERE
event_type = %(realm_creation_event_type)s event_type = %(realm_creation_event_type)s
) as realm_audit_log_table ON realm.id = realm_audit_log_table.realm_id ) as realm_audit_log_table ON realm.id = realm_audit_log_table.realm_id
LEFT OUTER JOIN (
SELECT
delivery_email,
realm_id
from
zerver_userprofile
WHERE
is_bot=False
AND is_active=True
AND role IN %(admin_roles)s
) as realm_admin_user ON realm.id = realm_admin_user.realm_id
WHERE WHERE
_14day_active_humans IS NOT NULL _14day_active_humans IS NOT NULL
or realm.plan_type = 3 or realm.plan_type = 3
@ -190,11 +205,25 @@ def realm_summary_table() -> str:
"active_users_audit:is_bot:day" "active_users_audit:is_bot:day"
].last_successful_fill(), ].last_successful_fill(),
"realm_creation_event_type": AuditLogEventType.REALM_CREATED, "realm_creation_event_type": AuditLogEventType.REALM_CREATED,
"admin_roles": (UserProfile.ROLE_REALM_ADMINISTRATOR, UserProfile.ROLE_REALM_OWNER),
}, },
) )
rows = dictfetchall(cursor) raw_rows = dictfetchall(cursor)
cursor.close() cursor.close()
rows: list[dict[str, Any]] = []
admin_emails: dict[str, str] = {}
# Process duplicate realm rows due to multiple admin users,
# and collect all admin user emails into one string.
for row in raw_rows:
realm_string_id = row["string_id"]
admin_email = row.pop("admin_email")
if realm_string_id in admin_emails:
admin_emails[realm_string_id] = admin_emails[realm_string_id] + ", " + admin_email
else:
admin_emails[realm_string_id] = admin_email
rows.append(row)
realm_messages_per_day_counts = get_realm_day_counts() realm_messages_per_day_counts = get_realm_day_counts()
total_arr = 0 total_arr = 0
num_active_sites = 0 num_active_sites = 0
@ -253,6 +282,10 @@ def realm_summary_table() -> str:
total_bot_count += int(row["bot_count"]) total_bot_count += int(row["bot_count"])
total_wau_count += int(row["wau_count"]) total_wau_count += int(row["wau_count"])
# Add admin users email string
if export:
row["admin_emails"] = admin_emails[realm_string_id]
total_row = [ total_row = [
"Total", "Total",
"", "",
@ -277,6 +310,9 @@ def realm_summary_table() -> str:
"", "",
] ]
if export:
total_row.pop(1)
content = loader.render_to_string( content = loader.render_to_string(
"corporate/activity/installation_activity_table.html", "corporate/activity/installation_activity_table.html",
dict( dict(
@ -285,15 +321,16 @@ def realm_summary_table() -> str:
num_active_sites=num_active_sites, num_active_sites=num_active_sites,
utctime=now.strftime("%Y-%m-%d %H:%M %Z"), utctime=now.strftime("%Y-%m-%d %H:%M %Z"),
billing_enabled=settings.BILLING_ENABLED, billing_enabled=settings.BILLING_ENABLED,
export=export,
), ),
) )
return content return content
@require_server_admin @require_server_admin
@typed_endpoint_without_parameters @typed_endpoint
def get_installation_activity(request: HttpRequest) -> HttpResponse: def get_installation_activity(request: HttpRequest, *, export: Json[bool] = False) -> HttpResponse:
content: str = realm_summary_table() content: str = realm_summary_table(export)
title = "Installation activity" title = "Installation activity"
return render( return render(

View File

@ -2,11 +2,16 @@
<p class="installation-activity-header">{{ utctime }}</p> <p class="installation-activity-header">{{ utctime }}</p>
<h4>Installation information:</h4> <h4>Other views:</h4>
<ul> <ul>
<li><a href="/stats/installation">Server total /stats style graphs</a></li> <li><a href="/stats/installation">Server total /stats style graphs</a></li>
<li><a href="/activity/remote">Remote servers</a></li> <li><a href="/activity/remote">Remote servers</a></li>
<li><a href="/activity/integrations">Integrations by client</a></li> <li><a href="/activity/integrations">Integrations by client</a></li>
{% if not export %}
<li><a href="/activity?export=true">Export installation chart</a></li>
{% else %}
<li><a href="/activity">Non-export installation chart</a></li>
{% endif %}
</ul> </ul>
<h4>Counts chart key:</h4> <h4>Counts chart key:</h4>
@ -33,7 +38,9 @@
<thead class="activity-head"> <thead class="activity-head">
<tr> <tr>
{% if not export %}
<th>Links</th> <th>Links</th>
{% endif %}
<th>Realm</th> <th>Realm</th>
<th>Created (green if ≤12wk)</th> <th>Created (green if ≤12wk)</th>
{% if billing_enabled %} {% if billing_enabled %}
@ -47,21 +54,26 @@
<th>WAU</th> <th>WAU</th>
<th>Total users</th> <th>Total users</th>
<th>Bots</th> <th>Bots</th>
{% if not export %}
<th></th> <th></th>
{% endif %}
<th colspan=8>Human messages sent, last 8 UTC days (today-so-far first)</th> <th colspan=8>Human messages sent, last 8 UTC days (today-so-far first)</th>
{% if export %}
<th>Admin emails</th>
{% endif %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for row in rows %} {% for row in rows %}
<tr> <tr>
{% if not export %}
<td> <td>
{{ row.realm_url }} {{ row.realm_url }}
{{ row.stats_link }} {{ row.stats_link }}
{{ row.support_link }} {{ row.support_link }}
</td> </td>
{% endif %}
<td> <td>
{{ row.activity_link }} {{ row.activity_link }}
</td> </td>
@ -113,14 +125,20 @@
<td class="number"> <td class="number">
{{ row.bot_count }} {{ row.bot_count }}
</td> </td>
{% if not export %}
<td>&nbsp;</td> <td>&nbsp;</td>
{% endif %}
{% if row.history %} {% if row.history %}
{{ row.history|safe }} {{ row.history|safe }}
{% else %} {% else %}
<td colspan=8></td> <td colspan=8></td>
{% endif %} {% endif %}
{% if export %}
<td>
{{ row.admin_emails }}
</td>
{% endif %}
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>