From f208813ea33f452676898bcabce5aa06a9def413 Mon Sep 17 00:00:00 2001 From: Umair Khan Date: Tue, 20 Dec 2016 14:41:46 +0500 Subject: [PATCH] Add Find My Team feature. --- static/styles/portico.css | 9 +++ .../emails/find_team/find_team_email.txt | 8 +++ .../emails/find_team/find_team_email_html.txt | 9 +++ templates/zerver/find_my_team.html | 48 ++++++++++++++ templates/zerver/portico.html | 5 ++ zerver/context_processors.py | 1 + zerver/forms.py | 29 +++++++++ zerver/tests/test_templates.py | 1 + zerver/tests/tests.py | 44 +++++++++++++ zerver/views/auth.py | 65 ++++++++++++++++++- zproject/settings.py | 1 + zproject/urls.py | 1 + 12 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 templates/zerver/emails/find_team/find_team_email.txt create mode 100644 templates/zerver/emails/find_team/find_team_email_html.txt create mode 100644 templates/zerver/find_my_team.html diff --git a/static/styles/portico.css b/static/styles/portico.css index c46ffd312d..4dffa4ff3d 100644 --- a/static/styles/portico.css +++ b/static/styles/portico.css @@ -1415,3 +1415,12 @@ label.label-title { .float-right { float: right; } + +#find_my_team .form-control { + height: 30px; +} + +#find_my_team .btn { + height: 40px; + border-radius: 5px; +} diff --git a/templates/zerver/emails/find_team/find_team_email.txt b/templates/zerver/emails/find_team/find_team_email.txt new file mode 100644 index 0000000000..37f1a39139 --- /dev/null +++ b/templates/zerver/emails/find_team/find_team_email.txt @@ -0,0 +1,8 @@ +Hello {{ user_profile.full_name }}, + +You can log into Zulip using {{ user_profile.email }} on following organization: + +- {{ user_profile.realm.name }} through {{ user_profile.realm.uri }} + +Cheers, +The Zulip Team diff --git a/templates/zerver/emails/find_team/find_team_email_html.txt b/templates/zerver/emails/find_team/find_team_email_html.txt new file mode 100644 index 0000000000..cf4d3fab60 --- /dev/null +++ b/templates/zerver/emails/find_team/find_team_email_html.txt @@ -0,0 +1,9 @@ +

Hello {{ user_profile.full_name }},

+ +

You can log into Zulip using {{ user_profile.email }} on following organization:

+ +

{{ user_profile.realm.name }}

+ +

Cheers, +
+The Zulip Team

diff --git a/templates/zerver/find_my_team.html b/templates/zerver/find_my_team.html new file mode 100644 index 0000000000..a75570af86 --- /dev/null +++ b/templates/zerver/find_my_team.html @@ -0,0 +1,48 @@ +{% extends "zerver/portico.html" %} + +{% block portico_content %} +
+
+

+

{{ _("Find your team") }}…

+

+ {% if emails %} +
+

+ We have checked the following email address(es). Any email address + not associated with Zulip was not sent the sign-in information. +

+ +
    + {% for email in emails %} +
  • {{ email }}
  • + {% endfor %} +
+
+ {% else %} +
+

+ We will send you an email with the sign-in information against + any email address associated with Zulip. +

+
+ {{ csrf_input }} + + +
{{ form.emails.help_text }}
+
+
+ {% if form.emails.errors %} + {% for error in form.emails.errors %} +
{{ error }}
+ {% endfor %} + {% endif %} +
+ {% endif %} +
+ +
+{% endblock %} diff --git a/templates/zerver/portico.html b/templates/zerver/portico.html index 85f5c79920..5e21efaa6c 100644 --- a/templates/zerver/portico.html +++ b/templates/zerver/portico.html @@ -84,6 +84,11 @@
  • ·
  • {{ _('Create new organization') }}
  • {% endif %} + {% if find_team_link_disabled %} + {% else %} +
  • ·
  • +
  • {{ _('Find my team') }}
  • + {% endif %} diff --git a/zerver/context_processors.py b/zerver/context_processors.py index 25eeb17eca..0acb028c1d 100644 --- a/zerver/context_processors.py +++ b/zerver/context_processors.py @@ -60,6 +60,7 @@ def add_settings(request): 'github_auth_enabled': github_auth_enabled(realm), 'development_environment': settings.DEVELOPMENT, 'support_email': settings.ZULIP_ADMINISTRATOR, + 'find_team_link_disabled': settings.FIND_TEAM_LINK_DISABLED, } diff --git a/zerver/forms.py b/zerver/forms.py index b2be52e7bd..4d6f6aaa1f 100644 --- a/zerver/forms.py +++ b/zerver/forms.py @@ -6,6 +6,7 @@ from django.contrib.auth.forms import SetPasswordForm, AuthenticationForm, \ PasswordResetForm from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse +from django.core.validators import validate_email from django.db.models.query import QuerySet from django.utils.translation import ugettext as _ from jinja2 import Markup as mark_safe @@ -201,3 +202,31 @@ Please contact %s to reactivate this group.""" % ( (user_profile.email, get_subdomain(self.request))) raise ValidationError(mark_safe(WRONG_SUBDOMAIN_ERROR)) return email + +class MultiEmailField(forms.Field): + def to_python(self, emails): + # type: (Text) -> List[Text] + """Normalize data to a list of strings.""" + if not emails: + return [] + + return [email.strip() for email in emails.split(',')] + + def validate(self, emails): + # type: (List[Text]) -> None + """Check if value consists only of valid emails.""" + super(MultiEmailField, self).validate(emails) + for email in emails: + validate_email(email) + +class FindMyTeamForm(forms.Form): + emails = MultiEmailField( + help_text="Add up to 10 comma-separated email addresses.") + + def clean_emails(self): + # type: () -> List[Text] + emails = self.cleaned_data['emails'] + if len(emails) > 10: + raise forms.ValidationError("Please enter at most 10 emails.") + + return emails diff --git a/zerver/tests/test_templates.py b/zerver/tests/test_templates.py index ee4995fc6c..7c0751d8d3 100644 --- a/zerver/tests/test_templates.py +++ b/zerver/tests/test_templates.py @@ -154,6 +154,7 @@ class TemplateTestCase(ZulipTestCase): full_name=get_form_value('John Doe'), terms=get_form_value(True), email=get_form_value(email), + emails=get_form_value(email), ), current_url=lambda: 'www.zulip.com', hubot_lozenges_dict={}, diff --git a/zerver/tests/tests.py b/zerver/tests/tests.py index 78e0541b37..5627f7d5ae 100644 --- a/zerver/tests/tests.py +++ b/zerver/tests/tests.py @@ -2304,3 +2304,47 @@ class TestLoginPage(ZulipTestCase): # type: () -> None result = self.client_get("/login/?subdomain=1") self.assertIn(WRONG_SUBDOMAIN_ERROR, result.content.decode('utf8')) + +class FindMyTeamTestCase(ZulipTestCase): + def test_template(self): + # type: () -> None + result = self.client_get('/find-my-team/') + self.assertIn("Find your team", result.content.decode('utf8')) + + def test_result(self): + # type: () -> None + url = '/find-my-team/?emails=iago@zulip.com,cordelia@zulip.com' + result = self.client_get(url) + content = result.content.decode('utf8') + self.assertIn("We have checked the following email address(es)", content) + self.assertIn("iago@zulip.com", content) + self.assertIn("cordelia@zulip.com", content) + + def test_find_team_zero_emails(self): + # type: () -> None + data = {'emails': ''} + result = self.client_post('/find-my-team/', data) + self.assertIn('This field is required', result.content.decode('utf8')) + self.assertEqual(result.status_code, 200) + + def test_find_team_one_email(self): + # type: () -> None + data = {'emails': 'hamlet@zulip.com'} + result = self.client_post('/find-my-team/', data) + self.assertEqual(result.status_code, 302) + self.assertEqual(result.url, '/find-my-team/?emails=hamlet%40zulip.com') + + def test_find_team_multiple_emails(self): + # type: () -> None + data = {'emails': 'hamlet@zulip.com,iago@zulip.com'} + result = self.client_post('/find-my-team/', data) + self.assertEqual(result.status_code, 302) + expected = '/find-my-team/?emails=hamlet%40zulip.com%2Ciago%40zulip.com' + self.assertEqual(result.url, expected) + + def test_find_team_more_than_ten_emails(self): + # type: () -> None + data = {'emails': ','.join(['hamlet-{}@zulip.com'.format(i) for i in range(11)])} + result = self.client_post('/find-my-team/', data) + self.assertEqual(result.status_code, 200) + self.assertIn("Please enter at most 10", result.content.decode('utf8')) diff --git a/zerver/views/auth.py b/zerver/views/auth.py index 1661177f6d..43aed96b34 100644 --- a/zerver/views/auth.py +++ b/zerver/views/auth.py @@ -12,12 +12,17 @@ from django.shortcuts import redirect from django.views.decorators.csrf import csrf_exempt from django.utils.translation import ugettext as _ from django.core import signing -from typing import Text +from django.template import loader +from django.core.validators import validate_email +from django import forms +from django.core.mail import send_mail from six.moves import urllib from typing import Any, Dict, Optional, Tuple, Text from confirmation.models import Confirmation -from zerver.forms import HomepageForm, OurAuthenticationForm, WRONG_SUBDOMAIN_ERROR +from zerver.forms import HomepageForm, OurAuthenticationForm, \ + WRONG_SUBDOMAIN_ERROR, FindMyTeamForm + from zerver.lib.request import REQ, has_request_variables, JsonableError from zerver.lib.response import json_success, json_error from zerver.lib.utils import get_subdomain @@ -26,6 +31,7 @@ from zerver.views import create_preregistration_user, get_realm_from_request, \ redirect_and_log_into_subdomain from zproject.backends import password_auth_enabled, dev_auth_enabled, google_auth_enabled from zproject.jinja2 import render_to_response +from zerver.lib.notifications import send_future_email import hashlib import hmac @@ -34,6 +40,61 @@ import logging import requests import time import ujson +import datetime +from typing import Text + +try: + import mailer + send_mail = mailer.send_mail +except ImportError: + # no mailer app present, stick with default + pass + +def send_find_my_team_emails(user_profile): + # type: (UserProfile) -> None + text_template = 'zerver/emails/find_team/find_team_email.txt' + html_template = 'zerver/emails/find_team/find_team_email_html.txt' + context = {'user_profile': user_profile} + text_content = loader.render_to_string(text_template, context) + html_content = loader.render_to_string(html_template, context) + sender = settings.NOREPLY_EMAIL_ADDRESS + recipients = [user_profile.email] + subject = "Your Zulip Team" + + send_mail(subject, text_content, sender, recipients, html_message=html_content) + +def find_my_team(request): + # type: (HttpRequest) -> HttpResponse + url = reverse('find-my-team') + + emails = [] # type: List[Text] + if request.method == 'POST': + form = FindMyTeamForm(request.POST) + if form.is_valid(): + emails = form.cleaned_data['emails'] + for user_profile in UserProfile.objects.filter(email__in=emails): + send_find_my_team_emails(user_profile) + + # Note: Show all the emails in the result otherwise this + # feature can be used to ascertain which email addresses + # are associated with Zulip. + data = urllib.parse.urlencode({'emails': ','.join(emails)}) + return redirect(url + "?" + data) + else: + form = FindMyTeamForm() + result = request.GET.get('emails') + if result: + for email in result.split(','): + try: + validate_email(email) + emails.append(email) + except forms.ValidationError: + pass + + return render_to_response('zerver/find_my_team.html', + {'form': form, 'current_url': lambda: url, + 'emails': emails}, + request=request) def maybe_send_to_registration(request, email, full_name=''): # type: (HttpRequest, Text, Text) -> HttpResponse diff --git a/zproject/settings.py b/zproject/settings.py index 44422bb858..c38a3b9d8b 100644 --- a/zproject/settings.py +++ b/zproject/settings.py @@ -157,6 +157,7 @@ DEFAULT_SETTINGS = {'TWITTER_CONSUMER_KEY': '', 'REGISTER_LINK_DISABLED': False, 'LOGIN_LINK_DISABLED': False, 'ABOUT_LINK_DISABLED': False, + 'FIND_TEAM_LINK_DISABLED': True, 'CUSTOM_LOGO_URL': None, 'VERBOSE_SUPPORT_OFFERS': False, 'STATSD_HOST': '', diff --git a/zproject/urls.py b/zproject/urls.py index c6ea2bb59a..427d1e238e 100644 --- a/zproject/urls.py +++ b/zproject/urls.py @@ -131,6 +131,7 @@ i18n_urls = [ name='landing-page'), url(r'^new-user/$', RedirectView.as_view(url='/hello', permanent=True)), url(r'^features/$', TemplateView.as_view(template_name='zerver/features.html')), + url(r'^find-my-team/$', zerver.views.auth.find_my_team, name='find-my-team'), ] # If a Terms of Service is supplied, add that route