mirror of
https://github.com/zulip/zulip.git
synced 2025-11-14 02:48:00 +00:00
Add customizable invite-new-user text.
This makes life a lot easier for people inviting users to a new Zulip organization, since they can give some form of context now. Modified by tabbott to clean up CSS, backend code flow, and improve the formatting of the emails. Fixes: #1409.
This commit is contained in:
@@ -94,8 +94,8 @@ class ConfirmationManager(models.Manager):
|
|||||||
|
|
||||||
def send_confirmation(self, obj, email_address, additional_context=None,
|
def send_confirmation(self, obj, email_address, additional_context=None,
|
||||||
subject_template_path=None, body_template_path=None, html_body_template_path=None,
|
subject_template_path=None, body_template_path=None, html_body_template_path=None,
|
||||||
host=None):
|
host=None, custom_body=None):
|
||||||
# type: (ContentType, Text, Optional[Dict[str, Any]], Optional[str], Optional[str], Optional[str], Optional[str]) -> Confirmation
|
# type: (ContentType, Text, Optional[Dict[str, Any]], Optional[str], Optional[str], Optional[str], Optional[str], Optional[str]) -> Confirmation
|
||||||
confirmation_key = generate_key()
|
confirmation_key = generate_key()
|
||||||
current_site = Site.objects.get_current()
|
current_site = Site.objects.get_current()
|
||||||
activate_url = self.get_activation_url(confirmation_key, host=host)
|
activate_url = self.get_activation_url(confirmation_key, host=host)
|
||||||
@@ -105,6 +105,7 @@ class ConfirmationManager(models.Manager):
|
|||||||
'confirmation_key': confirmation_key,
|
'confirmation_key': confirmation_key,
|
||||||
'target': obj,
|
'target': obj,
|
||||||
'days': getattr(settings, 'EMAIL_CONFIRMATION_DAYS', 10),
|
'days': getattr(settings, 'EMAIL_CONFIRMATION_DAYS', 10),
|
||||||
|
'custom_body': custom_body,
|
||||||
})
|
})
|
||||||
if additional_context is not None:
|
if additional_context is not None:
|
||||||
context.update(additional_context)
|
context.update(additional_context)
|
||||||
|
|||||||
@@ -2124,6 +2124,10 @@ button.topic_edit_cancel {
|
|||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#invite-user .custom_invite_body {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
#invite_status {
|
#invite_status {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
<p>
|
<p>
|
||||||
{{ referrer.full_name }} ({{ referrer.email }}) wants you to join them on Zulip -- the group communication tool you've always wished you had at work.
|
{{ referrer.full_name }} ({{ referrer.email }}) wants you to join them on Zulip -- the group communication tool you've always wished you had at work.
|
||||||
</p>
|
</p>
|
||||||
|
{% if custom_body %}
|
||||||
|
<p>Message from {{ referrer.full_name }}: {{ custom_body }}</p>
|
||||||
|
{% endif %}
|
||||||
<p>
|
<p>
|
||||||
To get started, visit the link below:
|
To get started, visit the link below:
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
Hi there,
|
Hi there,
|
||||||
|
|
||||||
{{ referrer.full_name }} ({{ referrer.email }}) wants you to join them on Zulip -- the group communication tool you've always wished you had at work.
|
{{ referrer.full_name }} ({{ referrer.email }}) wants you to join them on Zulip -- the group communication tool you've always wished you had at work.
|
||||||
|
{% if custom_body %}
|
||||||
|
Message from {{ referrer.full_name }}: {{ custom_body }}
|
||||||
|
{% endif %}
|
||||||
To get started, visit the link below:
|
To get started, visit the link below:
|
||||||
<{{ activate_url }}>
|
<{{ activate_url }}>
|
||||||
|
|
||||||
{% if verbose_support_offers %}
|
{% if verbose_support_offers %}
|
||||||
Feel free to give us a shout at <{{ support_email }}> if you have any questions.
|
Feel free to give us a shout at <{{ support_email }}> if you have any questions.
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ Hi there,
|
|||||||
|
|
||||||
{{ referrer.full_name }} ({{ referrer.email }}) wants you to join them on Zulip, an awesome web-based Zephyr client with desktop apps for Mac, Linux, and Windows, as well as native mobile apps.
|
{{ referrer.full_name }} ({{ referrer.email }}) wants you to join them on Zulip, an awesome web-based Zephyr client with desktop apps for Mac, Linux, and Windows, as well as native mobile apps.
|
||||||
|
|
||||||
|
{% if custom_body %}Message from {{ referrer.full_name }}: {{ custom_body }}
|
||||||
|
{% endif %}
|
||||||
To get started, visit the link below:
|
To get started, visit the link below:
|
||||||
<{{ activate_url }}>
|
<{{ activate_url }}>
|
||||||
|
|
||||||
{% if verbose_support_offers %}
|
{% if verbose_support_offers %}
|
||||||
Feel free to give us a shout at <{{ support_email }}> if you have any questions.
|
Feel free to give us a shout at <{{ support_email }}> if you have any questions.
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -15,6 +15,12 @@
|
|||||||
name="invitee_emails"
|
name="invitee_emails"
|
||||||
placeholder="{{ _('One or more email addresses...') }}"></textarea>
|
placeholder="{{ _('One or more email addresses...') }}"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<label class="control-label" for="custom_body">{{ _('Custom invitation message (if you want to add one)') }}</label>
|
||||||
|
<div class="controls">
|
||||||
|
<textarea rows="2" class="custom_invite_body"
|
||||||
|
name="custom_body"
|
||||||
|
placeholder="{{ _('Custom message') }}"></textarea>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert" id="invite_status"></div>
|
<div class="alert" id="invite_status"></div>
|
||||||
{% if development_environment %}
|
{% if development_environment %}
|
||||||
|
|||||||
@@ -2987,8 +2987,8 @@ def get_cross_realm_dicts():
|
|||||||
'full_name': user.full_name}
|
'full_name': user.full_name}
|
||||||
for user in users]
|
for user in users]
|
||||||
|
|
||||||
def do_send_confirmation_email(invitee, referrer):
|
def do_send_confirmation_email(invitee, referrer, body):
|
||||||
# type: (PreregistrationUser, UserProfile) -> None
|
# type: (PreregistrationUser, UserProfile, Optional[str]) -> None
|
||||||
"""
|
"""
|
||||||
Send the confirmation/welcome e-mail to an invited user.
|
Send the confirmation/welcome e-mail to an invited user.
|
||||||
|
|
||||||
@@ -3013,7 +3013,7 @@ def do_send_confirmation_email(invitee, referrer):
|
|||||||
subject_template_path=subject_template_path,
|
subject_template_path=subject_template_path,
|
||||||
body_template_path=body_template_path,
|
body_template_path=body_template_path,
|
||||||
html_body_template_path=html_body_template_path,
|
html_body_template_path=html_body_template_path,
|
||||||
host=referrer.realm.host)
|
host=referrer.realm.host, custom_body=body)
|
||||||
|
|
||||||
@statsd_increment("push_notifications")
|
@statsd_increment("push_notifications")
|
||||||
def handle_push_notification(user_profile_id, missed_message):
|
def handle_push_notification(user_profile_id, missed_message):
|
||||||
@@ -3123,8 +3123,8 @@ def validate_email(user_profile, email):
|
|||||||
|
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
def do_invite_users(user_profile, invitee_emails, streams):
|
def do_invite_users(user_profile, invitee_emails, streams, body=None):
|
||||||
# type: (UserProfile, SizedTextIterable, Iterable[Stream]) -> Tuple[Optional[str], Dict[str, Union[List[Tuple[Text, str]], bool]]]
|
# type: (UserProfile, SizedTextIterable, Iterable[Stream], Optional[str]) -> Tuple[Optional[str], Dict[str, Union[List[Tuple[Text, str]], bool]]]
|
||||||
validated_emails = [] # type: List[Text]
|
validated_emails = [] # type: List[Text]
|
||||||
errors = [] # type: List[Tuple[Text, str]]
|
errors = [] # type: List[Tuple[Text, str]]
|
||||||
skipped = [] # type: List[Tuple[Text, str]]
|
skipped = [] # type: List[Tuple[Text, str]]
|
||||||
@@ -3168,9 +3168,9 @@ def do_invite_users(user_profile, invitee_emails, streams):
|
|||||||
prereg_user.streams = streams
|
prereg_user.streams = streams
|
||||||
prereg_user.save()
|
prereg_user.save()
|
||||||
|
|
||||||
event = {"email": prereg_user.email, "referrer_email": user_profile.email}
|
event = {"email": prereg_user.email, "referrer_email": user_profile.email, "email_body": body}
|
||||||
queue_json_publish("invites", event,
|
queue_json_publish("invites", event,
|
||||||
lambda event: do_send_confirmation_email(prereg_user, user_profile))
|
lambda event: do_send_confirmation_email(prereg_user, user_profile, body))
|
||||||
|
|
||||||
if skipped:
|
if skipped:
|
||||||
ret_error = _("Some of those addresses are already using Zulip, "
|
ret_error = _("Some of those addresses are already using Zulip, "
|
||||||
|
|||||||
@@ -201,6 +201,14 @@ def find_key_by_email(address):
|
|||||||
if address in message.to:
|
if address in message.to:
|
||||||
return key_regex.search(message.body).groups()[0]
|
return key_regex.search(message.body).groups()[0]
|
||||||
|
|
||||||
|
def find_pattern_in_email(address, pattern):
|
||||||
|
# type: (Text, Text) -> Text
|
||||||
|
from django.core.mail import outbox
|
||||||
|
key_regex = re.compile(pattern)
|
||||||
|
for message in reversed(outbox):
|
||||||
|
if address in message.to:
|
||||||
|
return key_regex.search(message.body).group(0)
|
||||||
|
|
||||||
def message_ids(result):
|
def message_ids(result):
|
||||||
# type: (Dict[str, Any]) -> Set[int]
|
# type: (Dict[str, Any]) -> Set[int]
|
||||||
return set(message['id'] for message in result['messages'])
|
return set(message['id'] for message in result['messages'])
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ from zerver.lib.actions import do_deactivate_realm, do_set_realm_default_languag
|
|||||||
from zerver.lib.digest import send_digest_email
|
from zerver.lib.digest import send_digest_email
|
||||||
from zerver.lib.notifications import (
|
from zerver.lib.notifications import (
|
||||||
enqueue_welcome_emails, one_click_unsubscribe_link, send_local_email_template_with_delay)
|
enqueue_welcome_emails, one_click_unsubscribe_link, send_local_email_template_with_delay)
|
||||||
from zerver.lib.test_helpers import find_key_by_email, queries_captured, \
|
from zerver.lib.test_helpers import find_pattern_in_email, find_key_by_email, queries_captured, \
|
||||||
HostRequestMock
|
HostRequestMock
|
||||||
from zerver.lib.test_classes import (
|
from zerver.lib.test_classes import (
|
||||||
ZulipTestCase,
|
ZulipTestCase,
|
||||||
@@ -46,6 +46,8 @@ from zerver.context_processors import common_context
|
|||||||
import re
|
import re
|
||||||
import ujson
|
import ujson
|
||||||
|
|
||||||
|
from typing import Set, Optional
|
||||||
|
|
||||||
from six.moves import urllib
|
from six.moves import urllib
|
||||||
from six.moves import range
|
from six.moves import range
|
||||||
import six
|
import six
|
||||||
@@ -333,8 +335,8 @@ class LoginTest(ZulipTestCase):
|
|||||||
|
|
||||||
class InviteUserTest(ZulipTestCase):
|
class InviteUserTest(ZulipTestCase):
|
||||||
|
|
||||||
def invite(self, users, streams):
|
def invite(self, users, streams, body=''):
|
||||||
# type: (str, List[Text]) -> HttpResponse
|
# type: (str, List[Text], str) -> HttpResponse
|
||||||
"""
|
"""
|
||||||
Invites the specified users to Zulip with the specified streams.
|
Invites the specified users to Zulip with the specified streams.
|
||||||
|
|
||||||
@@ -346,14 +348,23 @@ class InviteUserTest(ZulipTestCase):
|
|||||||
|
|
||||||
return self.client_post("/json/invite_users",
|
return self.client_post("/json/invite_users",
|
||||||
{"invitee_emails": users,
|
{"invitee_emails": users,
|
||||||
"stream": streams})
|
"stream": streams,
|
||||||
|
"custom_body": body})
|
||||||
|
|
||||||
def check_sent_emails(self, correct_recipients):
|
def check_sent_emails(self, correct_recipients, custom_body=None):
|
||||||
# type: (List[str]) -> None
|
# type: (List[str], Optional[str]) -> None
|
||||||
from django.core.mail import outbox
|
from django.core.mail import outbox
|
||||||
self.assertEqual(len(outbox), len(correct_recipients))
|
self.assertEqual(len(outbox), len(correct_recipients))
|
||||||
email_recipients = [email.recipients()[0] for email in outbox]
|
email_recipients = [email.recipients()[0] for email in outbox]
|
||||||
self.assertEqual(sorted(email_recipients), sorted(correct_recipients))
|
self.assertEqual(sorted(email_recipients), sorted(correct_recipients))
|
||||||
|
if len(outbox) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
if custom_body is None:
|
||||||
|
self.assertNotIn("Message from", outbox[0].body)
|
||||||
|
else:
|
||||||
|
self.assertIn("Message from ", outbox[0].body)
|
||||||
|
self.assertIn(custom_body, outbox[0].body)
|
||||||
|
|
||||||
def test_bulk_invite_users(self):
|
def test_bulk_invite_users(self):
|
||||||
# type: () -> None
|
# type: () -> None
|
||||||
@@ -361,7 +372,7 @@ class InviteUserTest(ZulipTestCase):
|
|||||||
self.login('hamlet@zulip.com')
|
self.login('hamlet@zulip.com')
|
||||||
invitees = ['alice@zulip.com', 'bob@zulip.com']
|
invitees = ['alice@zulip.com', 'bob@zulip.com']
|
||||||
params = {
|
params = {
|
||||||
'invitee_emails': ujson.dumps(invitees)
|
'invitee_emails': ujson.dumps(invitees),
|
||||||
}
|
}
|
||||||
result = self.client_post('/json/bulk_invite_users', params)
|
result = self.client_post('/json/bulk_invite_users', params)
|
||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
@@ -379,6 +390,19 @@ class InviteUserTest(ZulipTestCase):
|
|||||||
self.assertTrue(find_key_by_email(invitee))
|
self.assertTrue(find_key_by_email(invitee))
|
||||||
self.check_sent_emails([invitee])
|
self.check_sent_emails([invitee])
|
||||||
|
|
||||||
|
def test_successful_invite_user_with_custom_body(self):
|
||||||
|
# type: () -> None
|
||||||
|
"""
|
||||||
|
A call to /json/invite_users with valid parameters causes an invitation
|
||||||
|
email to be sent.
|
||||||
|
"""
|
||||||
|
self.login("hamlet@zulip.com")
|
||||||
|
invitee = "alice-test@zulip.com"
|
||||||
|
body = "Custom Text."
|
||||||
|
self.assert_json_success(self.invite(invitee, ["Denmark"], body))
|
||||||
|
self.assertTrue(find_pattern_in_email(invitee, body))
|
||||||
|
self.check_sent_emails([invitee], custom_body=body)
|
||||||
|
|
||||||
def test_successful_invite_user_with_name(self):
|
def test_successful_invite_user_with_name(self):
|
||||||
# type: () -> None
|
# type: () -> None
|
||||||
"""
|
"""
|
||||||
@@ -458,7 +482,8 @@ earl-test@zulip.com""", ["Denmark"]))
|
|||||||
"""
|
"""
|
||||||
self.login("hamlet@zulip.com")
|
self.login("hamlet@zulip.com")
|
||||||
self.assert_json_error(
|
self.assert_json_error(
|
||||||
self.client_post("/json/invite_users", {"invitee_emails": "foo@zulip.com"}),
|
self.client_post("/json/invite_users", {"invitee_emails": "foo@zulip.com",
|
||||||
|
"custom_body": ''}),
|
||||||
"You must specify at least one stream for invitees to join.")
|
"You must specify at least one stream for invitees to join.")
|
||||||
|
|
||||||
for address in ("noatsign.com", "outsideyourdomain@example.net"):
|
for address in ("noatsign.com", "outsideyourdomain@example.net"):
|
||||||
@@ -486,7 +511,8 @@ earl-test@zulip.com""", ["Denmark"]))
|
|||||||
self.assert_json_error(
|
self.assert_json_error(
|
||||||
self.client_post("/json/invite_users",
|
self.client_post("/json/invite_users",
|
||||||
{"invitee_emails": "hamlet@zulip.com",
|
{"invitee_emails": "hamlet@zulip.com",
|
||||||
"stream": ["Denmark"]}),
|
"stream": ["Denmark"],
|
||||||
|
"custom_body": ''}),
|
||||||
"We weren't able to invite anyone.")
|
"We weren't able to invite anyone.")
|
||||||
self.assertRaises(PreregistrationUser.DoesNotExist,
|
self.assertRaises(PreregistrationUser.DoesNotExist,
|
||||||
lambda: PreregistrationUser.objects.get(
|
lambda: PreregistrationUser.objects.get(
|
||||||
@@ -505,7 +531,8 @@ earl-test@zulip.com""", ["Denmark"]))
|
|||||||
|
|
||||||
result = self.client_post("/json/invite_users",
|
result = self.client_post("/json/invite_users",
|
||||||
{"invitee_emails": "\n".join(existing + new),
|
{"invitee_emails": "\n".join(existing + new),
|
||||||
"stream": ["Denmark"]})
|
"stream": ["Denmark"],
|
||||||
|
"custom_body": ''})
|
||||||
self.assert_json_error(result,
|
self.assert_json_error(result,
|
||||||
"Some of those addresses are already using Zulip, \
|
"Some of those addresses are already using Zulip, \
|
||||||
so we didn't send them an invitation. We did send invitations to everyone else!")
|
so we didn't send them an invitation. We did send invitations to everyone else!")
|
||||||
|
|||||||
@@ -19,10 +19,14 @@ import re
|
|||||||
|
|
||||||
@authenticated_json_post_view
|
@authenticated_json_post_view
|
||||||
@has_request_variables
|
@has_request_variables
|
||||||
def json_invite_users(request, user_profile, invitee_emails_raw=REQ("invitee_emails")):
|
def json_invite_users(request, user_profile,
|
||||||
# type: (HttpRequest, UserProfile, str) -> HttpResponse
|
invitee_emails_raw=REQ("invitee_emails"),
|
||||||
|
body=REQ("custom_body", default=None)):
|
||||||
|
# type: (HttpRequest, UserProfile, str, Optional[str]) -> HttpResponse
|
||||||
if not invitee_emails_raw:
|
if not invitee_emails_raw:
|
||||||
return json_error(_("You must specify at least one email address."))
|
return json_error(_("You must specify at least one email address."))
|
||||||
|
if body == '':
|
||||||
|
body = None
|
||||||
|
|
||||||
invitee_emails = get_invitee_emails_set(invitee_emails_raw)
|
invitee_emails = get_invitee_emails_set(invitee_emails_raw)
|
||||||
|
|
||||||
@@ -44,7 +48,7 @@ def json_invite_users(request, user_profile, invitee_emails_raw=REQ("invitee_ema
|
|||||||
return json_error(_("Stream does not exist: %s. No invites were sent.") % (stream_name,))
|
return json_error(_("Stream does not exist: %s. No invites were sent.") % (stream_name,))
|
||||||
streams.append(stream)
|
streams.append(stream)
|
||||||
|
|
||||||
ret_error, error_data = do_invite_users(user_profile, invitee_emails, streams)
|
ret_error, error_data = do_invite_users(user_profile, invitee_emails, streams, body)
|
||||||
|
|
||||||
if ret_error is not None:
|
if ret_error is not None:
|
||||||
return json_error(data=error_data, msg=ret_error)
|
return json_error(data=error_data, msg=ret_error)
|
||||||
|
|||||||
@@ -148,7 +148,8 @@ class ConfirmationEmailWorker(QueueProcessingWorker):
|
|||||||
# type: (Mapping[str, Any]) -> None
|
# type: (Mapping[str, Any]) -> None
|
||||||
invitee = get_prereg_user_by_email(data["email"])
|
invitee = get_prereg_user_by_email(data["email"])
|
||||||
referrer = get_user_profile_by_email(data["referrer_email"])
|
referrer = get_user_profile_by_email(data["referrer_email"])
|
||||||
do_send_confirmation_email(invitee, referrer)
|
body = data["email_body"]
|
||||||
|
do_send_confirmation_email(invitee, referrer, body)
|
||||||
|
|
||||||
# queue invitation reminder for two days from now.
|
# queue invitation reminder for two days from now.
|
||||||
link = Confirmation.objects.get_link_for_object(invitee, host=referrer.realm.host)
|
link = Confirmation.objects.get_link_for_object(invitee, host=referrer.realm.host)
|
||||||
|
|||||||
Reference in New Issue
Block a user