mirror of https://github.com/zulip/zulip.git
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:
parent
8213ca135a
commit
ad1c3894d9
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 %}
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
|
@ -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>
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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': '',
|
||||
|
|
|
@ -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'}),
|
||||
|
|
Loading…
Reference in New Issue