From d21f5c9b750879e6ded10edefe06c51e5e81e802 Mon Sep 17 00:00:00 2001 From: Aman Agrawal Date: Tue, 26 Mar 2024 05:14:16 +0000 Subject: [PATCH] registration: Ask user how they found Zulip. --- corporate/views/installation_activity.py | 14 +++++++- .../activity/installation_activity_table.html | 5 +++ templates/zerver/register.html | 20 +++++++++++ web/e2e-tests/realm-creation.test.ts | 2 ++ web/src/portico/signup.ts | 36 +++++++++++++++++++ web/styles/portico/portico_signin.css | 11 ++++++ web/webpack.assets.json | 6 +--- zerver/actions/create_realm.py | 6 ++++ zerver/forms.py | 20 ++++++++--- zerver/lib/test_classes.py | 2 ++ zerver/models/realm_audit_logs.py | 12 +++++++ zerver/views/development/registration.py | 4 +++ zerver/views/registration.py | 30 ++++++++++++++++ 13 files changed, 158 insertions(+), 10 deletions(-) diff --git a/corporate/views/installation_activity.py b/corporate/views/installation_activity.py index 482abe48e1..d8c7e6681e 100644 --- a/corporate/views/installation_activity.py +++ b/corporate/views/installation_activity.py @@ -30,6 +30,7 @@ from corporate.views.support import get_plan_type_string from zerver.decorator import require_server_admin from zerver.lib.request import has_request_variables from zerver.models import Realm +from zerver.models.realm_audit_logs import RealmAuditLog from zerver.models.realms import get_org_type_display_name @@ -102,7 +103,8 @@ def realm_summary_table() -> str: coalesce(wau_table.value, 0) wau_count, coalesce(dau_table.value, 0) dau_count, coalesce(user_count_table.value, 0) user_profile_count, - coalesce(bot_count_table.value, 0) bot_count + coalesce(bot_count_table.value, 0) bot_count, + coalesce(realm_audit_log_table.how_realm_creator_found_zulip, '') how_realm_creator_found_zulip FROM zerver_realm as realm LEFT OUTER JOIN ( @@ -157,6 +159,15 @@ def realm_summary_table() -> str: AND subgroup = 'true' AND end_time = %(active_users_audit_end_time)s ) as bot_count_table ON realm.id = bot_count_table.realm_id + LEFT OUTER JOIN ( + SELECT + extra_data->>'how_realm_creator_found_zulip' as how_realm_creator_found_zulip, + realm_id + from + zerver_realmauditlog + WHERE + event_type = %(realm_creation_event_type)s + ) as realm_audit_log_table ON realm.id = realm_audit_log_table.realm_id WHERE _14day_active_humans IS NOT NULL or realm.plan_type = 3 @@ -178,6 +189,7 @@ def realm_summary_table() -> str: "active_users_audit_end_time": COUNT_STATS[ "active_users_audit:is_bot:day" ].last_successful_fill(), + "realm_creation_event_type": RealmAuditLog.REALM_CREATED, }, ) rows = dictfetchall(cursor) diff --git a/templates/corporate/activity/installation_activity_table.html b/templates/corporate/activity/installation_activity_table.html index 54db2f311a..8c0b00f61e 100644 --- a/templates/corporate/activity/installation_activity_table.html +++ b/templates/corporate/activity/installation_activity_table.html @@ -48,6 +48,7 @@ Bots Human messages sent, last 8 UTC days (today-so-far first) + Referrer @@ -118,6 +119,10 @@ {% else %} {% endif %} + + + {{ row.how_realm_creator_found_zulip }} + {% endfor %} diff --git a/templates/zerver/register.html b/templates/zerver/register.html index 89e8a6a2d4..851af234a9 100644 --- a/templates/zerver/register.html +++ b/templates/zerver/register.html @@ -196,6 +196,26 @@ Form is validated both client-side using jquery-validation (see signup.js) and s
{% endif %} + {% if creating_new_realm %} +
+ + + + + +
+ {% endif %} +
{% if terms_of_service %}
diff --git a/web/e2e-tests/realm-creation.test.ts b/web/e2e-tests/realm-creation.test.ts index 5ca0777281..ee2d4253c0 100644 --- a/web/e2e-tests/realm-creation.test.ts +++ b/web/e2e-tests/realm-creation.test.ts @@ -59,6 +59,8 @@ async function realm_creation_tests(page: Page): Promise { full_name: "Alice", password: "passwordwhichisnotreallycomplex", terms: true, + how_realm_creator_found_zulip: "other", + how_realm_creator_found_zulip_other_text: "test", }; // For some reason, page.click() does not work this for particular checkbox // so use page.$eval here to call the .click method in the browser. diff --git a/web/src/portico/signup.ts b/web/src/portico/signup.ts index 210b8a8618..7a2256de63 100644 --- a/web/src/portico/signup.ts +++ b/web/src/portico/signup.ts @@ -302,4 +302,40 @@ $(() => { $(e.target).hide(); }); + + $("#how-realm-creator-found-zulip select").on("change", function () { + const elements: Record = { + Other: "how-realm-creator-found-zulip-other", + Advertisement: "how-realm-creator-found-zulip-where-ad", + "At an organization that's using it": + "how-realm-creator-found-zulip-which-organization", + }; + + const hideElement = (element: string): void => { + const $element = $(`#${element}`); + $element.hide(); + $element.removeAttr("required"); + $(`#${element}-error`).hide(); + }; + + const showElement = (element: string): void => { + const $element = $(`#${element}`); + $element.show(); + $element.attr("required", "required"); + }; + + // Reset state + for (const element of Object.values(elements)) { + if (element) { + hideElement(element); + } + } + + // Show the additional input box if needed. + const selected_option = $("option:selected", this).text(); + const selected_element = elements[selected_option]; + if (selected_element) { + showElement(selected_element); + } + }); }); diff --git a/web/styles/portico/portico_signin.css b/web/styles/portico/portico_signin.css index 6f227d5121..6bdb198d07 100644 --- a/web/styles/portico/portico_signin.css +++ b/web/styles/portico/portico_signin.css @@ -1458,3 +1458,14 @@ button#register_auth_button_gitlab { max-width: 800px; } } + +#registration #how-realm-creator-found-zulip label { + top: -5px; +} + +#how-realm-creator-found-zulip-where-ad, +#how-realm-creator-found-zulip-other, +#how-realm-creator-found-zulip-which-organization { + margin-top: 5px; + display: none; +} diff --git a/web/webpack.assets.json b/web/webpack.assets.json index a826cef354..01936537dd 100644 --- a/web/webpack.assets.json +++ b/web/webpack.assets.json @@ -115,11 +115,7 @@ ], "desktop-login": ["./src/bundles/portico", "./src/portico/desktop-login"], "desktop-redirect": ["./src/bundles/portico", "./src/portico/desktop-redirect"], - "stats": [ - "./src/bundles/portico", - "./styles/portico/stats.css", - "./src/stats/stats" - ], + "stats": ["./src/bundles/portico", "./styles/portico/stats.css", "./src/stats/stats"], "app": ["./src/bundles/app"], "digest": ["./src/bundles/portico"] } diff --git a/zerver/actions/create_realm.py b/zerver/actions/create_realm.py index affb079190..176613f498 100644 --- a/zerver/actions/create_realm.py +++ b/zerver/actions/create_realm.py @@ -167,6 +167,8 @@ def do_create_realm( enable_read_receipts: Optional[bool] = None, enable_spectator_access: Optional[bool] = None, prereg_realm: Optional[PreregistrationRealm] = None, + how_realm_creator_found_zulip: Optional[str] = None, + how_realm_creator_found_zulip_extra_context: Optional[str] = None, ) -> Realm: if string_id in [settings.SOCIAL_AUTH_SUBDOMAIN, settings.SELF_HOSTING_MANAGEMENT_SUBDOMAIN]: raise AssertionError( @@ -243,6 +245,10 @@ def do_create_realm( realm=realm, event_type=RealmAuditLog.REALM_CREATED, event_time=realm.date_created, + extra_data={ + "how_realm_creator_found_zulip": how_realm_creator_found_zulip, + "how_realm_creator_found_zulip_extra_context": how_realm_creator_found_zulip_extra_context, + }, ) realm_default_email_address_visibility = RealmUserDefault.EMAIL_ADDRESS_VISIBILITY_EVERYONE diff --git a/zerver/forms.py b/zerver/forms.py index 27181eb8f5..ce56796561 100644 --- a/zerver/forms.py +++ b/zerver/forms.py @@ -36,6 +36,7 @@ from zerver.lib.soft_deactivation import queue_soft_reactivation from zerver.lib.subdomains import get_subdomain, is_root_domain_available from zerver.lib.users import check_full_name from zerver.models import Realm, UserProfile +from zerver.models.realm_audit_logs import RealmAuditLog from zerver.models.realms import ( DisposableEmailError, DomainNotAllowedForRealmError, @@ -140,6 +141,8 @@ class RealmDetailsForm(forms.Form): realm_name = forms.CharField(max_length=Realm.MAX_REALM_NAME_LENGTH) def __init__(self, *args: Any, **kwargs: Any) -> None: + # Since the superclass doesn't accept random extra kwargs, we + # remove it from the kwargs dict before initializing. self.realm_creation = kwargs["realm_creation"] del kwargs["realm_creation"] @@ -177,10 +180,6 @@ class RegistrationForm(RealmDetailsForm): ) def __init__(self, *args: Any, **kwargs: Any) -> None: - # Since the superclass doesn't except random extra kwargs, we - # remove it from the kwargs dict before initializing. - self.realm_creation = kwargs["realm_creation"] - super().__init__(*args, **kwargs) if settings.TERMS_OF_SERVICE_VERSION is not None: self.fields["terms"] = forms.BooleanField(required=True) @@ -196,6 +195,19 @@ class RegistrationForm(RealmDetailsForm): choices=[(lang["code"], lang["name"]) for lang in get_language_list()], required=self.realm_creation, ) + self.fields["how_realm_creator_found_zulip"] = forms.ChoiceField( + choices=RealmAuditLog.HOW_REALM_CREATOR_FOUND_ZULIP_OPTIONS.items(), + required=self.realm_creation, + ) + self.fields["how_realm_creator_found_zulip_other_text"] = forms.CharField( + max_length=100, required=False + ) + self.fields["how_realm_creator_found_zulip_where_ad"] = forms.CharField( + max_length=100, required=False + ) + self.fields["how_realm_creator_found_zulip_which_organization"] = forms.CharField( + max_length=100, required=False + ) def clean_full_name(self) -> str: try: diff --git a/zerver/lib/test_classes.py b/zerver/lib/test_classes.py index 461c4f0703..d897f5a6db 100644 --- a/zerver/lib/test_classes.py +++ b/zerver/lib/test_classes.py @@ -842,6 +842,8 @@ Output: "default_stream_group": default_stream_groups, "source_realm_id": source_realm_id, "is_demo_organization": is_demo_organization, + "how_realm_creator_found_zulip": "other", + "how_realm_creator_found_zulip_extra_context": "I found it on the internet.", } if enable_marketing_emails is not None: payload["enable_marketing_emails"] = enable_marketing_emails diff --git a/zerver/models/realm_audit_logs.py b/zerver/models/realm_audit_logs.py index 1a161dfcb9..8b4a54d18d 100644 --- a/zerver/models/realm_audit_logs.py +++ b/zerver/models/realm_audit_logs.py @@ -162,6 +162,18 @@ class AbstractRealmAuditLog(models.Model): REALM_IMPORTED, ] + HOW_REALM_CREATOR_FOUND_ZULIP_OPTIONS = { + "existing_user": "At an organization that's using it", + "search_engine": "Search engine", + "review_site": "Review site", + "personal_recommendation": "Personal recommendation", + "hacker_news": "Hacker News", + "ad": "Advertisement", + "other": "Other", + "forgot": "Don't remember", + "refuse_to_answer": "Prefer not to say", + } + class Meta: abstract = True diff --git a/zerver/views/development/registration.py b/zerver/views/development/registration.py index 08a47ba4e5..0cf5884832 100644 --- a/zerver/views/development/registration.py +++ b/zerver/views/development/registration.py @@ -85,6 +85,8 @@ def register_development_realm(request: HttpRequest) -> HttpResponse: password="test", realm_subdomain=realm_subdomain, terms="true", + how_realm_creator_found_zulip="ad", + how_realm_creator_found_zulip_extra_context="test", ) return accounts_register(request) @@ -120,6 +122,8 @@ def register_demo_development_realm(request: HttpRequest) -> HttpResponse: realm_subdomain=realm_subdomain, terms="true", is_demo_organization="true", + how_realm_creator_found_zulip="existing_user", + how_realm_creator_found_zulip_extra_context="test", ) return accounts_register(request) diff --git a/zerver/views/registration.py b/zerver/views/registration.py index 06ce6fee45..436fb82b7d 100644 --- a/zerver/views/registration.py +++ b/zerver/views/registration.py @@ -82,6 +82,7 @@ from zerver.models import ( UserProfile, ) from zerver.models.constants import MAX_LANGUAGE_ID_LENGTH +from zerver.models.realm_audit_logs import RealmAuditLog from zerver.models.realms import ( DisposableEmailError, DomainNotAllowedForRealmError, @@ -464,6 +465,32 @@ def registration_helper( realm_type = form.cleaned_data["realm_type"] realm_default_language = form.cleaned_data["realm_default_language"] is_demo_organization = form.cleaned_data["is_demo_organization"] + how_realm_creator_found_zulip = RealmAuditLog.HOW_REALM_CREATOR_FOUND_ZULIP_OPTIONS[ + form.cleaned_data["how_realm_creator_found_zulip"] + ] + how_realm_creator_found_zulip_extra_context = "" + if ( + how_realm_creator_found_zulip + == RealmAuditLog.HOW_REALM_CREATOR_FOUND_ZULIP_OPTIONS["other"] + ): + how_realm_creator_found_zulip_extra_context = form.cleaned_data[ + "how_realm_creator_found_zulip_other_text" + ] + elif ( + how_realm_creator_found_zulip + == RealmAuditLog.HOW_REALM_CREATOR_FOUND_ZULIP_OPTIONS["ad"] + ): + how_realm_creator_found_zulip_extra_context = form.cleaned_data[ + "how_realm_creator_found_zulip_where_ad" + ] + elif ( + how_realm_creator_found_zulip + == RealmAuditLog.HOW_REALM_CREATOR_FOUND_ZULIP_OPTIONS["existing_user"] + ): + how_realm_creator_found_zulip_extra_context = form.cleaned_data[ + "how_realm_creator_found_zulip_which_organization" + ] + realm = do_create_realm( string_id, realm_name, @@ -471,6 +498,8 @@ def registration_helper( default_language=realm_default_language, is_demo_organization=is_demo_organization, prereg_realm=prereg_realm, + how_realm_creator_found_zulip=how_realm_creator_found_zulip, + how_realm_creator_found_zulip_extra_context=how_realm_creator_found_zulip_extra_context, ) assert realm is not None @@ -686,6 +715,7 @@ def registration_helper( "email_address_visibility_moderators": RealmUserDefault.EMAIL_ADDRESS_VISIBILITY_MODERATORS, "email_address_visibility_nobody": RealmUserDefault.EMAIL_ADDRESS_VISIBILITY_NOBODY, "email_address_visibility_options_dict": UserProfile.EMAIL_ADDRESS_VISIBILITY_ID_TO_NAME_MAP, + "how_realm_creator_found_zulip_options": RealmAuditLog.HOW_REALM_CREATOR_FOUND_ZULIP_OPTIONS.items(), } # Add context for realm creation part of the form. context.update(get_realm_create_form_context())