diff --git a/analytics/tests/test_views.py b/analytics/tests/test_views.py index 03568bf133..1a061999b5 100644 --- a/analytics/tests/test_views.py +++ b/analytics/tests/test_views.py @@ -4,6 +4,7 @@ from typing import List, Optional import mock from django.utils.timezone import utc from django.http import HttpResponse +import ujson from analytics.lib.counts import COUNT_STATS, CountStat 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.timestamp import ceiling_to_day, \ 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): def test_stats(self) -> None: @@ -335,7 +338,7 @@ class TestSupportEndpoint(ZulipTestCase): self.assert_in_success_response(['user\n', '

King Hamlet

', 'Email: hamlet@zulip.com', 'Is active: True
', 'Admins: iago@zulip.com\n', - 'class="copy-button" data-admin-emails="iago@zulip.com"' + 'class="copy-button" data-copytext="iago@zulip.com"' ], result) def check_zulip_realm_query_result(result: HttpResponse) -> None: @@ -362,6 +365,39 @@ class TestSupportEndpoint(ZulipTestCase): 'scrub-realm-button">', '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(['preregistration user\n', + 'Email: {}'.format(email), + ], result) + if invite: + self.assert_in_success_response(['invite'], result) + self.assert_in_success_response(['Expires in: 1\xa0week, 3', + 'Status: Link has never been clicked'], result) + self.assert_in_success_response([], result) + else: + self.assert_not_in_success_response(['invite'], result) + self.assert_in_success_response(['Expires in: 1\xa0day', + 'Status: Link has never been clicked'], result) + + def check_realm_creation_query_result(result: HttpResponse, email: str) -> None: + self.assert_in_success_response(['preregistration user\n', + 'realm creation\n', + 'Link: http://zulip.testserver/accounts/do_confirm/', + 'Expires in: 1\xa0day
\n' + ], result) + + def check_multiuse_invite_link_query_result(result: HttpResponse) -> None: + self.assert_in_success_response(['multiuse invite\n', + 'Link: http://zulip.testserver/join/', + 'Expires in: 1\xa0week, 3' + ], result) + + def check_realm_reactivation_link_query_result(result: HttpResponse) -> None: + self.assert_in_success_response(['realm reactivation\n', + 'Link: http://zulip.testserver/reactivate/', + 'Expires in: 1\xa0day' + ], result) + cordelia_email = self.example_email("cordelia") self.login(cordelia_email) @@ -399,6 +435,36 @@ class TestSupportEndpoint(ZulipTestCase): check_zulip_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: cordelia = self.example_user("cordelia") self.login(cordelia.email) diff --git a/analytics/views.py b/analytics/views.py index 202e449898..4e530dfb98 100644 --- a/analytics/views.py +++ b/analytics/views.py @@ -20,6 +20,7 @@ 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.utils.timesince import timesince from django.core.validators import URLValidator from django.core.exceptions import ValidationError 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.models import BaseCount, InstallationCount, \ 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, \ to_non_negative_int, to_utc_datetime, zulip_login_required, require_non_guest_user 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.actions import do_change_plan_type, do_deactivate_realm, \ do_reactivate_realm, do_scrub_realm +from confirmation.settings import STATUS_ACTIVE 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 +from zerver.models import Client, get_realm, Realm, UserActivity, UserActivityInterval, \ + UserProfile, PreregistrationUser, MultiuseInvite if settings.ZILENCER_ENABLED: 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), ) +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 def support(request: HttpRequest) -> HttpResponse: context = {} # type: Dict[str, Any] @@ -1091,12 +1134,27 @@ def support(request: HttpRequest) -> HttpResponse: 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: return ", ".join(realm.get_human_admin_users().values_list("delivery_email", flat=True)) context["realm_admin_emails"] = realm_admin_emails context["get_discount_for_realm"] = get_discount_for_realm context["realm_icon_url"] = realm_icon_url + context["Confirmation"] = Confirmation return render(request, 'analytics/support.html', context=context) def get_user_activity_records_for_realm(realm: str, is_bot: bool) -> QuerySet: diff --git a/static/js/analytics/support.js b/static/js/analytics/support.js index c127d4ac41..c8d9b172ea 100644 --- a/static/js/analytics/support.js +++ b/static/js/analytics/support.js @@ -9,6 +9,6 @@ $(function () { }); $('a.copy-button').click(function () { - common.copy_data_attribute_value($(this), "admin-emails"); + common.copy_data_attribute_value($(this), "copytext"); }); }); diff --git a/templates/analytics/realm_details.html b/templates/analytics/realm_details.html index 216ab7ebea..ab6aec4878 100644 --- a/templates/analytics/realm_details.html +++ b/templates/analytics/realm_details.html @@ -5,7 +5,7 @@ activity
Date created: {{ realm.date_created|timesince }} ago
Admins: {{ realm_admin_emails(realm) }} - +
@@ -43,4 +43,3 @@
-
diff --git a/templates/analytics/support.html b/templates/analytics/support.html index eef59c7b1e..e39faa35b5 100644 --- a/templates/analytics/support.html +++ b/templates/analytics/support.html @@ -41,7 +41,7 @@ Date joined: {{ user.date_joined|timesince }} ago
Is active: {{ user.is_active }}
Is admin: {{ user.is_realm_admin }}
-
+
{% include "analytics/realm_details.html" %}
@@ -55,6 +55,61 @@ {% endfor %} + {% for confirmation in confirmations %} + {% set object = confirmation.object %} +
+ {% if confirmation.type == Confirmation.USER_REGISTRATION %} + preregistration user + {% set email = object.email %} + {% set realm = object.realm %} + {% set show_realm_details = True %} + {% elif confirmation.type == Confirmation.REALM_CREATION %} + preregistration user + realm creation + {% set email = object.email %} + {% set show_realm_details = False %} + {% elif confirmation.type == Confirmation.INVITATION %} + preregistration user + invite + {% set email = object.email %} + {% set realm = object.realm %} + {% set show_realm_details = True %} + {% elif confirmation.type == Confirmation.MULTIUSE_INVITE %} + multiuse invite + {% set realm = object.realm %} + {% set show_realm_details = False %} + {% elif confirmation.type == Confirmation.REALM_REACTIVATION %} + realm reactivation + {% set realm = object %} + {% set show_realm_details = False %} + {% endif %} +
+
+ {% if email %} + Email: {{ email }}
+ {% endif %} + Link: {{ confirmation.url }} + + +
+ Expires in: {{ confirmation.expires_in }}
+ {% if confirmation.link_status %} + Status: {{ confirmation.link_status }} + {% endif %} +
+ {% if show_realm_details %} +
+
+ {% include "analytics/realm_details.html" %} +
+ {% elif realm %} + Realm: {{ realm.string_id }} +
+ {% endif %} +
+
+
+ {% endfor %} {% endblock %} diff --git a/tools/linter_lib/custom_check.py b/tools/linter_lib/custom_check.py index b35378854c..a9fc0e74ef 100644 --- a/tools/linter_lib/custom_check.py +++ b/tools/linter_lib/custom_check.py @@ -631,7 +631,7 @@ html_rules = whitespace_rules + prose_style_rules + [ ('templates/zerver/app/markdown_help.html', ':heart:') ]), - '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."}, {'pattern': r'''\Walt=["'][^{"']''', 'description': "alt argument should be enclosed by _() or it should be an empty string.",