Repository: ocadotechnology/codeforlife-portal

Issue:
Add tick icon to password reset page



Discussion:

Resolving patch:
diff --git a/portal/forms/registration_new.py b/portal/forms/registration_new.py
new file mode 100644
--- /dev/null
+++ b/portal/forms/registration_new.py
@@ -0,0 +1,221 @@
+# -*- coding: utf-8 -*-
+# Code for Life
+#
+# Copyright (C) 2017, Ocado Innovation Limited
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# ADDITIONAL TERMS – Section 7 GNU General Public Licence
+#
+# This licence does not grant any right, title or interest in any “Ocado” logos,
+# trade names or the trademark “Ocado” or any other trademarks or domain names
+# owned by Ocado Innovation Limited or the Ocado group of companies or any other
+# distinctive brand features of “Ocado” as may be secured from time to time. You
+# must not distribute any modification of this program using the trademark
+# “Ocado” or claim any affiliation or association with Ocado or its employees.
+#
+# You are not authorised to use the name Ocado (or any of its trade names) or
+# the names of any author or contributor in advertising or for publicity purposes
+# pertaining to the distribution of this program, without the prior written
+# authorisation of Ocado.
+#
+# Any propagation, distribution or conveyance of this program must include this
+# copyright notice and these terms. You must not misrepresent the origins of this
+# program; modified versions of the program must be marked as such and not
+# identified as the original program.
+from django import forms
+from django.core.mail import EmailMultiAlternatives
+from django.contrib.auth import get_user_model
+from django.contrib.auth.models import User
+from django.contrib.auth import forms as django_auth_forms
+from django.contrib.auth.tokens import default_token_generator
+from django.contrib.sites.shortcuts import get_current_site
+from django.template import loader
+from django.utils.encoding import force_bytes
+from django.utils.http import urlsafe_base64_encode
+from django.utils.translation import ugettext, ugettext_lazy as _
+
+from portal.models import Student, Teacher
+from portal.helpers.password import password_strength_test
+
+
+class PasswordResetSetPasswordForm(django_auth_forms.SetPasswordForm):
+    def __init__(self, user, *args, **kwags):
+        super(PasswordResetSetPasswordForm, self).__init__(user, *args, **kwags)
+        self.fields['new_password1'].label = "Enter your new password"
+        self.fields['new_password1'].widget.attrs['placeholder'] = "Try at least 8 characters, upper and lower case letters, and numbers"
+        self.fields['new_password2'].label = "Confirm your new password"
+        self.fields['new_password2'].widget.attrs['placeholder'] = "Please repeat your new password"
+
+    def clean_new_password1(self):
+        new_password1 = self.cleaned_data.get('new_password1', None)
+        if hasattr(self.user, 'teacher'):
+            if not password_strength_test(new_password1):
+                raise forms.ValidationError(
+                    "Password not strong enough, consider using at least 8 characters, upper and " +
+                    "lower case letters, and numbers")
+        elif hasattr(self.user, 'student'):
+            if not password_strength_test(new_password1, length=6, upper=False, lower=False, numbers=False):
+                raise forms.ValidationError("Password not strong enough, consider using at least 6 characters")
+        return new_password1
+
+
+class TeacherPasswordResetForm(forms.Form):
+    email = forms.EmailField(
+        label='Email address',
+        max_length=254,
+        widget=forms.EmailInput(
+            attrs={'placeholder': 'my.email@address.com'}
+        )
+    )
+
+    def clean_email(self):
+        email = self.cleaned_data.get('email', None)
+        self.username = ""
+        # Check such an email exists
+        if User.objects.filter(email=email).exists():
+            teacher = Teacher.objects.filter(new_user__email=email)
+            # Check such an email is associated with a teacher
+            if teacher.exists():
+                self.username = teacher[0].new_user.username
+
+        return email
+
+    def send_mail(self, subject_template_name, email_template_name,
+                  context, from_email, to_email, html_email_template_name=None):
+        """
+        Sends a django.core.mail.EmailMultiAlternatives to `to_email`.
+        """
+        subject = loader.render_to_string(subject_template_name, context)
+        # Email subject *must not* contain newlines
+        subject = ''.join(subject.splitlines())
+        body = loader.render_to_string(email_template_name, context)
+
+        email_message = EmailMultiAlternatives(subject, body, from_email, [to_email])
+        if html_email_template_name is not None:
+            html_email = loader.render_to_string(html_email_template_name, context)
+            email_message.attach_alternative(html_email, 'text/html')
+
+        email_message.send()
+
+    def save(self, domain_override=None,
+             subject_template_name='registration/password_reset_subject.txt',
+             email_template_name='redesign/reset_password_email.html',
+             use_https=False, token_generator=default_token_generator,
+             from_email=None, request=None, html_email_template_name=None):
+        """
+        Generates a one-use only link for resetting password and sends to the
+        user.
+        """
+        UserModel = get_user_model()
+        if self.username:
+            active_users = UserModel._default_manager.filter(username=self.username, is_active=True)
+            for user in active_users:
+                # Make sure that no email is sent to a user that actually has
+                # a password marked as unusable
+                if not user.has_usable_password():
+                    continue
+                if not domain_override:
+                    current_site = get_current_site(request)
+                    site_name = current_site.name
+                    domain = current_site.domain
+                else:
+                    site_name = domain = domain_override
+                context = {
+                    'email': user.email,
+                    'domain': domain,
+                    'site_name': site_name,
+                    'uid': urlsafe_base64_encode(force_bytes(user.pk)),
+                    'user': user,
+                    'token': token_generator.make_token(user),
+                    'protocol': compute_protocol(use_https),
+                }
+
+                self.send_mail(subject_template_name, email_template_name,
+                               context, from_email, user.email,
+                               html_email_template_name=html_email_template_name)
+
+
+class StudentPasswordResetForm(forms.Form):
+    username = forms.CharField(
+        label='Username',
+        widget=forms.TextInput(attrs={'placeholder': "rosie_f"}))
+
+    def clean_username(self):
+        username = self.cleaned_data.get('username', None)
+        user_filter = User.objects.filter(username=username)
+        # Check such a username exists and such a username is not in use by a student part of a class/school
+        if not user_filter.exists() or not Student.objects.filter(class_field=None, new_user__username=username).exists():
+            username = ""
+        return username
+
+    def send_mail(self, subject_template_name, email_template_name,
+                  context, from_email, to_email, html_email_template_name=None):
+        """
+        Sends a django.core.mail.EmailMultiAlternatives to `to_email`.
+        """
+        subject = loader.render_to_string(subject_template_name, context)
+        # Email subject *must not* contain newlines
+        subject = ''.join(subject.splitlines())
+        body = loader.render_to_string(email_template_name, context)
+
+        email_message = EmailMultiAlternatives(subject, body, from_email, [to_email])
+        if html_email_template_name is not None:
+            html_email = loader.render_to_string(html_email_template_name, context)
+            email_message.attach_alternative(html_email, 'text/html')
+
+        email_message.send()
+
+    def save(self, domain_override=None,
+             subject_template_name='registration/password_reset_subject.txt',
+             email_template_name='redesign/reset_password_email.html',
+             use_https=False, token_generator=default_token_generator,
+             from_email=None, request=None, html_email_template_name=None):
+        """
+        Generates a one-use only link for resetting password and sends to the
+        user.
+        """
+        UserModel = get_user_model()
+        username = self.cleaned_data["username"]
+        if username:
+            active_users = UserModel._default_manager.filter(
+                username=username, is_active=True)
+            for user in active_users:
+                # Make sure that no email is sent to a user that actually has
+                # a password marked as unusable
+                if not user.has_usable_password():
+                    continue
+                if not domain_override:
+                    current_site = get_current_site(request)
+                    site_name = current_site.name
+                    domain = current_site.domain
+                else:
+                    site_name = domain = domain_override
+                context = {
+                    'email': user.email,
+                    'domain': domain,
+                    'site_name': site_name,
+                    'uid': urlsafe_base64_encode(force_bytes(user.pk)),
+                    'user': user,
+                    'token': token_generator.make_token(user),
+                    'protocol': compute_protocol(use_https),
+                }
+
+                self.send_mail(subject_template_name, email_template_name,
+                               context, from_email, user.email,
+                               html_email_template_name=html_email_template_name)
+
+
+def compute_protocol(use_https):
+    return 'https' if use_https else 'http'
diff --git a/portal/urls.py b/portal/urls.py
--- a/portal/urls.py
+++ b/portal/urls.py
@@ -80,6 +80,10 @@
 from portal.views.teacher.dashboard import dashboard_manage, organisation_allow_join as organisation_allow_join_new, \
     organisation_deny_join as organisation_deny_join_new, organisation_kick as organisation_kick_new, \
     organisation_toggle_admin as organisation_toggle_admin_new, teacher_disable_2FA as teacher_disable_2FA_new
+from portal.views.registration_new import teacher_password_reset as teacher_password_reset_new, \
+    password_reset_done as password_reset_done_new, student_password_reset as student_password_reset_new, \
+    password_reset_check_and_confirm as password_reset_check_and_confirm_new
+
 
 js_info_dict = {
     'packages': ('conf.locale',),
@@ -216,6 +220,11 @@
     url(r'^redesign/logout/$', logout_view_new, name='logout_new'),
     url(r'^redesign/verify_email/(?P<token>[0-9a-f]+)/$', verify_email_new, name='verify_email_new'),
     url(r'^redesign/change_email/(?P<token>[0-9a-f]+)/$', change_email, name='change_email'),
+    url(r'^redesign/user/password/reset/student/$', student_password_reset_new, name="student_password_reset_new"),
+    url(r'^redesign/user/password/reset/teacher/$', teacher_password_reset_new, name="teacher_password_reset_new"),
+    url(r'^redesign/user/password/reset/done/$', password_reset_done_new, name='reset_password_email_sent'),
+    url(r'^redesign/user/password/reset/(?P<uidb64>[0-9A-Za-z]+)-(?P<token>.+)/$', password_reset_check_and_confirm_new, name='password_reset_check_and_confirm_new'),
+    url(r'^redesign/teacher/password/reset/complete/$', TemplateView.as_view(template_name='redesign/reset_password_done.html'), name='password_reset_complete_new'),
     url(r'^redesign/teach/$', TemplateView.as_view(template_name='redesign/teach_new.html'), name='teach_new'),
     url(r'^redesign/teach/fuzzy_lookup/$', organisation_fuzzy_lookup_new, name='organisation_fuzzy_lookup_new'),
     url(r'^redesign/teach/onboarding-organisation/$', organisation_manage_new, name='onboarding-organisation'),
diff --git a/portal/views/registration_new.py b/portal/views/registration_new.py
new file mode 100644
--- /dev/null
+++ b/portal/views/registration_new.py
@@ -0,0 +1,227 @@
+# -*- coding: utf-8 -*-
+# Code for Life
+#
+# Copyright (C) 2017, Ocado Innovation Limited
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# ADDITIONAL TERMS – Section 7 GNU General Public Licence
+#
+# This licence does not grant any right, title or interest in any “Ocado” logos,
+# trade names or the trademark “Ocado” or any other trademarks or domain names
+# owned by Ocado Innovation Limited or the Ocado group of companies or any other
+# distinctive brand features of “Ocado” as may be secured from time to time. You
+# must not distribute any modification of this program using the trademark
+# “Ocado” or claim any affiliation or association with Ocado or its employees.
+#
+# You are not authorised to use the name Ocado (or any of its trade names) or
+# the names of any author or contributor in advertising or for publicity purposes
+# pertaining to the distribution of this program, without the prior written
+# authorisation of Ocado.
+#
+# Any propagation, distribution or conveyance of this program must include this
+# copyright notice and these terms. You must not misrepresent the origins of this
+# program; modified versions of the program must be marked as such and not
+# identified as the original program.
+
+import warnings
+
+from django.contrib.auth.forms import (
+    PasswordResetForm, SetPasswordForm,
+)
+from django.contrib.auth.tokens import default_token_generator
+from django.utils.http import urlsafe_base64_decode
+from django.core.urlresolvers import reverse_lazy
+from django.shortcuts import render
+from django.utils.encoding import force_text
+from django.contrib.auth.decorators import user_passes_test
+from django.contrib.auth.views import password_reset, password_reset_confirm
+from django.contrib.auth import get_user_model
+from django.template.response import TemplateResponse
+from django.utils.deprecation import RemovedInDjango20Warning
+from django.utils.translation import ugettext as _
+from django.views.decorators.debug import sensitive_post_parameters
+from django.views.decorators.cache import never_cache
+from django.views.decorators.csrf import csrf_protect
+from recaptcha import RecaptchaClient
+from django_recaptcha_field import create_form_subclass_with_recaptcha
+from deploy import captcha
+
+from portal.forms.registration_new import TeacherPasswordResetForm, PasswordResetSetPasswordForm, \
+    StudentPasswordResetForm
+from portal.permissions import not_logged_in, not_fully_logged_in
+from portal.helpers.emails_new import PASSWORD_RESET_EMAIL
+from portal import app_settings
+
+recaptcha_client = RecaptchaClient(app_settings.RECAPTCHA_PRIVATE_KEY, app_settings.RECAPTCHA_PUBLIC_KEY)
+
+
+@user_passes_test(not_logged_in, login_url=reverse_lazy('login_new'))
+def student_password_reset(request):
+    usertype = "STUDENT"
+    form = (StudentPasswordResetForm if not captcha.CAPTCHA_ENABLED
+            else decorate_with_captcha(StudentPasswordResetForm, request, recaptcha_client))
+    return password_reset(request, usertype, from_email=PASSWORD_RESET_EMAIL, template_name='redesign/reset_password_student.html',
+                          password_reset_form=form, is_admin_site=True)
+
+
+@user_passes_test(not_fully_logged_in, login_url=reverse_lazy('login_new'))
+def teacher_password_reset(request):
+    usertype = "TEACHER"
+    form = (TeacherPasswordResetForm if not captcha.CAPTCHA_ENABLED
+            else decorate_with_captcha(TeacherPasswordResetForm, request, recaptcha_client))
+    return password_reset(request, usertype, from_email=PASSWORD_RESET_EMAIL, template_name='redesign/reset_password_teach.html',
+                          password_reset_form=form, is_admin_site=True)
+
+
+def decorate_with_captcha(base_class, request, recaptcha_client):
+    form_with_captcha_class = create_form_subclass_with_recaptcha(base_class, recaptcha_client)
+
+    class FormWithCaptcha(form_with_captcha_class):
+
+        def __init__(self, *args, **kwargs):
+            super(FormWithCaptcha, self).__init__(request, *args, **kwargs)
+
+    return FormWithCaptcha
+
+
+@csrf_protect
+def password_reset(request, usertype, is_admin_site=False, template_name='redesign/reset_password_teach.html',
+                   email_template_name='redesign/reset_password_email.html',
+                   subject_template_name='registration/password_reset_subject.txt',
+                   password_reset_form=PasswordResetForm, token_generator=default_token_generator,
+                   from_email=None, current_app=None, extra_context=None, html_email_template_name=None):
+    if request.method == "POST":
+        form = password_reset_form(request.POST)
+        if form.is_valid():
+            opts = {
+                'use_https': request.is_secure(),
+                'token_generator': token_generator,
+                'from_email': from_email,
+                'email_template_name': email_template_name,
+                'subject_template_name': subject_template_name,
+                'request': request,
+                'html_email_template_name': html_email_template_name,
+            }
+            if is_admin_site:
+                warnings.warn(
+                    "The is_admin_site argument to "
+                    "django.contrib.auth.views.password_reset() is deprecated "
+                    "and will be removed in Django 2.0.", RemovedInDjango20Warning, 3
+                )
+                opts = dict(opts, domain_override=request.get_host())
+            form.save(**opts)
+
+            return render(request, 'redesign/reset_password_email_sent.html', {'usertype': usertype})
+    else:
+        form = password_reset_form()
+
+    context = {
+        'form': form,
+        'title': _('Password reset'),
+    }
+
+    update_context_and_apps(request, context, current_app, extra_context)
+
+    return TemplateResponse(request, template_name, context)
+
+
+def update_context_and_apps(request, context, current_app, extra_context):
+    if extra_context is not None:
+        context.update(extra_context)
+
+    if current_app is not None:
+        request.current_app = current_app
+
+
+def password_reset_done(request, template_name='redesign/reset_password_email_sent.html', current_app=None, extra_context=None):
+    context = {
+        'title': _('Password reset sent'),
+    }
+
+    update_context_and_apps(request, context, current_app, extra_context)
+
+    return TemplateResponse(request, template_name, context)
+
+
+@sensitive_post_parameters()
+@never_cache
+def password_reset_confirm(request, usertype, uidb64=None, token=None,
+                           template_name='redesign/reset_password_confirm.html',
+                           token_generator=default_token_generator,
+                           set_password_form=SetPasswordForm, current_app=None, extra_context=None):
+    """
+    View that checks the hash in a password reset link and presents a
+    form for entering a new password.
+    """
+    UserModel = get_user_model()
+    check_uidb64(uidb64, token)
+
+    try:
+        uid = force_text(urlsafe_base64_decode(uidb64))
+        user = UserModel._default_manager.get(pk=uid)
+    except (TypeError, ValueError, OverflowError, UserModel.DoesNotExist):
+        user = None
+
+    if user_is_authenticated(user, token_generator, token):
+        validlink = True
+        title = _('Enter new password')
+        if request.method == 'POST':
+            form = set_password_form(user, request.POST)
+            if form.is_valid():
+                form.save()
+                return render(request, 'redesign/reset_password_done.html', {'usertype': usertype})
+        else:
+            form = set_password_form(user)
+    else:
+        validlink = False
+        form = None
+        title = _('Password reset unsuccessful')
+
+    context = {
+        'form': form,
+        'title': title,
+        'validlink': validlink,
+    }
+
+    update_context_and_apps(request, context, current_app, extra_context)
+
+    return TemplateResponse(request, template_name, context)
+
+
+def check_uidb64(uidb64, token):
+    assert uidb64 is not None and token is not None  # checked by URLconf
+
+
+def user_is_authenticated(user, token_generator, token):
+    return user is not None and token_generator.check_token(user, token)
+
+
+@user_passes_test(not_fully_logged_in, login_url=reverse_lazy('login_new'))
+def password_reset_check_and_confirm(request, uidb64=None, token=None):
+    """
+    Customised standard django auth view with customised form to incorporate checking the password set is strong enough
+    """
+    UserModel = get_user_model()
+    try:
+        uid = urlsafe_base64_decode(uidb64)
+        user = UserModel._default_manager.get(pk=uid)
+    except (TypeError, ValueError, OverflowError, UserModel.DoesNotExist):
+        user = None
+    if user and hasattr(user, 'new_student'):
+        usertype = 'STUDENT'
+    else:
+        usertype = 'TEACHER'
+    return password_reset_confirm(request, usertype, set_password_form=PasswordResetSetPasswordForm, uidb64=uidb64,
+                                  token=token, extra_context={'usertype': usertype})


Test patch:
diff --git a/portal/tests/pageObjects/portal/login_page.py b/portal/tests/pageObjects/portal/login_page.py
--- a/portal/tests/pageObjects/portal/login_page.py
+++ b/portal/tests/pageObjects/portal/login_page.py
@@ -39,7 +39,8 @@
 from base_page import BasePage
 import play_page
 import email_verification_needed_page
-from portal.tests.pageObjects.registration.teacher_password_reset_form_page import TeacherPasswordResetFormPage
+from teacher_password_reset_form_page_new import TeacherPasswordResetFormPage
+from student_password_reset_form_page_new import StudentPasswordResetFormPage
 import teach.dashboard_page_new
 import teach.onboarding_organisation_page
 import teach.onboarding_classes_page
@@ -135,3 +136,13 @@ def has_independent_student_login_failed(self):
         errors = self.browser.find_element_by_id('independent_student_login_form').find_element_by_class_name('errorlist').text
         error = 'Incorrect username or password'
         return error in errors
+
+    def go_to_teacher_forgotten_password_page(self):
+        self.browser.find_element_by_id('teacher_forgotten_password_button').click()
+        return TeacherPasswordResetFormPage(self.browser)
+
+    def go_to_indep_forgotten_password_page(self):
+        self.browser.find_element_by_id('switchToIndependentStudent').click()
+
+        self.browser.find_element_by_id('student_forgotten_password_button').click()
+        return StudentPasswordResetFormPage(self.browser)
diff --git a/portal/tests/pageObjects/portal/password_reset_form_page_new.py b/portal/tests/pageObjects/portal/password_reset_form_page_new.py
new file mode 100644
--- /dev/null
+++ b/portal/tests/pageObjects/portal/password_reset_form_page_new.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+# Code for Life
+#
+# Copyright (C) 2017, Ocado Innovation Limited
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# ADDITIONAL TERMS – Section 7 GNU General Public Licence
+#
+# This licence does not grant any right, title or interest in any “Ocado” logos,
+# trade names or the trademark “Ocado” or any other trademarks or domain names
+# owned by Ocado Innovation Limited or the Ocado group of companies or any other
+# distinctive brand features of “Ocado” as may be secured from time to time. You
+# must not distribute any modification of this program using the trademark
+# “Ocado” or claim any affiliation or association with Ocado or its employees.
+#
+# You are not authorised to use the name Ocado (or any of its trade names) or
+# the names of any author or contributor in advertising or for publicity purposes
+# pertaining to the distribution of this program, without the prior written
+# authorisation of Ocado.
+#
+# Any propagation, distribution or conveyance of this program must include this
+# copyright notice and these terms. You must not misrepresent the origins of this
+# program; modified versions of the program must be marked as such and not
+# identified as the original program.
+from base_page import BasePage
+
+
+class PasswordResetPage(BasePage):
+    def __init__(self, browser):
+        super(PasswordResetPage, self).__init__(browser)
+
+        assert self.on_correct_page('reset_password_form_page')
+
+    def teacher_reset_password(self, new_password):
+        self.clear_and_fill('new_password1', new_password)
+        self.clear_and_fill('new_password2', new_password)
+
+        self.browser.find_element_by_id('teacher_update_button').click()
+
+        self.wait_for_element_by_id('reset_password_done_page')
+
+        return self
+
+    def student_reset_password(self, new_password):
+        self.clear_and_fill('new_password1', new_password)
+        self.clear_and_fill('new_password2', new_password)
+
+        self.browser.find_element_by_id('student_update_button').click()
+
+        self.wait_for_element_by_id('reset_password_done_page')
+
+        return self
+
+    def clear_and_fill(self, field, value):
+        self.browser.find_element_by_id('id_' + field).clear()
+        self.browser.find_element_by_id('id_' + field).send_keys(value)
diff --git a/portal/tests/pageObjects/portal/student_password_reset_form_page_new.py b/portal/tests/pageObjects/portal/student_password_reset_form_page_new.py
new file mode 100644
--- /dev/null
+++ b/portal/tests/pageObjects/portal/student_password_reset_form_page_new.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+# Code for Life
+#
+# Copyright (C) 2017, Ocado Innovation Limited
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# ADDITIONAL TERMS – Section 7 GNU General Public Licence
+#
+# This licence does not grant any right, title or interest in any “Ocado” logos,
+# trade names or the trademark “Ocado” or any other trademarks or domain names
+# owned by Ocado Innovation Limited or the Ocado group of companies or any other
+# distinctive brand features of “Ocado” as may be secured from time to time. You
+# must not distribute any modification of this program using the trademark
+# “Ocado” or claim any affiliation or association with Ocado or its employees.
+#
+# You are not authorised to use the name Ocado (or any of its trade names) or
+# the names of any author or contributor in advertising or for publicity purposes
+# pertaining to the distribution of this program, without the prior written
+# authorisation of Ocado.
+#
+# Any propagation, distribution or conveyance of this program must include this
+# copyright notice and these terms. You must not misrepresent the origins of this
+# program; modified versions of the program must be marked as such and not
+# identified as the original program.
+from base_page import BasePage
+
+
+class StudentPasswordResetFormPage(BasePage):
+    def __init__(self, browser):
+        super(StudentPasswordResetFormPage, self).__init__(browser)
+
+        self.wait_for_element_by_id('reset_password_student_page')
+
+        assert self.browser.find_element_by_id('id_username').get_attribute('placeholder') == 'rosie_f'
+
+    def cancel(self):
+        self.browser.find_element_by_id('cancel_button').click()
+        from home_page_new import HomePage
+
+        return HomePage(self.browser)
+
+    def reset_username_submit(self, username):
+        self.browser.find_element_by_id('id_username').send_keys(username)
+
+        self.browser.find_element_by_id('reset_button').click()
+
+        return self
diff --git a/portal/tests/pageObjects/portal/teacher_password_reset_form_page_new.py b/portal/tests/pageObjects/portal/teacher_password_reset_form_page_new.py
new file mode 100644
--- /dev/null
+++ b/portal/tests/pageObjects/portal/teacher_password_reset_form_page_new.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+# Code for Life
+#
+# Copyright (C) 2017, Ocado Innovation Limited
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# ADDITIONAL TERMS – Section 7 GNU General Public Licence
+#
+# This licence does not grant any right, title or interest in any “Ocado” logos,
+# trade names or the trademark “Ocado” or any other trademarks or domain names
+# owned by Ocado Innovation Limited or the Ocado group of companies or any other
+# distinctive brand features of “Ocado” as may be secured from time to time. You
+# must not distribute any modification of this program using the trademark
+# “Ocado” or claim any affiliation or association with Ocado or its employees.
+#
+# You are not authorised to use the name Ocado (or any of its trade names) or
+# the names of any author or contributor in advertising or for publicity purposes
+# pertaining to the distribution of this program, without the prior written
+# authorisation of Ocado.
+#
+# Any propagation, distribution or conveyance of this program must include this
+# copyright notice and these terms. You must not misrepresent the origins of this
+# program; modified versions of the program must be marked as such and not
+# identified as the original program.
+from base_page import BasePage
+from home_page_new import HomePage
+
+
+class TeacherPasswordResetFormPage(BasePage):
+    def __init__(self, browser):
+        super(TeacherPasswordResetFormPage, self).__init__(browser)
+
+        self.wait_for_element_by_id('reset_password_teacher_page')
+
+        assert self.browser.find_element_by_id('id_email').get_attribute('placeholder') == 'my.email@address.com'
+
+    def cancel(self):
+        self.browser.find_element_by_id('cancel_button').click()
+        return HomePage(self.browser)
+
+    def reset_email_submit(self, email):
+        self.browser.find_element_by_id('id_email').send_keys(email)
+
+        self.wait_for_element_by_id('reset_button')
+
+        self.browser.find_element_by_id('reset_button').click()
+        return self
diff --git a/portal/tests/test_independent_student_new.py b/portal/tests/test_independent_student_new.py
--- a/portal/tests/test_independent_student_new.py
+++ b/portal/tests/test_independent_student_new.py
@@ -35,14 +35,17 @@
 # program; modified versions of the program must be marked as such and not
 # identified as the original program.
 import re
+import time
 
 from django.core import mail
 from django_selenium_clean import selenium
 from selenium.webdriver.support.wait import WebDriverWait
 
 from base_test_new import BaseTest
+from pageObjects.portal.home_page_new import HomePage
 from utils.student_new import create_independent_student, submit_independent_student_signup_form
 from utils.messages import is_email_verified_message_showing
+from utils import email_new as email_utils
 
 
 class TestIndependentStudent(BaseTest):
@@ -73,3 +76,52 @@ def test_login_success(self):
         assert page.check_account_details({
             'name': name
         })
+
+    def test_reset_password(self):
+        page = self.go_to_homepage()
+
+        page, name, username, email, password = create_independent_student(page)
+        page = self.get_to_forgotten_password_page()
+
+        page.reset_username_submit(username)
+
+        self.wait_for_email()
+
+        page = email_utils.follow_reset_email_link(selenium, mail.outbox[0])
+
+        new_password = 'AnotherPassword12'
+
+        page.student_reset_password(new_password)
+
+        selenium.get(self.live_server_url + "/portal/redesign/home")
+        page = self \
+            .go_to_homepage() \
+            .go_to_login_page() \
+            .independent_student_login(username, new_password)
+
+        assert page.__class__.__name__ == 'PlayDashboardPage'
+
+        page = page.go_to_account_page()
+        assert page.check_account_details({
+            'name': name
+        })
+
+    def test_reset_password_fail(self):
+        page = self.get_to_forgotten_password_page()
+
+        fake_username = "fake_username"
+        page.reset_username_submit(fake_username)
+
+        time.sleep(5)
+
+        assert len(mail.outbox) == 0
+
+    def get_to_forgotten_password_page(self):
+        selenium.get(self.live_server_url + "/portal/redesign/home")
+        page = HomePage(selenium) \
+            .go_to_login_page() \
+            .go_to_indep_forgotten_password_page()
+        return page
+
+    def wait_for_email(self):
+        WebDriverWait(selenium, 2).until(lambda driver: len(mail.outbox) == 1)
diff --git a/portal/tests/test_organisation_new.py b/portal/tests/test_organisation_new.py
--- a/portal/tests/test_organisation_new.py
+++ b/portal/tests/test_organisation_new.py
@@ -47,7 +47,7 @@
 from utils.messages import is_organisation_created_message_showing
 
 from utils.messages import is_teacher_email_updated_message_showing
-from utils import email as email_utils
+from utils import email_new as email_utils
 
 from django_selenium_clean import selenium
 
diff --git a/portal/tests/test_teacher_new.py b/portal/tests/test_teacher_new.py
--- a/portal/tests/test_teacher_new.py
+++ b/portal/tests/test_teacher_new.py
@@ -37,6 +37,7 @@
 
 import time
 
+from selenium.webdriver.support.wait import WebDriverWait
 from django_selenium_clean import selenium
 from django.core import mail
 
@@ -47,7 +48,7 @@
 from utils.classes_new import create_class_directly
 from utils.student_new import create_school_student_directly
 from utils.messages import is_email_verified_message_showing, is_teacher_details_updated_message_showing, is_teacher_email_updated_message_showing
-from utils import email as email_utils
+from utils import email_new as email_utils
 
 
 class TestTeacher(BaseTest):
@@ -152,6 +153,47 @@ def test_change_email(self):
             'last_name': 'Teacher',
         })
 
+    def test_reset_password(self):
+        email, password = signup_teacher_directly()
+        create_organisation_directly(email)
+        klass, name, access_code = create_class_directly(email)
+        create_school_student_directly(access_code)
+
+        page = self.get_to_forgotten_password_page()
+
+        page.reset_email_submit(email)
+
+        self.wait_for_email()
+
+        page = email_utils.follow_reset_email_link(selenium, mail.outbox[0])
+
+        new_password = 'AnotherPassword12'
+
+        page.teacher_reset_password(new_password)
+
+        selenium.get(self.live_server_url + "/portal/redesign/home")
+        page = HomePage(selenium).go_to_login_page().login(email, new_password)
+        assert self.is_dashboard_page(page)
+
+    def test_reset_password_fail(self):
+        page = self.get_to_forgotten_password_page()
+        fake_email = "fake_email@fakeemail.com"
+        page.reset_email_submit(fake_email)
+
+        time.sleep(5)
+
+        assert len(mail.outbox) == 0
+
+    def get_to_forgotten_password_page(self):
+        selenium.get(self.live_server_url + "/portal/redesign/home")
+        page = HomePage(selenium) \
+            .go_to_login_page() \
+            .go_to_teacher_forgotten_password_page()
+        return page
+
+    def wait_for_email(self):
+        WebDriverWait(selenium, 2).until(lambda driver: len(mail.outbox) == 1)
+
     def is_dashboard_page(self, page):
         return page.__class__.__name__ == 'TeachDashboardPage'
 
diff --git a/portal/tests/utils/email.py b/portal/tests/utils/email.py
--- a/portal/tests/utils/email.py
+++ b/portal/tests/utils/email.py
@@ -44,24 +44,12 @@ def follow_verify_email_link_to_teach(page, email):
     return go_to_teach_page(page.browser)
 
 
-def follow_verify_email_link_to_onboarding(page, email):
-    _follow_verify_email_link(page, email)
-
-    return go_to_login_page(page.browser)
-
-
 def follow_verify_email_link_to_play(page, email):
     _follow_verify_email_link(page, email)
 
     return go_to_play_page(page.browser)
 
 
-def follow_verify_email_link_to_login(page, email):
-    _follow_verify_email_link(page, email)
-
-    return go_to_login_page(page.browser)
-
-
 def _follow_verify_email_link(page, email):
     message = str(email.message())
     prefix = '<p>Please go to <a href="'
@@ -88,18 +76,6 @@ def follow_change_email_link_to_teach(page, email):
     return go_to_teach_page(page.browser)
 
 
-def follow_change_email_link_to_dashboard(page, email):
-    _follow_change_email_link(page, email)
-
-    return go_to_login_page(page.browser)
-
-
-def follow_change_email_link_to_play(page, email):
-    _follow_change_email_link(page, email)
-
-    return go_to_play_page(page.browser)
-
-
 def _follow_change_email_link(page, email):
     message = str(email.message())
     prefix = 'please go to '
@@ -119,9 +95,3 @@ def go_to_teach_page(browser):
     from portal.tests.pageObjects.portal.teach_page import TeachPage
 
     return TeachPage(browser)
-
-
-def go_to_login_page(browser):
-    from portal.tests.pageObjects.portal.login_page import LoginPage
-
-    return LoginPage(browser)
diff --git a/portal/tests/utils/email_new.py b/portal/tests/utils/email_new.py
new file mode 100644
--- /dev/null
+++ b/portal/tests/utils/email_new.py
@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+# Code for Life
+#
+# Copyright (C) 2017, Ocado Innovation Limited
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# ADDITIONAL TERMS – Section 7 GNU General Public Licence
+#
+# This licence does not grant any right, title or interest in any “Ocado” logos,
+# trade names or the trademark “Ocado” or any other trademarks or domain names
+# owned by Ocado Innovation Limited or the Ocado group of companies or any other
+# distinctive brand features of “Ocado” as may be secured from time to time. You
+# must not distribute any modification of this program using the trademark
+# “Ocado” or claim any affiliation or association with Ocado or its employees.
+#
+# You are not authorised to use the name Ocado (or any of its trade names) or
+# the names of any author or contributor in advertising or for publicity purposes
+# pertaining to the distribution of this program, without the prior written
+# authorisation of Ocado.
+#
+# Any propagation, distribution or conveyance of this program must include this
+# copyright notice and these terms. You must not misrepresent the origins of this
+# program; modified versions of the program must be marked as such and not
+# identified as the original program.
+import string
+import re
+
+
+def follow_verify_email_link_to_onboarding(page, email):
+    _follow_verify_email_link(page, email)
+
+    return go_to_login_page(page.browser)
+
+
+def follow_verify_email_link_to_login(page, email):
+    _follow_verify_email_link(page, email)
+
+    return go_to_login_page(page.browser)
+
+
+def _follow_verify_email_link(page, email):
+    message = str(email.message())
+    prefix = '<p>Please go to <a href="'
+    i = string.find(message, prefix) + len(prefix)
+    suffix = '" rel="nofollow">'
+    j = string.find(message, suffix, i)
+    page.browser.get(message[i:j])
+
+
+def follow_reset_email_link(browser, email):
+    message = str(email.body)
+
+    link = re.search("http.+/", message).group(0)
+
+    browser.get(link)
+
+    from portal.tests.pageObjects.portal.password_reset_form_page_new import PasswordResetPage
+    return PasswordResetPage(browser)
+
+
+def follow_change_email_link_to_dashboard(page, email):
+    _follow_change_email_link(page, email)
+
+    return go_to_login_page(page.browser)
+
+
+def _follow_change_email_link(page, email):
+    message = str(email.message())
+    prefix = 'please go to '
+    i = string.find(message, prefix) + len(prefix)
+    suffix = ' to verify'
+    j = string.find(message, suffix, i)
+    page.browser.get(message[i:j])
+
+
+def go_to_login_page(browser):
+    from portal.tests.pageObjects.portal.login_page import LoginPage
+
+    return LoginPage(browser)
diff --git a/portal/tests/utils/student_new.py b/portal/tests/utils/student_new.py
--- a/portal/tests/utils/student_new.py
+++ b/portal/tests/utils/student_new.py
@@ -36,7 +36,7 @@
 # identified as the original program.
 from django.core import mail
 
-import email
+import email_new
 
 from portal.models import Class, Student
 
@@ -102,7 +102,7 @@ def create_independent_student(page):
 
     page = page.return_to_home_page()
 
-    page = email.follow_verify_email_link_to_login(page, mail.outbox[0])
+    page = email_new.follow_verify_email_link_to_login(page, mail.outbox[0])
     mail.outbox = []
 
     return page, name, username, email_address, password
diff --git a/portal/tests/utils/teacher_new.py b/portal/tests/utils/teacher_new.py
--- a/portal/tests/utils/teacher_new.py
+++ b/portal/tests/utils/teacher_new.py
@@ -35,12 +35,12 @@
 # program; modified versions of the program must be marked as such and not
 # identified as the original program.
 import random
-import email
+import email_new
 from django.core import mail
 import sys
 
 from portal.models import Teacher
-from portal.helpers.emails import generate_token
+from portal.helpers.emails_new import generate_token
 
 
 def generate_details(**kwargs):
@@ -69,7 +69,7 @@ def signup_teacher(page):
 
     page = page.return_to_home_page()
 
-    page = email.follow_verify_email_link_to_onboarding(page, mail.outbox[0])
+    page = email_new.follow_verify_email_link_to_onboarding(page, mail.outbox[0])
     mail.outbox = []
 
     return page, email_address, password