activity: Create interface for doing support operations.

This should grow into a tool that makes it much easier to do common
organization management tasks without using a manage.py shell.
This commit is contained in:
Vishnu Ks
2019-03-08 17:32:10 +05:30
committed by Tim Abbott
parent 42de9a0c71
commit 8eeb8280b4
6 changed files with 313 additions and 0 deletions

View File

@@ -3,6 +3,7 @@ from typing import List, Optional
import mock
from django.utils.timezone import utc
from django.http import HttpResponse
from analytics.lib.counts import COUNT_STATS, CountStat
from analytics.lib.time_utils import time_range
@@ -327,6 +328,94 @@ class TestGetChartData(ZulipTestCase):
{'chart_name': 'number_of_humans'})
self.assert_json_success(result)
class TestSupportEndpoint(ZulipTestCase):
def test_search(self) -> None:
def check_hamlet_user_result(result: HttpResponse) -> None:
self.assert_in_success_response(['<span class="label">user</span>\n', '<h3>King Hamlet</h3>',
'<b>Email</b>: hamlet@zulip.com', '<b>Is active</b>: True<br>',
'<b>Admins</b>: iago@zulip.com <br>'], result)
def check_zulip_realm_result(result: HttpResponse) -> None:
self.assert_in_success_response(['<input type="hidden" name="realm_id" value="1"', 'Zulip Dev</h3>',
'<option value="1" selected>Self Hosted</option>',
'<option value="2" >Limited</option>',
'input type="number" name="discount" value="None"'], result)
def check_lear_realm_result(result: HttpResponse) -> None:
self.assert_in_success_response(['<input type="hidden" name="realm_id" value="3"', 'Lear &amp; Co.</h3>',
'<option value="1" selected>Self Hosted</option>',
'<option value="2" >Limited</option>',
'input type="number" name="discount" value="None"'], result)
cordelia_email = self.example_email("cordelia")
self.login(cordelia_email)
result = self.client_get("/activity/support")
self.assertEqual(result.status_code, 302)
self.assertEqual(result["Location"], "/login/")
iago_email = self.example_email("iago")
self.login(iago_email)
result = self.client_get("/activity/support")
self.assert_in_success_response(['<input type="text" name="q" class="input-xxlarge search-query"'], result)
result = self.client_get("/activity/support", {"q": "hamlet@zulip.com"})
check_hamlet_user_result(result)
check_zulip_realm_result(result)
result = self.client_get("/activity/support", {"q": "lear"})
check_lear_realm_result(result)
result = self.client_get("/activity/support", {"q": "http://lear.testserver"})
check_lear_realm_result(result)
with self.settings(REALM_HOSTS={'zulip': 'localhost'}):
result = self.client_get("/activity/support", {"q": "http://localhost"})
check_zulip_realm_result(result)
result = self.client_get("/activity/support", {"q": "hamlet@zulip.com, lear"})
check_hamlet_user_result(result)
check_zulip_realm_result(result)
check_lear_realm_result(result)
result = self.client_get("/activity/support", {"q": "lear, Hamlet <hamlet@zulip.com>"})
check_hamlet_user_result(result)
check_zulip_realm_result(result)
check_lear_realm_result(result)
def test_change_plan_type(self) -> None:
cordelia_email = self.example_email("cordelia")
self.login(cordelia_email)
result = self.client_post("/activity/support", {"realm_id": "1", "plan_type": "2"})
self.assertEqual(result.status_code, 302)
self.assertEqual(result["Location"], "/login/")
iago_email = self.example_email("iago")
self.login(iago_email)
with mock.patch("analytics.views.do_change_plan_type") as m:
result = self.client_post("/activity/support", {"realm_id": "1", "plan_type": "2"})
m.assert_called_once_with(get_realm("zulip"), 2)
self.assert_in_success_response(["Plan type of Zulip Dev changed to limited from self hosted"], result)
def test_attach_discount(self) -> None:
cordelia_email = self.example_email("cordelia")
self.login(cordelia_email)
result = self.client_post("/activity/support", {"realm_id": "3", "discount": "25"})
self.assertEqual(result.status_code, 302)
self.assertEqual(result["Location"], "/login/")
iago_email = self.example_email("iago")
self.login(iago_email)
with mock.patch("analytics.views.attach_discount_to_realm") as m:
result = self.client_post("/activity/support", {"realm_id": "3", "discount": "25"})
m.assert_called_once_with(get_realm("lear"), 25)
self.assert_in_success_response(["Discount of Lear &amp; Co. changed to 25 from None"], 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

@@ -7,6 +7,8 @@ i18n_urlpatterns = [
# Server admin (user_profile.is_staff) visible stats pages
url(r'^activity$', analytics.views.get_activity,
name='analytics.views.get_activity'),
url(r'^activity/support$', analytics.views.support,
name='analytics.views.support'),
url(r'^realm_activity/(?P<realm_str>[\S]+)/$', analytics.views.get_realm_activity,
name='analytics.views.get_realm_activity'),
url(r'^user_activity/(?P<email>[\S]+)/$', analytics.views.get_user_activity,

View File

@@ -3,8 +3,11 @@ import itertools
import logging
import re
import time
import urllib
from collections import defaultdict
from datetime import datetime, timedelta
from decimal import Decimal
from typing import Any, Callable, Dict, List, \
Optional, Set, Tuple, Type, Union, cast
@@ -18,6 +21,8 @@ from django.shortcuts import render
from django.template import loader
from django.utils.timezone import now as timezone_now, utc as timezone_utc
from django.utils.translation import ugettext as _
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError
from jinja2 import Markup as mark_safe
from analytics.lib.counts import COUNT_STATS, CountStat
@@ -31,6 +36,14 @@ 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 convert_to_UTC, timestamp_to_datetime
from zerver.lib.realm_icon import realm_icon_url
from zerver.views.invite import get_invitee_emails_set
from zerver.lib.subdomains import get_subdomain_from_hostname
from zerver.lib.actions import do_change_plan_type
if settings.BILLING_ENABLED:
from corporate.lib.stripe import attach_discount_to_realm, get_discount_for_realm
from zerver.models import Client, get_realm, Realm, \
UserActivity, UserActivityInterval, UserProfile
@@ -1013,6 +1026,66 @@ def get_activity(request: HttpRequest) -> HttpResponse:
context=dict(data=data, title=title, is_home=True),
)
@require_server_admin
def support(request: HttpRequest) -> HttpResponse:
context = {} # type: Dict[str, Any]
if settings.BILLING_ENABLED and request.method == "POST":
realm_id = request.POST.get("realm_id", None)
realm = Realm.objects.get(id=realm_id)
new_plan_type = request.POST.get("plan_type", None)
if new_plan_type is not None:
new_plan_type = int(new_plan_type)
current_plan_type = realm.plan_type
do_change_plan_type(realm, new_plan_type)
msg = "Plan type of {} changed to {} from {} ".format(realm.name, get_plan_name(new_plan_type),
get_plan_name(current_plan_type))
context["plan_type_msg"] = msg
new_discount = request.POST.get("discount", None)
if new_discount is not None:
new_discount = Decimal(new_discount)
current_discount = get_discount_for_realm(realm)
attach_discount_to_realm(realm, new_discount)
msg = "Discount of {} changed to {} from {} ".format(realm.name, new_discount, current_discount)
context["discount_msg"] = msg
query = request.GET.get("q", None)
if query:
key_words = get_invitee_emails_set(query)
users = UserProfile.objects.filter(email__in=key_words)
if users:
for user in users:
user.realm.realm_icon_url = realm_icon_url(user.realm)
user.realm.admins = UserProfile.objects.filter(realm=user.realm, is_realm_admin=True)
user.realm.default_discount = get_discount_for_realm(user.realm)
context["users"] = users
realms = set(Realm.objects.filter(string_id__in=key_words))
for key_word in key_words:
try:
URLValidator()(key_word)
parse_result = urllib.parse.urlparse(key_word)
hostname = parse_result.hostname
if parse_result.port:
hostname = "{}:{}".format(hostname, parse_result.port)
subdomain = get_subdomain_from_hostname(hostname)
realm = get_realm(subdomain)
if realm is not None:
realms.add(realm)
except ValidationError:
pass
if realms:
for realm in realms:
realm.realm_icon_url = realm_icon_url(realm)
realm.admins = UserProfile.objects.filter(realm=realm, is_realm_admin=True)
realm.default_discount = get_discount_for_realm(realm)
context["realms"] = realms
return render(request, 'analytics/support.html', context=context)
def get_user_activity_records_for_realm(realm: str, is_bot: bool) -> QuerySet:
fields = [
'user_profile__full_name',

View File

@@ -48,3 +48,36 @@ tr.admin td:first-child {
font-weight: bold;
color: hsl(0, 100%, 39%);
}
.support-query-result {
background-color: hsl(210, 100%, 97%);
padding-left: 15px;
padding-top: 14px;
border-radius: 7px;
-webkit-box-shadow: 0 10px 7px -6px hsl(0, 2%, 45%);
-moz-box-shadow: 0 10px 7px -6px hsl(0, 2%, 45%);
box-shadow: 0 10px 7px -6px hsl(0, 2%, 45%);
}
.support-realm-icon {
max-width: 25px;
position: relative;
top: -2px;
}
.support-discount-form {
position: relative;
top: -25px;
}
.support-submit-button {
position: relative;
top: -5px;
border-color: hsl(0, 0%, 83%);
border-radius: 2px;
}
.support-search-button {
border-color: hsl(0, 0%, 83%);
border-radius: 2px;
}

View File

@@ -0,0 +1,115 @@
{% extends "zerver/base.html" %}
{# User Activity. #}
{% block title %}
<title>Info</title>
{% endblock %}
{% block customhead %}
{{ super() }}
{{ render_bundle('activity') }}
{% endblock %}
{% block content %}
<div class="container">
<br>
<form>
<center>
<input type="text" name="q" class="input-xxlarge search-query" placeholder="emails, string_ids, organization urls separated by commas" value="{{ request.GET.get('q', '') }}">
<button type="submit" class="btn support-search-button">Search</button>
</center>
</form>
{% if plan_type_msg %}
<div class="alert alert-success">
<center>
{{ plan_type_msg }}
</center>
</div>
{% endif %}
{% if discount_msg %}
<div class="alert alert-success">
<center>
{{ discount_msg }}
</center>
</div>
{% endif %}
<div id="query-results">
{% for user in users %}
<div class="support-query-result">
<span class="label">user</span>
<h3>{{ user.full_name }}</h3>
<b>Email</b>: {{ user.email }}<br>
<b>Date joined</b>: {{ user.date_joined|timesince }} ago<br>
<b>Is active</b>: {{ user.is_active }}<br>
<b>Is admin</b>: {{ user.is_realm_admin }}<br>
<br>
<div>
<span class="label">realm</span>
<h3><img src="{{ user.realm.realm_icon_url }}" class="support-realm-icon"> {{ user.realm.name }}</h3>
<b>URL</b>: <a target="_blank" href="{{ user.realm.uri }}">{{ user.realm.uri }}</a><br>
<b>Date created</b>: {{ user.realm.date_created|timesince }} ago<br>
<b>Is active</b>: {{ not user.realm.deactivated }}<br>
<b>Admins</b>: {% for admin in user.realm.admins %}{{ admin.email }} {% endfor %}<br>
<form method="POST">
{{ csrf_input }}
<input type="hidden" name="realm_id" value="{{ user.realm.id }}" />
<b>Plan type</b>:<br>
<select name="plan_type">
<option value="1" {% if user.realm.plan_type == 1 %}selected{% endif %}>Self Hosted</option>
<option value="2" {% if user.realm.plan_type == 2 %}selected{% endif %}>Limited</option>
<option value="3" {% if user.realm.plan_type == 3 %}selected{% endif %}>Standard</option>
<option value="4" {% if user.realm.plan_type == 4 %}selected{% endif %}>Standard Free</option>
</select>
<button type="submit" class="btn btn-small support-submit-button">Update</button>
</form>
<form method="POST" class="support-discount-form">
<b>Discount</b>:<br>
{{ csrf_input }}
<input type="hidden" name="realm_id" value="{{ user.realm.id }}" />
<input type="number" name="discount" value="{{user.realm.default_discount}}" required>
<button type="submit" class="btn btn-small support-submit-button">Update</button>
</form>
</div>
<hr>
</div>
{% endfor %}
{% for realm in realms %}
<div class="support-query-result">
<span class="label">realm</span>
<h3><img src="{{ realm.realm_icon_url }}" class="support-realm-icon"> {{ realm.name }}</h3>
<b>URL</b>: <a target="_blank" href="{{ realm.uri }}">{{ realm.uri }}</a><br>
<b>Date created</b>: {{ realm.date_created|timesince }} ago<br>
<b>Is active</b>: {{ not realm.deactivated }}<br>
<b>Admins</b>: {% for admin in realm.admins %}{{ admin.email }} {% endfor %}<br>
<b>Plan type</b>:
<form method="POST">
{{ csrf_input }}
<input type="hidden" name="realm_id" value="{{ realm.id }}" />
<select name="plan_type">
<option value="1" {% if realm.plan_type == 1 %}selected{% endif %}>Self Hosted</option>
<option value="2" {% if realm.plan_type == 2 %}selected{% endif %}>Limited</option>
<option value="3" {% if realm.plan_type == 3 %}selected{% endif %}>Standard</option>
<option value="4" {% if realm.plan_type == 4 %}selected{% endif %}>Standard Free</option>
</select>
<button type="submit" class="btn btn-small support-submit-button">Update</button>
</form>
<form method="POST" class="support-discount-form">
<b>Discount</b>:<br>
{{ csrf_input }}
<input type="hidden" name="realm_id" value="{{ realm.id }}" />
<input type="number" name="discount" value="{{ realm.default_discount}}" required>
<button type="submit" class="btn btn-small support-submit-button">Update</button>
</form>
<hr>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View File

@@ -733,6 +733,7 @@ def build_custom_checkers(by_lang):
'description': "`placeholder` value should be translatable.",
'exclude_line': [('templates/zerver/register.html', 'placeholder="acme"'),
('templates/zerver/register.html', 'placeholder="Acme or Aκμή"')],
'exclude': set(["templates/analytics/support.html"]),
'good_lines': ['<input class="stream-list-filter" type="text" placeholder="{{ _(\'Search streams\') }}" />'],
'bad_lines': ['<input placeholder="foo">']},
{'pattern': "placeholder='[^{]",