diff --git a/analytics/tests/test_views.py b/analytics/tests/test_views.py index 0e1e33c7b8..9279fbf267 100644 --- a/analytics/tests/test_views.py +++ b/analytics/tests/test_views.py @@ -24,6 +24,24 @@ class TestStatsEndpoint(ZulipTestCase): # Check that we get something back self.assert_in_response("Zulip analytics for", result) + def test_stats_for_realm(self) -> None: + user_profile = self.example_user('hamlet') + self.login(user_profile.email) + + result = self.client_get('/stats/realm/zulip/') + self.assertEqual(result.status_code, 302) + + user_profile = self.example_user('hamlet') + user_profile.is_staff = True + user_profile.save(update_fields=['is_staff']) + + result = self.client_get('/stats/realm/not_existing_realm/') + self.assertEqual(result.status_code, 302) + + result = self.client_get('/stats/realm/zulip/') + self.assertEqual(result.status_code, 200) + self.assert_in_response("Zulip analytics for", result) + class TestGetChartData(ZulipTestCase): def setUp(self) -> None: self.realm = get_realm('zulip') @@ -233,6 +251,28 @@ class TestGetChartData(ZulipTestCase): {'chart_name': 'number_of_humans'}) self.assert_json_error_contains(result, 'No analytics data available') + def test_get_chart_data_for_realm(self) -> None: + user_profile = self.example_user('hamlet') + self.login(user_profile.email) + + result = self.client_get('/json/analytics/chart_data/realm/zulip/', + {'chart_name': 'number_of_humans'}) + self.assert_json_error(result, "Must be an server administrator", 400) + + user_profile = self.example_user('hamlet') + user_profile.is_staff = True + user_profile.save(update_fields=['is_staff']) + stat = COUNT_STATS['realm_active_humans::day'] + self.insert_data(stat, [None], []) + + result = self.client_get('/json/analytics/chart_data/realm/not_existing_realm', + {'chart_name': 'number_of_humans'}) + self.assert_json_error(result, 'Invalid organization', 400) + + result = self.client_get('/json/analytics/chart_data/realm/zulip', + {'chart_name': 'number_of_humans'}) + self.assert_json_success(result) + class TestGetChartDataHelpers(ZulipTestCase): # last_successful_fill is in analytics/models.py, but get_chart_data is # the only function that uses it at the moment diff --git a/analytics/urls.py b/analytics/urls.py index b42d44686a..dafcbf064a 100644 --- a/analytics/urls.py +++ b/analytics/urls.py @@ -11,6 +11,8 @@ i18n_urlpatterns = [ name='analytics.views.get_realm_activity'), url(r'^user_activity/(?P[\S]+)/$', analytics.views.get_user_activity, name='analytics.views.get_user_activity'), + url(r'^stats/realm/(?P[\S]+)/$', analytics.views.stats_for_realm, + name='analytics.views.stats_for_realm'), # User-visible stats page url(r'^stats$', analytics.views.stats, @@ -29,6 +31,8 @@ v1_api_and_json_patterns = [ # get data for the graphs at /stats url(r'^analytics/chart_data$', rest_dispatch, {'GET': 'analytics.views.get_chart_data'}), + url(r'^analytics/chart_data/realm/(?P[\S]+)$', rest_dispatch, + {'GET': 'analytics.views.get_chart_data_for_realm'}), ] i18n_urlpatterns += [ diff --git a/analytics/views.py b/analytics/views.py index e97f275f0e..abdc519ad7 100644 --- a/analytics/views.py +++ b/analytics/views.py @@ -26,9 +26,10 @@ from analytics.lib.counts import COUNT_STATS, CountStat, process_count_stat from analytics.lib.time_utils import time_range from analytics.models import BaseCount, InstallationCount, \ RealmCount, StreamCount, UserCount, last_successful_fill -from zerver.decorator import require_server_admin, \ +from zerver.decorator import require_server_admin, require_server_admin_api, \ to_non_negative_int, to_utc_datetime, zulip_login_required from zerver.lib.exceptions import JsonableError +from zerver.lib.json_encoder_for_html import JSONEncoderForHTML from zerver.lib.request import REQ, has_request_variables from zerver.lib.response import json_success from zerver.lib.timestamp import ceiling_to_day, \ @@ -36,17 +37,48 @@ from zerver.lib.timestamp import ceiling_to_day, \ from zerver.models import Client, get_realm, Realm, \ UserActivity, UserActivityInterval, UserProfile -@zulip_login_required -def stats(request: HttpRequest) -> HttpResponse: +def render_stats(request: HttpRequest, realm: Realm) -> HttpRequest: + page_params = dict( + is_staff = request.user.is_staff, + stats_realm = realm.string_id, + debug_mode = False, + ) + return render(request, 'analytics/stats.html', - context=dict(realm_name = request.user.realm.name)) + context=dict(target_realm_name=realm.name, + page_params=JSONEncoderForHTML().encode(page_params))) + +@zulip_login_required +def stats(request: HttpRequest) -> HttpResponse: + realm = request.user.realm + return render_stats(request, realm) + +@require_server_admin +@has_request_variables +def stats_for_realm(request: HttpRequest, realm_str: str) -> HttpResponse: + realm = get_realm(realm_str) + if realm is None: + return HttpResponseNotFound("Realm %s does not exist" % (realm_str,)) + + return render_stats(request, realm) + +@require_server_admin_api +@has_request_variables +def get_chart_data_for_realm(request: HttpRequest, user_profile: UserProfile, + realm_str: str, **kwargs: Any) -> HttpResponse: + realm = get_realm(realm_str) + if realm is None: + raise JsonableError(_("Invalid organization")) + + return get_chart_data(request=request, user_profile=user_profile, realm=realm, **kwargs) @has_request_variables def get_chart_data(request: HttpRequest, user_profile: UserProfile, chart_name: Text=REQ(), 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)) -> HttpResponse: + end: Optional[datetime]=REQ(converter=to_utc_datetime, default=None), + realm: Optional[Realm]=None) -> HttpResponse: if chart_name == 'number_of_humans': stat = COUNT_STATS['realm_active_humans::day'] tables = [RealmCount] @@ -88,7 +120,8 @@ def get_chart_data(request: HttpRequest, user_profile: UserProfile, chart_name: raise JsonableError(_("Start time is later than end time. Start: %(start)s, End: %(end)s") % {'start': start, 'end': end}) - realm = user_profile.realm + if realm is None: + realm = user_profile.realm if start is None: start = realm.date_created if end is None: diff --git a/static/js/stats/stats.js b/static/js/stats/stats.js index ce13fbad90..4fa04b894c 100644 --- a/static/js/stats/stats.js +++ b/static/js/stats/stats.js @@ -72,9 +72,17 @@ $(function () { }); }); + function get_chart_data(data, callback) { + var url; + if (page_params.is_staff) { + url = '/json/analytics/chart_data/realm/' + page_params.stats_realm; + } else { + url = '/json/analytics/chart_data'; + } + $.get({ - url: '/json/analytics/chart_data', + url: url, data: data, idempotent: true, success: function (data) { diff --git a/templates/analytics/stats.html b/templates/analytics/stats.html index 62cdb78688..186511bf97 100644 --- a/templates/analytics/stats.html +++ b/templates/analytics/stats.html @@ -1,5 +1,11 @@ {% extends "zerver/base.html" %} +{% block page_params %} + +{% endblock %} + {% block customhead %} {% stylesheet 'portico' %} {% endblock %} @@ -14,7 +20,7 @@
-

{% trans %}Zulip analytics for {{ realm_name }}{% endtrans %}

+

{% trans %}Zulip analytics for {{ target_realm_name }}{% endtrans %}

diff --git a/zerver/decorator.py b/zerver/decorator.py index 134187c817..0d333333d1 100644 --- a/zerver/decorator.py +++ b/zerver/decorator.py @@ -452,7 +452,8 @@ def require_server_admin(view_func: ViewFuncT) -> ViewFuncT: def require_server_admin_api(view_func: ViewFuncT) -> ViewFuncT: @zulip_login_required @wraps(view_func) - def _wrapped_view_func(request: HttpRequest, user_profile: UserProfile, *args: Any, **kwargs: Any) -> HttpResponse: + def _wrapped_view_func(request: HttpRequest, user_profile: UserProfile, *args: Any, + **kwargs: Any) -> HttpResponse: if not user_profile.is_staff: raise JsonableError(_("Must be an server administrator")) return view_func(request, user_profile, *args, **kwargs)