Add interface for creating new realms.

This is controlled by settings.OPEN_REALM_CREATION; if that setting is
off, this feature doesn't do anything.
This commit is contained in:
Vishnu Ks 2016-06-03 04:32:58 +05:30 committed by Tim Abbott
parent 8213ca135a
commit ad1c3894d9
14 changed files with 245 additions and 15 deletions

View File

@ -665,6 +665,10 @@ a.bottom-signup-button {
margin-left: 10px;
}
.new-organization-button {
margin-top: 10px;
}
.login-form .control-label, .register-form .control-label {
margin-bottom: 2px;
}

View File

@ -0,0 +1,38 @@
{% extends "zerver/portico_signup.html" %}
{# Home page for not logged-in users. #}
{# This is where we pitch the app and solicit signups. #}
{% block portico_content %}
<script type="text/javascript">
$(function () {
autofocus('#email');
});
</script>
<div class="app register-page">
<div class="app-main register-page-container">
<div class="register-form">
<p class="lead">
<div class="register-page-header">{{ _("Let's get started") }}…</div>
</p>
<form class="form-inline" id="send_confirm" name="email_form"
action="{{ current_url() }}" method="post">
{{ csrf_input }}
<input type="text" class="email required" placeholder="{{ _("Enter your work email address") }}"
id="email" name="email"/>
<input type="submit" class="new-organization-button btn btn-primary btn-large register-button" value="{{ _("Create organization") }}"/>
</form>
<div id="errors"></div>
{% if form.email.errors %}
{% for error in form.email.errors %}
<div class="alert alert-error">{{ error }}</div>
{% endfor %}
{% endif %}
</div>
</div>
<div class="footer-padder"></div>
</div>
{% endblock %}

View File

@ -67,6 +67,12 @@
<a href="{{ url('register') }}">{{ _('Register') }}</a>
{% endif %}
</li>
<li><span class="little-bullet">·</span></li>
<li>
{% if open_realm_creation %}
<a href="/create_realm">{{ _('Create new organization') }}</a>
{% endif %}
</li>
{% endif %}
</ul>
</div>

View File

@ -0,0 +1,13 @@
{% extends "zerver/portico.html" %}
{% block for_you %}{{ _('got a bit lost there.') }}{% endblock %}
{% block portico_content %}
<br/>
<p class="lead">{{ _('New organization creation disabled') }}</p>
<p>{{ _('This server does not allow members of the public to create new organizations.') }}</p>
<p>{% trans %}Zulip is open source, so you can install your own Zulip server by following the instructions on <a href="https://www.zulip.org/">www.zulip.org</a>{% endtrans %}</p>
{% endblock %}

View File

@ -46,6 +46,24 @@ Form is validated both client-side using jquery-validate (see signup.js) and ser
{% endif %}
</div>
</div>
{% if creating_new_team %}
<div class="control-group">
<label for="id_team_name" class="control-label">{{ _('Organization name') }}</label>
<div class="controls">
<input id="id_team_name" class="required" type="text"
placeholder="{{ _("E.g. Acme") }}"
name="realm_name" maxlength="100" />
{% if form.realm_name.errors %}
{% for error in form.realm_name.errors %}
<div class="alert alert-error">{{ error }}</div>
{% endfor %}
{% endif %}
<br /><span class="small">{{ _('You can change this later on the admin page.') }}</span>
</div>
</div>
{% endif %}
{% if password_auth_enabled %}
<div class="control-group">
<label for="id_password" class="control-label">{{ _('Password') }}</label>

View File

@ -21,6 +21,7 @@ def add_settings(request):
'api_site_required': settings.EXTERNAL_API_PATH != "api.zulip.com",
'email_integration_enabled': settings.EMAIL_GATEWAY_BOT != "",
'email_gateway_example': settings.EMAIL_GATEWAY_EXAMPLE,
'open_realm_creation': settings.OPEN_REALM_CREATION,
'password_auth_enabled': password_auth_enabled(realm),
'dev_auth_enabled': dev_auth_enabled(),
'google_auth_enabled': google_auth_enabled(),

View File

@ -8,14 +8,18 @@ from django.contrib.auth.forms import SetPasswordForm, AuthenticationForm, \
from django.conf import settings
from django.db.models.query import QuerySet
from jinja2 import Markup as mark_safe
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
import logging
from zerver.models import Realm, get_user_profile_by_email, UserProfile, \
completely_open, resolve_email_to_domain, get_realm, get_unique_open_realm
from zerver.lib.actions import do_change_password, is_inactive
completely_open, resolve_email_to_domain, get_realm, \
get_unique_open_realm, split_email_to_domain
from zerver.lib.actions import do_change_password, is_inactive, user_email_is_unique
from zproject.backends import password_auth_enabled
import DNS
from six import text_type
SIGNUP_STRING = u'Your e-mail does not match any existing open organization. ' + \
u'Use a different e-mail address, or contact %s with questions.' % (settings.ZULIP_ADMINISTRATOR,)
@ -25,6 +29,13 @@ if settings.ZULIP_COM:
u"<a href=\"https://blogs.dropbox.com/tech/2015/09/open-sourcing-zulip-a-dropbox-hack-week-project/\">Zulip is open source</a>, so you can install your own Zulip server " + \
u"by following the instructions on <a href=\"https://www.zulip.org\">www.zulip.org</a>!"
def get_registration_string(domain):
# type: (text_type) -> text_type
register_url = reverse('register') + domain
register_account_string = _('The organization with the domain already exists. Please register your account <a href=%(url)s>here</a>.') % {'url': register_url}
return register_account_string
def has_valid_realm(value):
# type: (str) -> bool
# Checks if there is a realm without invite_required
@ -54,10 +65,11 @@ class RegistrationForm(forms.Form):
# actually required for a realm
password = forms.CharField(widget=forms.PasswordInput, max_length=100,
required=False)
realm_name = forms.CharField(max_length=100, required=False)
if not settings.VOYAGER:
terms = forms.BooleanField(required=True)
class ToSForm(forms.Form):
full_name = forms.CharField(max_length=100)
terms = forms.BooleanField(required=True)
@ -84,6 +96,27 @@ class HomepageForm(forms.Form):
return data
raise ValidationError(mark_safe(SIGNUP_STRING))
class RealmCreationForm(forms.Form):
# This form determines whether users can
# create a new realm. Be careful when modifying the
# validators.
email = forms.EmailField(validators=[user_email_is_unique,])
def __init__(self, *args, **kwargs):
# type: (*Any, **Any) -> None
self.domain = kwargs.get("domain")
if "domain" in kwargs:
del kwargs["domain"]
super(RealmCreationForm, self).__init__(*args, **kwargs)
def clean_email(self):
# type: () -> text_type
data = self.cleaned_data['email']
domain = split_email_to_domain(data)
if (get_realm(domain) is not None):
raise ValidationError(mark_safe(get_registration_string(domain)))
return data
class LoggingSetPasswordForm(SetPasswordForm):
def save(self, commit=True):
# type: (bool) -> UserProfile

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('zerver', '0018_realm_emoji_message'),
]
operations = [
migrations.AddField(
model_name='preregistrationuser',
name='realm_creation',
field=models.BooleanField(default=False),
),
]

View File

@ -502,6 +502,7 @@ class PreregistrationUser(models.Model):
referred_by = models.ForeignKey(UserProfile, null=True) # Optional[UserProfile]
streams = models.ManyToManyField('Stream') # type: Manager
invited_at = models.DateTimeField(auto_now=True) # type: datetime.datetime
realm_creation = models.BooleanField(default=False)
# status: whether an object has been confirmed.
# if confirmed, set to confirmation.settings.STATUS_ACTIVE

View File

@ -560,3 +560,52 @@ class EmailUnsubscribeTests(AuthedTestCase):
self.assertEqual(0, len(ScheduledJob.objects.filter(
type=ScheduledJob.EMAIL, filter_string__iexact=email)))
class RealmCreationTest(AuthedTestCase):
def test_create_realm(self):
# type: () -> None
username = "user1"
password = "test"
domain = "test.com"
email = "user1@test.com"
# Make sure the realm does not exist
self.assertIsNone(get_realm("test.com"))
with self.settings(OPEN_REALM_CREATION=True):
# Create new realm with the email
result = self.client.post('/create_realm/', {'email': email})
self.assertEquals(result.status_code, 302)
self.assertTrue(result["Location"].endswith(
"/accounts/send_confirm/%s@%s" % (username, domain)))
result = self.client.get(result["Location"])
self.assertIn("Check your email so we can get started.", result.content)
# Visit the confirmation link.
from django.core.mail import outbox
for message in reversed(outbox):
if email in message.to:
confirmation_link_pattern = re.compile(settings.EXTERNAL_HOST + "(\S+)>")
confirmation_url = confirmation_link_pattern.search(
message.body).groups()[0]
break
else:
raise ValueError("Couldn't find a confirmation email.")
result = self.client.get(confirmation_url)
self.assertEquals(result.status_code, 200)
result = self.submit_reg_form_for_user(username, password, domain)
self.assertEquals(result.status_code, 302)
# Make sure the realm is created
realm = get_realm("test.com")
self.assertIsNotNone(realm)
self.assertEqual(realm.domain, domain)
self.assertEqual(get_user_profile_by_email(email).realm, realm)
self.assertTrue(result["Location"].endswith("/invite/"))
result = self.client.get(result["Location"])
self.assertIn("You're the first one here!", result.content)

View File

@ -26,7 +26,7 @@ from zerver.models import Message, UserProfile, Stream, Subscription, Huddle, \
completely_open, get_unique_open_realm, remote_user_to_email, email_allowed_for_realm, \
get_cross_realm_users
from zerver.lib.actions import do_change_password, do_change_full_name, do_change_is_admin, \
do_activate_user, do_create_user, \
do_activate_user, do_create_user, do_create_realm, set_default_streams, \
internal_send_message, update_user_presence, do_events_register, \
get_status_dict, do_change_enable_offline_email_notifications, \
do_change_enable_digest_emails, do_set_realm_name, do_set_realm_restricted_to_domain, \
@ -35,7 +35,7 @@ from zerver.lib.actions import do_change_password, do_change_full_name, do_chang
user_email_is_unique, do_invite_users, do_refer_friend, compute_mit_user_fullname, \
do_set_muted_topics, clear_followup_emails_queue, do_update_pointer, realm_user_count
from zerver.lib.push_notifications import num_push_devices_for_user
from zerver.forms import RegistrationForm, HomepageForm, ToSForm, \
from zerver.forms import RegistrationForm, HomepageForm, RealmCreationForm, ToSForm, \
CreateUserForm, is_inactive, OurAuthenticationForm
from django.views.decorators.csrf import csrf_exempt
from django_auth_ldap.backend import LDAPBackend, _LDAPUser
@ -86,6 +86,7 @@ def accounts_register(request):
confirmation = Confirmation.objects.get(confirmation_key=key)
prereg_user = confirmation.content_object
email = prereg_user.email
realm_creation = prereg_user.realm_creation
mit_beta_user = isinstance(confirmation.content_object, MitUser)
try:
existing_user_profile = get_user_profile_by_email(email)
@ -93,9 +94,10 @@ def accounts_register(request):
existing_user_profile = None
validators.validate_email(email)
# If OPEN_REALM_CREATION is enabled all user sign ups should go through the
# special URL with domain name so that REALM can be identified if multiple realms exist
unique_open_realm = get_unique_open_realm()
if unique_open_realm:
if unique_open_realm is not None:
realm = unique_open_realm
domain = realm.domain
elif not mit_beta_user and prereg_user.referred_by:
@ -117,6 +119,9 @@ def accounts_register(request):
domain = resolve_email_to_domain(email)
realm = get_realm(domain)
if realm_creation and not settings.OPEN_REALM_CREATION:
return render_to_response("zerver/realm_creation_disabled.html")
if realm and realm.deactivated:
# The user is trying to register for a deactivated realm. Advise them to
# contact support.
@ -196,6 +201,11 @@ def accounts_register(request):
# SSO users don't need no passwords
password = None
if realm_creation:
domain = split_email_to_domain(email)
(realm, _) = do_create_realm(domain, form.cleaned_data['realm_name'])
set_default_streams(realm, settings.DEFAULT_NEW_REALM_STREAMS)
full_name = form.cleaned_data['full_name']
short_name = email_to_username(email)
first_in_realm = len(UserProfile.objects.filter(realm=realm, is_bot=False)) == 0
@ -236,6 +246,7 @@ def accounts_register(request):
# password_auth_enabled is normally set via our context processor,
# but for the registration form, there is no logged in user yet, so
# we have to set it here.
'creating_new_team': realm_creation,
'password_auth_enabled': password_auth_enabled(realm),
},
request=request)
@ -667,8 +678,8 @@ def logout_then_login(request, **kwargs):
# type: (HttpRequest, **Any) -> HttpResponse
return django_logout_then_login(request, kwargs)
def create_preregistration_user(email, request):
# type: (text_type, HttpRequest) -> HttpResponse
def create_preregistration_user(email, request, realm_creation=False):
# type: (text_type, HttpRequest, bool) -> HttpResponse
domain = request.session.get("domain")
if completely_open(domain):
# Clear the "domain" from the session object; it's no longer needed
@ -677,14 +688,15 @@ def create_preregistration_user(email, request):
# The user is trying to sign up for a completely open realm,
# so create them a PreregistrationUser for that realm
return PreregistrationUser.objects.create(email=email,
realm=get_realm(domain))
realm=get_realm(domain),
realm_creation=realm_creation)
# MIT users who are not explicitly signing up for an open realm
# require special handling (They may already have an (inactive)
# account, for example)
if split_email_to_domain(email) == "mit.edu":
return MitUser.objects.get_or_create(email=email)[0]
return PreregistrationUser.objects.create(email=email)
return MitUser.objects.get_or_create(email=email, realm_creation=realm_creation)[0]
return PreregistrationUser.objects.create(email=email, realm_creation=realm_creation)
def accounts_home_with_domain(request, domain):
# type: (HttpRequest, str) -> HttpResponse
@ -699,18 +711,48 @@ def accounts_home_with_domain(request, domain):
else:
return HttpResponseRedirect(reverse('zerver.views.accounts_home'))
def send_registration_completion_email(email, request):
# type: (str, HttpRequest) -> HttpResponse
def send_registration_completion_email(email, request, realm_creation=False):
# type: (str, HttpRequest, bool) -> HttpResponse
"""
Send an email with a confirmation link to the provided e-mail so the user
can complete their registration.
"""
prereg_user = create_preregistration_user(email, request)
prereg_user = create_preregistration_user(email, request, realm_creation)
context = {'support_email': settings.ZULIP_ADMINISTRATOR,
'voyager': settings.VOYAGER}
Confirmation.objects.send_confirmation(prereg_user, email,
additional_context=context)
"""
When settings.OPEN_REALM_CREATION is enabled public users can create new realm. For creating the realm the user should
not be the member of any current realm. The realm is created with domain same as the that of the user's email.
When there is no unique_open_realm user registrations are made by visiting /register/domain_of_the_realm.
"""
def create_realm(request):
# type: (HttpRequest) -> HttpResponse
if not settings.OPEN_REALM_CREATION:
return render_to_response("zerver/realm_creation_disabled.html")
if request.method == 'POST':
form = RealmCreationForm(request.POST, domain=request.session.get("domain"))
if form.is_valid():
email = form.cleaned_data['email']
send_registration_completion_email(email, request, realm_creation=True)
return HttpResponseRedirect(reverse('send_confirm', kwargs={'email': email}))
try:
email = request.POST['email']
user_email_is_unique(email)
except ValidationError:
# if the user user is already registered he can't create a new realm as a realm
# with the same domain as user's email already exists
return HttpResponseRedirect(reverse('django.contrib.auth.views.login') + '?email=' + urllib.parse.quote_plus(email))
else:
form = RealmCreationForm(domain=request.session.get("domain"))
return render_to_response('zerver/create_realm.html',
{'form': form, 'current_url': request.get_full_path},
request=request)
def accounts_home(request):
# type: (HttpRequest) -> HttpResponse
if request.method == 'POST':

View File

@ -20,3 +20,5 @@ EMAIL_GATEWAY_BOT = "emailgateway@zulip.com"
EXTRA_INSTALLED_APPS = ["zilencer", "analytics"]
# Disable Camo in development
CAMO_URI = ''
OPEN_REALM_CREATION = True

View File

@ -142,6 +142,7 @@ DEFAULT_SETTINGS = {'TWITTER_CONSUMER_KEY': '',
'ZULIP_COM': False,
'ZULIP_COM_STAGING': False,
'STATSD_HOST': '',
'OPEN_REALM_CREATION': False,
'REMOTE_POSTGRES_HOST': '',
'REMOTE_POSTGRES_SSLMODE': '',
'GOOGLE_CLIENT_ID': '',

View File

@ -68,6 +68,9 @@ i18n_urls = [
# Portico-styled page used to provide email confirmation of terms acceptance.
url(r'^accounts/accept_terms/$', 'zerver.views.accounts_accept_terms'),
# Realm Creation
url(r'^create_realm/$', 'zerver.views.create_realm'),
# Login/registration
url(r'^register/$', 'zerver.views.accounts_home', name='register'),
url(r'^login/$', 'zerver.views.login_page', {'template_name': 'zerver/login.html'}),