Repository: ocadotechnology/codeforlife-portal

Issue:
Student pages have the wrong URL redirect for logging in
If a student tries to access a student page without having logged in (or after timeout) they are redirected to the play page, when they should actually be redirected to the login page.


Discussion:

Resolving patch:
diff --git a/portal/forms/play.py b/portal/forms/play.py
--- a/portal/forms/play.py
+++ b/portal/forms/play.py
@@ -131,8 +131,7 @@ def clean_name(self):
     def clean_password(self):
         password = self.cleaned_data.get('password', None)
 
-        if password and not password_strength_test(password, length=6, upper=False, lower=False,
-                                                   numbers=False):
+        if password and not password_strength_test(password, length=6, upper=False, lower=False, numbers=False):
             raise forms.ValidationError(
                 "Password not strong enough, consider using at least 6 characters")
 
@@ -143,10 +142,10 @@ def clean(self):
         confirm_password = self.cleaned_data.get('confirm_password', None)
         current_password = self.cleaned_data.get('current_password', None)
 
-        if (password or confirm_password) and password != confirm_password:
+        if password is not None and (password or confirm_password) and password != confirm_password:
             raise forms.ValidationError("Your new passwords do not match")
 
-        if not self.user.check_password(current_password):
+        if current_password and not self.user.check_password(current_password):
             raise forms.ValidationError("Your current password was incorrect")
 
         return self.cleaned_data
diff --git a/portal/views/play.py b/portal/views/play.py
--- a/portal/views/play.py
+++ b/portal/views/play.py
@@ -56,14 +56,14 @@
 recaptcha_client = RecaptchaClient(app_settings.RECAPTCHA_PRIVATE_KEY, app_settings.RECAPTCHA_PUBLIC_KEY)
 
 
-@login_required(login_url=reverse_lazy('play'))
-@user_passes_test(logged_in_as_student, login_url=reverse_lazy('play'))
+@login_required(login_url=reverse_lazy('login_view'))
+@user_passes_test(logged_in_as_student, login_url=reverse_lazy('login_view'))
 def student_details(request):
     return render(request, 'portal/play/student_details.html')
 
 
-@login_required(login_url=reverse_lazy('play'))
-@user_passes_test(logged_in_as_student, login_url=reverse_lazy('play'))
+@login_required(login_url=reverse_lazy('login_view'))
+@user_passes_test(logged_in_as_student, login_url=reverse_lazy('login_view'))
 def student_edit_account(request):
     student = request.user.new_student
 
@@ -74,7 +74,7 @@ def student_edit_account(request):
             changing_email = False
 
             # check not default value for CharField
-            if (data['password'] != ''):
+            if data['password'] != '':
                 student.new_user.set_password(data['password'])
                 student.new_user.save()
                 update_session_auth_hash(request, form.user)
@@ -110,8 +110,8 @@ def username_labeller(request):
     return request.user.username
 
 
-@login_required(login_url=reverse_lazy('play'))
-@user_passes_test(logged_in_as_student, login_url=reverse_lazy('play'))
+@login_required(login_url=reverse_lazy('login_view'))
+@user_passes_test(logged_in_as_student, login_url=reverse_lazy('login_view'))
 @ratelimit('ip', labeller=username_labeller, periods=['1m'], increment=lambda req, res: hasattr(res, 'count') and res.count)
 def student_join_organisation(request):
     increment_count = False


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
@@ -114,28 +114,8 @@ def _independent_student_login(self, username, password):
         self.browser.find_element_by_id('id_independent_student-password').send_keys(password)
         self.browser.find_element_by_name('independent_student_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
-
-    def has_student_login_failed(self):
-        if not self.element_exists_by_css('.errorlist'):
-            return False
-
-        errors = self.browser.find_element_by_id('form-login-school').find_element_by_class_name('errorlist').text
-        error = 'Invalid name, class access code or password'
-        return error in errors
-
-    def has_independent_student_login_failed(self):
-        if not self.element_exists_by_css('.errorlist'):
-            return False
-
-        errors = self.browser.find_element_by_id('independent_student_login_form').find_element_by_class_name('errorlist').text
-        error = 'Incorrect username or password'
+    def has_login_failed(self, form_id, error):
+        errors = self.browser.find_element_by_id(form_id).find_element_by_class_name('errorlist').text
         return error in errors
 
     def go_to_teacher_forgotten_password_page(self):
diff --git a/portal/tests/pageObjects/portal/play/account_page.py b/portal/tests/pageObjects/portal/play/account_page.py
--- a/portal/tests/pageObjects/portal/play/account_page.py
+++ b/portal/tests/pageObjects/portal/play/account_page.py
@@ -35,6 +35,7 @@
 # program; modified versions of the program must be marked as such and not
 # identified as the original program.
 from play_base_page import PlayBasePage
+from portal.tests.pageObjects.portal.play.dashboard_page import PlayDashboardPage
 
 
 class PlayAccountPage(PlayBasePage):
@@ -51,3 +52,38 @@ def check_account_details(self, details):
 
         return correct
 
+    def submit_empty_form(self):
+        self.browser.find_element_by_id('update_button').click()
+        return self
+
+    def update_password_failure(self, new_password, confirm_new_password, old_password):
+        self._update_password(new_password, confirm_new_password, old_password)
+        return self
+
+    def update_password_success(self, new_password, confirm_new_password, old_password):
+        self._update_password(new_password, confirm_new_password, old_password)
+        return PlayDashboardPage(self.browser)
+
+    def update_name_failure(self, new_name, password):
+        self._update_name(new_name, password)
+        return self
+
+    def update_name_success(self, new_name, password):
+        self._update_name(new_name, password)
+        return PlayDashboardPage(self.browser)
+
+    def _update_password(self, new_password, confirm_new_password, old_password):
+        self.browser.find_element_by_id('id_password').send_keys(new_password)
+        self.browser.find_element_by_id('id_confirm_password').send_keys(confirm_new_password)
+        self.browser.find_element_by_id('id_current_password').send_keys(old_password)
+        self.browser.find_element_by_id('update_button').click()
+
+    def _update_name(self, new_name, password):
+        self.browser.find_element_by_id('id_name').clear()
+        self.browser.find_element_by_id('id_name').send_keys(new_name)
+        self.browser.find_element_by_id('id_current_password').send_keys(password)
+        self.browser.find_element_by_id('update_button').click()
+
+    def was_form_invalid(self, error):
+        errors = self.browser.find_element_by_id('student_account_form').find_element_by_class_name('errorlist').text
+        return error in errors
diff --git a/portal/tests/pageObjects/portal/play/join_school_or_club_page.py b/portal/tests/pageObjects/portal/play/join_school_or_club_page.py
--- a/portal/tests/pageObjects/portal/play/join_school_or_club_page.py
+++ b/portal/tests/pageObjects/portal/play/join_school_or_club_page.py
@@ -49,3 +49,19 @@ def join_a_school_or_club(self, access_code):
         assert self.element_exists_by_css('.success')
 
         return self
+
+    def join_a_school_or_club_failure(self, access_code):
+        self.browser.find_element_by_id('id_access_code').send_keys(access_code)
+        self.browser.find_element_by_id('request_join_a_school_or_club_button').click()
+        assert not self.element_exists_by_css('.success')
+
+        return self
+
+    def has_join_request_failed(self, error):
+        errors = self.browser.find_element_by_id('join_class_form').find_element_by_class_name('errorlist').text
+        return error in errors
+
+    def revoke_join_request(self):
+        self.browser.find_element_by_name('revoke_join_request').click()
+
+        return self
diff --git a/portal/tests/pageObjects/portal/signup_page.py b/portal/tests/pageObjects/portal/signup_page.py
--- a/portal/tests/pageObjects/portal/signup_page.py
+++ b/portal/tests/pageObjects/portal/signup_page.py
@@ -72,10 +72,6 @@ def independent_student_signup(self, name, username, email_address, password, co
         else:
             return self
 
-    def has_independent_student_signup_failed(self):
-        if not self.element_exists_by_css('.errorlist'):
-            return False
-
+    def has_independent_student_signup_failed(self, error):
         errors = self.browser.find_element_by_id('form-signup-independent-student').find_element_by_class_name('errorlist').text
-        error = 'Password not strong enough, consider using at least 6 characters'
         return error in errors
diff --git a/portal/tests/test_independent_student.py b/portal/tests/test_independent_student.py
--- a/portal/tests/test_independent_student.py
+++ b/portal/tests/test_independent_student.py
@@ -49,7 +49,8 @@
 from base_test import BaseTest
 from pageObjects.portal.home_page import HomePage
 from utils.student import create_independent_student, submit_independent_student_signup_form
-from utils.messages import is_email_verified_message_showing
+from utils.messages import is_email_verified_message_showing, is_indep_student_join_request_received_message_showing, \
+    is_indep_student_join_request_revoked_message_showing, is_student_details_updated_message_showing
 from utils import email as email_utils
 
 
@@ -62,14 +63,51 @@ def test_signup(self):
     def test_signup_failed(self):
         page = self.go_to_homepage()
         page = submit_independent_student_signup_form(page, password='test')
-        assert page.has_independent_student_signup_failed()
+        assert page.has_independent_student_signup_failed(
+            'Password not strong enough, consider using at least 6 characters')
+
+    def test_signup_invalid_name(self):
+        page = self.go_to_homepage().go_to_signup_page()
+        page = page.independent_student_signup('Florian!', 'Florian', 'e@mail.com', 'Password1', 'Password1',
+                                               success=False)
+
+        assert self.is_signup_page(page)
+        assert page.has_independent_student_signup_failed(
+            'Names may only contain letters, numbers, dashes, underscores, and spaces.')
+
+    def test_signup_invalid_username(self):
+        page = self.go_to_homepage().go_to_signup_page()
+        page = page.independent_student_signup('Florian', '///', 'e@mail.com', 'Password1', 'Password1',
+                                               success=False)
+
+        assert self.is_signup_page(page)
+        assert page.has_independent_student_signup_failed(
+            'Usernames may only contain letters, numbers, dashes, and underscores.')
+
+    def test_signup_username_already_in_use(self):
+        page = self.go_to_homepage()
+        page, name, username, email, password = create_independent_student(page)
+        page = self.go_to_homepage().go_to_signup_page()
+        page = page.independent_student_signup('Florian', username, 'e@mail.com', 'Password1', 'Password1',
+                                               success=False)
+
+        assert self.is_signup_page(page)
+        assert page.has_independent_student_signup_failed('That username is already in use')
+
+    def test_signup_password_do_not_match(self):
+        page = self.go_to_homepage().go_to_signup_page()
+        page = page.independent_student_signup('Florian', 'Florian', 'e@mail.com', 'Password1', 'Password2',
+                                               success=False)
+
+        assert self.is_signup_page(page)
+        assert page.has_independent_student_signup_failed('Your passwords do not match')
 
     def test_login_failure(self):
         page = self.go_to_homepage()
         page = page.go_to_login_page()
         page = page.independent_student_login_failure('Non existent username', 'Incorrect password')
 
-        assert page.has_independent_student_login_failed()
+        assert page.has_login_failed('independent_student_login_form', 'Incorrect username or password')
 
     def test_login_success(self):
         page = self.go_to_homepage()
@@ -121,6 +159,96 @@ def test_reset_password_fail(self):
 
         assert len(mail.outbox) == 0
 
+    def test_update_name_success(self):
+        homepage = self.go_to_homepage()
+
+        play_page, student_name, student_username, student_email, password = create_independent_student(homepage)
+
+        page = play_page \
+            .independent_student_login(student_username, password) \
+            .go_to_account_page().update_name_success('New name', password)
+
+        assert self.is_dashboard(page)
+        assert is_student_details_updated_message_showing(selenium)
+
+    def test_update_name_failure(self):
+        homepage = self.go_to_homepage()
+
+        play_page, student_name, student_username, student_email, password = create_independent_student(homepage)
+
+        page = play_page \
+            .independent_student_login(student_username, password) \
+            .go_to_account_page().update_name_failure('Name!', password)
+
+        assert self.is_account_page(page)
+        assert page.was_form_invalid('Names may only contain letters, numbers, dashes, underscores, and spaces.')
+
+    def test_update_details_empty(self):
+        homepage = self.go_to_homepage()
+
+        play_page, student_name, student_username, student_email, password = create_independent_student(homepage)
+
+        page = play_page \
+            .independent_student_login(student_username, password) \
+            .go_to_account_page().submit_empty_form()
+
+        assert self.is_account_page(page)
+        assert page.was_form_invalid('This field is required.')
+
+    def test_join_class_nonexistent_class(self):
+        homepage = self.go_to_homepage()
+
+        play_page, student_name, student_username, student_email, password = create_independent_student(homepage)
+
+        page = play_page \
+            .independent_student_login(student_username, password) \
+            .go_to_join_a_school_or_club_page() \
+            .join_a_school_or_club_failure('AA123')
+
+        assert self.is_join_class_page(page)
+        assert page.has_join_request_failed('Cannot find the school or club and/or class')
+
+    def test_join_class_not_accepting_requests(self):
+        teacher_email, teacher_password = signup_teacher_directly()
+        create_organisation_directly(teacher_email)
+        klass, class_name, access_code = create_class_directly(teacher_email)
+        create_school_student_directly(access_code)
+
+        homepage = self.go_to_homepage()
+
+        play_page, student_name, student_username, student_email, password = create_independent_student(homepage)
+
+        page = play_page \
+            .independent_student_login(student_username, password) \
+            .go_to_join_a_school_or_club_page() \
+            .join_a_school_or_club_failure(access_code)
+
+        assert self.is_join_class_page(page)
+        assert page.has_join_request_failed('Cannot find the school or club and/or class')
+
+    def test_join_class_revoked(self):
+        teacher_email, teacher_password = signup_teacher_directly()
+        create_organisation_directly(teacher_email)
+        klass, class_name, access_code = create_class_directly(teacher_email)
+        create_school_student_directly(access_code)
+        klass.always_accept_requests = True
+        klass.save()
+
+        homepage = self.go_to_homepage()
+
+        play_page, student_name, student_username, student_email, password = create_independent_student(homepage)
+
+        page = play_page \
+            .independent_student_login(student_username, password) \
+            .go_to_join_a_school_or_club_page() \
+            .join_a_school_or_club(access_code)
+
+        assert is_indep_student_join_request_received_message_showing(selenium)
+
+        page.revoke_join_request()
+
+        assert is_indep_student_join_request_revoked_message_showing(selenium)
+
     def test_join_class_accepted(self):
         teacher_email, teacher_password = signup_teacher_directly()
         create_organisation_directly(teacher_email)
@@ -188,3 +316,15 @@ def get_to_forgotten_password_page(self):
 
     def wait_for_email(self):
         WebDriverWait(selenium, 2).until(lambda driver: len(mail.outbox) == 1)
+
+    def is_signup_page(self, page):
+        return page.__class__.__name__ == 'SignupPage'
+
+    def is_dashboard(self, page):
+        return page.__class__.__name__ == 'PlayDashboardPage'
+
+    def is_account_page(self, page):
+        return page.__class__.__name__ == 'PlayAccountPage'
+
+    def is_join_class_page(self, page):
+        return page.__class__.__name__ == 'JoinSchoolOrClubPage'
diff --git a/portal/tests/test_school_student.py b/portal/tests/test_school_student.py
--- a/portal/tests/test_school_student.py
+++ b/portal/tests/test_school_student.py
@@ -40,6 +40,7 @@
 from utils.teacher import signup_teacher_directly
 from utils.organisation import create_organisation_directly
 from utils.classes import create_class_directly
+from utils.messages import is_student_details_updated_message_showing
 from utils.student import create_school_student_directly
 
 from django_selenium_clean import selenium
@@ -69,7 +70,125 @@ def test_login_failure(self):
             .go_to_login_page()\
             .student_login_failure(student_name, access_code, 'some other password')
 
-        assert page.has_student_login_failed()
+        assert page.has_login_failed('form-login-school', 'Invalid name, class access code or password')
+
+    def test_login_nonexistent_class(self):
+        email, password = signup_teacher_directly()
+        create_organisation_directly(email)
+        _, class_name, access_code = create_class_directly(email)
+        student_name, student_password, _ = create_school_student_directly(access_code)
+
+        selenium.get(self.live_server_url)
+        page = HomePage(selenium) \
+            .go_to_login_page() \
+            .student_login_failure(student_name, 'WRONG', student_password)
+
+        assert page.has_login_failed('form-login-school', 'Invalid name, class access code or password')
+
+    def test_login_empty_class(self):
+        email, password = signup_teacher_directly()
+        create_organisation_directly(email)
+        _, class_name, access_code = create_class_directly(email)
+        student_name, student_password, _ = create_school_student_directly(access_code)
+        _, class_name2, access_code2 = create_class_directly(email)
+
+        selenium.get(self.live_server_url)
+        page = HomePage(selenium) \
+            .go_to_login_page() \
+            .student_login_failure(student_name, access_code2, student_password)
+
+        assert page.has_login_failed('form-login-school', 'Invalid name, class access code or password')
+
+    def test_update_password_form_empty(self):
+        email, password = signup_teacher_directly()
+        create_organisation_directly(email)
+        _, class_name, access_code = create_class_directly(email)
+        student_name, student_password, _ = create_school_student_directly(access_code)
+
+        selenium.get(self.live_server_url)
+        page = HomePage(selenium) \
+            .go_to_login_page() \
+            .student_login(student_name, access_code, student_password)
+        assert self.is_dashboard(page)
+
+        page = page.go_to_account_page().submit_empty_form()
+        assert self.is_account_page(page)
+        assert page.was_form_invalid('This field is required.')
+
+    def test_update_password_current_password_wrong(self):
+        email, password = signup_teacher_directly()
+        create_organisation_directly(email)
+        _, class_name, access_code = create_class_directly(email)
+        student_name, student_password, _ = create_school_student_directly(access_code)
+
+        selenium.get(self.live_server_url)
+        page = HomePage(selenium) \
+            .go_to_login_page() \
+            .student_login(student_name, access_code, student_password)
+        assert self.is_dashboard(page)
+
+        page = page.go_to_account_page().update_password_failure('NewPassword', 'NewPassword', 'WrongPassword')
+        assert self.is_account_page(page)
+        assert page.was_form_invalid('Your current password was incorrect')
+
+    def test_update_password_passwords_not_match(self):
+        email, password = signup_teacher_directly()
+        create_organisation_directly(email)
+        _, class_name, access_code = create_class_directly(email)
+        student_name, student_password, _ = create_school_student_directly(access_code)
+
+        selenium.get(self.live_server_url)
+        page = HomePage(selenium) \
+            .go_to_login_page() \
+            .student_login(student_name, access_code, student_password)
+        assert self.is_dashboard(page)
+
+        page = page.go_to_account_page().update_password_failure('NewPassword', 'OtherPassword', student_password)
+        assert self.is_account_page(page)
+        assert page.was_form_invalid('Your new passwords do not match')
+
+    def test_update_password_too_weak(self):
+        email, password = signup_teacher_directly()
+        create_organisation_directly(email)
+        _, class_name, access_code = create_class_directly(email)
+        student_name, student_password, _ = create_school_student_directly(access_code)
+
+        selenium.get(self.live_server_url)
+        page = HomePage(selenium) \
+            .go_to_login_page() \
+            .student_login(student_name, access_code, student_password)
+        assert self.is_dashboard(page)
+
+        page = page.go_to_account_page().update_password_failure('tiny', 'tiny', student_password)
+        assert self.is_account_page(page)
+        assert page.was_form_invalid('Password not strong enough, consider using at least 6 characters')
+
+    def test_update_password_success(self):
+        email, password = signup_teacher_directly()
+        create_organisation_directly(email)
+        _, class_name, access_code = create_class_directly(email)
+        student_name, student_password, _ = create_school_student_directly(access_code)
+
+        selenium.get(self.live_server_url)
+        page = HomePage(selenium) \
+            .go_to_login_page() \
+            .student_login(student_name, access_code, student_password)
+        assert self.is_dashboard(page)
+
+        new_password = 'NewPassword'
+
+        page = page.go_to_account_page().update_password_failure(new_password, new_password, student_password)
+        assert is_student_details_updated_message_showing(selenium)
+
+        page.logout()
+        selenium.get(self.live_server_url)
+        page = HomePage(selenium) \
+            .go_to_login_page() \
+            .student_login(student_name, access_code, new_password)
+        assert self.is_dashboard(page)
 
     def is_dashboard(self, page):
         return page.__class__.__name__ == 'PlayDashboardPage'
+
+    def is_account_page(self, page):
+        return page.__class__.__name__ == 'PlayAccountPage'
diff --git a/portal/tests/test_teacher.py b/portal/tests/test_teacher.py
--- a/portal/tests/test_teacher.py
+++ b/portal/tests/test_teacher.py
@@ -76,7 +76,7 @@ def test_login_failure(self):
         page = HomePage(selenium)
         page = page.go_to_login_page()
         page = page.login_failure('non-existent-email@codeforlife.com', 'Incorrect password')
-        assert page.has_login_failed()
+        assert page.has_login_failed('form-login-teacher', 'Incorrect email address or password')
 
     def test_login_success(self):
         email, password = signup_teacher_directly()
diff --git a/portal/tests/utils/messages.py b/portal/tests/utils/messages.py
--- a/portal/tests/utils/messages.py
+++ b/portal/tests/utils/messages.py
@@ -48,6 +48,18 @@ def is_teacher_details_updated_message_showing(browser):
     return is_message_showing(browser, 'Your account details have been successfully changed.')
 
 
+def is_student_details_updated_message_showing(browser):
+    return is_message_showing(browser, 'Your account details have been changed successfully.')
+
+
+def is_indep_student_join_request_received_message_showing(browser):
+    return is_message_showing(browser, 'Your request to join a school has been received successfully.')
+
+
+def is_indep_student_join_request_revoked_message_showing(browser):
+    return is_message_showing(browser, 'Your request to join a school has been cancelled successfully.')
+
+
 def is_teacher_email_updated_message_showing(browser):
     return is_message_showing(browser, 'Your account details have been successfully changed. Your email will be '
                                        'changed once you have verified it, until then you can still log in with your '