Repository: ocadotechnology/codeforlife-portal

Issue:
Add tests for login form
Coverage has decreased in pull request #274, because the tests for the login form have not been added.


Discussion:

Resolving patch:
diff --git a/portal/urls.py b/portal/urls.py
--- a/portal/urls.py
+++ b/portal/urls.py
@@ -65,6 +65,8 @@
 
 from game.views.level import play_default_level
 
+from portal.views.home_new import home_new, teach_new, play_new, about_new, help_and_support_new, terms_new, login_popup
+
 js_info_dict = {
     'packages': ('conf.locale',),
 }
@@ -194,20 +196,20 @@
     url(r'^teach/solutions_navigation/(?P<levelName>[A-Z0-9]+)/$', default_solution, name='default_solution'),
     url(r'^(?P<levelName>[A-Z0-9]+)/$', play_default_level, name='play_default_level'),
 
-    url(r'^redesign/home', TemplateView.as_view(template_name='redesign/home_new.html'), name='home_new'),
+    url(r'^redesign/home', home_new, name='home_new'),
     url(r'^redesign/register', TemplateView.as_view(template_name='redesign/register_popup.html'), name='register_popup'),
-    url(r'^redesign/login', TemplateView.as_view(template_name='redesign/login_popup.html'), name='login_popup'),
-    url(r'^redesign/teach/$', TemplateView.as_view(template_name='redesign/teach_new.html'), name='teach_new'),
+    url(r'^redesign/login', login_popup, name='login_popup'),
+    url(r'^redesign/teach/$', teach_new, name='teach_new'),
     url(r'^redesign/teach/onboarding1/$', TemplateView.as_view(template_name='redesign/teach_new/onboarding_school.html'), name='onboarding1'),
     url(r'^redesign/teach/onboarding1-join', TemplateView.as_view(template_name='redesign/teach_new/onboarding_join_pending.html'), name='onboarding1-join'),
     url(r'^redesign/teach/onboarding2', TemplateView.as_view(template_name='redesign/teach_new/onboarding_classes.html'), name='onboarding2'),
     url(r'^redesign/teach/onboarding3', TemplateView.as_view(template_name='redesign/teach_new/onboarding_students.html'), name='onboarding3'),
     url(r'^redesign/teach/onboarding4', TemplateView.as_view(template_name='redesign/teach_new/onboarding_print.html'), name='onboarding4'),
     url(r'^redesign/teach/onboarding5', TemplateView.as_view(template_name='redesign/teach_new/onboarding_complete.html'), name='onboarding5'),
-    url(r'^redesign/play', TemplateView.as_view(template_name='redesign/play_new.html'), name='play_new'),
-    url(r'^redesign/about', TemplateView.as_view(template_name='redesign/about_new.html'), name='about_new'),
-    url(r'^redesign/help', TemplateView.as_view(template_name='redesign/help-and-support_new.html'), name='help_new'),
-    url(r'^redesign/terms', TemplateView.as_view(template_name='redesign/terms_new.html'), name='terms_new'),
+    url(r'^redesign/play', play_new, name='play_new'),
+    url(r'^redesign/about', about_new, name='about_new'),
+    url(r'^redesign/help', help_and_support_new, name='help_new'),
+    url(r'^redesign/terms', terms_new, name='terms_new'),
     url(r'^redesign/teach/materials', TemplateView.as_view(template_name='redesign/teach_new/materials_new.html'), name='materials_new'),
     url(r'^redesign/teach/dashboard', TemplateView.as_view(template_name='redesign/teach_new/dashboard.html'), name='dashboard'),
     url(r'^redesign/teach/class', TemplateView.as_view(template_name='redesign/teach_new/class_new.html'), name='class_new'),
diff --git a/portal/views/home_new.py b/portal/views/home_new.py
new file mode 100644
--- /dev/null
+++ b/portal/views/home_new.py
@@ -0,0 +1,179 @@
+# -*- coding: utf-8 -*-
+# Code for Life
+#
+# Copyright (C) 2016, 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 functools import partial
+
+from django.shortcuts import render
+from django.http import HttpResponseRedirect
+from django.core.urlresolvers import reverse, reverse_lazy
+from django.contrib import messages as messages
+from django.contrib.auth import authenticate, login, logout
+from django.contrib.auth.forms import AuthenticationForm
+from recaptcha import RecaptchaClient
+from django_recaptcha_field import create_form_subclass_with_recaptcha
+
+from portal.forms.teach import TeacherLoginForm
+from portal.helpers.emails import send_verification_email, is_verified
+from portal.utils import using_two_factor
+from portal import app_settings
+from ratelimit.decorators import ratelimit
+
+recaptcha_client = RecaptchaClient(app_settings.RECAPTCHA_PRIVATE_KEY, app_settings.RECAPTCHA_PUBLIC_KEY)
+
+
+def teach_email_labeller(request):
+    if request.method == 'POST' and 'login' in request.POST:
+        return request.POST['login-email']
+
+    return ''
+
+
+def login_popup(request):
+    return render_forms(request, 'redesign/login_popup.html')
+
+
+def home_new(request):
+    return render_forms(request, 'redesign/home_new.html')
+
+
+def teach_new(request):
+    return render_forms(request, 'redesign/teach_new.html')
+
+
+def play_new(request):
+    return render_forms(request, 'redesign/play_new.html')
+
+
+def about_new(request):
+    return render_forms(request, 'redesign/about_new.html')
+
+
+def help_and_support_new(request):
+    return render_forms(request, 'redesign/help-and-support_new.html')
+
+
+def terms_new(request):
+    return render_forms(request, 'redesign/terms_new.html')
+
+
+@ratelimit('ip', periods=['1m'], increment=lambda req, res: hasattr(res, 'count') and res.count)
+@ratelimit('email', labeller=teach_email_labeller, ip=False, periods=['1m'], increment=lambda req, res: hasattr(res, 'count') and res.count)
+def render_forms(request, rendered_url):
+    invalid_form = False
+
+    limits = getattr(request, 'limits', {'ip': [0], 'email': [0]})
+    captcha_limit = 5
+
+    LoginFormWithCaptcha = partial(create_form_subclass_with_recaptcha(TeacherLoginForm, recaptcha_client), request)
+    InputLoginForm = compute_input_login_form(LoginFormWithCaptcha, limits, captcha_limit)
+    OutputLoginForm = compute_output_login_form(LoginFormWithCaptcha, limits, captcha_limit)
+
+    login_form = OutputLoginForm(prefix='login')
+
+    if request.method == 'POST':
+        if 'login' in request.POST:
+            login_form = InputLoginForm(request.POST, prefix='login')
+            if login_form.is_valid():
+                return process_login_form(request, login_form)
+
+            else:
+                login_form = OutputLoginForm(request.POST, prefix='login')
+                invalid_form = True
+
+    res = render(request, rendered_url, {
+        'login_form': login_form,
+        'logged_in_as_teacher': is_logged_in_as_teacher(request),
+    })
+
+    res.count = invalid_form
+    return res
+
+
+def compute_use_captcha(limits, captcha_limit):
+    using_captcha = (limits['ip'][0] > captcha_limit or limits['email'][0] > captcha_limit)
+    return using_captcha
+
+
+def compute_should_use_captcha(limits, captcha_limit):
+    should_use_captcha = (limits['ip'][0] >= captcha_limit or limits['email'][0] >= captcha_limit)
+    return should_use_captcha
+
+
+def compute_input_login_form(LoginFormWithCaptcha, limits, captcha_limit):
+    InputLoginForm = LoginFormWithCaptcha if compute_use_captcha(limits, captcha_limit) else TeacherLoginForm
+    return InputLoginForm
+
+
+def compute_output_login_form(LoginFormWithCaptcha, limits, captcha_limit):
+    OutputLoginForm = LoginFormWithCaptcha if compute_should_use_captcha(limits, captcha_limit) else TeacherLoginForm
+    return OutputLoginForm
+
+
+def process_login_form(request, login_form):
+    user = login_form.user
+    if not is_verified(user):
+        send_verification_email(request, user)
+        return render(request, 'portal/email_verification_needed.html', {'user': user})
+
+    login(request, login_form.user)
+
+    if using_two_factor(request.user):
+        return render(request, 'portal/2FA_redirect.html', {
+            'form': AuthenticationForm(),
+            'username': request.user.username,
+            'password': login_form.cleaned_data['password'],
+        })
+    else:
+        link = reverse('two_factor:profile')
+        messages.info(
+            request, ("You are not currently set up with two-factor authentication. " +
+                      "Use your phone or tablet to enhance your account's security. " +
+                      "Click <a href='" + link + "'>here</a> to find out more and " +
+                      "set it up or go to your account page at any time."),
+            extra_tags='safe')
+
+    next_url = request.GET.get('next', None)
+    if next_url:
+        return HttpResponseRedirect(next_url)
+
+    return HttpResponseRedirect(reverse_lazy('dashboard'))
+
+
+def is_logged_in_as_teacher(request):
+    logged_in_as_teacher = hasattr(request.user, 'userprofile') and \
+                           hasattr(request.user.userprofile, 'teacher') and \
+                           (request.user.is_verified() or not using_two_factor(request.user))
+    return logged_in_as_teacher


Test patch:
diff --git a/portal/tests/pageObjects/portal/base_page.py b/portal/tests/pageObjects/portal/base_page.py
--- a/portal/tests/pageObjects/portal/base_page.py
+++ b/portal/tests/pageObjects/portal/base_page.py
@@ -130,6 +130,10 @@ def go_to_teach_page(self):
         self.browser.find_element_by_id('teach_button').click()
         return teach_page.TeachPage(self.browser)
 
+    def go_to_login_page(self):
+        self.browser.find_element_by_id('login-button').click()
+        return login_page.LoginPage(self.browser)
+
     def go_to_terms_page(self):
         self.browser.find_element_by_id('terms_button').click()
         return terms_page.TermsPage(self.browser)
@@ -153,4 +157,5 @@ def is_on_403_forbidden(self):
 import home_page
 import play_page
 import teach_page
+import login_page
 import terms_page
diff --git a/portal/tests/pageObjects/portal/home_page_new.py b/portal/tests/pageObjects/portal/home_page_new.py
new file mode 100644
--- /dev/null
+++ b/portal/tests/pageObjects/portal/home_page_new.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+# Code for Life
+#
+# Copyright (C) 2016, 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 login_page
+from base_page import BasePage
+
+
+class HomePageNew(BasePage):
+    def __init__(self, browser):
+        super(HomePageNew, self).__init__(browser)
+        assert self.on_correct_page('home_page_new')
+
+    def go_to_login_page(self):
+        self.browser.find_element_by_id('login-button').click()
+        return login_page.LoginPage(self.browser)
diff --git a/portal/tests/pageObjects/portal/login_page.py b/portal/tests/pageObjects/portal/login_page.py
new file mode 100644
--- /dev/null
+++ b/portal/tests/pageObjects/portal/login_page.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+# Code for Life
+#
+# Copyright (C) 2016, 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 selenium.webdriver.support.ui import Select
+
+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
+import teach.dashboard_page_new
+import home_page_new
+
+
+class LoginPage(BasePage):
+    def __init__(self, browser):
+        super(LoginPage, self).__init__(browser)
+
+        assert self.on_correct_page('login_page')
+
+    def login(self, email, password):
+        self._login(email, password)
+
+        return teach.dashboard_page_new.TeachDashboardPageNew(self.browser)
+
+    def login_failure(self, email, password):
+        self._login(email, password)
+        return home_page_new.HomePageNew(self.browser)
+
+    def _login(self, email, password):
+        self.browser.find_element_by_id('id_login-email').send_keys(email)
+        self.browser.find_element_by_id('id_login-password').send_keys(password)
+        self.browser.find_element_by_name('login').click()
+
+    def has_login_failed(self):
+        if not self.element_exists_by_css('.errorlist'):
+            return False
+
+        errors = self.browser.find_element_by_id('form-login-teacher').find_element_by_class_name('errorlist').text
+        error = 'Incorrect email address or password'
+        return error in errors
diff --git a/portal/tests/pageObjects/portal/teach/dashboard_page_new.py b/portal/tests/pageObjects/portal/teach/dashboard_page_new.py
new file mode 100644
--- /dev/null
+++ b/portal/tests/pageObjects/portal/teach/dashboard_page_new.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+# Code for Life
+#
+# Copyright (C) 2016, 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 teach_base_page import TeachBasePage
+
+
+class TeachDashboardPageNew(TeachBasePage):
+    def __init__(self, browser):
+        super(TeachDashboardPageNew, self).__init__(browser)
+
+        assert self.on_correct_page('teach_dashboard_page_new')
diff --git a/portal/tests/test_teacher_new.py b/portal/tests/test_teacher_new.py
new file mode 100644
--- /dev/null
+++ b/portal/tests/test_teacher_new.py
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+# Code for Life
+#
+# Copyright (C) 2016, 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 re
+import time
+
+from django.core import mail
+from django_selenium_clean import selenium
+from selenium.webdriver.support.wait import WebDriverWait
+
+from base_test import BaseTest
+from pageObjects.portal.home_page import HomePage
+from pageObjects.portal.home_page_new import HomePageNew
+from utils.teacher import signup_teacher, signup_teacher_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
+
+
+class TestTeacherNew(BaseTest):
+
+    def test_login_failure(self):
+        selenium.get(self.live_server_url + "/portal/redesign/home")
+        page = HomePageNew(selenium)
+        page = page.go_to_login_page()
+        time.sleep(1)
+        page = page.login_failure('non-existent-email@codeforlife.com', 'Incorrect password')
+        assert self.is_home_page_new(page)
+
+    def test_login_success(self):
+        selenium.get(self.live_server_url)
+        page = HomePage(selenium)
+        page, email, password = signup_teacher(page)
+        selenium.get(self.live_server_url + "/portal/redesign/home")
+        page = HomePageNew(selenium)
+        page = page.go_to_login_page()
+        time.sleep(1)
+        page = page.login(email, password)
+        assert self.is_teacher_dashboard_new(page)
+
+    def is_teacher_dashboard_new(self, page):
+        return page.__class__.__name__ == 'TeachDashboardPageNew'
+
+    def is_home_page_new(self, page):
+        return page.__class__.__name__ == 'HomePageNew'