mirror of https://github.com/zulip/zulip.git
Add Find My Team feature.
This commit is contained in:
parent
268770489b
commit
f208813ea3
|
@ -1415,3 +1415,12 @@ label.label-title {
|
||||||
.float-right {
|
.float-right {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#find_my_team .form-control {
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#find_my_team .btn {
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
|
@ -0,0 +1,9 @@
|
||||||
|
<p>Hello {{ user_profile.full_name }},<p>
|
||||||
|
|
||||||
|
<p>You can log into Zulip using {{ user_profile.email }} on following organization:</p>
|
||||||
|
|
||||||
|
<p><a href="{{ user_profile.realm.uri }}">{{ user_profile.realm.name }}</a></p>
|
||||||
|
|
||||||
|
<p>Cheers,
|
||||||
|
<br>
|
||||||
|
The Zulip Team</p>
|
|
@ -0,0 +1,48 @@
|
||||||
|
{% extends "zerver/portico.html" %}
|
||||||
|
|
||||||
|
{% block portico_content %}
|
||||||
|
<div class="app find-team-page">
|
||||||
|
<div class="app-main find-team-page-container">
|
||||||
|
<p class="lead">
|
||||||
|
<h3 class="find-team-page-header">{{ _("Find your team") }}…</h3>
|
||||||
|
</p>
|
||||||
|
{% if emails %}
|
||||||
|
<div id="results">
|
||||||
|
<p>
|
||||||
|
We have checked the following email address(es). Any email address
|
||||||
|
not associated with Zulip was not sent the sign-in information.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{% for email in emails %}
|
||||||
|
<li>{{ email }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="find-team-form">
|
||||||
|
<p>
|
||||||
|
We will send you an email with the sign-in information against
|
||||||
|
any email address associated with Zulip.
|
||||||
|
</p>
|
||||||
|
<form class="form-inline" id="find_my_team" name="email_form"
|
||||||
|
action="{{ current_url() }}" method="post">
|
||||||
|
{{ csrf_input }}
|
||||||
|
<input type="text" class="form-control required"
|
||||||
|
placeholder="{{ _("Enter email addresses") }}"
|
||||||
|
id="emails" name="emails"/>
|
||||||
|
<button type="submit" class="btn btn-primary">{{ _('Find team') }}</button>
|
||||||
|
<div><i>{{ form.emails.help_text }}</i></div>
|
||||||
|
</form>
|
||||||
|
<div id="errors"></div>
|
||||||
|
{% if form.emails.errors %}
|
||||||
|
{% for error in form.emails.errors %}
|
||||||
|
<div class="alert alert-error">{{ error }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="footer-padder"></div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -84,6 +84,11 @@
|
||||||
<li><span class="little-bullet">·</span></li>
|
<li><span class="little-bullet">·</span></li>
|
||||||
<li><a href="{{ server_uri }}/create_realm">{{ _('Create new organization') }}</a></li>
|
<li><a href="{{ server_uri }}/create_realm">{{ _('Create new organization') }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if find_team_link_disabled %}
|
||||||
|
{% else %}
|
||||||
|
<li><span class="little-bullet">·</span></li>
|
||||||
|
<li><a href="{{ server_uri }}/find-my-team">{{ _('Find my team') }}</a></li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -60,6 +60,7 @@ def add_settings(request):
|
||||||
'github_auth_enabled': github_auth_enabled(realm),
|
'github_auth_enabled': github_auth_enabled(realm),
|
||||||
'development_environment': settings.DEVELOPMENT,
|
'development_environment': settings.DEVELOPMENT,
|
||||||
'support_email': settings.ZULIP_ADMINISTRATOR,
|
'support_email': settings.ZULIP_ADMINISTRATOR,
|
||||||
|
'find_team_link_disabled': settings.FIND_TEAM_LINK_DISABLED,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ from django.contrib.auth.forms import SetPasswordForm, AuthenticationForm, \
|
||||||
PasswordResetForm
|
PasswordResetForm
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.core.validators import validate_email
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from jinja2 import Markup as mark_safe
|
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)))
|
(user_profile.email, get_subdomain(self.request)))
|
||||||
raise ValidationError(mark_safe(WRONG_SUBDOMAIN_ERROR))
|
raise ValidationError(mark_safe(WRONG_SUBDOMAIN_ERROR))
|
||||||
return email
|
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
|
||||||
|
|
|
@ -154,6 +154,7 @@ class TemplateTestCase(ZulipTestCase):
|
||||||
full_name=get_form_value('John Doe'),
|
full_name=get_form_value('John Doe'),
|
||||||
terms=get_form_value(True),
|
terms=get_form_value(True),
|
||||||
email=get_form_value(email),
|
email=get_form_value(email),
|
||||||
|
emails=get_form_value(email),
|
||||||
),
|
),
|
||||||
current_url=lambda: 'www.zulip.com',
|
current_url=lambda: 'www.zulip.com',
|
||||||
hubot_lozenges_dict={},
|
hubot_lozenges_dict={},
|
||||||
|
|
|
@ -2304,3 +2304,47 @@ class TestLoginPage(ZulipTestCase):
|
||||||
# type: () -> None
|
# type: () -> None
|
||||||
result = self.client_get("/login/?subdomain=1")
|
result = self.client_get("/login/?subdomain=1")
|
||||||
self.assertIn(WRONG_SUBDOMAIN_ERROR, result.content.decode('utf8'))
|
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'))
|
||||||
|
|
|
@ -12,12 +12,17 @@ from django.shortcuts import redirect
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.core import signing
|
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 six.moves import urllib
|
||||||
from typing import Any, Dict, Optional, Tuple, Text
|
from typing import Any, Dict, Optional, Tuple, Text
|
||||||
|
|
||||||
from confirmation.models import Confirmation
|
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.request import REQ, has_request_variables, JsonableError
|
||||||
from zerver.lib.response import json_success, json_error
|
from zerver.lib.response import json_success, json_error
|
||||||
from zerver.lib.utils import get_subdomain
|
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
|
redirect_and_log_into_subdomain
|
||||||
from zproject.backends import password_auth_enabled, dev_auth_enabled, google_auth_enabled
|
from zproject.backends import password_auth_enabled, dev_auth_enabled, google_auth_enabled
|
||||||
from zproject.jinja2 import render_to_response
|
from zproject.jinja2 import render_to_response
|
||||||
|
from zerver.lib.notifications import send_future_email
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
|
@ -34,6 +40,61 @@ import logging
|
||||||
import requests
|
import requests
|
||||||
import time
|
import time
|
||||||
import ujson
|
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=''):
|
def maybe_send_to_registration(request, email, full_name=''):
|
||||||
# type: (HttpRequest, Text, Text) -> HttpResponse
|
# type: (HttpRequest, Text, Text) -> HttpResponse
|
||||||
|
|
|
@ -157,6 +157,7 @@ DEFAULT_SETTINGS = {'TWITTER_CONSUMER_KEY': '',
|
||||||
'REGISTER_LINK_DISABLED': False,
|
'REGISTER_LINK_DISABLED': False,
|
||||||
'LOGIN_LINK_DISABLED': False,
|
'LOGIN_LINK_DISABLED': False,
|
||||||
'ABOUT_LINK_DISABLED': False,
|
'ABOUT_LINK_DISABLED': False,
|
||||||
|
'FIND_TEAM_LINK_DISABLED': True,
|
||||||
'CUSTOM_LOGO_URL': None,
|
'CUSTOM_LOGO_URL': None,
|
||||||
'VERBOSE_SUPPORT_OFFERS': False,
|
'VERBOSE_SUPPORT_OFFERS': False,
|
||||||
'STATSD_HOST': '',
|
'STATSD_HOST': '',
|
||||||
|
|
|
@ -131,6 +131,7 @@ i18n_urls = [
|
||||||
name='landing-page'),
|
name='landing-page'),
|
||||||
url(r'^new-user/$', RedirectView.as_view(url='/hello', permanent=True)),
|
url(r'^new-user/$', RedirectView.as_view(url='/hello', permanent=True)),
|
||||||
url(r'^features/$', TemplateView.as_view(template_name='zerver/features.html')),
|
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
|
# If a Terms of Service is supplied, add that route
|
||||||
|
|
Loading…
Reference in New Issue