mirror of https://github.com/zulip/zulip.git
stats: Support fetching StreamCount based on stream_id.
As a preliminary step for including graphs of StreamCount data in our analytics pages, add API support for fetching the chart data. Care is taken to limit access to streams that the current user has access to, which isn't necessary in similar views where the current user is a server administrator by assumption. Fixes part of #19653. Co-authored-by: Tim Abbott <tabbott@zulip.com>
This commit is contained in:
parent
8a837320a9
commit
d94bdb3900
|
@ -6,7 +6,7 @@ from typing_extensions import override
|
|||
|
||||
from analytics.lib.counts import COUNT_STATS, CountStat
|
||||
from analytics.lib.time_utils import time_range
|
||||
from analytics.models import FillState, RealmCount, UserCount
|
||||
from analytics.models import FillState, RealmCount, StreamCount, UserCount
|
||||
from analytics.views.stats import rewrite_client_arrays, sort_by_totals, sort_client_labels
|
||||
from zerver.lib.test_classes import ZulipTestCase
|
||||
from zerver.lib.timestamp import ceiling_to_day, ceiling_to_hour, datetime_to_timestamp
|
||||
|
@ -74,6 +74,7 @@ class TestGetChartData(ZulipTestCase):
|
|||
super().setUp()
|
||||
self.realm = get_realm("zulip")
|
||||
self.user = self.example_user("hamlet")
|
||||
self.stream_id = self.get_stream_id(self.get_streams(self.user)[0])
|
||||
self.login_user(self.user)
|
||||
self.end_times_hour = [
|
||||
ceiling_to_hour(self.realm.date_created) + timedelta(hours=i) for i in range(4)
|
||||
|
@ -116,6 +117,17 @@ class TestGetChartData(ZulipTestCase):
|
|||
)
|
||||
for i, subgroup in enumerate(user_subgroups)
|
||||
)
|
||||
StreamCount.objects.bulk_create(
|
||||
StreamCount(
|
||||
property=stat.property,
|
||||
subgroup=subgroup,
|
||||
end_time=insert_time,
|
||||
value=100 + i,
|
||||
stream_id=self.stream_id,
|
||||
realm=self.realm,
|
||||
)
|
||||
for i, subgroup in enumerate(realm_subgroups)
|
||||
)
|
||||
FillState.objects.create(property=stat.property, end_time=fill_time, state=FillState.DONE)
|
||||
|
||||
def test_number_of_humans(self) -> None:
|
||||
|
@ -252,6 +264,49 @@ class TestGetChartData(ZulipTestCase):
|
|||
},
|
||||
)
|
||||
|
||||
def test_messages_sent_by_stream(self) -> None:
|
||||
stat = COUNT_STATS["messages_in_stream:is_bot:day"]
|
||||
self.insert_data(stat, ["true", "false"], [])
|
||||
|
||||
result = self.client_get(
|
||||
f"/json/analytics/chart_data/stream/{self.stream_id}",
|
||||
{
|
||||
"chart_name": "messages_sent_by_stream",
|
||||
},
|
||||
)
|
||||
data = self.assert_json_success(result)
|
||||
self.assertEqual(
|
||||
data,
|
||||
{
|
||||
"msg": "",
|
||||
"end_times": [datetime_to_timestamp(dt) for dt in self.end_times_day],
|
||||
"frequency": CountStat.DAY,
|
||||
"everyone": {"bot": self.data(100), "human": self.data(101)},
|
||||
"display_order": None,
|
||||
"result": "success",
|
||||
},
|
||||
)
|
||||
|
||||
result = self.api_get(
|
||||
self.example_user("polonius"),
|
||||
f"/api/v1/analytics/chart_data/stream/{self.stream_id}",
|
||||
{
|
||||
"chart_name": "messages_sent_by_stream",
|
||||
},
|
||||
)
|
||||
self.assert_json_error(result, "Not allowed for guest users")
|
||||
|
||||
# Verify we correctly forbid access to stats of streams in other realms.
|
||||
result = self.api_get(
|
||||
self.mit_user("sipbtest"),
|
||||
f"/api/v1/analytics/chart_data/stream/{self.stream_id}",
|
||||
{
|
||||
"chart_name": "messages_sent_by_stream",
|
||||
},
|
||||
subdomain="zephyr",
|
||||
)
|
||||
self.assert_json_error(result, "Invalid stream ID")
|
||||
|
||||
def test_include_empty_subgroups(self) -> None:
|
||||
FillState.objects.create(
|
||||
property="realm_active_humans::day",
|
||||
|
|
|
@ -16,6 +16,7 @@ from analytics.views.stats import (
|
|||
get_chart_data_for_realm,
|
||||
get_chart_data_for_remote_installation,
|
||||
get_chart_data_for_remote_realm,
|
||||
get_chart_data_for_stream,
|
||||
stats,
|
||||
stats_for_installation,
|
||||
stats_for_realm,
|
||||
|
@ -56,6 +57,7 @@ i18n_urlpatterns: List[Union[URLPattern, URLResolver]] = [
|
|||
v1_api_and_json_patterns = [
|
||||
# get data for the graphs at /stats
|
||||
rest_path("analytics/chart_data", GET=get_chart_data),
|
||||
rest_path("analytics/chart_data/stream/<stream_id>", GET=get_chart_data_for_stream),
|
||||
rest_path("analytics/chart_data/realm/<realm_str>", GET=get_chart_data_for_realm),
|
||||
rest_path("analytics/chart_data/installation", GET=get_chart_data_for_installation),
|
||||
rest_path(
|
||||
|
|
|
@ -33,9 +33,10 @@ from zerver.lib.exceptions import JsonableError
|
|||
from zerver.lib.i18n import get_and_set_request_language, get_language_translation_data
|
||||
from zerver.lib.request import REQ, has_request_variables
|
||||
from zerver.lib.response import json_success
|
||||
from zerver.lib.streams import access_stream_by_id
|
||||
from zerver.lib.timestamp import convert_to_UTC
|
||||
from zerver.lib.validator import to_non_negative_int
|
||||
from zerver.models import Client, Realm, UserProfile, get_realm
|
||||
from zerver.models import Client, Realm, Stream, UserProfile, get_realm
|
||||
|
||||
if settings.ZILENCER_ENABLED:
|
||||
from zilencer.models import RemoteInstallationCount, RemoteRealmCount, RemoteZulipServer
|
||||
|
@ -156,6 +157,21 @@ def get_chart_data_for_realm(
|
|||
return get_chart_data(request, user_profile, realm=realm, **kwargs)
|
||||
|
||||
|
||||
@require_non_guest_user
|
||||
@has_request_variables
|
||||
def get_chart_data_for_stream(
|
||||
request: HttpRequest, /, user_profile: UserProfile, stream_id: int
|
||||
) -> HttpResponse:
|
||||
stream, ignored_sub = access_stream_by_id(
|
||||
user_profile,
|
||||
stream_id,
|
||||
require_active=True,
|
||||
allow_realm_admin=True,
|
||||
)
|
||||
|
||||
return get_chart_data(request, user_profile, stream=stream)
|
||||
|
||||
|
||||
@require_server_admin_api
|
||||
@has_request_variables
|
||||
def get_chart_data_for_remote_realm(
|
||||
|
@ -237,11 +253,15 @@ def get_chart_data(
|
|||
min_length: Optional[int] = REQ(converter=to_non_negative_int, default=None),
|
||||
start: Optional[datetime] = REQ(converter=to_utc_datetime, default=None),
|
||||
end: Optional[datetime] = REQ(converter=to_utc_datetime, default=None),
|
||||
# These last several parameters are only used by functions
|
||||
# wrapping get_chart_data; the callers are responsible for
|
||||
# parsing/validation/authorization for them.
|
||||
realm: Optional[Realm] = None,
|
||||
for_installation: bool = False,
|
||||
remote: bool = False,
|
||||
remote_realm_id: Optional[int] = None,
|
||||
server: Optional["RemoteZulipServer"] = None,
|
||||
stream: Optional[Stream] = None,
|
||||
) -> HttpResponse:
|
||||
TableType: TypeAlias = Union[
|
||||
Type["RemoteInstallationCount"],
|
||||
|
@ -265,7 +285,9 @@ def get_chart_data(
|
|||
else:
|
||||
aggregate_table = RealmCount
|
||||
|
||||
tables: Union[Tuple[TableType], Tuple[TableType, Type[UserCount]]]
|
||||
tables: Union[
|
||||
Tuple[TableType], Tuple[TableType, Type[UserCount]], Tuple[TableType, Type[StreamCount]]
|
||||
]
|
||||
|
||||
if chart_name == "number_of_humans":
|
||||
stats = [
|
||||
|
@ -315,6 +337,16 @@ def get_chart_data(
|
|||
subgroup_to_label = {stats[0]: {None: "read"}}
|
||||
labels_sort_function = None
|
||||
include_empty_subgroups = True
|
||||
elif chart_name == "messages_sent_by_stream":
|
||||
if stream is None:
|
||||
raise JsonableError(
|
||||
_("Missing stream for chart: {chart_name}").format(chart_name=chart_name)
|
||||
)
|
||||
stats = [COUNT_STATS["messages_in_stream:is_bot:day"]]
|
||||
tables = (aggregate_table, StreamCount)
|
||||
subgroup_to_label = {stats[0]: {"false": "human", "true": "bot"}}
|
||||
labels_sort_function = None
|
||||
include_empty_subgroups = True
|
||||
else:
|
||||
raise JsonableError(_("Unknown chart name: {chart_name}").format(chart_name=chart_name))
|
||||
|
||||
|
@ -397,6 +429,7 @@ def get_chart_data(
|
|||
InstallationCount: "everyone",
|
||||
RealmCount: "everyone",
|
||||
UserCount: "user",
|
||||
StreamCount: "everyone",
|
||||
}
|
||||
if settings.ZILENCER_ENABLED:
|
||||
aggregation_level[RemoteInstallationCount] = "everyone"
|
||||
|
@ -408,6 +441,9 @@ def get_chart_data(
|
|||
RealmCount: realm.id,
|
||||
UserCount: user_profile.id,
|
||||
}
|
||||
if stream is not None:
|
||||
id_value[StreamCount] = stream.id
|
||||
|
||||
if settings.ZILENCER_ENABLED:
|
||||
if server is not None:
|
||||
id_value[RemoteInstallationCount] = server.id
|
||||
|
|
Loading…
Reference in New Issue