diff --git a/static/styles/portico.css b/static/styles/portico.css index 1f81fee8b8..6ed4caee19 100644 --- a/static/styles/portico.css +++ b/static/styles/portico.css @@ -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; } diff --git a/templates/zerver/create_realm.html b/templates/zerver/create_realm.html new file mode 100644 index 0000000000..698cadda2a --- /dev/null +++ b/templates/zerver/create_realm.html @@ -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 %} + + +
+
+ +
+

+

{{ _("Let's get started") }}…
+

+
+ {{ csrf_input }} + + +
+
+ {% if form.email.errors %} + {% for error in form.email.errors %} +
{{ error }}
+ {% endfor %} + {% endif %} +
+ +
+ +
+{% endblock %} diff --git a/templates/zerver/portico.html b/templates/zerver/portico.html index ab6ede32df..60c3129f06 100644 --- a/templates/zerver/portico.html +++ b/templates/zerver/portico.html @@ -67,6 +67,12 @@ {{ _('Register') }} {% endif %} +
  • ·
  • +
  • + {% if open_realm_creation %} + {{ _('Create new organization') }} + {% endif %} +
  • {% endif %} diff --git a/templates/zerver/realm_creation_disabled.html b/templates/zerver/realm_creation_disabled.html new file mode 100644 index 0000000000..8e88d65984 --- /dev/null +++ b/templates/zerver/realm_creation_disabled.html @@ -0,0 +1,13 @@ +{% extends "zerver/portico.html" %} + +{% block for_you %}{{ _('got a bit lost there.') }}{% endblock %} + +{% block portico_content %} + +
    +

    {{ _('New organization creation disabled') }}

    + +

    {{ _('This server does not allow members of the public to create new organizations.') }}

    +

    {% trans %}Zulip is open source, so you can install your own Zulip server by following the instructions on www.zulip.org{% endtrans %}

    + +{% endblock %} diff --git a/templates/zerver/register.html b/templates/zerver/register.html index 694e6ba7df..f67a847a89 100644 --- a/templates/zerver/register.html +++ b/templates/zerver/register.html @@ -46,6 +46,24 @@ Form is validated both client-side using jquery-validate (see signup.js) and ser {% endif %} + + {% if creating_new_team %} +
    + +
    + + {% if form.realm_name.errors %} + {% for error in form.realm_name.errors %} +
    {{ error }}
    + {% endfor %} + {% endif %} +
    {{ _('You can change this later on the admin page.') }} +
    +
    + {% endif %} + {% if password_auth_enabled %}
    diff --git a/zerver/context_processors.py b/zerver/context_processors.py index 1b9285ad38..1dd099ce5d 100644 --- a/zerver/context_processors.py +++ b/zerver/context_processors.py @@ -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(), diff --git a/zerver/forms.py b/zerver/forms.py index 119885093f..14f99aef66 100644 --- a/zerver/forms.py +++ b/zerver/forms.py @@ -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"Zulip is open source, so you can install your own Zulip server " + \ u"by following the instructions on www.zulip.org!" + +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 here.') % {'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 diff --git a/zerver/migrations/0019_preregistrationuser_realm_creation.py b/zerver/migrations/0019_preregistrationuser_realm_creation.py new file mode 100644 index 0000000000..3a7f879d7f --- /dev/null +++ b/zerver/migrations/0019_preregistrationuser_realm_creation.py @@ -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), + ), + ] diff --git a/zerver/models.py b/zerver/models.py index 1dddd4a30e..2c0f2e03f8 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -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 diff --git a/zerver/tests/test_signup.py b/zerver/tests/test_signup.py index 82e1ed8689..a687f5236c 100644 --- a/zerver/tests/test_signup.py +++ b/zerver/tests/test_signup.py @@ -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) diff --git a/zerver/views/__init__.py b/zerver/views/__init__.py index f01f4c065e..bc94fd0463 100644 --- a/zerver/views/__init__.py +++ b/zerver/views/__init__.py @@ -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': diff --git a/zproject/dev_settings.py b/zproject/dev_settings.py index 3e11673244..f126203710 100644 --- a/zproject/dev_settings.py +++ b/zproject/dev_settings.py @@ -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 + diff --git a/zproject/settings.py b/zproject/settings.py index d7efecbc90..7cac880380 100644 --- a/zproject/settings.py +++ b/zproject/settings.py @@ -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': '', diff --git a/zproject/urls.py b/zproject/urls.py index 901904b272..81d6683975 100644 --- a/zproject/urls.py +++ b/zproject/urls.py @@ -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'}),