support: Show confirmation links in search.

Fixes #13060 #12784
This commit is contained in:
Vishnu KS 2019-09-18 18:34:36 +05:30 committed by Tim Abbott
parent e080b42fe5
commit ec955f8f78
6 changed files with 187 additions and 9 deletions

View File

@ -4,6 +4,7 @@ from typing import List, Optional
import mock import mock
from django.utils.timezone import utc from django.utils.timezone import utc
from django.http import HttpResponse from django.http import HttpResponse
import ujson
from analytics.lib.counts import COUNT_STATS, CountStat from analytics.lib.counts import COUNT_STATS, CountStat
from analytics.lib.time_utils import time_range from analytics.lib.time_utils import time_range
@ -14,7 +15,9 @@ from analytics.views import rewrite_client_arrays, \
from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.timestamp import ceiling_to_day, \ from zerver.lib.timestamp import ceiling_to_day, \
ceiling_to_hour, datetime_to_timestamp ceiling_to_hour, datetime_to_timestamp
from zerver.models import Client, get_realm from zerver.lib.actions import do_create_multiuse_invite_link, \
do_send_realm_reactivation_email
from zerver.models import Client, get_realm, MultiuseInvite
class TestStatsEndpoint(ZulipTestCase): class TestStatsEndpoint(ZulipTestCase):
def test_stats(self) -> None: def test_stats(self) -> None:
@ -335,7 +338,7 @@ class TestSupportEndpoint(ZulipTestCase):
self.assert_in_success_response(['<span class="label">user</span>\n', '<h3>King Hamlet</h3>', 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>Email</b>: hamlet@zulip.com', '<b>Is active</b>: True<br>',
'<b>Admins</b>: iago@zulip.com\n', '<b>Admins</b>: iago@zulip.com\n',
'class="copy-button" data-admin-emails="iago@zulip.com"' 'class="copy-button" data-copytext="iago@zulip.com"'
], result) ], result)
def check_zulip_realm_query_result(result: HttpResponse) -> None: def check_zulip_realm_query_result(result: HttpResponse) -> None:
@ -362,6 +365,39 @@ class TestSupportEndpoint(ZulipTestCase):
'scrub-realm-button">', 'scrub-realm-button">',
'data-string-id="lear"'], result) 'data-string-id="lear"'], result)
def check_preregistration_user_query_result(result: HttpResponse, email: str, invite: Optional[bool]=False) -> None:
self.assert_in_success_response(['<span class="label">preregistration user</span>\n',
'<b>Email</b>: {}'.format(email),
], result)
if invite:
self.assert_in_success_response(['<span class="label">invite</span>'], result)
self.assert_in_success_response(['<b>Expires in</b>: 1\xa0week, 3',
'<b>Status</b>: Link has never been clicked'], result)
self.assert_in_success_response([], result)
else:
self.assert_not_in_success_response(['<span class="label">invite</span>'], result)
self.assert_in_success_response(['<b>Expires in</b>: 1\xa0day',
'<b>Status</b>: Link has never been clicked'], result)
def check_realm_creation_query_result(result: HttpResponse, email: str) -> None:
self.assert_in_success_response(['<span class="label">preregistration user</span>\n',
'<span class="label">realm creation</span>\n',
'<b>Link</b>: http://zulip.testserver/accounts/do_confirm/',
'<b>Expires in</b>: 1\xa0day<br>\n'
], result)
def check_multiuse_invite_link_query_result(result: HttpResponse) -> None:
self.assert_in_success_response(['<span class="label">multiuse invite</span>\n',
'<b>Link</b>: http://zulip.testserver/join/',
'<b>Expires in</b>: 1\xa0week, 3'
], result)
def check_realm_reactivation_link_query_result(result: HttpResponse) -> None:
self.assert_in_success_response(['<span class="label">realm reactivation</span>\n',
'<b>Link</b>: http://zulip.testserver/reactivate/',
'<b>Expires in</b>: 1\xa0day'
], result)
cordelia_email = self.example_email("cordelia") cordelia_email = self.example_email("cordelia")
self.login(cordelia_email) self.login(cordelia_email)
@ -399,6 +435,36 @@ class TestSupportEndpoint(ZulipTestCase):
check_zulip_realm_query_result(result) check_zulip_realm_query_result(result)
check_lear_realm_query_result(result) check_lear_realm_query_result(result)
self.client_post('/accounts/home/', {'email': self.nonreg_email("test")})
self.login(iago_email)
result = self.client_get("/activity/support", {"q": self.nonreg_email("test")})
check_preregistration_user_query_result(result, self.nonreg_email("test"))
check_zulip_realm_query_result(result)
stream_ids = [self.get_stream_id("Denmark")]
invitee_emails = [self.nonreg_email("test1")]
self.client_post("/json/invites", {"invitee_emails": invitee_emails,
"stream_ids": ujson.dumps(stream_ids), "invite_as": 1})
result = self.client_get("/activity/support", {"q": self.nonreg_email("test1")})
check_preregistration_user_query_result(result, self.nonreg_email("test1"), invite=True)
check_zulip_realm_query_result(result)
email = self.nonreg_email('alice')
self.client_post('/new/', {'email': email})
result = self.client_get("/activity/support", {"q": email})
check_realm_creation_query_result(result, email)
do_create_multiuse_invite_link(self.example_user("hamlet"), invited_as=1)
result = self.client_get("/activity/support", {"q": "zulip"})
check_multiuse_invite_link_query_result(result)
check_zulip_realm_query_result(result)
MultiuseInvite.objects.all().delete()
do_send_realm_reactivation_email(get_realm("zulip"))
result = self.client_get("/activity/support", {"q": "zulip"})
check_realm_reactivation_link_query_result(result)
check_zulip_realm_query_result(result)
def test_change_plan_type(self) -> None: def test_change_plan_type(self) -> None:
cordelia = self.example_user("cordelia") cordelia = self.example_user("cordelia")
self.login(cordelia.email) self.login(cordelia.email)

View File

@ -20,6 +20,7 @@ from django.shortcuts import render
from django.template import loader from django.template import loader
from django.utils.timezone import now as timezone_now, utc as timezone_utc from django.utils.timezone import now as timezone_now, utc as timezone_utc
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.timesince import timesince
from django.core.validators import URLValidator from django.core.validators import URLValidator
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from jinja2 import Markup as mark_safe from jinja2 import Markup as mark_safe
@ -28,6 +29,7 @@ from analytics.lib.counts import COUNT_STATS, CountStat
from analytics.lib.time_utils import time_range from analytics.lib.time_utils import time_range
from analytics.models import BaseCount, InstallationCount, \ from analytics.models import BaseCount, InstallationCount, \
RealmCount, StreamCount, UserCount, last_successful_fill, installation_epoch RealmCount, StreamCount, UserCount, last_successful_fill, installation_epoch
from confirmation.models import Confirmation, confirmation_url, _properties
from zerver.decorator import require_server_admin, require_server_admin_api, \ from zerver.decorator import require_server_admin, require_server_admin_api, \
to_non_negative_int, to_utc_datetime, zulip_login_required, require_non_guest_user to_non_negative_int, to_utc_datetime, zulip_login_required, require_non_guest_user
from zerver.lib.exceptions import JsonableError from zerver.lib.exceptions import JsonableError
@ -39,12 +41,13 @@ from zerver.views.invite import get_invitee_emails_set
from zerver.lib.subdomains import get_subdomain_from_hostname from zerver.lib.subdomains import get_subdomain_from_hostname
from zerver.lib.actions import do_change_plan_type, do_deactivate_realm, \ from zerver.lib.actions import do_change_plan_type, do_deactivate_realm, \
do_reactivate_realm, do_scrub_realm do_reactivate_realm, do_scrub_realm
from confirmation.settings import STATUS_ACTIVE
if settings.BILLING_ENABLED: if settings.BILLING_ENABLED:
from corporate.lib.stripe import attach_discount_to_realm, get_discount_for_realm from corporate.lib.stripe import attach_discount_to_realm, get_discount_for_realm
from zerver.models import Client, get_realm, Realm, \ from zerver.models import Client, get_realm, Realm, UserActivity, UserActivityInterval, \
UserActivity, UserActivityInterval, UserProfile UserProfile, PreregistrationUser, MultiuseInvite
if settings.ZILENCER_ENABLED: if settings.ZILENCER_ENABLED:
from zilencer.models import RemoteInstallationCount, RemoteRealmCount, \ from zilencer.models import RemoteInstallationCount, RemoteRealmCount, \
@ -1027,6 +1030,46 @@ def get_activity(request: HttpRequest) -> HttpResponse:
context=dict(data=data, title=title, is_home=True), context=dict(data=data, title=title, is_home=True),
) )
def get_confirmations(types: List[int], object_ids: List[int],
hostname: Optional[str]=None) -> List[Dict[str, Any]]:
lowest_datetime = timezone_now() - timedelta(days=30)
confirmations = Confirmation.objects.filter(type__in=types, object_id__in=object_ids,
date_sent__gte=lowest_datetime)
confirmation_dicts = []
for confirmation in confirmations:
realm = confirmation.realm
content_object = confirmation.content_object
if realm is not None:
realm_host = realm.host
elif isinstance(content_object, Realm):
realm_host = content_object.host
else:
realm_host = hostname
type = confirmation.type
days_to_activate = _properties[type].validity_in_days
expiry_date = confirmation.date_sent + timedelta(days=days_to_activate)
if hasattr(content_object, "status"):
if content_object.status == STATUS_ACTIVE:
link_status = "Link has been clicked"
else:
link_status = "Link has never been clicked"
else:
link_status = ""
if timezone_now() < expiry_date:
expires_in = timesince(confirmation.date_sent, expiry_date)
else:
expires_in = "Expired"
url = confirmation_url(confirmation.confirmation_key, realm_host, type)
confirmation_dicts.append({"object": confirmation.content_object,
"url": url, "type": type, "link_status": link_status,
"expires_in": expires_in})
return confirmation_dicts
@require_server_admin @require_server_admin
def support(request: HttpRequest) -> HttpResponse: def support(request: HttpRequest) -> HttpResponse:
context = {} # type: Dict[str, Any] context = {} # type: Dict[str, Any]
@ -1091,12 +1134,27 @@ def support(request: HttpRequest) -> HttpResponse:
context["realms"] = realms context["realms"] = realms
confirmations = [] # type: List[Dict[str, Any]]
preregistration_users = PreregistrationUser.objects.filter(email__in=key_words)
confirmations += get_confirmations([Confirmation.USER_REGISTRATION, Confirmation.INVITATION,
Confirmation.REALM_CREATION], preregistration_users,
hostname=request.get_host())
multiuse_invites = MultiuseInvite.objects.filter(realm__in=realms)
confirmations += get_confirmations([Confirmation.MULTIUSE_INVITE], multiuse_invites)
confirmations += get_confirmations([Confirmation.REALM_REACTIVATION], [realm.id for realm in realms])
context["confirmations"] = confirmations
def realm_admin_emails(realm: Realm) -> str: def realm_admin_emails(realm: Realm) -> str:
return ", ".join(realm.get_human_admin_users().values_list("delivery_email", flat=True)) return ", ".join(realm.get_human_admin_users().values_list("delivery_email", flat=True))
context["realm_admin_emails"] = realm_admin_emails context["realm_admin_emails"] = realm_admin_emails
context["get_discount_for_realm"] = get_discount_for_realm context["get_discount_for_realm"] = get_discount_for_realm
context["realm_icon_url"] = realm_icon_url context["realm_icon_url"] = realm_icon_url
context["Confirmation"] = Confirmation
return render(request, 'analytics/support.html', context=context) return render(request, 'analytics/support.html', context=context)
def get_user_activity_records_for_realm(realm: str, is_bot: bool) -> QuerySet: def get_user_activity_records_for_realm(realm: str, is_bot: bool) -> QuerySet:

View File

@ -9,6 +9,6 @@ $(function () {
}); });
$('a.copy-button').click(function () { $('a.copy-button').click(function () {
common.copy_data_attribute_value($(this), "admin-emails"); common.copy_data_attribute_value($(this), "copytext");
}); });
}); });

View File

@ -5,7 +5,7 @@
<a target="_blank" href="/realm_activity/{{ realm.string_id }}/">activity</a><br> <a target="_blank" href="/realm_activity/{{ realm.string_id }}/">activity</a><br>
<b>Date created</b>: {{ realm.date_created|timesince }} ago<br> <b>Date created</b>: {{ realm.date_created|timesince }} ago<br>
<b>Admins</b>: {{ realm_admin_emails(realm) }} <b>Admins</b>: {{ realm_admin_emails(realm) }}
<a title="Copy emails" class="copy-button" data-admin-emails="{{ realm_admin_emails(realm) }}"> <a title="Copy emails" class="copy-button" data-copytext="{{ realm_admin_emails(realm) }}">
<i class="fa fa-copy"></i> <i class="fa fa-copy"></i>
</a> </a>
<form method="POST"> <form method="POST">
@ -43,4 +43,3 @@
<input type="hidden" name="scrub_realm" value="scrub_realm" /> <input type="hidden" name="scrub_realm" value="scrub_realm" />
<button data-string-id="{{realm.string_id}}" class="button rounded btn-danger small scrub-realm-button">Scrub realm (danger)</button> <button data-string-id="{{realm.string_id}}" class="button rounded btn-danger small scrub-realm-button">Scrub realm (danger)</button>
</form> </form>
<hr>

View File

@ -41,7 +41,7 @@
<b>Date joined</b>: {{ user.date_joined|timesince }} ago<br> <b>Date joined</b>: {{ user.date_joined|timesince }} ago<br>
<b>Is active</b>: {{ user.is_active }}<br> <b>Is active</b>: {{ user.is_active }}<br>
<b>Is admin</b>: {{ user.is_realm_admin }}<br> <b>Is admin</b>: {{ user.is_realm_admin }}<br>
<br> <hr>
<div> <div>
{% include "analytics/realm_details.html" %} {% include "analytics/realm_details.html" %}
</div> </div>
@ -55,6 +55,61 @@
</div> </div>
{% endfor %} {% endfor %}
{% for confirmation in confirmations %}
{% set object = confirmation.object %}
<div class="support-query-result new-style">
{% if confirmation.type == Confirmation.USER_REGISTRATION %}
<span class="label">preregistration user</span>
{% set email = object.email %}
{% set realm = object.realm %}
{% set show_realm_details = True %}
{% elif confirmation.type == Confirmation.REALM_CREATION %}
<span class="label">preregistration user</span>
<span class="label">realm creation</span>
{% set email = object.email %}
{% set show_realm_details = False %}
{% elif confirmation.type == Confirmation.INVITATION %}
<span class="label">preregistration user</span>
<span class="label">invite</span>
{% set email = object.email %}
{% set realm = object.realm %}
{% set show_realm_details = True %}
{% elif confirmation.type == Confirmation.MULTIUSE_INVITE %}
<span class="label">multiuse invite</span>
{% set realm = object.realm %}
{% set show_realm_details = False %}
{% elif confirmation.type == Confirmation.REALM_REACTIVATION %}
<span class="label">realm reactivation</span>
{% set realm = object %}
{% set show_realm_details = False %}
{% endif %}
<br>
<br>
{% if email %}
<b>Email</b>: {{ email }}<br>
{% endif %}
<b>Link</b>: {{ confirmation.url }}
<a title="Copy link" class="copy-button" data-copytext="{{ confirmation.url }}">
<i class="fa fa-copy"></i>
</a><br>
<b>Expires in</b>: {{ confirmation.expires_in }}<br>
{% if confirmation.link_status %}
<b>Status</b>: {{ confirmation.link_status }}
{% endif %}
<br>
{% if show_realm_details %}
<hr>
<div>
{% include "analytics/realm_details.html" %}
</div>
{% elif realm %}
<b>Realm</b>: {{ realm.string_id }}
<br>
{% endif %}
<br>
</div>
<br>
{% endfor %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -631,7 +631,7 @@ html_rules = whitespace_rules + prose_style_rules + [
('templates/zerver/app/markdown_help.html', ('templates/zerver/app/markdown_help.html',
'<td class="rendered_markdown"><img alt=":heart:" class="emoji" src="/static/generated/emoji/images/emoji/heart.png" title=":heart:" /></td>') '<td class="rendered_markdown"><img alt=":heart:" class="emoji" src="/static/generated/emoji/images/emoji/heart.png" title=":heart:" /></td>')
]), ]),
'exclude': set(["templates/zerver/emails", "templates/analytics/realm_details.html"]), 'exclude': set(["templates/zerver/emails", "templates/analytics/realm_details.html", "templates/analytics/support.html"]),
'description': "`title` value should be translatable."}, 'description': "`title` value should be translatable."},
{'pattern': r'''\Walt=["'][^{"']''', {'pattern': r'''\Walt=["'][^{"']''',
'description': "alt argument should be enclosed by _() or it should be an empty string.", 'description': "alt argument should be enclosed by _() or it should be an empty string.",