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.
This commit is contained in:
Shubham Dhama 2018-04-15 22:13:48 +05:30 committed by Tim Abbott
parent 40dc48a033
commit b26c38bc47
6 changed files with 101 additions and 9 deletions

View File

@ -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

View File

@ -11,6 +11,8 @@ i18n_urlpatterns = [
name='analytics.views.get_realm_activity'),
url(r'^user_activity/(?P<email>[\S]+)/$', analytics.views.get_user_activity,
name='analytics.views.get_user_activity'),
url(r'^stats/realm/(?P<realm_str>[\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<realm_str>[\S]+)$', rest_dispatch,
{'GET': 'analytics.views.get_chart_data_for_realm'}),
]
i18n_urlpatterns += [

View File

@ -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,6 +120,7 @@ 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})
if realm is None:
realm = user_profile.realm
if start is None:
start = realm.date_created

View File

@ -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) {

View File

@ -1,5 +1,11 @@
{% extends "zerver/base.html" %}
{% block page_params %}
<script type="text/javascript">
var page_params = {{ page_params|safe }};
</script>
{% endblock %}
{% block customhead %}
{% stylesheet 'portico' %}
{% endblock %}
@ -14,7 +20,7 @@
<div class="page-content">
<div id="id_stats_errors" class="alert alert-error"></div>
<div class="center-charts">
<h1 class="analytics-page-header">{% trans %}Zulip analytics for {{ realm_name }}{% endtrans %}</h1>
<h1 class="analytics-page-header">{% trans %}Zulip analytics for {{ target_realm_name }}{% endtrans %}</h1>
<div class="left">
<div class="chart-container">

View File

@ -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)