Files
upstream-zulip/corporate/views/realm_activity.py
upstream-mirror 64898b2c79
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
2026-09-15 15:05:30 +05:00

190 lines
5.8 KiB
Python

import itertools
import re
from collections.abc import Collection
from dataclasses import dataclass
from datetime import datetime
from typing import Any
from django.db.models import QuerySet
from django.http import HttpRequest, HttpResponse, HttpResponseNotFound
from django.shortcuts import render
from django.utils.timezone import now as timezone_now
from markupsafe import Markup
from corporate.lib.activity import (
format_optional_datetime,
make_table,
realm_stats_link,
user_activity_link,
)
from zerver.decorator import require_server_admin
from zerver.models import Realm, UserActivity
from zerver.models.users import UserProfile
@dataclass
class UserActivitySummary:
user_name: str
user_id: int
user_type: str
messages_sent: int
last_heard_from: datetime | None
last_message_sent: datetime | None
bot_owner_id: int | None
def get_user_activity_records_for_realm(realm: str) -> QuerySet[UserActivity]:
fields = [
"user_profile__full_name",
"user_profile__delivery_email",
"user_profile__is_bot",
"user_profile__bot_type",
"query",
"count",
"last_visit",
]
records = (
UserActivity.objects.filter(
user_profile__realm__string_id=realm,
user_profile__is_active=True,
)
.order_by("user_profile__delivery_email", "-last_visit")
.select_related("user_profile")
.only(*fields)
)
return records
def get_user_activity_summary(records: Collection[UserActivity]) -> UserActivitySummary:
if records:
first_record = next(iter(records))
name = first_record.user_profile.full_name
user_profile_id = first_record.user_profile.id
bot_owner_id = None
if not first_record.user_profile.is_bot:
user_type = "Human"
else:
assert first_record.user_profile.bot_type is not None
bot_type = first_record.user_profile.bot_type
user_type = UserProfile.BOT_TYPES[bot_type]
if first_record.user_profile.bot_owner is not None:
bot_owner_id = first_record.user_profile.bot_owner.id
messages = 0
heard_from: datetime | None = None
last_sent: datetime | None = None
for record in records:
query = str(record.query)
visit = record.last_visit
if heard_from is None:
heard_from = visit
else:
heard_from = max(visit, heard_from)
if ("send_message" in query) or re.search(r"/api/.*/external/.*", query):
messages += record.count
if last_sent is None:
last_sent = visit
else:
last_sent = max(visit, last_sent)
return UserActivitySummary(
user_name=name,
user_id=user_profile_id,
user_type=user_type,
messages_sent=messages,
last_heard_from=heard_from,
last_message_sent=last_sent,
bot_owner_id=bot_owner_id,
)
def realm_user_summary_table(
all_records: QuerySet[UserActivity], admin_emails: set[str], title: str, stats_link: Markup
) -> str:
user_records: dict[str, UserActivitySummary] = {}
def by_email(record: UserActivity) -> str:
return record.user_profile.delivery_email
for email, records in itertools.groupby(all_records, by_email):
user_records[email] = get_user_activity_summary(list(records))
def is_recent(val: datetime) -> bool:
age = timezone_now() - val
return age.total_seconds() < 5 * 60
cols = [
"Name",
"Email",
"User type",
"Messages sent",
"Last heard from (UTC)",
"Last message sent (UTC)",
]
rows = []
for email, user_summary in user_records.items():
email_link = user_activity_link(email, user_summary.user_id)
if user_summary.bot_owner_id is not None:
bot_owner_link = user_activity_link("", user_summary.bot_owner_id)
user_name = user_summary.user_name + " " + bot_owner_link
cells = [
user_name,
email_link,
user_summary.user_type,
user_summary.messages_sent,
]
else:
cells = [
user_summary.user_name,
email_link,
user_summary.user_type,
user_summary.messages_sent,
]
cells.append(format_optional_datetime(user_summary.last_heard_from))
cells.append(format_optional_datetime(user_summary.last_message_sent))
row_class = ""
if user_summary.last_heard_from and is_recent(user_summary.last_heard_from):
row_class += " recently_active"
if email in admin_emails:
row_class += " admin"
row = dict(cells=cells, row_class=row_class)
rows.append(row)
def by_last_heard_from(row: dict[str, Any]) -> str:
return row["cells"][4]
rows = sorted(rows, key=by_last_heard_from, reverse=True)
content = make_table(title, cols, rows, title_link=stats_link, has_row_class=True)
return content
@require_server_admin
def get_realm_activity(request: HttpRequest, realm_str: str) -> HttpResponse:
try:
admins = Realm.objects.get(string_id=realm_str).get_human_admin_users()
except Realm.DoesNotExist:
return HttpResponseNotFound()
admin_emails = {admin.delivery_email for admin in admins}
all_records = get_user_activity_records_for_realm(realm_str)
realm_stats = realm_stats_link(realm_str)
title = realm_str
content = realm_user_summary_table(all_records, admin_emails, title, realm_stats)
return render(
request,
"corporate/activity/activity.html",
context=dict(
data=content,
title=title,
is_home=False,
),
)