registration: Ask user how they found Zulip.

This commit is contained in:
Aman Agrawal 2024-03-26 05:14:16 +00:00 committed by Tim Abbott
parent 293992fe60
commit d21f5c9b75
13 changed files with 158 additions and 10 deletions

View File

@ -30,6 +30,7 @@ from corporate.views.support import get_plan_type_string
from zerver.decorator import require_server_admin from zerver.decorator import require_server_admin
from zerver.lib.request import has_request_variables from zerver.lib.request import has_request_variables
from zerver.models import Realm from zerver.models import Realm
from zerver.models.realm_audit_logs import RealmAuditLog
from zerver.models.realms import get_org_type_display_name 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(wau_table.value, 0) wau_count,
coalesce(dau_table.value, 0) dau_count, coalesce(dau_table.value, 0) dau_count,
coalesce(user_count_table.value, 0) user_profile_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 FROM
zerver_realm as realm zerver_realm as realm
LEFT OUTER JOIN ( LEFT OUTER JOIN (
@ -157,6 +159,15 @@ def realm_summary_table() -> str:
AND subgroup = 'true' AND subgroup = 'true'
AND end_time = %(active_users_audit_end_time)s AND end_time = %(active_users_audit_end_time)s
) as bot_count_table ON realm.id = bot_count_table.realm_id ) 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 WHERE
_14day_active_humans IS NOT NULL _14day_active_humans IS NOT NULL
or realm.plan_type = 3 or realm.plan_type = 3
@ -178,6 +189,7 @@ def realm_summary_table() -> str:
"active_users_audit_end_time": COUNT_STATS[ "active_users_audit_end_time": COUNT_STATS[
"active_users_audit:is_bot:day" "active_users_audit:is_bot:day"
].last_successful_fill(), ].last_successful_fill(),
"realm_creation_event_type": RealmAuditLog.REALM_CREATED,
}, },
) )
rows = dictfetchall(cursor) rows = dictfetchall(cursor)

View File

@ -48,6 +48,7 @@
<th>Bots</th> <th>Bots</th>
<th></th> <th></th>
<th colspan=8>Human messages sent, last 8 UTC days (today-so-far first)</th> <th colspan=8>Human messages sent, last 8 UTC days (today-so-far first)</th>
<th>Referrer</th>
</tr> </tr>
</thead> </thead>
@ -118,6 +119,10 @@
{% else %} {% else %}
<td colspan=8></td> <td colspan=8></td>
{% endif %} {% endif %}
<td>
{{ row.how_realm_creator_found_zulip }}
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@ -196,6 +196,26 @@ Form is validated both client-side using jquery-validation (see signup.js) and s
<hr /> <hr />
{% endif %} {% endif %}
{% if creating_new_realm %}
<div class="input-group input-box" id="how-realm-creator-found-zulip">
<label for="how_realm_creator_found_zulip">
{{ _('How did you first hear about Zulip?') }}
{% if not corporate_enabled %}
<i class="fa fa-question-circle-o" aria-hidden="true" data-tippy-content="{% trans %}This value is used only if you sign up for a plan, in which case it will be sent to the Zulip team.{% endtrans %}"></i>
{% endif %}
</label>
<select name="how_realm_creator_found_zulip" class="required">
<option value="" selected disabled>{{ _('Select an option') }}</option>
{% for option_id, option_name in how_realm_creator_found_zulip_options %}
<option value="{{ option_id }}">{{ option_name }}</option>
{% endfor %}
</select>
<input id="how-realm-creator-found-zulip-other" type="text" placeholder="{{ _('Please describe') }}" name="how_realm_creator_found_zulip_other_text" maxlength="100"/>
<input id="how-realm-creator-found-zulip-where-ad" type="text" placeholder="{{ _('Where did you see the ad?') }}" name="how_realm_creator_found_zulip_where_ad" maxlength="100"/>
<input id="how-realm-creator-found-zulip-which-organization" type="text" placeholder="{{ _('Which organization?') }}" name="how_realm_creator_found_zulip_which_organization" maxlength="100"/>
</div>
{% endif %}
<div class="input-group margin terms-of-service"> <div class="input-group margin terms-of-service">
{% if terms_of_service %} {% if terms_of_service %}
<div class="input-group"> <div class="input-group">

View File

@ -59,6 +59,8 @@ async function realm_creation_tests(page: Page): Promise<void> {
full_name: "Alice", full_name: "Alice",
password: "passwordwhichisnotreallycomplex", password: "passwordwhichisnotreallycomplex",
terms: true, 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 // 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. // so use page.$eval here to call the .click method in the browser.

View File

@ -302,4 +302,40 @@ $(() => {
$(e.target).hide(); $(e.target).hide();
}); });
$("#how-realm-creator-found-zulip select").on("change", function () {
const elements: Record<string, string> = {
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);
}
});
}); });

View File

@ -1458,3 +1458,14 @@ button#register_auth_button_gitlab {
max-width: 800px; 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;
}

View File

@ -115,11 +115,7 @@
], ],
"desktop-login": ["./src/bundles/portico", "./src/portico/desktop-login"], "desktop-login": ["./src/bundles/portico", "./src/portico/desktop-login"],
"desktop-redirect": ["./src/bundles/portico", "./src/portico/desktop-redirect"], "desktop-redirect": ["./src/bundles/portico", "./src/portico/desktop-redirect"],
"stats": [ "stats": ["./src/bundles/portico", "./styles/portico/stats.css", "./src/stats/stats"],
"./src/bundles/portico",
"./styles/portico/stats.css",
"./src/stats/stats"
],
"app": ["./src/bundles/app"], "app": ["./src/bundles/app"],
"digest": ["./src/bundles/portico"] "digest": ["./src/bundles/portico"]
} }

View File

@ -167,6 +167,8 @@ def do_create_realm(
enable_read_receipts: Optional[bool] = None, enable_read_receipts: Optional[bool] = None,
enable_spectator_access: Optional[bool] = None, enable_spectator_access: Optional[bool] = None,
prereg_realm: Optional[PreregistrationRealm] = 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: ) -> Realm:
if string_id in [settings.SOCIAL_AUTH_SUBDOMAIN, settings.SELF_HOSTING_MANAGEMENT_SUBDOMAIN]: if string_id in [settings.SOCIAL_AUTH_SUBDOMAIN, settings.SELF_HOSTING_MANAGEMENT_SUBDOMAIN]:
raise AssertionError( raise AssertionError(
@ -243,6 +245,10 @@ def do_create_realm(
realm=realm, realm=realm,
event_type=RealmAuditLog.REALM_CREATED, event_type=RealmAuditLog.REALM_CREATED,
event_time=realm.date_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 realm_default_email_address_visibility = RealmUserDefault.EMAIL_ADDRESS_VISIBILITY_EVERYONE

View File

@ -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.subdomains import get_subdomain, is_root_domain_available
from zerver.lib.users import check_full_name from zerver.lib.users import check_full_name
from zerver.models import Realm, UserProfile from zerver.models import Realm, UserProfile
from zerver.models.realm_audit_logs import RealmAuditLog
from zerver.models.realms import ( from zerver.models.realms import (
DisposableEmailError, DisposableEmailError,
DomainNotAllowedForRealmError, DomainNotAllowedForRealmError,
@ -140,6 +141,8 @@ class RealmDetailsForm(forms.Form):
realm_name = forms.CharField(max_length=Realm.MAX_REALM_NAME_LENGTH) realm_name = forms.CharField(max_length=Realm.MAX_REALM_NAME_LENGTH)
def __init__(self, *args: Any, **kwargs: Any) -> None: 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"] self.realm_creation = kwargs["realm_creation"]
del kwargs["realm_creation"] del kwargs["realm_creation"]
@ -177,10 +180,6 @@ class RegistrationForm(RealmDetailsForm):
) )
def __init__(self, *args: Any, **kwargs: Any) -> None: 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) super().__init__(*args, **kwargs)
if settings.TERMS_OF_SERVICE_VERSION is not None: if settings.TERMS_OF_SERVICE_VERSION is not None:
self.fields["terms"] = forms.BooleanField(required=True) 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()], choices=[(lang["code"], lang["name"]) for lang in get_language_list()],
required=self.realm_creation, 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: def clean_full_name(self) -> str:
try: try:

View File

@ -842,6 +842,8 @@ Output:
"default_stream_group": default_stream_groups, "default_stream_group": default_stream_groups,
"source_realm_id": source_realm_id, "source_realm_id": source_realm_id,
"is_demo_organization": is_demo_organization, "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: if enable_marketing_emails is not None:
payload["enable_marketing_emails"] = enable_marketing_emails payload["enable_marketing_emails"] = enable_marketing_emails

View File

@ -162,6 +162,18 @@ class AbstractRealmAuditLog(models.Model):
REALM_IMPORTED, 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: class Meta:
abstract = True abstract = True

View File

@ -85,6 +85,8 @@ def register_development_realm(request: HttpRequest) -> HttpResponse:
password="test", password="test",
realm_subdomain=realm_subdomain, realm_subdomain=realm_subdomain,
terms="true", terms="true",
how_realm_creator_found_zulip="ad",
how_realm_creator_found_zulip_extra_context="test",
) )
return accounts_register(request) return accounts_register(request)
@ -120,6 +122,8 @@ def register_demo_development_realm(request: HttpRequest) -> HttpResponse:
realm_subdomain=realm_subdomain, realm_subdomain=realm_subdomain,
terms="true", terms="true",
is_demo_organization="true", is_demo_organization="true",
how_realm_creator_found_zulip="existing_user",
how_realm_creator_found_zulip_extra_context="test",
) )
return accounts_register(request) return accounts_register(request)

View File

@ -82,6 +82,7 @@ from zerver.models import (
UserProfile, UserProfile,
) )
from zerver.models.constants import MAX_LANGUAGE_ID_LENGTH from zerver.models.constants import MAX_LANGUAGE_ID_LENGTH
from zerver.models.realm_audit_logs import RealmAuditLog
from zerver.models.realms import ( from zerver.models.realms import (
DisposableEmailError, DisposableEmailError,
DomainNotAllowedForRealmError, DomainNotAllowedForRealmError,
@ -464,6 +465,32 @@ def registration_helper(
realm_type = form.cleaned_data["realm_type"] realm_type = form.cleaned_data["realm_type"]
realm_default_language = form.cleaned_data["realm_default_language"] realm_default_language = form.cleaned_data["realm_default_language"]
is_demo_organization = form.cleaned_data["is_demo_organization"] 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( realm = do_create_realm(
string_id, string_id,
realm_name, realm_name,
@ -471,6 +498,8 @@ def registration_helper(
default_language=realm_default_language, default_language=realm_default_language,
is_demo_organization=is_demo_organization, is_demo_organization=is_demo_organization,
prereg_realm=prereg_realm, 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 assert realm is not None
@ -686,6 +715,7 @@ def registration_helper(
"email_address_visibility_moderators": RealmUserDefault.EMAIL_ADDRESS_VISIBILITY_MODERATORS, "email_address_visibility_moderators": RealmUserDefault.EMAIL_ADDRESS_VISIBILITY_MODERATORS,
"email_address_visibility_nobody": RealmUserDefault.EMAIL_ADDRESS_VISIBILITY_NOBODY, "email_address_visibility_nobody": RealmUserDefault.EMAIL_ADDRESS_VISIBILITY_NOBODY,
"email_address_visibility_options_dict": UserProfile.EMAIL_ADDRESS_VISIBILITY_ID_TO_NAME_MAP, "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. # Add context for realm creation part of the form.
context.update(get_realm_create_form_context()) context.update(get_realm_create_form_context())