Repository: ocadotechnology/codeforlife-portal

Issue:
Class page - release students



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
@@ -337,34 +337,6 @@ def clean(self):
         return self.cleaned_data
 
 
-class TeacherMoveStudentsDestinationForm(forms.Form):
-    new_class = forms.ChoiceField(label='Choose a new class from the drop down menu for the selected students.', widget=forms.Select(attrs={'class': 'wide'}))
-
-    def __init__(self, classes, *args, **kwargs):
-        self.classes = classes
-        class_choices = []
-        for klass in classes:
-            class_choices.append((klass.id, klass.name + ' (' + klass.access_code + '), ' + klass.teacher.new_user.first_name + ' ' + klass.teacher.new_user.last_name))
-        super(TeacherMoveStudentsDestinationForm, self).__init__(*args, **kwargs)
-        self.fields['new_class'].choices = class_choices
-
-
-class TeacherMoveStudentDisambiguationForm(forms.Form):
-    orig_name = forms.CharField(
-        label='Original Name',
-        widget=forms.TextInput(
-            attrs={'readonly': 'readonly', 'placeholder': 'Original Name', 'type': 'hidden'}))
-    name = forms.CharField(
-        label='Name',
-        widget=forms.TextInput(attrs={'placeholder': 'Name', 'style': 'margin : 0px'}))
-
-    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")
-        return name
-
-
 def validateStudentNames(klass, names):
     validationErrors = []
 
@@ -404,6 +376,34 @@ def check_passwords(password, confirm_password):
         raise forms.ValidationError('The password and the confirmation password do not match')
 
 
+class TeacherMoveStudentsDestinationForm(forms.Form):
+    new_class = forms.ChoiceField(label='Choose a new class from the drop down menu for the selected students.', widget=forms.Select(attrs={'class': 'wide'}))
+
+    def __init__(self, classes, *args, **kwargs):
+        self.classes = classes
+        class_choices = []
+        for klass in classes:
+            class_choices.append((klass.id, klass.name + ' (' + klass.access_code + '), ' + klass.teacher.new_user.first_name + ' ' + klass.teacher.new_user.last_name))
+        super(TeacherMoveStudentsDestinationForm, self).__init__(*args, **kwargs)
+        self.fields['new_class'].choices = class_choices
+
+
+class TeacherMoveStudentDisambiguationForm(forms.Form):
+    orig_name = forms.CharField(
+        label='Original Name',
+        widget=forms.TextInput(
+            attrs={'readonly': 'readonly', 'placeholder': 'Original Name', 'type': 'hidden'}))
+    name = forms.CharField(
+        label='Name',
+        widget=forms.TextInput(attrs={'placeholder': 'Name', 'style': 'margin : 0px'}))
+
+    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")
+        return name
+
+
 class BaseTeacherMoveStudentsDisambiguationFormSet(forms.BaseFormSet):
     def __init__(self, destination, *args, **kwargs):
         self.destination = destination
@@ -423,6 +423,56 @@ def clean(self):
         self.strippedNames = names
 
 
+class TeacherDismissStudentsForm(forms.Form):
+    orig_name = forms.CharField(
+        label='Original Name',
+        widget=forms.TextInput(
+            attrs={'readonly': 'readonly', 'placeholder': 'Original Name', 'style': 'background-color: lightgray; margin: 0; border: 0'}))
+    name = forms.CharField(
+        label='New Name',
+        widget=forms.TextInput(attrs={'placeholder': 'New Name', 'style': 'margin : 0px'}))
+    email = forms.EmailField(
+        label='Email',
+        widget=forms.EmailInput(attrs={'placeholder': 'Email Address', 'style': 'margin : 0px'}))
+    confirm_email = forms.EmailField(
+        label='Confirm Email',
+        widget=forms.EmailInput(
+            attrs={'placeholder': 'Confirm Email Address', 'style': 'margin : 0px'}))
+
+    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")
+
+        if User.objects.filter(username=name).exists():
+            raise forms.ValidationError('That username is already in use')
+
+        return name
+
+    def clean(self):
+        email = self.cleaned_data.get('email', None)
+        confirm_email = self.cleaned_data.get('confirm_email', None)
+
+        if (email or confirm_email) and email != confirm_email:
+            raise forms.ValidationError('Your new emails do not match')
+
+        return self.cleaned_data
+
+
+class BaseTeacherDismissStudentsFormSet(forms.BaseFormSet):
+    def clean(self):
+        if any(self.errors):
+            return
+
+        names = [form.cleaned_data['name'] for form in self.forms]
+
+        validationErrors = validateStudentNames(None, names)
+
+        if len(validationErrors) > 0:
+            raise forms.ValidationError(validationErrors)
+
+
 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
@@ -76,7 +76,8 @@
     materials_viewer as materials_viewer_new, teacher_print_reminder_cards as teacher_print_reminder_cards_new, \
     teacher_delete_students as teacher_delete_students_new, teacher_delete_class as teacher_delete_class_new, \
     teacher_class_password_reset as teacher_class_password_reset_new, teacher_move_students as teacher_move_students_new, \
-    teacher_move_students_to_class as teacher_move_students_to_class_new
+    teacher_move_students_to_class as teacher_move_students_to_class_new,\
+    teacher_dismiss_students as teacher_dismiss_students_new
 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
@@ -242,6 +243,7 @@
     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/(?P<access_code>[A-Z0-9]+)/password_reset/$', teacher_class_password_reset_new, name='teacher_class_password_reset_new'),
+    url(r'^redesign/teach/class/(?P<access_code>[A-Z0-9]+)/students/dismiss/$', teacher_dismiss_students_new, name='teacher_dismiss_students_new'),
     url(r'^redesign/teach/class/move/(?P<access_code>[A-Z0-9]+)/$', teacher_move_class_new, name='teacher_move_class_new'),
     url(r'^redesign/teach/class/(?P<access_code>[A-Z0-9]+)/students/move/$', teacher_move_students_new, name='teacher_move_students_new'),
     url(r'^redesign/teach/class/(?P<access_code>[A-Z0-9]+)/students/move/disambiguate/$', teacher_move_students_to_class_new, name='teacher_move_students_to_class_new'),
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
@@ -58,9 +58,11 @@
 from portal.models import Teacher, Class, Student
 from portal.forms.teach_new import TeacherEditAccountForm, ClassCreationForm, ClassEditForm, ClassMoveForm, \
     TeacherEditStudentForm, TeacherSetStudentPass, TeacherMoveStudentsDestinationForm, \
-    TeacherMoveStudentDisambiguationForm, BaseTeacherMoveStudentsDisambiguationFormSet, StudentCreationForm
+    TeacherMoveStudentDisambiguationForm, BaseTeacherMoveStudentsDisambiguationFormSet, StudentCreationForm, \
+    TeacherDismissStudentsForm, BaseTeacherDismissStudentsFormSet
 from portal.permissions import logged_in_as_teacher
-from portal.helpers.generators import generate_access_code, generate_password
+from portal.helpers.generators import generate_access_code, generate_password, generate_new_student_name
+from portal.helpers.emails_new import send_verification_email
 from portal.views.teacher.pdfs import PDF_DATA
 from portal.templatetags.app_tags import cloud_storage
 
@@ -433,6 +435,64 @@ def teacher_student_reset(request, pk):
                    '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_dismiss_students(request, access_code):
+    klass = get_object_or_404(Class, access_code=access_code)
+
+    check_if_dismiss_authorised(request, klass)
+
+    # get student objects for students to be deleted, confirming they are in the class
+    student_ids = json.loads(request.POST.get('transfer_students', '[]'))
+    students = [get_object_or_404(Student, id=i, class_field=klass) for i in student_ids]
+
+    TeacherDismissStudentsFormSet = formset_factory(wraps(TeacherDismissStudentsForm)(partial(TeacherDismissStudentsForm)), extra=0, formset=BaseTeacherDismissStudentsFormSet)
+
+    if is_right_dismiss_form(request):
+        formset = TeacherDismissStudentsFormSet(request.POST)
+        if formset.is_valid():
+            return process_dismiss_student_form(request, formset, klass, access_code)
+
+    else:
+        initial_data = [{'orig_name': student.new_user.first_name,
+                         'name': generate_new_student_name(student.new_user.first_name),
+                         'email': ''}
+                        for student in students]
+
+        formset = TeacherDismissStudentsFormSet(initial=initial_data)
+
+    return render(request, 'redesign/teach_new/teacher_dismiss_students_new.html',
+                  {'formset': formset,
+                   'class': klass,
+                   'students': students})
+
+
+def check_if_dismiss_authorised(request, klass):
+    # check user is authorised to deal with class
+    if request.user.new_teacher != klass.teacher:
+        raise Http404
+
+
+def is_right_dismiss_form(request):
+    return request.method == 'POST' and 'submit_dismiss' in request.POST
+
+
+def process_dismiss_student_form(request, formset, klass, access_code):
+    for data in formset.cleaned_data:
+        student = get_object_or_404(Student, class_field=klass, new_user__first_name__iexact=data['orig_name'])
+        student.class_field = None
+        student.new_user.first_name = data['name']
+        student.new_user.username = data['name']
+        student.new_user.email = data['email']
+        student.save()
+        student.new_user.save()
+
+        send_verification_email(request, student.new_user)
+
+    messages.success(request, 'The students have been removed successfully from the class.')
+    return HttpResponseRedirect(reverse_lazy('view_class', kwargs={'access_code': 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_class_password_reset(request, access_code):
@@ -532,7 +592,7 @@ def teacher_move_students_to_class(request, access_code):
 
     TeacherMoveStudentDisambiguationFormSet = formset_factory(wraps(TeacherMoveStudentDisambiguationForm)(partial(TeacherMoveStudentDisambiguationForm)), extra=0, formset=BaseTeacherMoveStudentsDisambiguationFormSet)
 
-    if is_right_form(request):
+    if is_right_move_form(request):
         formset = TeacherMoveStudentDisambiguationFormSet(new_class, request.POST)
         if formset.is_valid():
             return process_move_students_form(request, formset, old_class, new_class)
@@ -562,7 +622,7 @@ def check_if_move_authorised(request, old_class, new_class):
         raise Http404
 
 
-def is_right_form(request):
+def is_right_move_form(request):
     return request.method == 'POST' and 'submit_disambiguation' in request.POST
 
 


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
@@ -40,6 +40,7 @@
 import edit_student_page
 import onboarding_student_list_page
 import move_students_page_new
+import dismiss_students_page_new
 
 
 class TeachClassPage(TeachBasePage):
@@ -84,6 +85,11 @@ def move_students_none_selected(self):
 
         return self
 
+    def dismiss_students(self):
+        self.browser.find_element_by_id('dismissSelectedStudents').click()
+
+        return dismiss_students_page_new.TeachDismissStudentsPage(self.browser)
+
     def cancel_dialog(self):
         self.browser.find_element_by_xpath(
             "//div[contains(@class,'ui-dialog')]//span[contains(text(),'Cancel')]").click()
diff --git a/portal/tests/pageObjects/portal/teach/dismiss_students_page_new.py b/portal/tests/pageObjects/portal/teach/dismiss_students_page_new.py
new file mode 100644
--- /dev/null
+++ b/portal/tests/pageObjects/portal/teach/dismiss_students_page_new.py
@@ -0,0 +1,60 @@
+# -*- 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 class_page_new
+
+from teach_base_page_new import TeachBasePage
+
+
+class TeachDismissStudentsPage(TeachBasePage):
+    def __init__(self, browser):
+        super(TeachDismissStudentsPage, self).__init__(browser)
+
+        assert self.on_correct_page('dismiss_students_page')
+
+    def enter_email(self, email):
+        self.browser.find_element_by_id("id_form-0-email").send_keys(email)
+        self.browser.find_element_by_id("id_form-0-confirm_email").send_keys(email)
+        return self
+
+    def cancel(self):
+        self.browser.find_element_by_id('cancel_button').click()
+        return class_page_new.TeachClassPage(self.browser)
+
+    def dismiss(self):
+        self.browser.find_element_by_id('dismiss_button').click()
+        return class_page_new.TeachClassPage(self.browser)
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
@@ -261,3 +261,23 @@ def test_move(self):
         page = page.go_to_top().logout().go_to_login_page().login(email_2, password_2)
         page = page.go_to_class_page()
         assert page.student_exists(student_name_1)
+
+    def test_dismiss(self):
+        email, password = signup_teacher_directly()
+        create_organisation_directly(email)
+        _, class_name, access_code = create_class_directly(email)
+        student_name_1, student_password_1, _ = create_school_student_directly(access_code)
+        student_name_2, student_password_2, _ = 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)
+        page = page.go_to_class_page()
+        assert page.student_exists(student_name_1)
+
+        page = page.toggle_select_student().dismiss_students()
+        assert page.__class__.__name__ == 'TeachDismissStudentsPage'
+        page = page.cancel()
+        assert page.__class__.__name__ == 'TeachClassPage'
+
+        page = page.toggle_select_student().dismiss_students().enter_email("student_email@gmail.com").dismiss()
+        assert not page.student_exists(student_name_1)