2021-06-18 00:07:45 +02:00
|
|
|
import re
|
2022-06-28 00:43:57 +02:00
|
|
|
import sys
|
2021-06-17 23:49:07 +02:00
|
|
|
from datetime import datetime
|
2022-06-23 20:07:19 +02:00
|
|
|
from typing import Any, Collection, Dict, List, Optional, Sequence
|
2022-12-01 16:48:46 +01:00
|
|
|
from urllib.parse import urlencode
|
2021-06-17 23:49:07 +02:00
|
|
|
|
|
|
|
from django.conf import settings
|
2021-07-26 17:32:10 +02:00
|
|
|
from django.db.backends.utils import CursorWrapper
|
2021-06-17 23:49:07 +02:00
|
|
|
from django.template import loader
|
|
|
|
from django.urls import reverse
|
2022-11-16 06:28:44 +01:00
|
|
|
from markupsafe import Markup
|
2021-06-17 23:49:07 +02:00
|
|
|
|
2022-12-01 16:48:46 +01:00
|
|
|
from zerver.lib.url_encoding import append_url_query_string
|
|
|
|
from zerver.models import UserActivity, get_realm
|
2022-06-23 20:07:19 +02:00
|
|
|
|
2022-06-28 00:43:57 +02:00
|
|
|
if sys.version_info < (3, 9): # nocoverage
|
|
|
|
from backports import zoneinfo
|
|
|
|
else: # nocoverage
|
|
|
|
import zoneinfo
|
|
|
|
|
|
|
|
eastern_tz = zoneinfo.ZoneInfo("America/New_York")
|
2021-06-17 23:49:07 +02:00
|
|
|
|
|
|
|
|
|
|
|
if settings.BILLING_ENABLED:
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
def make_table(
|
|
|
|
title: str, cols: Sequence[str], rows: Sequence[Any], has_row_class: bool = False
|
|
|
|
) -> str:
|
|
|
|
if not has_row_class:
|
|
|
|
|
|
|
|
def fix_row(row: Any) -> Dict[str, Any]:
|
|
|
|
return dict(cells=row, row_class=None)
|
|
|
|
|
|
|
|
rows = list(map(fix_row, rows))
|
|
|
|
|
|
|
|
data = dict(title=title, cols=cols, rows=rows)
|
|
|
|
|
|
|
|
content = loader.render_to_string(
|
|
|
|
"analytics/ad_hoc_query.html",
|
|
|
|
dict(data=data),
|
|
|
|
)
|
|
|
|
|
|
|
|
return content
|
|
|
|
|
|
|
|
|
2021-07-26 17:32:10 +02:00
|
|
|
def dictfetchall(cursor: CursorWrapper) -> List[Dict[str, Any]]:
|
2022-12-14 10:46:29 +01:00
|
|
|
"""Returns all rows from a cursor as a dict"""
|
2021-06-17 23:49:07 +02:00
|
|
|
desc = cursor.description
|
|
|
|
return [dict(zip((col[0] for col in desc), row)) for row in cursor.fetchall()]
|
|
|
|
|
|
|
|
|
|
|
|
def format_date_for_activity_reports(date: Optional[datetime]) -> str:
|
|
|
|
if date:
|
|
|
|
return date.astimezone(eastern_tz).strftime("%Y-%m-%d %H:%M")
|
|
|
|
else:
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
2022-11-16 06:28:44 +01:00
|
|
|
def user_activity_link(email: str, user_profile_id: int) -> Markup:
|
2021-06-18 00:06:02 +02:00
|
|
|
from analytics.views.user_activity import get_user_activity
|
2021-06-17 23:49:07 +02:00
|
|
|
|
2021-10-13 21:16:34 +02:00
|
|
|
url = reverse(get_user_activity, kwargs=dict(user_profile_id=user_profile_id))
|
2023-03-21 07:10:20 +01:00
|
|
|
return Markup('<a href="{url}">{email}</a>').format(url=url, email=email)
|
2021-06-17 23:49:07 +02:00
|
|
|
|
|
|
|
|
2022-11-16 06:28:44 +01:00
|
|
|
def realm_activity_link(realm_str: str) -> Markup:
|
2021-06-18 00:07:45 +02:00
|
|
|
from analytics.views.realm_activity import get_realm_activity
|
2021-06-17 23:49:07 +02:00
|
|
|
|
|
|
|
url = reverse(get_realm_activity, kwargs=dict(realm_str=realm_str))
|
2023-03-21 07:10:20 +01:00
|
|
|
return Markup('<a href="{url}">{realm_str}</a>').format(url=url, realm_str=realm_str)
|
2021-06-17 23:49:07 +02:00
|
|
|
|
|
|
|
|
2022-11-16 06:28:44 +01:00
|
|
|
def realm_stats_link(realm_str: str) -> Markup:
|
2021-06-17 23:49:07 +02:00
|
|
|
from analytics.views.stats import stats_for_realm
|
|
|
|
|
|
|
|
url = reverse(stats_for_realm, kwargs=dict(realm_str=realm_str))
|
2023-03-21 07:10:20 +01:00
|
|
|
return Markup('<a href="{url}"><i class="fa fa-pie-chart"></i></a>').format(url=url)
|
2021-06-17 23:49:07 +02:00
|
|
|
|
|
|
|
|
2022-12-01 16:48:46 +01:00
|
|
|
def realm_support_link(realm_str: str) -> Markup:
|
|
|
|
support_url = reverse("support")
|
|
|
|
query = urlencode({"q": realm_str})
|
|
|
|
url = append_url_query_string(support_url, query)
|
2023-03-21 07:10:20 +01:00
|
|
|
return Markup('<a href="{url}">{realm_str}</a>').format(url=url, realm_str=realm_str)
|
2022-12-01 16:48:46 +01:00
|
|
|
|
|
|
|
|
|
|
|
def realm_url_link(realm_str: str) -> Markup:
|
|
|
|
url = get_realm(realm_str).uri
|
2023-03-21 07:10:20 +01:00
|
|
|
return Markup('<a href="{url}"><i class="fa fa-home"></i></a>').format(url=url)
|
2022-12-01 16:48:46 +01:00
|
|
|
|
|
|
|
|
2022-11-16 06:28:44 +01:00
|
|
|
def remote_installation_stats_link(server_id: int, hostname: str) -> Markup:
|
2021-06-17 23:49:07 +02:00
|
|
|
from analytics.views.stats import stats_for_remote_installation
|
|
|
|
|
|
|
|
url = reverse(stats_for_remote_installation, kwargs=dict(remote_server_id=server_id))
|
2023-03-21 07:10:20 +01:00
|
|
|
return Markup('<a href="{url}"><i class="fa fa-pie-chart"></i>{hostname}</a>').format(
|
|
|
|
url=url, hostname=hostname
|
|
|
|
)
|
2021-06-18 00:07:45 +02:00
|
|
|
|
|
|
|
|
2022-06-23 20:07:19 +02:00
|
|
|
def get_user_activity_summary(records: Collection[UserActivity]) -> Dict[str, Any]:
|
2021-10-13 21:16:34 +02:00
|
|
|
#: The type annotation used above is clearly overly permissive.
|
|
|
|
#: We should perhaps use TypedDict to clearly lay out the schema
|
|
|
|
#: for the user activity summary.
|
|
|
|
summary: Dict[str, Any] = {}
|
2021-06-18 00:07:45 +02:00
|
|
|
|
2022-06-15 23:55:20 +02:00
|
|
|
def update(action: str, record: UserActivity) -> None:
|
2021-06-18 00:07:45 +02:00
|
|
|
if action not in summary:
|
|
|
|
summary[action] = dict(
|
|
|
|
count=record.count,
|
|
|
|
last_visit=record.last_visit,
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
summary[action]["count"] += record.count
|
|
|
|
summary[action]["last_visit"] = max(
|
|
|
|
summary[action]["last_visit"],
|
|
|
|
record.last_visit,
|
|
|
|
)
|
|
|
|
|
|
|
|
if records:
|
2022-06-23 20:07:19 +02:00
|
|
|
first_record = next(iter(records))
|
|
|
|
summary["name"] = first_record.user_profile.full_name
|
|
|
|
summary["user_profile_id"] = first_record.user_profile.id
|
2021-06-18 00:07:45 +02:00
|
|
|
|
|
|
|
for record in records:
|
|
|
|
client = record.client.name
|
2021-07-26 16:46:53 +02:00
|
|
|
query = str(record.query)
|
2021-06-18 00:07:45 +02:00
|
|
|
|
|
|
|
update("use", record)
|
|
|
|
|
|
|
|
if client == "API":
|
|
|
|
m = re.match("/api/.*/external/(.*)", query)
|
|
|
|
if m:
|
|
|
|
client = m.group(1)
|
|
|
|
update(client, record)
|
|
|
|
|
|
|
|
if client.startswith("desktop"):
|
|
|
|
update("desktop", record)
|
|
|
|
if client == "website":
|
|
|
|
update("website", record)
|
|
|
|
if ("send_message" in query) or re.search("/api/.*/external/.*", query):
|
|
|
|
update("send", record)
|
|
|
|
if query in [
|
|
|
|
"/json/update_pointer",
|
|
|
|
"/json/users/me/pointer",
|
|
|
|
"/api/v1/update_pointer",
|
|
|
|
"update_pointer_backend",
|
|
|
|
]:
|
|
|
|
update("pointer", record)
|
|
|
|
update(client, record)
|
|
|
|
|
|
|
|
return summary
|