Repository: ocadotechnology/codeforlife-portal

Issue:
Class page - edit student details



Discussion:

Resolving patch:
diff --git a/portal/forms/teach_new.py b/portal/forms/teach_new.py
--- a/portal/forms/teach_new.py
+++ b/portal/forms/teach_new.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 # Code for Life
 #
-# Copyright (C) 2016, Ocado Innovation Limited
+# 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
@@ -118,8 +118,7 @@ def clean(self):
         password = self.cleaned_data.get('teacher_password', None)
         confirm_password = self.cleaned_data.get('teacher_confirm_password', None)
 
-        if password and confirm_password and password != confirm_password:
-            raise forms.ValidationError('Your passwords do not match')
+        check_passwords(password, confirm_password)
 
         return self.cleaned_data
 
@@ -183,8 +182,7 @@ def clean(self):
         return self.cleaned_data
 
     def check_password_errors(self, password, confirm_password, current_password):
-        if (password or confirm_password) and password != confirm_password:
-            raise forms.ValidationError('Your new passwords do not match')
+        check_passwords(password, confirm_password)
 
         if not self.user.check_password(current_password):
             raise forms.ValidationError('Your current password was incorrect')
@@ -290,6 +288,55 @@ def __init__(self, teachers, *args, **kwargs):
         self.fields['new_teacher'].choices = teacher_choices
 
 
+class TeacherEditStudentForm(forms.Form):
+    name = forms.CharField(label='Name', widget=forms.TextInput(attrs={'placeholder': 'Name'}))
+
+    def __init__(self, student, *args, **kwargs):
+        self.student = student
+        self.klass = student.class_field
+        super(TeacherEditStudentForm, self).__init__(*args, **kwargs)
+
+    def clean_name(self):
+        name = stripStudentName(self.cleaned_data.get('name', ''))
+
+        if name == '':
+            raise forms.ValidationError("'" + self.cleaned_data.get('name', '') + "' is not a valid name")
+
+        students = Student.objects.filter(class_field=self.klass,
+                                          new_user__first_name__iexact=name)
+        if students.exists() and students[0] != self.student:
+            raise forms.ValidationError("There is already a student called '" + name + "' in this class")
+
+        return name
+
+
+class TeacherSetStudentPass(forms.Form):
+    password = forms.CharField(
+        label='New password',
+        widget=forms.PasswordInput(attrs={'placeholder': "New password"}))
+    confirm_password = forms.CharField(
+        label='Confirm new password',
+        widget=forms.PasswordInput(attrs={'placeholder': "Confirm new password"}))
+
+    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):
+            raise forms.ValidationError(
+                "Password not strong enough, consider using at least 6 characters")
+
+        return password
+
+    def clean(self):
+        password = self.cleaned_data.get('password', None)
+        confirm_password = self.cleaned_data.get('confirm_password', None)
+
+        check_passwords(password, confirm_password)
+
+        return self.cleaned_data
+
+
 def validateStudentNames(klass, names):
     validationErrors = []
 
@@ -324,6 +371,11 @@ def find_duplicates(names, lower_names, validationErrors):
             duplicates_found.append(duplicate)
 
 
+def check_passwords(password, confirm_password):
+    if password is not None and (password or confirm_password) and password != confirm_password:
+        raise forms.ValidationError('The password and the confirmation password do not match')
+
+
 class StudentCreationForm(forms.Form):
     names = forms.CharField(label='names', widget=forms.Textarea)
 
diff --git a/portal/urls.py b/portal/urls.py
--- a/portal/urls.py
+++ b/portal/urls.py
@@ -69,9 +69,9 @@
 from portal.views.email_new import verify_email_new, change_email
 from portal.views.home_new import login_view, logout_view_new, register_view
 from portal.views.organisation_new import organisation_fuzzy_lookup_new, organisation_manage_new
-from portal.views.teacher.teach_new import teacher_classes_new, teacher_class_new, \
-    materials_viewer_new, teacher_print_reminder_cards_new, teacher_view_class, teacher_delete_class_new, \
-    teacher_edit_class_new, teacher_move_class_new
+from portal.views.teacher.teach_new import teacher_classes_new, teacher_class_new, teacher_view_class, \
+    teacher_delete_class_new, teacher_edit_class_new, teacher_move_class_new, teacher_edit_student_new, \
+    teacher_student_reset_new, materials_viewer_new, teacher_print_reminder_cards_new
 from portal.views.teacher.dashboard import dashboard_manage, organisation_allow_join_new, organisation_deny_join_new, \
     organisation_kick_new, organisation_toggle_admin_new, teacher_disable_2FA_new
 
@@ -232,6 +232,8 @@
     url(r'^redesign/teach/class/(?P<access_code>[A-Z0-9]+)/$', teacher_view_class, name='view_class'),
     url(r'^redesign/teach/class/delete/(?P<access_code>[A-Z0-9]+)/$', teacher_delete_class_new, name='teacher_delete_class_new'),
     url(r'^redesign/teach/class/edit/(?P<access_code>[A-Z0-9]+)/$', teacher_edit_class_new, name='teacher_edit_class_new'),
+    url(r'^redesign/teach/class/student/edit/(?P<pk>[0-9]+)/$', teacher_edit_student_new, name='teacher_edit_student_new'),
+    url(r'^redesign/teach/class/student/reset/(?P<pk>[0-9]+)/$', teacher_student_reset_new, name='teacher_student_reset_new'),
     url(r'^redesign/teach/class/move/(?P<access_code>[A-Z0-9]+)/$', teacher_move_class_new, name='teacher_move_class_new'),
 
     url(r'^api/', include([
diff --git a/portal/views/teacher/teach_new.py b/portal/views/teacher/teach_new.py
--- a/portal/views/teacher/teach_new.py
+++ b/portal/views/teacher/teach_new.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 # Code for Life
 #
-# Copyright (C) 2016, Ocado Innovation Limited
+# 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
@@ -55,7 +55,8 @@
 from reportlab.lib.styles import ParagraphStyle
 
 from portal.models import Class, Student, Teacher
-from portal.forms.teach_new import ClassCreationForm, StudentCreationForm, ClassMoveForm, ClassEditForm
+from portal.forms.teach_new import ClassCreationForm, StudentCreationForm, ClassMoveForm, ClassEditForm, \
+    TeacherEditStudentForm, TeacherSetStudentPass
 from portal.permissions import logged_in_as_teacher
 from portal.helpers.generators import generate_access_code, generate_password
 from portal.views.teacher.pdfs import PDF_DATA
@@ -329,6 +330,87 @@ def process_edit_class_form(request, klass, form):
     return HttpResponseRedirect(reverse_lazy('view_class', kwargs={'access_code': klass.access_code}))
 
 
+@login_required(login_url=reverse_lazy('login_new'))
+@user_passes_test(logged_in_as_teacher, login_url=reverse_lazy('login_new'))
+def teacher_edit_student_new(request, pk):
+    student = get_object_or_404(Student, id=pk)
+
+    check_if_authorised(request, student)
+
+    name_form = TeacherEditStudentForm(student, initial={
+        'name': student.new_user.first_name
+    })
+
+    password_form = TeacherSetStudentPass()
+    set_password_mode = False
+
+    if request.method == 'POST':
+        if 'update_details' in request.POST:
+            name_form = TeacherEditStudentForm(student, request.POST)
+            if name_form.is_valid():
+                name = name_form.cleaned_data['name']
+                student.new_user.first_name = name
+                student.new_user.save()
+                student.save()
+
+                messages.success(request, "The student's details have been changed successfully.")
+
+        else:
+            password_form = TeacherSetStudentPass(request.POST)
+            if password_form.is_valid():
+                return process_reset_password_form(request, student, password_form)
+            set_password_mode = True
+
+    return render(request, 'redesign/teach_new/teacher_edit_student_new.html', {
+        'name_form': name_form,
+        'password_form': password_form,
+        'student': student,
+        'class': student.class_field,
+        'set_password_mode': set_password_mode,
+    })
+
+
+def process_reset_password_form(request, student, password_form):
+    # check not default value for CharField
+    new_password = password_form.cleaned_data['password']
+    if new_password:
+        student.new_user.set_password(new_password)
+        student.new_user.save()
+        name_pass = [{'name': student.new_user.first_name, 'password': new_password}]
+        return render(request, 'redesign/teach_new/teacher_student_reset_new.html',
+                      {'student': student,
+                       'class': student.class_field,
+                       'password': new_password,
+                       'query_data': json.dumps(name_pass)})
+
+
+def check_if_authorised(request, student):
+    # check user is authorised to edit student
+    if request.user.new_teacher != student.class_field.teacher:
+        raise Http404
+
+
+@login_required(login_url=reverse_lazy('login_new'))
+@user_passes_test(logged_in_as_teacher, login_url=reverse_lazy('login_new'))
+def teacher_student_reset_new(request, pk):
+    student = get_object_or_404(Student, id=pk)
+
+    # check user is authorised to edit student
+    if request.user.new_teacher != student.class_field.teacher:
+        raise Http404
+
+    new_password = generate_password(6)
+    student.new_user.set_password(new_password)
+    student.new_user.save()
+    name_pass = [{'name': student.new_user.first_name, 'password': new_password}]
+
+    return render(request, 'redesign/teach_new/teacher_student_reset_new.html',
+                  {'student': student,
+                   'class': student.class_field,
+                   'password': new_password,
+                   'query_data': json.dumps(name_pass)})
+
+
 @login_required(login_url=reverse_lazy('login_new'))
 @user_passes_test(logged_in_as_teacher, login_url=reverse_lazy('login_new'))
 def teacher_move_class_new(request, access_code):


Test patch:
diff --git a/portal/tests/pageObjects/portal/teach/class_page_new.py b/portal/tests/pageObjects/portal/teach/class_page_new.py
--- a/portal/tests/pageObjects/portal/teach/class_page_new.py
+++ b/portal/tests/pageObjects/portal/teach/class_page_new.py
@@ -36,9 +36,10 @@
 # identified as the original program.
 from teach_base_page_new import TeachBasePage
 import dashboard_page_new
-import onboarding_student_list_page
 import dashboard_page_new
 import class_settings_page_new
+import edit_student_page
+import onboarding_student_list_page
 
 
 class TeachClassPage(TeachBasePage):
@@ -96,3 +97,7 @@ def has_students(self):
     def go_to_class_settings_page(self):
         self.browser.find_element_by_id('class_settings_button').click()
         return class_settings_page_new.TeachClassSettingsPage(self.browser)
+
+    def go_to_edit_student_page(self):
+        self.browser.find_element_by_id("edit_student_button").click()
+        return edit_student_page.EditStudentPage(self.browser)
diff --git a/portal/tests/pageObjects/portal/teach/dashboard_page_new.py b/portal/tests/pageObjects/portal/teach/dashboard_page_new.py
--- a/portal/tests/pageObjects/portal/teach/dashboard_page_new.py
+++ b/portal/tests/pageObjects/portal/teach/dashboard_page_new.py
@@ -156,6 +156,17 @@ def does_class_exist(self, name, access_code):
     def is_teacher_non_admin(self):
         return "Make admin" in self.browser.find_element_by_id('teachers_table').text
 
+    def have_classes(self):
+        return self.element_exists_by_id('classes_table')
+
+    def does_not_have_classes(self):
+        return self.element_does_not_exist_by_id('classes_table')
+
+    def does_class_exist(self, name, access_code):
+        return self.have_classes() and \
+               (name in self.browser.find_element_by_id('classes_table').text) and \
+               (access_code in self.browser.find_element_by_id('classes_table').text)
+
     def click_kick_button(self):
         self.browser.find_element_by_id('kick_button').click()
 
diff --git a/portal/tests/pageObjects/portal/teach/edit_student_page.py b/portal/tests/pageObjects/portal/teach/edit_student_page.py
new file mode 100644
--- /dev/null
+++ b/portal/tests/pageObjects/portal/teach/edit_student_page.py
@@ -0,0 +1,76 @@
+# -*- 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 selenium.webdriver.support.ui import Select
+import edit_student_password_page
+
+from teach_base_page_new import TeachBasePage
+
+
+class EditStudentPage(TeachBasePage):
+    def __init__(self, browser):
+        super(EditStudentPage, self).__init__(browser)
+
+        assert self.on_correct_page('edit_student_page')
+
+    def type_student_name(self, name):
+        self.browser.find_element_by_id('id_name').clear()
+        self.browser.find_element_by_id('id_name').send_keys(name)
+        return self
+
+    def type_student_password(self, password):
+        self.browser.find_element_by_id('id_password').send_keys(password)
+        self.browser.find_element_by_id('id_confirm_password').send_keys(password)
+        return self
+
+    def click_update_button(self):
+        self.browser.find_element_by_id("update_name_button").click()
+        return self
+
+    def click_set_password_form_button(self):
+        self.browser.find_element_by_id("request-password-setter").click()
+        return self
+
+    def click_set_password_button(self):
+        self.browser.find_element_by_id("set_new_password_button").click()
+        return edit_student_password_page.EditStudentPasswordPage(self.browser)
+
+    def click_generate_password_button(self):
+        self.browser.find_element_by_id("generate_password_button").click()
+        return edit_student_password_page.EditStudentPasswordPage(self.browser)
+
+    def is_student_name(self, name):
+        return name in self.browser.find_element_by_id('student_details').text
diff --git a/portal/tests/pageObjects/portal/teach/edit_student_password_page.py b/portal/tests/pageObjects/portal/teach/edit_student_password_page.py
new file mode 100644
--- /dev/null
+++ b/portal/tests/pageObjects/portal/teach/edit_student_password_page.py
@@ -0,0 +1,49 @@
+# -*- 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 selenium.webdriver.support.ui import Select
+
+from teach_base_page_new import TeachBasePage
+
+
+class EditStudentPasswordPage(TeachBasePage):
+    def __init__(self, browser):
+        super(EditStudentPasswordPage, self).__init__(browser)
+
+        assert self.on_correct_page('edit_student_password_page')
+
+    def is_student_password(self, password):
+        return password in self.browser.find_element_by_id('password_text').text
diff --git a/portal/tests/test_teacher_student_new.py b/portal/tests/test_teacher_student_new.py
--- a/portal/tests/test_teacher_student_new.py
+++ b/portal/tests/test_teacher_student_new.py
@@ -111,3 +111,54 @@ def test_add_to_existing_class(self):
         page = page.go_back_to_class()
 
         assert page.student_exists(new_student_name)
+
+    def test_update_student_name(self):
+        email, password = signup_teacher_directly()
+        create_organisation_directly(email)
+        _, class_name, access_code = create_class_directly(email)
+        name, password, student = create_school_student_directly(access_code)
+
+        selenium.get(self.live_server_url + "/portal/redesign/home")
+        page = HomePage(selenium).go_to_login_page().login(email, password).go_to_class_page().go_to_edit_student_page()
+
+        assert page.is_student_name(name)
+
+        new_student_name = "new name"
+
+        page = page.type_student_name(new_student_name)
+        page = page.click_update_button()
+
+        assert page.is_student_name(new_student_name)
+
+    def test_update_student_password(self):
+        email, password = signup_teacher_directly()
+        create_organisation_directly(email)
+        _, class_name, access_code = create_class_directly(email)
+        name, password, student = create_school_student_directly(access_code)
+
+        selenium.get(self.live_server_url + "/portal/redesign/home")
+        page = HomePage(selenium).go_to_login_page().login(email, password).go_to_class_page().go_to_edit_student_page()
+
+        assert page.is_student_name(name)
+
+        new_student_password = "new_password"
+
+        page = page.click_set_password_form_button().type_student_password(new_student_password)
+        page = page.click_set_password_button()
+
+        assert page.is_student_password(new_student_password)
+
+    def test_generate_random_student_password(self):
+        email, password = signup_teacher_directly()
+        create_organisation_directly(email)
+        _, class_name, access_code = create_class_directly(email)
+        name, password, student = create_school_student_directly(access_code)
+
+        selenium.get(self.live_server_url + "/portal/redesign/home")
+        page = HomePage(selenium).go_to_login_page().login(email, password).go_to_class_page().go_to_edit_student_page()
+
+        assert page.is_student_name(name)
+
+        page = page.click_generate_password_button()
+
+        assert page.__class__.__name__ == 'EditStudentPasswordPage'
diff --git a/portal/tests/utils/organisation_new.py b/portal/tests/utils/organisation_new.py
--- a/portal/tests/utils/organisation_new.py
+++ b/portal/tests/utils/organisation_new.py
@@ -82,3 +82,12 @@ def create_organisation(page, password):
     page = page.create_organisation(name, password, postcode)
 
     return page, name, postcode
+
+
+def join_teacher_to_organisation(teacher_email, org_name, postcode, is_admin=False):
+    teacher = Teacher.objects.get(new_user__email=teacher_email)
+    school = School.objects.get(name=org_name, postcode=postcode)
+
+    teacher.school = school
+    teacher.is_admin = is_admin
+    teacher.save()