From a99aa32c59eb7cdd90715c24ff50e0aa4cd1f6a7 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 27 Apr 2016 16:26:04 +0000 Subject: [PATCH 1/2] 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 --- ietf/ietfauth/forms.py | 269 +++---------- ietf/ietfauth/htpasswd.py | 24 ++ ietf/ietfauth/tests.py | 270 ++++++++++--- ietf/ietfauth/urls.py | 15 +- ietf/ietfauth/utils.py | 1 - ietf/ietfauth/views.py | 374 +++++++++++------- ietf/person/models.py | 15 +- .../registration/add_email_email.txt | 7 +- .../registration/change_password.html | 2 +- .../{confirm.html => confirm_account.html} | 6 +- .../registration/confirm_new_email.html | 35 +- .../registration/confirm_profile_update.html | 25 +- .../templates/registration/creation_email.txt | 9 +- ietf/templates/registration/edit_profile.html | 151 ++----- .../registration/password_reset.html | 1 - .../registration/password_reset_email.txt | 9 +- 16 files changed, 641 insertions(+), 572 deletions(-) create mode 100644 ietf/ietfauth/htpasswd.py rename ietf/templates/registration/{confirm.html => confirm_account.html} (72%) diff --git a/ietf/ietfauth/forms.py b/ietf/ietfauth/forms.py index b5af137ea..59c9cc0e6 100644 --- a/ietf/ietfauth/forms.py +++ b/ietf/ietfauth/forms.py @@ -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 %s 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 create one.".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 - diff --git a/ietf/ietfauth/htpasswd.py b/ietf/ietfauth/htpasswd.py new file mode 100644 index 000000000..bebaa284f --- /dev/null +++ b/ietf/ietfauth/htpasswd.py @@ -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() diff --git a/ietf/ietfauth/tests.py b/ietf/ietfauth/tests.py index a613c9b53..633dc8320 100644 --- a/ietf/ietfauth/tests.py +++ b/ietf/ietfauth/tests.py @@ -1,83 +1,261 @@ -# Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). -# All rights reserved. Contact: Pasi Eronen -# -# 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)) diff --git a/ietf/ietfauth/urls.py b/ietf/ietfauth/urls.py index d7156642f..89c9f9fb0 100644 --- a/ietf/ietfauth/urls.py +++ b/ietf/ietfauth/urls.py @@ -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[a-z0-9.@]+)/(?P.+)$', 'url_login'), url(r'^testemail/$', 'test_email'), url(r'^create/$', 'create_account', name='create_account'), - url(r'^confirm/(?P[\w.@+-]+)/(?P[\d]+)/(?P[\w]+)/(?P[a-f0-9]+)/$', 'confirm_account', name='confirm_account'), - url(r'^reset/$', 'password_reset_view', name='password_reset'), - url(r'^reset/confirm/(?P[\w.@+-]+)/(?P[\d]+)/(?P[\w]+)/(?P[a-f0-9]+)/$', 'confirm_password_reset', name='confirm_password_reset'), - url(r'^add_email/confirm/(?P[\w.@+-]+)/(?P[\d]+)/(?P[\w.@+-]+)/(?P[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[^/]+)/$', 'confirm_account', name='confirm_account'), + url(r'^reset/$', 'password_reset', name='password_reset'), + url(r'^reset/confirm/(?P[^/]+)/$', 'confirm_password_reset', name='confirm_password_reset'), + url(r'^confirmnewemail/(?P[^/]+)/$', 'confirm_new_email', name='confirm_new_email'), ) diff --git a/ietf/ietfauth/utils.py b/ietf/ietfauth/utils.py index e28587aec..0680762e2 100644 --- a/ietf/ietfauth/utils.py +++ b/ietf/ietfauth/utils.py @@ -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() - diff --git a/ietf/ietfauth/views.py b/ietf/ietfauth/views.py index 8b0501111..909eabbc7 100644 --- a/ietf/ietfauth/views.py +++ b/ietf/ietfauth/views.py @@ -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) diff --git a/ietf/person/models.py b/ietf/person/models.py index db4f0f679..d09d7ae70 100644 --- a/ietf/person/models.py +++ b/ietf/person/models.py @@ -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() diff --git a/ietf/templates/registration/add_email_email.txt b/ietf/templates/registration/add_email_email.txt index 33a218c0c..6df1632a4 100644 --- a/ietf/templates/registration/add_email_email.txt +++ b/ietf/templates/registration/add_email_email.txt @@ -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. diff --git a/ietf/templates/registration/change_password.html b/ietf/templates/registration/change_password.html index 26321558a..ca4debd71 100644 --- a/ietf/templates/registration/change_password.html +++ b/ietf/templates/registration/change_password.html @@ -18,7 +18,7 @@ {% else %}

Change password

-

You can change the password below for your user {{ username }} below.

+

You can change the password below for your user {{ email }} below.

{% csrf_token %} {% bootstrap_form form %} diff --git a/ietf/templates/registration/confirm.html b/ietf/templates/registration/confirm_account.html similarity index 72% rename from ietf/templates/registration/confirm.html rename to ietf/templates/registration/confirm_account.html index 16ad9b94c..8abd56ebe 100644 --- a/ietf/templates/registration/confirm.html +++ b/ietf/templates/registration/confirm_account.html @@ -12,13 +12,13 @@ {% if success %}

Account creation successful

-

Your account with login name {{ email }} has been created, using the password you have selected.

- Sign in +

Your account with login {{ email }} has been created, using the password you have selected.

+ Sign in {% else %}

Complete account creation

-

In order to complete the setup of your account with login name {{ email }}, please choose a password:

+

In order to complete the setup of your account with login {{ email }}, please choose a password:

{% csrf_token %} {% bootstrap_form form %} diff --git a/ietf/templates/registration/confirm_new_email.html b/ietf/templates/registration/confirm_new_email.html index 2eadeec2f..0568c2960 100644 --- a/ietf/templates/registration/confirm_new_email.html +++ b/ietf/templates/registration/confirm_new_email.html @@ -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 %}

Confirm new email address

- {% if success %} -

Your account with login name {{ username }} has been updated to include the email address {{ email }}.

- Edit profile + {% if not can_confirm %} +

An error has occured when attempting to add the email address {{ email }} to your account {{ username }}.

+ + {% bootstrap_form_errors form %} + +

+ Edit profile +

+ {% elif new_email_obj %} +

Your account {{ username }} has been updated to include the email address {{ email }}.

+ +

+ Edit profile +

{% else %} -

An error has occured when attempting to add the email address '{{ email }}' to your account '{{ username }}'.

-

    - {% for field,msgs in error.items %} - {% for msg in msgs %} -
  • {{field}}: {{msg}} - {% endfor %} - {% endfor %} -
+

Confirm that you want to add the email address {{ email }} to your account {{ username }}.

+ + + {% csrf_token %} + {% buttons %} + + {% endbuttons %} +
{% endif %} {% endblock %} diff --git a/ietf/templates/registration/confirm_profile_update.html b/ietf/templates/registration/confirm_profile_update.html index e2066c59d..c0d385c01 100644 --- a/ietf/templates/registration/confirm_profile_update.html +++ b/ietf/templates/registration/confirm_profile_update.html @@ -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 %} -

Profile update successful

-

Your account has been successfully updated to reflect the changes you submitted.

- {% for email in new_emails %} -

A confirmation email has been sent to {{email}}. The email will be activated after you click on the link it contains.

- {% endfor %} - Edit profile - {% else %} -

Profile update unsuccessful

-

An error has occurred when attempting to update your account.

- {% if error %} -

{{ error }}

- {% endif %} - {% endif %} +

Profile update successful

+ +

Your account has been updated to reflect the changes you submitted.

+ + {% for email in email_confirmations %} +

A confirmation email has been sent to {{email}}. The email will be activated after you click on the link it contains.

+ {% endfor %} + + Edit profile {% endblock %} diff --git a/ietf/templates/registration/creation_email.txt b/ietf/templates/registration/creation_email.txt index 63496825f..1edd103ac 100644 --- a/ietf/templates/registration/creation_email.txt +++ b/ietf/templates/registration/creation_email.txt @@ -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 %} \ No newline at end of file +{% endautoescape %} diff --git a/ietf/templates/registration/edit_profile.html b/ietf/templates/registration/edit_profile.html index 146d0e69f..9c4bccad5 100644 --- a/ietf/templates/registration/edit_profile.html +++ b/ietf/templates/registration/edit_profile.html @@ -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 @@
{% csrf_token %} + {% bootstrap_form_errors person_form 'non_fields' %} +
@@ -20,32 +22,6 @@
-
- -
- {% for role in roles %} -
-
- -
-
-
Email to use for {{ role.name|lower }} role in {{ role.group.acronym|upper }} ({{ role.group.type }}).
-
-
- {% endfor %} -
-
-
@@ -54,8 +30,8 @@ {% for email in emails %}
{% endfor %} @@ -67,85 +43,23 @@
+ {% for f in new_email_forms %} + {% bootstrap_field f.new_email layout="horizontal" show_label=False %} + {% endfor %} +
- +
+ +
-
- -
-
-
- {{person_form.name|add_class:"form-control"}} -
-
-
The preferred form of your name.
-
-
-
-
+ {% for role in roles %} + {% bootstrap_field role.email_form.email layout="horizontal" show_label=False %} + {% endfor %} -
- -
-
-
- {{person_form.ascii|add_class:"form-control"}} -
-
-
Your name as rendered in ASCII (Latin, unaccented) characters.
-
-
-
-
- -
- -
-
-
- {{person_form.ascii_short|add_class:"form-control"}} -
-
- - 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). - -
-
-
-
- -
- -
-
-
- {{person_form.affiliation|add_class:"form-control"}} -
-
-
Employer, university, sponsor, etc.
-
-
-
-
- -
- -
-
-
- {{person_form.address|add_class:"form-control"}} -
-
-
Postal mailing address.
-
-
-
-
+ {% bootstrap_form person_form layout="horizontal" %}
@@ -158,23 +72,24 @@ {% block js %} {% endblock %} diff --git a/ietf/templates/registration/password_reset.html b/ietf/templates/registration/password_reset.html index 76f531416..f65127384 100644 --- a/ietf/templates/registration/password_reset.html +++ b/ietf/templates/registration/password_reset.html @@ -27,6 +27,5 @@ {% endbuttons %} - {% endif %} {% endblock %} diff --git a/ietf/templates/registration/password_reset_email.txt b/ietf/templates/registration/password_reset_email.txt index baea1b952..1ccea847e 100644 --- a/ietf/templates/registration/password_reset_email.txt +++ b/ietf/templates/registration/password_reset_email.txt @@ -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 %} \ No newline at end of file +{% endautoescape %} From 5bcf36e149b5c6e83390a5cbd0af96142c66e78e Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Thu, 5 May 2016 14:55:31 +0000 Subject: [PATCH 2/2] Fix a couple of bugs in the account registration cleanup - Legacy-Id: 11167 --- ietf/ietfauth/tests.py | 3 ++- ietf/ietfauth/views.py | 17 +++++++++++------ ietf/templates/registration/create.html | 6 ++++-- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/ietf/ietfauth/tests.py b/ietf/ietfauth/tests.py index 633dc8320..7aa5200b1 100644 --- a/ietf/ietfauth/tests.py +++ b/ietf/ietfauth/tests.py @@ -8,7 +8,7 @@ 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_utils import TestCase, login_testing_unauthorized, unicontent from ietf.utils.test_data import make_test_data from ietf.utils.mail import outbox, empty_outbox from ietf.person.models import Person, Email @@ -89,6 +89,7 @@ class IetfAuthTests(TestCase): empty_outbox() r = self.client.post(url, { 'email': email }) self.assertEqual(r.status_code, 200) + self.assertTrue("Account created" in unicontent(r.content)) self.assertEqual(len(outbox), 1) # go to confirm page diff --git a/ietf/ietfauth/views.py b/ietf/ietfauth/views.py index 909eabbc7..903d39d8e 100644 --- a/ietf/ietfauth/views.py +++ b/ietf/ietfauth/views.py @@ -80,7 +80,8 @@ def index(request): # return HttpResponseRedirect(redirect_to) def create_account(request): - success = False + to_email = None + if request.method == 'POST': form = RegistrationForm(request.POST) if form.is_valid(): @@ -103,7 +104,7 @@ def create_account(request): return render(request, 'registration/create.html', { 'form': form, - 'success': success, + 'to_email': to_email, }) def confirm_account(request, auth): @@ -140,10 +141,12 @@ def confirm_account(request, auth): name=email, ascii=email) if not email_obj: - email_obj = Email.objects.create(address=email, - person=person) - email_obj.person = person - email_obj.save() + 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() @@ -315,6 +318,8 @@ def confirm_password_reset(request, auth): user.save() # password is also stored in htpasswd file save_htpasswd_file(user.username, password) + + success = True else: form = PasswordForm() diff --git a/ietf/templates/registration/create.html b/ietf/templates/registration/create.html index a61f362f0..107896457 100644 --- a/ietf/templates/registration/create.html +++ b/ietf/templates/registration/create.html @@ -9,10 +9,12 @@ {% block content %} {% origin %} - {% if success %} + {% if to_email %}

Account created successfully

+

Your account creation request has been successfully received.

-

We have sent you an email with instructions on how to complete the process.

+ +

We have sent an email to {{ to_email }} with instructions on how to complete the process.

{% else %}