From b26c38bc47014feb021973dc984a9f3a21c96479 Mon Sep 17 00:00:00 2001 From: Shubham Dhama Date: Sun, 15 Apr 2018 22:13:48 +0530 Subject: [PATCH] analytics: Make stats of all realms accessible to server admins. In this commit: Two new URLs are added, to make all realms accessible for server admins. One is for the stats page itself and another for getting chart data i.e. chart data API requests. For the above two new URLs corresponding two view functions are added. --- analytics/tests/test_views.py | 40 ++++++++++++++++++++++++++++++ analytics/urls.py | 4 +++ analytics/views.py | 45 +++++++++++++++++++++++++++++----- static/js/stats/stats.js | 10 +++++++- templates/analytics/stats.html | 8 +++++- zerver/decorator.py | 3 ++- 6 files changed, 101 insertions(+), 9 deletions(-) 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)