backend: Add support for multiuse user invite link.

This commit is contained in:
Vishnu Ks
2017-08-10 20:34:17 +00:00
committed by Tim Abbott
parent 68ccfe78e6
commit b4fedaa765
6 changed files with 161 additions and 15 deletions

View File

@@ -98,6 +98,7 @@ class Confirmation(models.Model):
EMAIL_CHANGE = 3 EMAIL_CHANGE = 3
UNSUBSCRIBE = 4 UNSUBSCRIBE = 4
SERVER_REGISTRATION = 5 SERVER_REGISTRATION = 5
MULTIUSE_INVITE = 6
type = models.PositiveSmallIntegerField() # type: int type = models.PositiveSmallIntegerField() # type: int
def __unicode__(self): def __unicode__(self):
@@ -117,6 +118,8 @@ _properties = {
Confirmation.EMAIL_CHANGE: ConfirmationType('zerver.views.user_settings.confirm_email_change'), Confirmation.EMAIL_CHANGE: ConfirmationType('zerver.views.user_settings.confirm_email_change'),
Confirmation.UNSUBSCRIBE: ConfirmationType('zerver.views.unsubscribe.email_unsubscribe', Confirmation.UNSUBSCRIBE: ConfirmationType('zerver.views.unsubscribe.email_unsubscribe',
validity_in_days=1000000), # should never expire validity_in_days=1000000), # should never expire
Confirmation.MULTIUSE_INVITE: ConfirmationType('zerver.views.registration.accounts_home_from_multiuse_invite',
validity_in_days=settings.INVITATION_LINK_VALIDITY_DAYS)
} }
# Conirmation pathways for which there is no content_object that we need to # Conirmation pathways for which there is no content_object that we need to

View File

@@ -15,7 +15,7 @@ $(function () {
<div class="lead"> <div class="lead">
<h1 class="get-started">{{ _("Sign up for Zulip") }}</h1> <h1 class="get-started">{{ _("Sign up for Zulip") }}</h1>
</div> </div>
<div class="app-main register-page-container white-box {% if realm_invite_required %}closed-realm{% endif %}"> <div class="app-main register-page-container white-box {% if realm_invite_required and not from_multiuse_invite %}closed-realm{% endif %}">
<div class="register-form new-style"> <div class="register-form new-style">
{% if realm_name %} {% if realm_name %}
<div class="left-side"> <div class="left-side">

View File

@@ -118,6 +118,7 @@ class HomepageForm(forms.Form):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# type: (*Any, **Any) -> None # type: (*Any, **Any) -> None
self.realm = kwargs.pop('realm', None) self.realm = kwargs.pop('realm', None)
self.from_multiuse_invite = kwargs.pop('from_multiuse_invite', False)
super(HomepageForm, self).__init__(*args, **kwargs) super(HomepageForm, self).__init__(*args, **kwargs)
def clean_email(self): def clean_email(self):
@@ -131,6 +132,7 @@ class HomepageForm(forms.Form):
# Otherwise, the user is trying to join a specific realm. # Otherwise, the user is trying to join a specific realm.
realm = self.realm realm = self.realm
from_multiuse_invite = self.from_multiuse_invite
if realm is None and not settings.REALMS_HAVE_SUBDOMAINS: if realm is None and not settings.REALMS_HAVE_SUBDOMAINS:
realm = get_realm_by_email_domain(email) realm = get_realm_by_email_domain(email)
@@ -144,7 +146,7 @@ class HomepageForm(forms.Form):
"correspond to any existing " "correspond to any existing "
"organization.").format(email=email)) "organization.").format(email=email))
if realm.invite_required: if not from_multiuse_invite and realm.invite_required:
raise ValidationError(_("Please request an invite for {email} " raise ValidationError(_("Please request an invite for {email} "
"from the organization " "from the organization "
"administrator.").format(email=email)) "administrator.").format(email=email))

View File

@@ -11,12 +11,12 @@ from django.utils.timezone import now as timezone_now
from mock import patch, MagicMock from mock import patch, MagicMock
from zerver.lib.test_helpers import MockLDAP from zerver.lib.test_helpers import MockLDAP
from confirmation.models import Confirmation, create_confirmation_link from confirmation.models import Confirmation, create_confirmation_link, MultiuseInvite, \
generate_key, confirmation_url
from zilencer.models import Deployment from zilencer.models import Deployment
from zerver.forms import HomepageForm, WRONG_SUBDOMAIN_ERROR from zerver.forms import HomepageForm, WRONG_SUBDOMAIN_ERROR
from zerver.lib.actions import do_change_password from zerver.lib.actions import do_change_password, gather_subscriptions
from zerver.views.auth import login_or_register_remote_user from zerver.views.auth import login_or_register_remote_user
from zerver.views.invite import get_invitee_emails_set from zerver.views.invite import get_invitee_emails_set
from zerver.views.registration import confirmation_key, \ from zerver.views.registration import confirmation_key, \
@@ -64,8 +64,7 @@ import ujson
from typing import Any, Dict, List, Optional, Set, Text from typing import Any, Dict, List, Optional, Set, Text
from six.moves import urllib from six.moves import urllib, range, zip
from six.moves import range
import os import os
class RedirectAndLogIntoSubdomainTestCase(ZulipTestCase): class RedirectAndLogIntoSubdomainTestCase(ZulipTestCase):
@@ -798,6 +797,124 @@ class InviteeEmailsParserTests(TestCase):
expected_set = {self.email1, self.email2, self.email3} expected_set = {self.email1, self.email2, self.email3}
self.assertEqual(get_invitee_emails_set(emails_raw), expected_set) self.assertEqual(get_invitee_emails_set(emails_raw), expected_set)
class MultiuseInviteTest(ZulipTestCase):
def setUp(self):
# type: () -> None
self.realm = get_realm('zulip')
self.realm.invite_required = True
self.realm.save()
def generate_multiuse_invite_link(self, streams=None, date_sent=None):
# type: (List[Stream], Optional[datetime.datetime]) -> Text
invite = MultiuseInvite(realm=self.realm, referred_by=self.example_user("iago"))
invite.save()
if streams is not None:
invite.streams = streams
invite.save()
if date_sent is None:
date_sent = timezone_now()
key = generate_key()
Confirmation.objects.create(content_object=invite, date_sent=date_sent,
confirmation_key=key, type=Confirmation.MULTIUSE_INVITE)
return confirmation_url(key, self.realm.host, Confirmation.MULTIUSE_INVITE)
def check_user_able_to_register(self, email, invite_link):
# type: (Text, Text) -> None
password = "password"
result = self.client_post(invite_link, {'email': email})
self.assertEqual(result.status_code, 302)
self.assertTrue(result["Location"].endswith(
"/accounts/send_confirm/%s" % (email,)))
result = self.client_get(result["Location"])
self.assert_in_response("Check your email so we can get started.", result)
confirmation_url = self.get_confirmation_url_from_outbox(email)
result = self.client_get(confirmation_url)
self.assertEqual(result.status_code, 200)
result = self.submit_reg_form_for_user(email, password)
self.assertEqual(result.status_code, 302)
from django.core.mail import outbox
outbox.pop()
def check_user_subscribed_only_to_streams(self, user_name, streams):
# type: (str, List[Stream]) -> None
sorted(streams, key=lambda x: x.name)
subscribed_streams = gather_subscriptions(self.nonreg_user(user_name))[0]
self.assertEqual(len(subscribed_streams), len(streams))
for x, y in zip(subscribed_streams, streams):
self.assertEqual(x["name"], y.name)
def test_valid_multiuse_link(self):
# type: () -> None
email1 = self.nonreg_email("test")
email2 = self.nonreg_email("test1")
email3 = self.nonreg_email("alice")
date_sent = timezone_now() - datetime.timedelta(days=settings.INVITATION_LINK_VALIDITY_DAYS - 1)
invite_link = self.generate_multiuse_invite_link(date_sent=date_sent)
self.check_user_able_to_register(email1, invite_link)
self.check_user_able_to_register(email2, invite_link)
self.check_user_able_to_register(email3, invite_link)
def test_expired_multiuse_link(self):
# type: () -> None
email = self.nonreg_email('newuser')
date_sent = timezone_now() - datetime.timedelta(days=settings.INVITATION_LINK_VALIDITY_DAYS)
invite_link = self.generate_multiuse_invite_link(date_sent=date_sent)
result = self.client_post(invite_link, {'email': email})
self.assertEqual(result.status_code, 200)
self.assert_in_response("Whoops. The confirmation link has expired.", result)
def test_invalid_multiuse_link(self):
# type: () -> None
email = self.nonreg_email('newuser')
invite_link = "/join/invalid_key/"
result = self.client_post(invite_link, {'email': email})
self.assertEqual(result.status_code, 200)
self.assert_in_response("Whoops. The confirmation link is malformed.", result)
def test_invalid_multiuse_link_in_open_realm(self):
# type: () -> None
self.realm.invite_required = False
self.realm.save()
email = self.nonreg_email('newuser')
invite_link = "/join/invalid_key/"
with self.settings(REALMS_HAVE_SUBDOMAINS=True):
with patch('zerver.views.registration.get_realm_from_request', return_value=self.realm):
with patch('zerver.views.registration.get_realm', return_value=self.realm):
self.check_user_able_to_register(email, invite_link)
def test_multiuse_link_with_specified_streams(self):
# type: () -> None
name1 = "newuser"
name2 = "bob"
email1 = self.nonreg_email(name1)
email2 = self.nonreg_email(name2)
stream_names = ["Rome", "Scotland", "Venice"]
streams = [get_stream(stream_name, self.realm) for stream_name in stream_names]
invite_link = self.generate_multiuse_invite_link(streams=streams)
self.check_user_able_to_register(email1, invite_link)
self.check_user_subscribed_only_to_streams(name1, streams)
stream_names = ["Rome", "Verona"]
streams = [get_stream(stream_name, self.realm) for stream_name in stream_names]
invite_link = self.generate_multiuse_invite_link(streams=streams)
self.check_user_able_to_register(email2, invite_link)
self.check_user_subscribed_only_to_streams(name2, streams)
class EmailUnsubscribeTests(ZulipTestCase): class EmailUnsubscribeTests(ZulipTestCase):
def test_error_unsubscribe(self): def test_error_unsubscribe(self):

View File

@@ -12,7 +12,7 @@ from django.template import RequestContext, loader
from django.utils.timezone import now from django.utils.timezone import now
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core import validators from django.core import validators
from zerver.models import UserProfile, Realm, Stream, PreregistrationUser, \ from zerver.models import UserProfile, Realm, Stream, PreregistrationUser, MultiuseInvite, \
name_changes_disabled, email_to_username, \ name_changes_disabled, email_to_username, \
completely_open, get_unique_open_realm, email_allowed_for_realm, \ completely_open, get_unique_open_realm, email_allowed_for_realm, \
get_realm, get_realm_by_email_domain, get_user_profile_by_email get_realm, get_realm_by_email_domain, get_user_profile_by_email
@@ -34,8 +34,9 @@ from zerver.lib.utils import get_subdomain
from zerver.lib.timezone import get_all_timezones from zerver.lib.timezone import get_all_timezones
from zproject.backends import password_auth_enabled from zproject.backends import password_auth_enabled
from confirmation.models import Confirmation, RealmCreationKey, check_key_is_valid, \ from confirmation.models import Confirmation, RealmCreationKey, ConfirmationKeyException, \
create_confirmation_link check_key_is_valid, create_confirmation_link, get_object_from_key, \
render_confirmation_key_error
import logging import logging
import requests import requests
@@ -358,18 +359,26 @@ def redirect_to_deactivation_notice():
# type: () -> HttpResponse # type: () -> HttpResponse
return HttpResponseRedirect(reverse('zerver.views.registration.show_deactivation_notice')) return HttpResponseRedirect(reverse('zerver.views.registration.show_deactivation_notice'))
def accounts_home(request): def accounts_home(request, multiuse_object=None):
# type: (HttpRequest) -> HttpResponse # type: (HttpRequest, Optional[MultiuseInvite]) -> HttpResponse
realm = get_realm_from_request(request) realm = get_realm_from_request(request)
if realm and realm.deactivated: if realm and realm.deactivated:
return redirect_to_deactivation_notice() return redirect_to_deactivation_notice()
from_multiuse_invite = False
streams_to_subscribe = None
if multiuse_object:
realm = multiuse_object.realm
streams_to_subscribe = multiuse_object.streams.all()
from_multiuse_invite = True
if request.method == 'POST': if request.method == 'POST':
form = HomepageForm(request.POST, realm=realm) form = HomepageForm(request.POST, realm=realm, from_multiuse_invite=from_multiuse_invite)
if form.is_valid(): if form.is_valid():
email = form.cleaned_data['email'] email = form.cleaned_data['email']
try: try:
send_registration_completion_email(email, request) send_registration_completion_email(email, request, streams=streams_to_subscribe)
except smtplib.SMTPException as e: except smtplib.SMTPException as e:
logging.error('Error in accounts_home: %s' % (str(e),)) logging.error('Error in accounts_home: %s' % (str(e),))
return HttpResponseRedirect("/config-error/smtp") return HttpResponseRedirect("/config-error/smtp")
@@ -385,9 +394,21 @@ def accounts_home(request):
form = HomepageForm(realm=realm) form = HomepageForm(realm=realm)
return render(request, return render(request,
'zerver/accounts_home.html', 'zerver/accounts_home.html',
context={'form': form, 'current_url': request.get_full_path}, context={'form': form, 'current_url': request.get_full_path,
'from_multiuse_invite': from_multiuse_invite},
) )
def accounts_home_from_multiuse_invite(request, confirmation_key):
# type: (HttpRequest, str) -> HttpResponse
multiuse_object = None
try:
multiuse_object = get_object_from_key(confirmation_key)
except ConfirmationKeyException as exception:
realm = get_realm_from_request(request)
if realm is None or realm.invite_required:
return render_confirmation_key_error(request, exception)
return accounts_home(request, multiuse_object=multiuse_object)
def generate_204(request): def generate_204(request):
# type: (HttpRequest) -> HttpResponse # type: (HttpRequest) -> HttpResponse
return HttpResponse(content=None, status=204) return HttpResponse(content=None, status=204)

View File

@@ -138,6 +138,9 @@ i18n_urls = [
url(r'^register/$', zerver.views.registration.accounts_home, name='register'), url(r'^register/$', zerver.views.registration.accounts_home, name='register'),
url(r'^login/$', zerver.views.auth.login_page, {'template_name': 'zerver/login.html'}, name='zerver.views.auth.login_page'), url(r'^login/$', zerver.views.auth.login_page, {'template_name': 'zerver/login.html'}, name='zerver.views.auth.login_page'),
url(r'^join/(?P<confirmation_key>\S+)/$', zerver.views.registration.accounts_home_from_multiuse_invite,
name='zerver.views.registration.accounts_home_from_multiuse_invite'),
# API and integrations documentation # API and integrations documentation
url(r'^api/$', APIView.as_view(template_name='zerver/api.html')), url(r'^api/$', APIView.as_view(template_name='zerver/api.html')),
url(r'^api/endpoints/$', zerver.views.integrations.api_endpoint_docs, name='zerver.views.integrations.api_endpoint_docs'), url(r'^api/endpoints/$', zerver.views.integrations.api_endpoint_docs, name='zerver.views.integrations.api_endpoint_docs'),