Refactor account code to use the built-in Django signing framework

(reusing code previously written for the community lists) instead of
the a custom MD5 scheme, add tests of all views, rewrite custom form
handling code to use plain forms and ensure that the data is properly
validated and errors visible in the UI. Move help texts on the Person
form up to the model.
 - Legacy-Id: 11136
This commit is contained in:
Ole Laursen 2016-04-27 16:26:04 +00:00
parent 210188bf86
commit a99aa32c59
16 changed files with 641 additions and 572 deletions

View file

@ -1,235 +1,96 @@
import datetime
import hashlib
import subprocess
import re
from django import forms
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.sites.models import Site
from django.utils.translation import ugettext_lazy as _
from django.utils.html import mark_safe
from django.core.urlresolvers import reverse as urlreverse
from ietf.utils.mail import send_mail
from ietf.person.models import Person, Email, Alias
from ietf.group.models import Role
from ietf.person.models import Person, Email
class RegistrationForm(forms.Form):
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):
email = self.cleaned_data.get('email', '')
if not email:
return email
if email.lower() != email:
raise forms.ValidationError(_('The supplied address contained uppercase letters. Please use a lowercase email address.'))
if User.objects.filter(username=email).count():
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', '')
raise forms.ValidationError('The supplied address contained uppercase letters. Please use a lowercase email address.')
if User.objects.filter(username=email).exists():
raise forms.ValidationError('An account with the email address you provided already exists.')
return email
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)
password2 = forms.CharField(label=_("Password confirmation"), widget=forms.PasswordInput,
help_text=_("Enter the same password as above, for verification."))
def clean_password_confirmation(self):
password = self.cleaned_data.get("password", "")
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):
password1 = self.cleaned_data.get("password1", "")
password2 = self.cleaned_data["password2"]
if password1 != password2:
raise forms.ValidationError(_("The two password fields didn't match."))
return password2
def ascii_cleaner(supposedly_ascii):
outside_printable_ascii_pattern = r'[^\x20-\x7F]'
if re.search(outside_printable_ascii_pattern, supposedly_ascii):
raise forms.ValidationError("Please only enter ASCII characters.")
return supposedly_ascii
def get_password(self):
return self.cleaned_data.get('password1')
class PersonForm(ModelForm):
class Meta:
model = Person
exclude = ('time', 'user')
def create_user(self):
user = User.objects.create(username=self.username,
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 clean_ascii(self):
return ascii_cleaner(self.cleaned_data.get("ascii") or u"")
def get_user(self):
return User.objects.get(username=self.username)
def clean_ascii_short(self):
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):
if self.update_user:
user = self.get_user()
else:
user = self.create_user()
user.set_password(self.get_password())
user.save()
self.save_password_file()
return user
class NewEmailForm(forms.Form):
new_email = forms.EmailField(label="New email address", required=False)
def clean_new_email(self):
email = self.cleaned_data.get("new_email", "")
if email:
existing = Email.objects.filter(address=email).first()
if existing:
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):
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,261 @@
# Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies).
# All rights reserved. Contact: Pasi Eronen <pasi.eronen@nokia.com>
#
# 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.
# -*- coding: utf-8 -*-
import os, shutil
from urlparse import urlsplit
from django.core.urlresolvers import reverse as urlreverse
from pyquery import PyQuery
from ietf.utils.test_utils import TestCase, login_testing_unauthorized, unicontent
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
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):
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):
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()
# 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)
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(urlsplit(r["Location"])[2], "/accounts/profile/")
# try logging out
r = self.client.get('/accounts/logout/')
r = self.client.get(urlreverse("account_logout"))
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(urlsplit(r["Location"])[2], "/accounts/login/")
# 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(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.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):
make_test_data()
url = urlreverse('ietf.ietfauth.views.profile')
login_testing_unauthorized(self, "plain", url)
username = "plain"
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
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue("plain" in unicontent(r))
# post
# ... fill in
# ask for reset, wrong username
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',
url(r'^$', 'index', name='account_index'),
# url(r'^login/$', 'ietf_login'),
url(r'^login/$', login),
url(r'^logout/$', logout),
url(r'^login/$', login, name="account_login"),
url(r'^logout/$', logout, name="account_logout"),
# url(r'^loggedin/$', 'ietf_loggedin'),
# 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'),
url(r'^testemail/$', 'test_email'),
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'^reset/$', 'password_reset_view', 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'^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'^ajax/check_username/$', 'ajax_check_username', name='ajax_check_username'),
url(r'^create/confirm/(?P<auth>[^/]+)/$', 'confirm_account', name='confirm_account'),
url(r'^reset/$', 'password_reset', name='password_reset'),
url(r'^reset/confirm/(?P<auth>[^/]+)/$', 'confirm_password_reset', name='confirm_password_reset'),
url(r'^confirmnewemail/(?P<auth>[^/]+)/$', 'confirm_new_email', name='confirm_new_email'),
)

View file

@ -138,4 +138,3 @@ def is_authorized_in_doc_stream(user, doc):
group_req = Q()
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
import datetime
import hashlib
#import json
from django.conf import settings
from django.template import RequestContext
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.decorators import login_required
#from django.contrib.auth.models import User
#from django.utils.http import urlquote
#from django.utils.translation import ugettext as _
from django.core.exceptions import ValidationError
import django.core.signing
from django.contrib.sites.models import Site
from django.contrib.auth.models import User
from ietf.group.models import Role
from ietf.ietfauth.forms import RegistrationForm, PasswordForm, RecoverPasswordForm, TestEmailForm, PersonForm
from ietf.person.models import Person, Email
from ietf.ietfauth.forms import RegistrationForm, PasswordForm, ResetPasswordForm, TestEmailForm
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):
return render_to_response('registration/index.html', context_instance=RequestContext(request))
return render(request, 'registration/index.html')
# def url_login(request, user, passwd):
# user = authenticate(username=user, password=passwd)
@ -81,154 +79,251 @@ def index(request):
# redirect_to = settings.LOGIN_REDIRECT_URL
# return HttpResponseRedirect(redirect_to)
@login_required
def profile(request):
roles = []
person = None
try:
person = request.user.person
except Person.DoesNotExist:
return render_to_response('registration/missing_person.html', context_instance=RequestContext(request))
if request.method == 'POST':
form = PersonForm(request.POST, instance=person)
success = False
new_emails = None
error = None
if form.is_valid():
try:
form.save()
success = True
new_emails = form.new_emails
except Exception as e:
error = e
return render_to_response('registration/confirm_profile_update.html',
{ 'success': success, 'new_emails': new_emails, 'error': error} ,
context_instance=RequestContext(request))
else:
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')
person_form = PersonForm(instance=person)
return render_to_response('registration/edit_profile.html',
{ 'user': request.user, 'emails': emails, 'person': person,
'roles': roles, 'person_form': person_form } ,
context_instance=RequestContext(request))
def confirm_new_email(request, username, date, email, hash):
valid = hashlib.md5('%s%s%s%s' % (settings.SECRET_KEY, date, email, username)).hexdigest() == hash
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
try:
# First, check whether this address exists (to give a more sensible
# error when a duplicate is created).
existing_email = Email.objects.get(address=email)
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',
{ 'username': username, 'email': email,
'success': success, 'error': error,
'record': new_email},
context_instance=RequestContext(request))
def create_account(request):
success = False
if request.method == 'POST':
form = RegistrationForm(request.POST)
if form.is_valid():
form.request = request
form.save()
success = True
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_to_response('registration/create.html',
{'form': form,
'success': success},
context_instance=RequestContext(request))
return render(request, 'registration/create.html', {
'form': form,
'success': success,
})
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")
def process_confirmation(request, username, date, realm, hash):
valid = hashlib.md5('%s%s%s%s' % (settings.SECRET_KEY, date, username, realm)).hexdigest() == hash
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
if request.method == 'POST':
form = PasswordForm(request.POST, username=username)
form = PasswordForm(request.POST)
if form.is_valid():
form.save() # Also updates the httpd password file
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)
email_obj.person = person
email_obj.save()
person.user = user
person.save()
success = True
else:
form = PasswordForm(username=username)
return form, username, success
form = PasswordForm()
def confirm_account(request, username, date, realm, hash):
form, username, success = process_confirmation(request, username, date, realm, hash)
return render_to_response('registration/confirm.html',
{'form': form, 'email': username, 'success': success},
context_instance=RequestContext(request))
return render(request, 'registration/confirm_account.html', {
'form': form,
'email': email,
'success': success,
})
@login_required
def profile(request):
roles = []
person = None
def password_reset_view(request):
try:
person = request.user.person
except Person.DoesNotExist:
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':
person_form = PersonForm(request.POST, instance=person)
for r in roles:
r.email_form = RoleEmailForm(r, request.POST, prefix="role_%s" % r.pk)
for e in request.POST.getlist("new_email", []):
new_email_forms.append(NewEmailForm({ "new_email": e }))
forms_valid = [person_form.is_valid()] + [r.email_form.is_valid() for r in roles] + [f.is_valid() for f in new_email_forms]
email_confirmations = []
if all(forms_valid):
updated_person = person_form.save()
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:
for r in roles:
r.email_form = RoleEmailForm(r, prefix="role_%s" % r.pk)
person_form = PersonForm(instance=person)
return render(request, 'registration/edit_profile.html', {
'user': request.user,
'person': person,
'person_form': person_form,
'roles': roles,
'emails': emails,
'new_email_forms': new_email_forms,
})
def confirm_new_email(request, auth):
try:
username, email = django.core.signing.loads(auth, salt="add_email", max_age=settings.DAYS_TO_EXPIRE_REGISTRATION_LINK * 24 * 60 * 60)
except django.core.signing.BadSignature:
raise Http404("Invalid or expired auth")
person = get_object_or_404(Person, user__username=username)
# 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)
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
if request.method == 'POST':
form = RecoverPasswordForm(request.POST)
form = ResetPasswordForm(request.POST)
if form.is_valid():
form.request = request
form.save()
to_email = form.cleaned_data['email']
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
else:
form = RecoverPasswordForm()
return render_to_response('registration/password_reset.html',
{'form': form,
'success': success},
context_instance=RequestContext(request))
form = ResetPasswordForm()
return render(request, 'registration/password_reset.html', {
'form': form,
'success': success,
})
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 confirm_password_reset(request, auth):
try:
email = django.core.signing.loads(auth, salt="password_reset", max_age=settings.DAYS_TO_EXPIRE_REGISTRATION_LINK * 24 * 60 * 60)
except django.core.signing.BadSignature:
raise Http404("Invalid or expired auth")
user = get_object_or_404(User, username=email)
success = False
if request.method == 'POST':
form = PasswordForm(request.POST)
if form.is_valid():
password = form.cleaned_data["password"]
user.set_password(password)
user.save()
# password is also stored in htpasswd file
save_htpasswd_file(user.username, password)
else:
form = PasswordForm()
return render(request, 'registration/change_password.html', {
'form': form,
'email': email,
'success': success,
})
# 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):
"""Set email address to which email generated in the system will be sent."""
if settings.SERVER_MODE == "production":
@ -249,11 +344,10 @@ def test_email(request):
else:
form = TestEmailForm(initial=dict(email=request.COOKIES.get('testmailcc')))
r = render_to_response('ietfauth/testemail.html',
dict(form=form,
cookie=cookie if cookie != None else request.COOKIES.get("testmailcc", "")
),
context_instance=RequestContext(request))
r = render(request, 'ietfauth/testemail.html', {
"form": form,
"cookie": cookie if cookie != None else request.COOKIES.get("testmailcc", "")
})
if cookie != None:
r.set_cookie("testmailcc", cookie)

View file

@ -14,12 +14,15 @@ from ietf.utils.mail import send_mail_preformatted
class PersonInfo(models.Model):
time = models.DateTimeField(default=datetime.datetime.now) # When this Person record entered the system
name = models.CharField(max_length=255, db_index=True) # The normal unicode form of the name. This must be
# set to the same value as the ascii-form if equal.
ascii = models.CharField(max_length=255) # The normal ascii-form of the name.
ascii_short = models.CharField(max_length=32, null=True, blank=True) # The short ascii-form of the name. Also in alias table if non-null
address = models.TextField(max_length=255, blank=True)
affiliation = models.CharField(max_length=255, blank=True)
# The normal unicode form of the name. This must be
# set to the same value as the ascii-form if equal.
name = models.CharField(max_length=255, db_index=True, help_text="Preferred form of name.")
# The normal ascii-form of the name.
ascii = models.CharField("ASCII", max_length=255, help_text="Name as rendered in ASCII (Latin, unaccented) characters.")
# The short ascii-form of the name. Also in alias table if non-null
ascii_short = models.CharField("ASCII short", 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):
return self.plain_name()

View file

@ -1,12 +1,9 @@
{% autoescape off %}
Hello,
We have received a request to add the email address '{{ email }}'
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:
{% 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 %}
https://{{ domain }}{% url "confirm_new_email" user today email auth %}
https://{{ domain }}{% url "confirm_new_email" auth %}
This link will expire in {{ expire }} days.

View file

@ -18,7 +18,7 @@
{% else %}
<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">
{% csrf_token %}
{% bootstrap_form form %}

View file

@ -12,13 +12,13 @@
{% if success %}
<h1>Account creation successful</h1>
<p>Your account with login name {{ 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>
<p>Your account with login {{ email }} has been created, using the password you have selected.</p>
<a type="a" class="btn btn-primary" href="{% url "account_login" %}" rel="nofollow">Sign in</a>
{% else %}
<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">
{% csrf_token %}
{% bootstrap_form form %}

View file

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

View file

@ -2,23 +2,18 @@
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% block title %}Profile update{% endblock %}
{% block title %}Profile update successful{% endblock %}
{% block content %}
{% origin %}
{% if success %}
<h1>Profile update successful</h1>
<p>Your account has been successfully updated to reflect the changes you submitted.</p>
{% 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>
{% endfor %}
<a class="btn btn-default" href="{% url "ietf.ietfauth.views.profile" %}">Edit profile</a>
{% else %}
<h1>Profile update unsuccessful</h1>
<p >An error has occurred when attempting to update your account.<p>
{% if error %}
<p class="alert alert-danger">{{ error }}</p>
{% endif %}
{% endif %}
<h1>Profile update successful</h1>
<p>Your account has been updated to reflect the changes you submitted.</p>
{% for email in email_confirmations %}
<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>
{% endfor %}
<a class="btn btn-default" href="{% url "ietf.ietfauth.views.profile" %}">Edit profile</a>
{% endblock %}

View file

@ -1,12 +1,9 @@
{% autoescape off %}
Hello,
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:
{% 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 %}
https://{{ domain }}{% url "confirm_account" username today realm auth %}
https://{{ domain }}{% url "confirm_account" auth %}
This link will expire in {{ expire }} days.
@ -14,4 +11,4 @@ Best regards,
The datatracker login manager service
(for the IETF Secretariat)
{% endautoescape %}
{% endautoescape %}

View file

@ -2,7 +2,7 @@
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load widget_tweaks %}
{% load widget_tweaks bootstrap3 %}
{% block title %}Profile for {{ user }}{% endblock %}
@ -13,6 +13,8 @@
<form class="form-horizontal" method="post">
{% csrf_token %}
{% bootstrap_form_errors person_form 'non_fields' %}
<div class="form-group">
<label class="col-sm-2 control-label">User name</label>
<div class="col-sm-10">
@ -20,32 +22,6 @@
</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">
<label class="col-sm-2 control-label">Email addresses</label>
<div class="col-sm-10">
@ -54,8 +30,8 @@
{% for email in emails %}
<div class="checkbox">
<label>
<input type="checkbox" name="{{email.address}}" {% if email.active %}checked{% endif %} onchange="style_email(0, this)">
{{email}}
<input type="checkbox" name="active_emails" value="{{ email.pk }}" {% if email.active %}checked{% endif %}>
{{ email }}
</label>
</div>
{% endfor %}
@ -67,85 +43,23 @@
</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="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 class="form-group">
<label class="col-sm-2 control-label">{{person_form.name.label}}</label>
<div class="col-sm-10">
<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>
{% for role in roles %}
{% bootstrap_field role.email_form.email layout="horizontal" show_label=False %}
{% endfor %}
<div class="form-group">
<label class="col-sm-2 control-label">{{person_form.ascii.label|upper}}</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>
{% bootstrap_form person_form layout="horizontal" %}
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
@ -158,23 +72,24 @@
{% block js %}
<script>
$(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>
{% endblock %}

View file

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

View file

@ -1,12 +1,9 @@
{% autoescape off %}
Hello,
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:
{% 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 %}
https://{{ domain }}{% url "confirm_password_reset" username today realm auth %}
https://{{ domain }}{% url "confirm_password_reset" auth %}
This link will expire in {{ expire }} days.
@ -17,4 +14,4 @@ Best regards,
The datatracker login manager service
(for the IETF Secretariat)
{% endautoescape %}
{% endautoescape %}