Merge account registration fixes branch

- Legacy-Id: 11169
This commit is contained in:
Ole Laursen 2016-05-05 15:44:47 +00:00
commit 793bc3c2fa
17 changed files with 642 additions and 565 deletions

View file

@ -1,235 +1,96 @@
import datetime import re
import hashlib
import subprocess
from django import forms from django import forms
from django.forms import ModelForm from django.forms import ModelForm
from django.conf import settings from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.sites.models import Site from django.utils.html import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.core.urlresolvers import reverse as urlreverse
from ietf.utils.mail import send_mail from ietf.person.models import Person, Email
from ietf.person.models import Person, Email, Alias
from ietf.group.models import Role
class RegistrationForm(forms.Form): class RegistrationForm(forms.Form):
email = forms.EmailField(label="Your email (lowercase)") email = forms.EmailField(label="Your email (lowercase)")
realm = 'IETF'
expire = 3
def save(self, *args, **kwargs):
# why is there a save when it doesn't save?
self.send_email()
return True
def send_email(self):
domain = Site.objects.get_current().domain
subject = 'Confirm registration at %s' % domain
from_email = settings.DEFAULT_FROM_EMAIL
to_email = self.cleaned_data['email']
today = datetime.date.today().strftime('%Y%m%d')
auth = hashlib.md5('%s%s%s%s' % (settings.SECRET_KEY, today, to_email, self.realm)).hexdigest()
context = {
'domain': domain,
'today': today,
'realm': self.realm,
'auth': auth,
'username': to_email,
'expire': settings.DAYS_TO_EXPIRE_REGISTRATION_LINK,
}
send_mail(self.request, to_email, from_email, subject, 'registration/creation_email.txt', context)
def clean_email(self): def clean_email(self):
email = self.cleaned_data.get('email', '') email = self.cleaned_data.get('email', '')
if not email: if not email:
return email return email
if email.lower() != email: if email.lower() != email:
raise forms.ValidationError(_('The supplied address contained uppercase letters. Please use a lowercase email address.')) raise forms.ValidationError('The supplied address contained uppercase letters. Please use a lowercase email address.')
if User.objects.filter(username=email).count(): if User.objects.filter(username=email).exists():
raise forms.ValidationError(_('An account with the email address you provided already exists.')) raise forms.ValidationError('An account with the email address you provided already exists.')
return email
class RecoverPasswordForm(RegistrationForm):
realm = 'IETF'
def send_email(self):
domain = Site.objects.get_current().domain
subject = 'Password reset at %s' % domain
from_email = settings.DEFAULT_FROM_EMAIL
today = datetime.date.today().strftime('%Y%m%d')
to_email = self.cleaned_data['email']
today = datetime.date.today().strftime('%Y%m%d')
auth = hashlib.md5('%s%s%s%s' % (settings.SECRET_KEY, today, to_email, self.realm)).hexdigest()
context = {
'domain': domain,
'today': today,
'realm': self.realm,
'auth': auth,
'username': to_email,
'expire': settings.DAYS_TO_EXPIRE_REGISTRATION_LINK,
}
send_mail(self.request, to_email, from_email, subject, 'registration/password_reset_email.txt', context)
def clean_email(self):
email = self.cleaned_data.get('email', '')
return email return email
class PasswordForm(forms.Form): class PasswordForm(forms.Form):
password = forms.CharField(widget=forms.PasswordInput)
password_confirmation = forms.CharField(widget=forms.PasswordInput,
help_text="Enter the same password as above, for verification.")
password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput) def clean_password_confirmation(self):
password2 = forms.CharField(label=_("Password confirmation"), widget=forms.PasswordInput, password = self.cleaned_data.get("password", "")
help_text=_("Enter the same password as above, for verification.")) password_confirmation = self.cleaned_data["password_confirmation"]
if password != password_confirmation:
raise forms.ValidationError("The two password fields didn't match.")
return password_confirmation
def __init__(self, *args, **kwargs):
self.username = kwargs.pop('username')
self.update_user = User.objects.filter(username=self.username).count() > 0
super(PasswordForm, self).__init__(*args, **kwargs)
def clean_password2(self): def ascii_cleaner(supposedly_ascii):
password1 = self.cleaned_data.get("password1", "") outside_printable_ascii_pattern = r'[^\x20-\x7F]'
password2 = self.cleaned_data["password2"] if re.search(outside_printable_ascii_pattern, supposedly_ascii):
if password1 != password2: raise forms.ValidationError("Please only enter ASCII characters.")
raise forms.ValidationError(_("The two password fields didn't match.")) return supposedly_ascii
return password2
def get_password(self): class PersonForm(ModelForm):
return self.cleaned_data.get('password1') class Meta:
model = Person
exclude = ('time', 'user')
def create_user(self): def clean_ascii(self):
user = User.objects.create(username=self.username, return ascii_cleaner(self.cleaned_data.get("ascii") or u"")
email=self.username)
email = Email.objects.filter(address=self.username)
person = None
if email.count():
email = email[0]
if email.person:
person = email.person
else:
email = None
if not person:
person = Person.objects.create(user=user,
name=self.username,
ascii=self.username)
if not email:
email = Email.objects.create(address=self.username,
person=person)
email.person = person
email.save()
person.user = user
person.save()
return user
def get_user(self): def clean_ascii_short(self):
return User.objects.get(username=self.username) return ascii_cleaner(self.cleaned_data.get("ascii_short") or u"")
def save_password_file(self):
if getattr(settings, 'USE_PYTHON_HTDIGEST', None):
pass_file = settings.HTPASSWD_FILE
realm = settings.HTDIGEST_REALM
password = self.get_password()
username = self.username
prefix = '%s:%s:' % (username, realm)
key = hashlib.md5(prefix + password).hexdigest()
f = open(pass_file, 'r+')
pos = f.tell()
line = f.readline()
while line:
if line.startswith(prefix):
break
pos=f.tell()
line = f.readline()
f.seek(pos)
f.write('%s%s\n' % (prefix, key))
f.close()
else:
p = subprocess.Popen([settings.HTPASSWD_COMMAND, "-b", settings.HTPASSWD_FILE, self.username, self.get_password()], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = p.communicate()
def save(self): class NewEmailForm(forms.Form):
if self.update_user: new_email = forms.EmailField(label="New email address", required=False)
user = self.get_user()
else: def clean_new_email(self):
user = self.create_user() email = self.cleaned_data.get("new_email", "")
user.set_password(self.get_password()) if email:
user.save() existing = Email.objects.filter(address=email).first()
self.save_password_file() if existing:
return user raise forms.ValidationError("Email address '%s' is already assigned to account '%s' (%s)" % (existing, existing.person and existing.person.user, existing.person))
return email
class RoleEmailForm(forms.Form):
email = forms.ModelChoiceField(label="Role email", queryset=Email.objects.all())
def __init__(self, role, *args, **kwargs):
super(RoleEmailForm, self).__init__(*args, **kwargs)
f = self.fields["email"]
f.label = u"%s in %s" % (role.name, role.group.acronym.upper())
f.help_text = u"Email to use for <i>%s</i> role in %s" % (role.name, role.group.name)
f.queryset = f.queryset.filter(models.Q(person=role.person_id) | models.Q(role=role))
f.initial = role.email_id
f.choices = [(e.pk, e.address if e.active else u"({})".format(e.address)) for e in f.queryset]
class ResetPasswordForm(forms.Form):
email = forms.EmailField(label="Your email (lowercase)")
def clean_email(self):
email = self.cleaned_data["email"]
if not User.objects.filter(username=email).exists():
raise forms.ValidationError(mark_safe("Didn't find a matching account. If you don't have an account yet, you can <a href=\"{}\">create one</a>.".format(urlreverse("create_account"))))
return email
class TestEmailForm(forms.Form): class TestEmailForm(forms.Form):
email = forms.EmailField(required=False) email = forms.EmailField(required=False)
class PersonForm(ModelForm):
request = None
new_emails = []
class Meta:
model = Person
exclude = ('time','user')
def confirm_address(self,email):
person = self.instance
domain = Site.objects.get_current().domain
user = person.user
if len(email) == 0:
return
subject = 'Confirm email address for %s' % person.name
from_email = settings.DEFAULT_FROM_EMAIL
to_email = email
today = datetime.date.today().strftime('%Y%m%d')
auth = hashlib.md5('%s%s%s%s' % (settings.SECRET_KEY, today, to_email, user)).hexdigest()
context = {
'today': today,
'domain': domain,
'user': user,
'email': email,
'expire': settings.DAYS_TO_EXPIRE_REGISTRATION_LINK,
'auth': auth,
}
send_mail(self.request, to_email, from_email, subject, 'registration/add_email_email.txt', context)
def save(self, force_insert=False, force_update=False, commit=True):
m = super(PersonForm, self).save(commit=False)
self.new_emails = [v for k,v in self.data.items() if k[:10] == u'new_email_' and u'@' in v]
for email in self.new_emails:
self.confirm_address(email)
# Process email active flags
emails = Email.objects.filter(person=self.instance)
for email in emails:
email.active = self.data.__contains__(email.address)
if commit:
email.save()
# Process email for roles
for k,v in self.data.items():
if k[:11] == u'role_email_':
role = Role.objects.get(id=k[11:])
email = Email.objects.get(address = v)
role.email = email
if commit:
role.save()
# Make sure the alias table contains any new and/or old names.
old_names = set([x.name for x in Alias.objects.filter(person=self.instance)])
curr_names = set([x for x in [self.instance.name,
self.instance.ascii,
self.instance.ascii_short,
self.data['name'],
self.data['ascii'],
self.data['ascii_short']] if len(x)])
new_names = curr_names - old_names
for name in new_names:
alias = Alias(person=self.instance,name=name)
if commit:
alias.save()
if commit:
m.save()
return m

24
ietf/ietfauth/htpasswd.py Normal file
View file

@ -0,0 +1,24 @@
import subprocess, hashlib
from django.conf import settings
def save_htpasswd_file(username, password):
if getattr(settings, 'USE_PYTHON_HTDIGEST', None):
pass_file = settings.HTPASSWD_FILE
realm = settings.HTDIGEST_REALM
prefix = '%s:%s:' % (username, realm)
key = hashlib.md5(prefix + password).hexdigest()
f = open(pass_file, 'r+')
pos = f.tell()
line = f.readline()
while line:
if line.startswith(prefix):
break
pos=f.tell()
line = f.readline()
f.seek(pos)
f.write('%s%s\n' % (prefix, key))
f.close()
else:
p = subprocess.Popen([settings.HTPASSWD_COMMAND, "-b", settings.HTPASSWD_FILE, username, password], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = p.communicate()

View file

@ -1,83 +1,262 @@
# Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). # -*- coding: utf-8 -*-
# All rights reserved. Contact: Pasi Eronen <pasi.eronen@nokia.com> import os, shutil
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# * Neither the name of the Nokia Corporation and/or its
# subsidiary(-ies) nor the names of its contributors may be used
# to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from urlparse import urlsplit from urlparse import urlsplit
from pyquery import PyQuery
from django.core.urlresolvers import reverse as urlreverse from django.core.urlresolvers import reverse as urlreverse
from django.contrib.auth.models import User
from django.conf import settings
from ietf.utils.test_utils import TestCase, login_testing_unauthorized, unicontent from ietf.utils.test_utils import TestCase, login_testing_unauthorized, unicontent
from ietf.utils.test_data import make_test_data from ietf.utils.test_data import make_test_data
from ietf.utils.mail import outbox, empty_outbox
from ietf.person.models import Person, Email
from ietf.group.models import Group, Role, RoleName
class IetfAuthTests(TestCase): class IetfAuthTests(TestCase):
def setUp(self):
self.saved_htpasswd_file = settings.HTPASSWD_FILE
self.htpasswd_dir = os.path.abspath("tmp-htpasswd-dir")
os.mkdir(self.htpasswd_dir)
settings.HTPASSWD_FILE = os.path.join(self.htpasswd_dir, "htpasswd")
open(settings.HTPASSWD_FILE, 'a').close() # create empty file
self.saved_htdigest_realm = getattr(settings, "HTDIGEST_REALM", None)
settings.HTDIGEST_REALM = "test-realm"
def tearDown(self):
shutil.rmtree(self.htpasswd_dir)
settings.HTPASSWD_FILE = self.saved_htpasswd_file
settings.HTDIGEST_REALM = self.saved_htdigest_realm
def test_index(self): def test_index(self):
self.assertEqual(self.client.get(urlreverse("ietf.ietfauth.views.index")).status_code, 200) self.assertEqual(self.client.get(urlreverse("ietf.ietfauth.views.index")).status_code, 200)
def test_login(self): def test_login_and_logout(self):
make_test_data() make_test_data()
# try logging in without a next # try logging in without a next
r = self.client.get('/accounts/login/') r = self.client.get(urlreverse("account_login"))
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
r = self.client.post('/accounts/login/', {"username":"plain", "password":"plain+password"}) r = self.client.post(urlreverse("account_login"), {"username":"plain", "password":"plain+password"})
self.assertEqual(r.status_code, 302) self.assertEqual(r.status_code, 302)
self.assertEqual(urlsplit(r["Location"])[2], "/accounts/profile/") self.assertEqual(urlsplit(r["Location"])[2], "/accounts/profile/")
# try logging out # try logging out
r = self.client.get('/accounts/logout/') r = self.client.get(urlreverse("account_logout"))
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
r = self.client.get('/accounts/profile/') r = self.client.get(urlreverse("account_profile"))
self.assertEqual(r.status_code, 302) self.assertEqual(r.status_code, 302)
self.assertEqual(urlsplit(r["Location"])[2], "/accounts/login/") self.assertEqual(urlsplit(r["Location"])[2], "/accounts/login/")
# try logging in with a next # try logging in with a next
r = self.client.post('/accounts/login/?next=/foobar', {"username":"plain", "password":"plain+password"}) r = self.client.post(urlreverse("account_login") + "?next=/foobar", {"username":"plain", "password":"plain+password"})
self.assertEqual(r.status_code, 302) self.assertEqual(r.status_code, 302)
self.assertEqual(urlsplit(r["Location"])[2], "/foobar") self.assertEqual(urlsplit(r["Location"])[2], "/foobar")
def extract_confirm_url(self, confirm_email):
# dig out confirm_email link
msg = confirm_email.get_payload(decode=True)
line_start = "http"
confirm_url = None
for line in msg.split("\n"):
if line.strip().startswith(line_start):
confirm_url = line.strip()
self.assertTrue(confirm_url)
return confirm_url
def username_in_htpasswd_file(self, username):
with open(settings.HTPASSWD_FILE) as f:
for l in f:
if l.startswith(username + ":" + settings.HTDIGEST_REALM):
return True
return False
def test_create_account(self):
make_test_data()
url = urlreverse('create_account')
# get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# register email
email = 'new-account@example.com'
empty_outbox()
r = self.client.post(url, { 'email': email })
self.assertEqual(r.status_code, 200)
self.assertTrue("Account created" in unicontent(r))
self.assertEqual(len(outbox), 1)
# go to confirm page
confirm_url = self.extract_confirm_url(outbox[-1])
r = self.client.get(confirm_url)
self.assertEqual(r.status_code, 200)
# password mismatch
r = self.client.post(confirm_url, { 'password': 'secret', 'password_confirmation': 'nosecret' })
self.assertEqual(r.status_code, 200)
self.assertEqual(User.objects.filter(username=email).count(), 0)
# confirm
r = self.client.post(confirm_url, { 'password': 'secret', 'password_confirmation': 'secret' })
self.assertEqual(r.status_code, 200)
self.assertEqual(User.objects.filter(username=email).count(), 1)
self.assertEqual(Person.objects.filter(user__username=email).count(), 1)
self.assertEqual(Email.objects.filter(person__user__username=email).count(), 1)
self.assertTrue(self.username_in_htpasswd_file(email))
def test_profile(self): def test_profile(self):
make_test_data() make_test_data()
url = urlreverse('ietf.ietfauth.views.profile') username = "plain"
login_testing_unauthorized(self, "plain", url) email_address = Email.objects.filter(person__user__username=username).first().address
url = urlreverse('account_profile')
login_testing_unauthorized(self, username, url)
# get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('.form-control-static:contains("%s")' % username)), 1)
self.assertEqual(len(q('[name="active_emails"][value="%s"][checked]' % email_address)), 1)
base_data = {
"name": u"Test Nãme",
"ascii": u"Test Name",
"ascii_short": u"T. Name",
"address": "Test address",
"affiliation": "Test Org",
"active_emails": email_address,
}
# edit details - faulty ASCII
faulty_ascii = base_data.copy()
faulty_ascii["ascii"] = u"Test Nãme"
r = self.client.post(url, faulty_ascii)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q("form .has-error")) > 0)
# edit details
r = self.client.post(url, base_data)
self.assertEqual(r.status_code, 200)
person = Person.objects.get(user__username=username)
self.assertEqual(person.name, u"Test Nãme")
self.assertEqual(person.ascii, u"Test Name")
self.assertEqual(Person.objects.filter(alias__name=u"Test Name", user__username=username).count(), 1)
self.assertEqual(Person.objects.filter(alias__name=u"Test Nãme", user__username=username).count(), 1)
self.assertEqual(Email.objects.filter(address=email_address, person__user__username=username, active=True).count(), 1)
# deactivate address
without_email_address = { k: v for k, v in base_data.iteritems() if k != "active_emails" }
r = self.client.post(url, without_email_address)
self.assertEqual(r.status_code, 200)
self.assertEqual(Email.objects.filter(address=email_address, person__user__username="plain", active=True).count(), 0)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('[name="%s"][checked]' % email_address)), 0)
# add email address
empty_outbox()
new_email_address = "plain2@example.com"
with_new_email_address = base_data.copy()
with_new_email_address["new_email"] = new_email_address
r = self.client.post(url, with_new_email_address)
self.assertEqual(r.status_code, 200)
self.assertEqual(len(outbox), 1)
# confirm new email address
confirm_url = self.extract_confirm_url(outbox[-1])
r = self.client.get(confirm_url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('[name="action"][value=confirm]')), 1)
r = self.client.post(confirm_url, { "action": "confirm" })
self.assertEqual(r.status_code, 200)
self.assertEqual(Email.objects.filter(address=new_email_address, person__user__username=username, active=1).count(), 1)
# check that we can't re-add it - that would give a duplicate
r = self.client.get(confirm_url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('[name="action"][value="confirm"]')), 0)
# change role email
role = Role.objects.create(
person=Person.objects.get(user__username=username),
email=Email.objects.get(address=email_address),
name=RoleName.objects.get(slug="chair"),
group=Group.objects.get(acronym="mars"),
)
role_email_input_name = "role_%s-email" % role.pk
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('[name="%s"]' % role_email_input_name)), 1)
with_changed_role_email = base_data.copy()
with_changed_role_email["active_emails"] = new_email_address
with_changed_role_email[role_email_input_name] = new_email_address
r = self.client.post(url, with_changed_role_email)
self.assertEqual(r.status_code, 200)
updated_roles = Role.objects.filter(person=role.person, name=role.name, group=role.group)
self.assertEqual(len(updated_roles), 1)
self.assertEqual(updated_roles[0].email_id, new_email_address)
def test_reset_password(self):
url = urlreverse('password_reset')
user = User.objects.create(username="someone@example.com", email="someone@example.com")
user.set_password("forgotten")
user.save()
p = Person.objects.create(name="Some One", ascii="Some One", user=user)
Email.objects.create(address=user.username, person=p)
# get # get
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertTrue("plain" in unicontent(r))
# post # ask for reset, wrong username
# ... fill in r = self.client.post(url, { 'email': "nobody@example.com" })
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q("form .has-error")) > 0)
# we're missing tests of the other views # ask for reset
empty_outbox()
r = self.client.post(url, { 'email': user.username })
self.assertEqual(r.status_code, 200)
self.assertEqual(len(outbox), 1)
# go to change password page
confirm_url = self.extract_confirm_url(outbox[-1])
r = self.client.get(confirm_url)
self.assertEqual(r.status_code, 200)
# password mismatch
r = self.client.post(confirm_url, { 'password': 'secret', 'password_confirmation': 'nosecret' })
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q("form .has-error")) > 0)
# confirm
r = self.client.post(confirm_url, { 'password': 'secret', 'password_confirmation': 'secret' })
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q("form .has-error")), 0)
self.assertTrue(self.username_in_htpasswd_file(user.username))

View file

@ -6,17 +6,16 @@ from django.contrib.auth.views import login, logout
urlpatterns = patterns('ietf.ietfauth.views', urlpatterns = patterns('ietf.ietfauth.views',
url(r'^$', 'index', name='account_index'), url(r'^$', 'index', name='account_index'),
# url(r'^login/$', 'ietf_login'), # url(r'^login/$', 'ietf_login'),
url(r'^login/$', login), url(r'^login/$', login, name="account_login"),
url(r'^logout/$', logout), url(r'^logout/$', logout, name="account_logout"),
# url(r'^loggedin/$', 'ietf_loggedin'), # url(r'^loggedin/$', 'ietf_loggedin'),
# url(r'^loggedout/$', 'logged_out'), # url(r'^loggedout/$', 'logged_out'),
url(r'^profile/$', 'profile'), url(r'^profile/$', 'profile', name="account_profile"),
# (r'^login/(?P<user>[a-z0-9.@]+)/(?P<passwd>.+)$', 'url_login'), # (r'^login/(?P<user>[a-z0-9.@]+)/(?P<passwd>.+)$', 'url_login'),
url(r'^testemail/$', 'test_email'), url(r'^testemail/$', 'test_email'),
url(r'^create/$', 'create_account', name='create_account'), url(r'^create/$', 'create_account', name='create_account'),
url(r'^confirm/(?P<username>[\w.@+-]+)/(?P<date>[\d]+)/(?P<realm>[\w]+)/(?P<hash>[a-f0-9]+)/$', 'confirm_account', name='confirm_account'), url(r'^create/confirm/(?P<auth>[^/]+)/$', 'confirm_account', name='confirm_account'),
url(r'^reset/$', 'password_reset_view', name='password_reset'), url(r'^reset/$', 'password_reset', name='password_reset'),
url(r'^reset/confirm/(?P<username>[\w.@+-]+)/(?P<date>[\d]+)/(?P<realm>[\w]+)/(?P<hash>[a-f0-9]+)/$', 'confirm_password_reset', name='confirm_password_reset'), url(r'^reset/confirm/(?P<auth>[^/]+)/$', 'confirm_password_reset', name='confirm_password_reset'),
url(r'^add_email/confirm/(?P<username>[\w.@+-]+)/(?P<date>[\d]+)/(?P<email>[\w.@+-]+)/(?P<hash>[a-f0-9]+)/$', 'confirm_new_email', name='confirm_new_email'), url(r'^confirmnewemail/(?P<auth>[^/]+)/$', 'confirm_new_email', name='confirm_new_email'),
# url(r'^ajax/check_username/$', 'ajax_check_username', name='ajax_check_username'),
) )

View file

@ -138,4 +138,3 @@ def is_authorized_in_doc_stream(user, doc):
group_req = Q() group_req = Q()
return Role.objects.filter(Q(name__in=("chair", "secr", "delegate", "auth"), person__user=user) & group_req).exists() return Role.objects.filter(Q(name__in=("chair", "secr", "delegate", "auth"), person__user=user) & group_req).exists()

View file

@ -32,27 +32,25 @@
# Copyright The IETF Trust 2007, All Rights Reserved # Copyright The IETF Trust 2007, All Rights Reserved
import datetime
import hashlib
#import json
from django.conf import settings from django.conf import settings
from django.template import RequestContext
from django.http import Http404 #, HttpResponse, HttpResponseRedirect from django.http import Http404 #, HttpResponse, HttpResponseRedirect
from django.shortcuts import render_to_response from django.shortcuts import render, redirect, get_object_or_404
#from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login #from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
#from django.contrib.auth.models import User
#from django.utils.http import urlquote #from django.utils.http import urlquote
#from django.utils.translation import ugettext as _ import django.core.signing
from django.core.exceptions import ValidationError from django.contrib.sites.models import Site
from django.contrib.auth.models import User
from ietf.group.models import Role from ietf.group.models import Role
from ietf.ietfauth.forms import RegistrationForm, PasswordForm, RecoverPasswordForm, TestEmailForm, PersonForm from ietf.ietfauth.forms import RegistrationForm, PasswordForm, ResetPasswordForm, TestEmailForm
from ietf.person.models import Person, Email from ietf.ietfauth.forms import PersonForm, RoleEmailForm, NewEmailForm
from ietf.ietfauth.htpasswd import save_htpasswd_file
from ietf.person.models import Person, Email, Alias
from ietf.utils.mail import send_mail
def index(request): def index(request):
return render_to_response('registration/index.html', context_instance=RequestContext(request)) return render(request, 'registration/index.html')
# def url_login(request, user, passwd): # def url_login(request, user, passwd):
# user = authenticate(username=user, password=passwd) # user = authenticate(username=user, password=passwd)
@ -81,154 +79,256 @@ def index(request):
# redirect_to = settings.LOGIN_REDIRECT_URL # redirect_to = settings.LOGIN_REDIRECT_URL
# return HttpResponseRedirect(redirect_to) # return HttpResponseRedirect(redirect_to)
def create_account(request):
to_email = None
if request.method == 'POST':
form = RegistrationForm(request.POST)
if form.is_valid():
to_email = form.cleaned_data['email']
auth = django.core.signing.dumps(to_email, salt="create_account")
domain = Site.objects.get_current().domain
subject = 'Confirm registration at %s' % domain
from_email = settings.DEFAULT_FROM_EMAIL
send_mail(request, to_email, from_email, subject, 'registration/creation_email.txt', {
'domain': domain,
'auth': auth,
'username': to_email,
'expire': settings.DAYS_TO_EXPIRE_REGISTRATION_LINK,
})
else:
form = RegistrationForm()
return render(request, 'registration/create.html', {
'form': form,
'to_email': to_email,
})
def confirm_account(request, auth):
try:
email = django.core.signing.loads(auth, salt="create_account", max_age=settings.DAYS_TO_EXPIRE_REGISTRATION_LINK * 24 * 60 * 60)
except django.core.signing.BadSignature:
raise Http404("Invalid or expired auth")
if User.objects.filter(username=email).exists():
return redirect("account_profile")
success = False
if request.method == 'POST':
form = PasswordForm(request.POST)
if form.is_valid():
password = form.cleaned_data["password"]
user = User.objects.create(username=email, email=email)
user.set_password(password)
user.save()
# password is also stored in htpasswd file
save_htpasswd_file(email, password)
# make sure the rest of the person infrastructure is
# well-connected
email_obj = Email.objects.filter(address=email).first()
person = None
if email_obj and email_obj.person:
person = email_obj.person
if not person:
person = Person.objects.create(user=user,
name=email,
ascii=email)
if not email_obj:
email_obj = Email.objects.create(address=email, person=person)
else:
if not email_obj.person:
email_obj.person = person
email_obj.save()
person.user = user
person.save()
success = True
else:
form = PasswordForm()
return render(request, 'registration/confirm_account.html', {
'form': form,
'email': email,
'success': success,
})
@login_required @login_required
def profile(request): def profile(request):
roles = [] roles = []
person = None person = None
try: try:
person = request.user.person person = request.user.person
except Person.DoesNotExist: except Person.DoesNotExist:
return render_to_response('registration/missing_person.html', context_instance=RequestContext(request)) return render(request, 'registration/missing_person.html')
roles = Role.objects.filter(person=person, group__state='active').order_by('name__name', 'group__name')
emails = Email.objects.filter(person=person).order_by('-active','-time')
new_email_forms = []
if request.method == 'POST': if request.method == 'POST':
form = PersonForm(request.POST, instance=person) person_form = PersonForm(request.POST, instance=person)
success = False for r in roles:
new_emails = None r.email_form = RoleEmailForm(r, request.POST, prefix="role_%s" % r.pk)
error = None
if form.is_valid(): for e in request.POST.getlist("new_email", []):
try: new_email_forms.append(NewEmailForm({ "new_email": e }))
form.save()
success = True forms_valid = [person_form.is_valid()] + [r.email_form.is_valid() for r in roles] + [f.is_valid() for f in new_email_forms]
new_emails = form.new_emails
except Exception as e: email_confirmations = []
error = e
if all(forms_valid):
return render_to_response('registration/confirm_profile_update.html', updated_person = person_form.save()
{ 'success': success, 'new_emails': new_emails, 'error': error} ,
context_instance=RequestContext(request)) for f in new_email_forms:
to_email = f.cleaned_data["new_email"]
if not to_email:
continue
email_confirmations.append(to_email)
auth = django.core.signing.dumps([person.user.username, to_email], salt="add_email")
domain = Site.objects.get_current().domain
subject = u'Confirm email address for %s' % person.name
from_email = settings.DEFAULT_FROM_EMAIL
send_mail(request, to_email, from_email, subject, 'registration/add_email_email.txt', {
'domain': domain,
'auth': auth,
'email': to_email,
'person': person,
'expire': settings.DAYS_TO_EXPIRE_REGISTRATION_LINK,
})
for r in roles:
e = r.email_form.cleaned_data["email"]
if r.email_id != e.pk:
r.email = e
r.save()
active_emails = request.POST.getlist("active_emails", [])
for email in emails:
email.active = email.pk in active_emails
email.save()
# Make sure the alias table contains any new and/or old names.
existing_aliases = set(Alias.objects.filter(person=person).values_list("name", flat=True))
curr_names = set(x for x in [updated_person.name, updated_person.ascii, updated_person.ascii_short] if x)
new_aliases = curr_names - existing_aliases
for name in new_aliases:
Alias.objects.create(person=updated_person, name=name)
return render(request, 'registration/confirm_profile_update.html', {
'email_confirmations': email_confirmations,
})
else: else:
roles = Role.objects.filter(person=person,group__state='active').order_by('name__name','group__name') for r in roles:
emails = Email.objects.filter(person=person).order_by('-active','-time') r.email_form = RoleEmailForm(r, prefix="role_%s" % r.pk)
person_form = PersonForm(instance=person) person_form = PersonForm(instance=person)
return render_to_response('registration/edit_profile.html', return render(request, 'registration/edit_profile.html', {
{ 'user': request.user, 'emails': emails, 'person': person, 'user': request.user,
'roles': roles, 'person_form': person_form } , 'person': person,
context_instance=RequestContext(request)) 'person_form': person_form,
'roles': roles,
def confirm_new_email(request, username, date, email, hash): 'emails': emails,
valid = hashlib.md5('%s%s%s%s' % (settings.SECRET_KEY, date, email, username)).hexdigest() == hash 'new_email_forms': new_email_forms,
if not valid: })
raise Http404
request_date = datetime.date(int(date[:4]), int(date[4:6]), int(date[6:]))
if datetime.date.today() > (request_date + datetime.timedelta(days=settings.DAYS_TO_EXPIRE_REGISTRATION_LINK)):
raise Http404
success = False
person = None
error = None
new_email = None
def confirm_new_email(request, auth):
try: try:
# First, check whether this address exists (to give a more sensible username, email = django.core.signing.loads(auth, salt="add_email", max_age=settings.DAYS_TO_EXPIRE_REGISTRATION_LINK * 24 * 60 * 60)
# error when a duplicate is created). except django.core.signing.BadSignature:
existing_email = Email.objects.get(address=email) raise Http404("Invalid or expired auth")
print existing_email
existing_person = existing_email.person
print existing_person
error = {'address': ["Email address '%s' is already assigned to user '%s' (%s)" %
(email, existing_person.user, existing_person.name)]}
except Exception:
try:
person = Person.objects.get(user__username=username)
new_email = Email(address=email, person=person, active=True, time=datetime.datetime.now())
new_email.full_clean()
new_email.save()
success = True
except Person.DoesNotExist:
error = {'person': ["No such user: %s" % (username)]}
except ValidationError as e:
error = e.message_dict
return render_to_response('registration/confirm_new_email.html', person = get_object_or_404(Person, user__username=username)
{ 'username': username, 'email': email,
'success': success, 'error': error,
'record': new_email},
context_instance=RequestContext(request))
# do another round of validation since the situation may have
# changed since submitting the request
form = NewEmailForm({ "new_email": email })
can_confirm = form.is_valid() and email
new_email_obj = None
if request.method == 'POST' and can_confirm and request.POST.get("action") == "confirm":
new_email_obj = Email.objects.create(address=email, person=person)
def create_account(request): return render(request, 'registration/confirm_new_email.html', {
'username': username,
'email': email,
'can_confirm': can_confirm,
'form': form,
'new_email_obj': new_email_obj,
})
def password_reset(request):
success = False success = False
if request.method == 'POST': if request.method == 'POST':
form = RegistrationForm(request.POST) form = ResetPasswordForm(request.POST)
if form.is_valid(): if form.is_valid():
form.request = request to_email = form.cleaned_data['email']
form.save()
auth = django.core.signing.dumps(to_email, salt="password_reset")
domain = Site.objects.get_current().domain
subject = 'Confirm password reset at %s' % domain
from_email = settings.DEFAULT_FROM_EMAIL
send_mail(request, to_email, from_email, subject, 'registration/password_reset_email.txt', {
'domain': domain,
'auth': auth,
'username': to_email,
'expire': settings.DAYS_TO_EXPIRE_REGISTRATION_LINK,
})
success = True success = True
else: else:
form = RegistrationForm() form = ResetPasswordForm()
return render_to_response('registration/create.html', return render(request, 'registration/password_reset.html', {
{'form': form, 'form': form,
'success': success}, 'success': success,
context_instance=RequestContext(request)) })
def process_confirmation(request, username, date, realm, hash): def confirm_password_reset(request, auth):
valid = hashlib.md5('%s%s%s%s' % (settings.SECRET_KEY, date, username, realm)).hexdigest() == hash try:
if not valid: email = django.core.signing.loads(auth, salt="password_reset", max_age=settings.DAYS_TO_EXPIRE_REGISTRATION_LINK * 24 * 60 * 60)
raise Http404 except django.core.signing.BadSignature:
request_date = datetime.date(int(date[:4]), int(date[4:6]), int(date[6:])) raise Http404("Invalid or expired auth")
if datetime.date.today() > (request_date + datetime.timedelta(days=settings.DAYS_TO_EXPIRE_REGISTRATION_LINK)):
raise Http404 user = get_object_or_404(User, username=email)
success = False success = False
if request.method == 'POST': if request.method == 'POST':
form = PasswordForm(request.POST, username=username) form = PasswordForm(request.POST)
if form.is_valid(): if form.is_valid():
form.save() # Also updates the httpd password file password = form.cleaned_data["password"]
user.set_password(password)
user.save()
# password is also stored in htpasswd file
save_htpasswd_file(user.username, password)
success = True success = True
else: else:
form = PasswordForm(username=username) form = PasswordForm()
return form, username, success
def confirm_account(request, username, date, realm, hash): return render(request, 'registration/change_password.html', {
form, username, success = process_confirmation(request, username, date, realm, hash) 'form': form,
return render_to_response('registration/confirm.html', 'email': email,
{'form': form, 'email': username, 'success': success}, 'success': success,
context_instance=RequestContext(request)) })
def password_reset_view(request):
success = False
if request.method == 'POST':
form = RecoverPasswordForm(request.POST)
if form.is_valid():
form.request = request
form.save()
success = True
else:
form = RecoverPasswordForm()
return render_to_response('registration/password_reset.html',
{'form': form,
'success': success},
context_instance=RequestContext(request))
def confirm_password_reset(request, username, date, realm, hash):
form, username, success = process_confirmation(request, username, date, realm, hash)
return render_to_response('registration/change_password.html',
{'form': form,
'success': success,
'username': username},
context_instance=RequestContext(request))
# def ajax_check_username(request):
# username = request.GET.get('username', '')
# error = False
# if User.objects.filter(username=username).count():
# error = _('This email address is already registered')
# return HttpResponse(json.dumps({'error': error}), content_type='text/plain')
def test_email(request): def test_email(request):
"""Set email address to which email generated in the system will be sent.""" """Set email address to which email generated in the system will be sent."""
if settings.SERVER_MODE == "production": if settings.SERVER_MODE == "production":
@ -249,11 +349,10 @@ def test_email(request):
else: else:
form = TestEmailForm(initial=dict(email=request.COOKIES.get('testmailcc'))) form = TestEmailForm(initial=dict(email=request.COOKIES.get('testmailcc')))
r = render_to_response('ietfauth/testemail.html', r = render(request, 'ietfauth/testemail.html', {
dict(form=form, "form": form,
cookie=cookie if cookie != None else request.COOKIES.get("testmailcc", "") "cookie": cookie if cookie != None else request.COOKIES.get("testmailcc", "")
), })
context_instance=RequestContext(request))
if cookie != None: if cookie != None:
r.set_cookie("testmailcc", cookie) r.set_cookie("testmailcc", cookie)

View file

@ -14,12 +14,15 @@ from ietf.utils.mail import send_mail_preformatted
class PersonInfo(models.Model): class PersonInfo(models.Model):
time = models.DateTimeField(default=datetime.datetime.now) # When this Person record entered the system time = models.DateTimeField(default=datetime.datetime.now) # When this Person record entered the system
name = models.CharField("Full Name (Unicode)", max_length=255, db_index=True) # The normal unicode form of the name. This must be # The normal unicode form of the name. This must be
# set to the same value as the ascii-form if equal. # set to the same value as the ascii-form if equal.
ascii = models.CharField("Full Name (ASCII)", max_length=255) # The normal ascii-form of the name. name = models.CharField("Full Name (Unicode)", max_length=255, db_index=True, help_text="Preferred form of name.")
ascii_short = models.CharField("Abbreviated Name (ASCII)", max_length=32, null=True, blank=True) # The short ascii-form of the name. Also in alias table if non-null # The normal ascii-form of the name.
address = models.TextField(max_length=255, blank=True) ascii = models.CharField("Full Name (ASCII)", max_length=255, help_text="Name as rendered in ASCII (Latin, unaccented) characters.")
affiliation = models.CharField(max_length=255, blank=True) # The short ascii-form of the name. Also in alias table if non-null
ascii_short = models.CharField("Abbreviated Name (ASCII)", max_length=32, null=True, blank=True, help_text="Example: A. Nonymous. Fill in this with initials and surname only if taking the initials and surname of the ASCII name above produces an incorrect initials-only form. (Blank is OK).")
affiliation = models.CharField(max_length=255, blank=True, help_text="Employer, university, sponsor, etc.")
address = models.TextField(max_length=255, blank=True, help_text="Postal mailing address.")
def __unicode__(self): def __unicode__(self):
return self.plain_name() return self.plain_name()

View file

@ -1,12 +1,9 @@
{% autoescape off %} {% autoescape off %}
Hello, Hello,
We have received a request to add the email address '{{ email }}' {% filter wordwrap:73 %}We have received a request to add the email address {{ email }} to the user account '{{ person.user }}' at {{ domain }}. If you requested this change, please confirm that this is your email address by clicking on following link:{% endfilter %}
to the user account '{{ user }}' at '{{ domain }}'.
If you requested this change, please confirm that this is your email
address by clicking on following link:
https://{{ domain }}{% url "confirm_new_email" user today email auth %} https://{{ domain }}{% url "confirm_new_email" auth %}
This link will expire in {{ expire }} days. This link will expire in {{ expire }} days.

View file

@ -18,7 +18,7 @@
{% else %} {% else %}
<h1>Change password</h1> <h1>Change password</h1>
<p>You can change the password below for your user {{ username }} below.</p> <p>You can change the password below for your user {{ email }} below.</p>
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form form %} {% bootstrap_form form %}

View file

@ -12,13 +12,13 @@
{% if success %} {% if success %}
<h1>Account creation successful</h1> <h1>Account creation successful</h1>
<p>Your account with login name {{ email }} has been created, using the password you have selected.</p> <p>Your account with login {{ email }} has been created, using the password you have selected.</p>
<a type="a" class="btn btn-primary" href="/accounts/login/" rel="nofollow">Sign in</a> <a type="a" class="btn btn-primary" href="{% url "account_login" %}" rel="nofollow">Sign in</a>
{% else %} {% else %}
<h1>Complete account creation</h1> <h1>Complete account creation</h1>
<p>In order to complete the setup of your account with login name {{ email }}, please choose a password:</p> <p>In order to complete the setup of your account with login {{ email }}, please choose a password:</p>
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form form %} {% bootstrap_form form %}

View file

@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #} {# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %} {% load origin bootstrap3 %}
{% block title %}Confirm new email address{% endblock %} {% block title %}Confirm new email address{% endblock %}
@ -8,17 +8,28 @@
{% origin %} {% origin %}
<h1>Confirm new email address</h1> <h1>Confirm new email address</h1>
{% if success %} {% if not can_confirm %}
<p>Your account with login name {{ username }} has been updated to include the email address {{ email }}.</p> <p class="alert alert-danger">An error has occured when attempting to add the email address {{ email }} to your account {{ username }}.</p>
<a class="btn btn-default" href="{% url "ietf.ietfauth.views.profile" %}">Edit profile</a>
{% bootstrap_form_errors form %}
<p>
<a class="btn btn-default" href="{% url "account_profile" %}">Edit profile</a>
</p>
{% elif new_email_obj %}
<p>Your account {{ username }} has been updated to include the email address {{ email }}.</p>
<p>
<a class="btn btn-default" href="{% url "account_profile" %}">Edit profile</a>
</p>
{% else %} {% else %}
<p class="alert alert-danger">An error has occured when attempting to add the email address '{{ email }}' to your account '{{ username }}'.<p> <p>Confirm that you want to add the email address {{ email }} to your account {{ username }}.</p>
<ul>
{% for field,msgs in error.items %} <form method="post">
{% for msg in msgs %} {% csrf_token %}
<li><b>{{field}}</b>: {{msg}} {% buttons %}
{% endfor %} <button type="submit" class="btn btn-warning" name="action" value="confirm">Confirm email address</button>
{% endfor %} {% endbuttons %}
</ul> </form>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -2,23 +2,18 @@
{# Copyright The IETF Trust 2015, All Rights Reserved #} {# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %} {% load origin %}
{% block title %}Profile update{% endblock %} {% block title %}Profile update successful{% endblock %}
{% block content %} {% block content %}
{% origin %} {% origin %}
{% if success %}
<h1>Profile update successful</h1>
<p>Your account has been successfully updated to reflect the changes you submitted.</p> <h1>Profile update successful</h1>
{% for email in new_emails %}
<p class="alert alert-info"><b>A confirmation email has been sent to {{email}}.</b> The email will be activated after you click on the link it contains.</p> <p>Your account has been updated to reflect the changes you submitted.</p>
{% endfor %}
<a class="btn btn-default" href="{% url "ietf.ietfauth.views.profile" %}">Edit profile</a> {% for email in email_confirmations %}
{% else %} <p class="alert alert-info"><b>A confirmation email has been sent to {{email}}.</b> The email will be activated after you click on the link it contains.</p>
<h1>Profile update unsuccessful</h1> {% endfor %}
<p >An error has occurred when attempting to update your account.<p>
{% if error %} <a class="btn btn-default" href="{% url "ietf.ietfauth.views.profile" %}">Edit profile</a>
<p class="alert alert-danger">{{ error }}</p>
{% endif %}
{% endif %}
{% endblock %} {% endblock %}

View file

@ -9,10 +9,12 @@
{% block content %} {% block content %}
{% origin %} {% origin %}
{% if success %} {% if to_email %}
<h1>Account created successfully</h1> <h1>Account created successfully</h1>
<p>Your account creation request has been successfully received.</p> <p>Your account creation request has been successfully received.</p>
<p>We have sent you an email with instructions on how to complete the process.</p>
<p>We have sent an email to {{ to_email }} with instructions on how to complete the process.</p>
{% else %} {% else %}
<div class="row"> <div class="row">

View file

@ -1,12 +1,9 @@
{% autoescape off %} {% autoescape off %}
Hello, Hello,
We have received an account creation request for {{ username }} {% filter wordwrap:73 %}We have received an account creation request for {{ username }} at {{ domain }}. In order to set a new password for the {{ username }} account, please go to the following link and follow the instructions there:{% endfilter %}
at {{ domain }}. In order to set a new password for the
{{ username }} account, please go to the following link and
follow the instructions there:
https://{{ domain }}{% url "confirm_account" username today realm auth %} https://{{ domain }}{% url "confirm_account" auth %}
This link will expire in {{ expire }} days. This link will expire in {{ expire }} days.
@ -14,4 +11,4 @@ Best regards,
The datatracker login manager service The datatracker login manager service
(for the IETF Secretariat) (for the IETF Secretariat)
{% endautoescape %} {% endautoescape %}

View file

@ -2,7 +2,7 @@
{# Copyright The IETF Trust 2015, All Rights Reserved #} {# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %} {% load origin %}
{% load widget_tweaks %} {% load widget_tweaks bootstrap3 %}
{% block title %}Profile for {{ user }}{% endblock %} {% block title %}Profile for {{ user }}{% endblock %}
@ -13,6 +13,8 @@
<form class="form-horizontal" method="post"> <form class="form-horizontal" method="post">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form_errors person_form 'non_fields' %}
<div class="form-group"> <div class="form-group">
<label class="col-sm-2 control-label">User name</label> <label class="col-sm-2 control-label">User name</label>
<div class="col-sm-10"> <div class="col-sm-10">
@ -20,32 +22,6 @@
</div> </div>
</div> </div>
<div class="form-group">
<label class="col-sm-2 control-label">Roles</label>
<div class="col-sm-10">
{% for role in roles %}
<div class="row">
<div class="col-sm-5">
<select class="form-control" name="role_email_{{role.id}}">
{% for email in emails %}
<option value="{{email.address}}" {% if email.address == role.email.address %}selected{% endif %}>
{% if email.active %}
{{email}}
{% else %}
({{email}})
{% endif %}
</option>
{% endfor %}
</select>
</div>
<div class="col-sm-7">
<div class="help-block">Email to use for <i>{{ role.name|lower }}</i> role in {{ role.group.acronym|upper }} ({{ role.group.type }}).</div>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="form-group"> <div class="form-group">
<label class="col-sm-2 control-label">Email addresses</label> <label class="col-sm-2 control-label">Email addresses</label>
<div class="col-sm-10"> <div class="col-sm-10">
@ -54,8 +30,8 @@
{% for email in emails %} {% for email in emails %}
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" name="{{email.address}}" {% if email.active %}checked{% endif %} onchange="style_email(0, this)"> <input type="checkbox" name="active_emails" value="{{ email.pk }}" {% if email.active %}checked{% endif %}>
{{email}} {{ email }}
</label> </label>
</div> </div>
{% endfor %} {% endfor %}
@ -67,85 +43,23 @@
</div> </div>
</div> </div>
{% for f in new_email_forms %}
{% bootstrap_field f.new_email layout="horizontal" show_label=False %}
{% endfor %}
<div class="form-group"> <div class="form-group">
<div class="col-sm-offset-2 col-sm-10"> <div class="col-sm-offset-2 col-sm-10">
<button class="btn btn-default btn-sm" onclick="add_email(); return false">Add email address</button> <div class="new-emails"></div>
<button class="btn btn-default btn-sm add-email">Add email address</button>
</div> </div>
</div> </div>
<div class="form-group"> {% for role in roles %}
<label class="col-sm-2 control-label">{{person_form.name.label}}</label> {% bootstrap_field role.email_form.email layout="horizontal" show_label=False %}
<div class="col-sm-10"> {% endfor %}
<div class="row">
<div class="col-sm-5">
{{person_form.name|add_class:"form-control"}}
</div>
<div class="col-sm-7">
<div class="help-block">The preferred form of your name.</div>
</div>
</div>
</div>
</div>
<div class="form-group"> {% bootstrap_form person_form layout="horizontal" %}
<label class="col-sm-2 control-label">{{person_form.ascii.label}}</label>
<div class="col-sm-10">
<div class="row">
<div class="col-sm-5">
{{person_form.ascii|add_class:"form-control"}}
</div>
<div class="col-sm-7">
<div class="help-block">Your name as rendered in ASCII (Latin, unaccented) characters.</div>
</div>
</div>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">{{person_form.ascii_short.label}}</label>
<div class="col-sm-10">
<div class="row">
<div class="col-sm-5">
{{person_form.ascii_short|add_class:"form-control"}}
</div>
<div class="col-sm-7">
<span class="help-block">
Example: A. Nonymous. Fill in this with initials and surname only if
taking the initials and surname of your ASCII name above produces an incorrect
initials-only form. (Blank is ok).
</span>
</div>
</div>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">{{person_form.affiliation.label}}</label>
<div class="col-sm-10">
<div class="row">
<div class="col-sm-5">
{{person_form.affiliation|add_class:"form-control"}}
</div>
<div class="col-sm-7">
<div class="help-block">Employer, university, sponsor, etc.</div>
</div>
</div>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">Postal address</label>
<div class="col-sm-10">
<div class="row">
<div class="col-sm-5">
{{person_form.address|add_class:"form-control"}}
</div>
<div class="col-sm-7">
<div class="help-block">Postal mailing address.</div>
</div>
</div>
</div>
</div>
<div class="form-group"> <div class="form-group">
<div class="col-sm-offset-2 col-sm-10"> <div class="col-sm-offset-2 col-sm-10">
@ -158,23 +72,24 @@
{% block js %} {% block js %}
<script> <script>
$(document).ready(function() { $(document).ready(function() {
$("input[type=checkbox]").each(style_email); $("input[name=active_emails]").on("change keypress click", function () {
if (this.checked) {
$(this).parent().addClass("text-success");;
$(this).parent().removeClass("text-danger line-through");
} else {
$(this).parent().addClass("text-danger line-through");
$(this).parent().removeClass("text-success");
}
}).trigger("change");
$(".add-email").click(function(e) {
e.preventDefault();
var container = $(this).closest("form").find(".new-emails");
$('<input class="form-control" name="new_email" placeholder="Enter new email address...">').appendTo(container).focus();
})
}); });
function style_email(i, e) {
if (e.checked) {
$(e).parent().addClass("text-success");;
$(e).parent().removeClass("text-danger line-through");
} else {
$(e).parent().addClass("text-danger line-through");
$(e).parent().removeClass("text-success");
}
}
function add_email() {
$("#emails").append('<input type="email" class="form-control" name="new_email_' + $.now() + '" placeholder="Enter new email address...">');
$("#emails").children().last().focus();
}
</script> </script>
{% endblock %} {% endblock %}

View file

@ -27,6 +27,5 @@
<button type="submit" class="btn btn-warning">Reset password</button> <button type="submit" class="btn btn-warning">Reset password</button>
{% endbuttons %} {% endbuttons %}
</form> </form>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -1,12 +1,9 @@
{% autoescape off %} {% autoescape off %}
Hello, Hello,
We have received a password reset request for {{ username }} {% filter wordwrap:73 %}We have received a password reset request for {{ username }} at {{ domain }}. In order to set a new password for the {{ username }} account, please go to the following link and follow the instructions there:{% endfilter %}
at {{ domain }}. In order to set a new password for the
{{ username }} account, please go to the following link and
follow the instructions there:
https://{{ domain }}{% url "confirm_password_reset" username today realm auth %} https://{{ domain }}{% url "confirm_password_reset" auth %}
This link will expire in {{ expire }} days. This link will expire in {{ expire }} days.
@ -17,4 +14,4 @@ Best regards,
The datatracker login manager service The datatracker login manager service
(for the IETF Secretariat) (for the IETF Secretariat)
{% endautoescape %} {% endautoescape %}