mirror of
https://github.com/zulip/zulip.git
synced 2025-11-14 10:57:58 +00:00
backend: Add support for multiuse user invite link.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
Reference in New Issue
Block a user