diff --git a/lms/djangoapps/pocs/tests/factories.py b/lms/djangoapps/pocs/tests/factories.py new file mode 100644 index 0000000000..1e98ae8846 --- /dev/null +++ b/lms/djangoapps/pocs/tests/factories.py @@ -0,0 +1,17 @@ +from factory.django import DjangoModelFactory +from pocs.models import PersonalOnlineCourse +from pocs.models import PocMembership +from pocs.models import PocFutureMembership + + +class PocFactory(DjangoModelFactory): + FACTORY_FOR = PersonalOnlineCourse + display_name = "Test POC" + + +class PocMembershipFactory(DjangoModelFactory): + FACTORY_FOR = PocMembership + active = False + +class PocFutureMembershipFactory(DjangoModelFactory): + FACTORY_FOR = PocFutureMembership diff --git a/lms/djangoapps/pocs/tests/test_utils.py b/lms/djangoapps/pocs/tests/test_utils.py new file mode 100644 index 0000000000..9094215b9e --- /dev/null +++ b/lms/djangoapps/pocs/tests/test_utils.py @@ -0,0 +1,484 @@ +from pocs.models import ( + PocMembership, + PocFutureMembership, +) +from pocs.tests.factories import ( + PocFactory, + PocMembershipFactory, + PocFutureMembershipFactory, +) +from student.roles import CoursePocCoachRole +from student.tests.factories import ( + AdminFactory, + UserFactory, +) +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory + + +class TestEmailEnrollmentState(ModuleStoreTestCase): + """unit tests for the EmailEnrollmentState class + """ + + def setUp(self): + """ + Set up tests + """ + course = CourseFactory.create() + coach = AdminFactory.create() + role = CoursePocCoachRole(course.id) + role.add_users(coach) + self.poc = PocFactory(course_id=course.id, coach=coach) + + def create_user(self): + """provide a legitimate django user for testing + """ + if getattr(self, 'user', None) is None: + self.user = UserFactory() + + def register_user_in_poc(self): + """create registration of self.user in self.poc + + registration will be inactive + """ + self.create_user() + PocMembershipFactory(poc=self.poc, student=self.user) + + def create_one(self, email=None): + """Create a single EmailEnrollmentState object and return it + """ + from pocs.utils import EmailEnrollmentState + if email is None: + email = self.user.email + return EmailEnrollmentState(self.poc, email) + + def test_enrollment_state_for_non_user(self): + """verify behavior for non-user email address + """ + ee_state = self.create_one(email='nobody@nowhere.com') + for attr in ['user', 'member', 'full_name', 'in_poc']: + value = getattr(ee_state, attr, 'missing attribute') + self.assertFalse(value, "{}: {}".format(value, attr)) + + def test_enrollment_state_for_non_member_user(self): + """verify behavior for email address of user who is not a poc memeber + """ + self.create_user() + ee_state = self.create_one() + self.assertTrue(ee_state.user) + self.assertFalse(ee_state.in_poc) + self.assertEqual(ee_state.member, self.user) + self.assertEqual(ee_state.full_name, self.user.profile.name) + + def test_enrollment_state_for_member_user(self): + """verify behavior for email address of user who is a poc member + """ + self.create_user() + self.register_user_in_poc() + ee_state = self.create_one() + for attr in ['user', 'in_poc']: + self.assertTrue( + getattr(ee_state, attr, False), + "attribute {} is missing or False".format(attr) + ) + self.assertEqual(ee_state.member, self.user) + self.assertEqual(ee_state.full_name, self.user.profile.name) + + def test_enrollment_state_to_dict(self): + """verify dict representation of EmailEnrollmentState + """ + self.create_user() + self.register_user_in_poc() + ee_state = self.create_one() + ee_dict = ee_state.to_dict() + expected = { + 'user': True, + 'member': self.user, + 'in_poc': True, + } + for expected_key, expected_value in expected.iteritems(): + self.assertTrue(expected_key in ee_dict) + self.assertEqual(expected_value, ee_dict[expected_key]) + + def test_enrollment_state_repr(self): + self.create_user() + self.register_user_in_poc() + ee_state = self.create_one() + representation = repr(ee_state) + self.assertTrue('user=True' in representation) + self.assertTrue('in_poc=True' in representation) + member = 'member={}'.format(self.user) + self.assertTrue(member in representation) + + +# TODO: deal with changes in behavior for auto_enroll +class TestGetEmailParams(ModuleStoreTestCase): + """tests for pocs.utils.get_email_params + """ + def setUp(self): + """ + Set up tests + """ + course = CourseFactory.create() + coach = AdminFactory.create() + role = CoursePocCoachRole(course.id) + role.add_users(coach) + self.poc = PocFactory(course_id=course.id, coach=coach) + self.all_keys = [ + 'site_name', 'course', 'course_url', 'registration_url', + 'course_about_url', 'auto_enroll' + ] + self.url_keys = [k for k in self.all_keys if 'url' in k] + self.course_keys = [k for k in self.url_keys if 'course' in k] + + def call_FUT(self, auto_enroll=False, secure=False): + from pocs.utils import get_email_params + return get_email_params(self.poc, auto_enroll, secure) + + def test_params_have_expected_keys(self): + params = self.call_FUT() + self.assertFalse(set(params.keys()) - set(self.all_keys)) + + def test_poc_id_in_params(self): + expected_course_id = self.poc.course_id.to_deprecated_string() + params = self.call_FUT() + self.assertEqual(params['course'], self.poc) + for url_key in self.url_keys: + self.assertTrue('http://' in params[url_key]) + for url_key in self.course_keys: + self.assertTrue(expected_course_id in params[url_key]) + + def test_security_respected(self): + secure = self.call_FUT(secure=True) + for url_key in self.url_keys: + self.assertTrue('https://' in secure[url_key]) + insecure = self.call_FUT(secure=False) + for url_key in self.url_keys: + self.assertTrue('http://' in insecure[url_key]) + + def test_auto_enroll_passed_correctly(self): + not_auto = self.call_FUT(auto_enroll=False) + self.assertFalse(not_auto['auto_enroll']) + auto = self.call_FUT(auto_enroll=True) + self.assertTrue(auto['auto_enroll']) + + +# TODO: deal with changes in behavior for auto_enroll +class TestEnrollEmail(ModuleStoreTestCase): + """tests for the enroll_email function from pocs.utils + """ + def setUp(self): + course = CourseFactory.create() + coach = AdminFactory.create() + role = CoursePocCoachRole(course.id) + role.add_users(coach) + self.poc = PocFactory(course_id=course.id, coach=coach) + self.outbox = self.get_outbox() + + def create_user(self): + """provide a legitimate django user for testing + """ + if getattr(self, 'user', None) is None: + self.user = UserFactory() + + def register_user_in_poc(self): + """create registration of self.user in self.poc + + registration will be inactive + """ + self.create_user() + PocMembershipFactory(poc=self.poc, student=self.user) + + def get_outbox(self): + """Return the django mail outbox""" + from django.core import mail + return mail.outbox + + def check_membership(self, email=None, user=None, future=False): + """Verify tjat an appropriate Poc Membership exists""" + if not email and not user: + self.fail( + "must provide user or email address to check Poc Membership" + ) + if future and email: + membership = PocFutureMembership.objects.filter( + poc=self.poc, email=email + ) + elif not future: + if not user: + user = self.user + membership = PocMembership.objects.filter( + poc=self.poc, student=user + ) + self.assertTrue(membership.exists()) + + def check_enrollment_state(self, state, in_poc, member, user): + """Verify an enrollment state object against provided arguments + + state.in_poc will always be a boolean + state.user will always be a boolean + state.member will be a Django user object or None + """ + self.assertEqual(in_poc, state.in_poc) + self.assertEqual(member, state.member) + self.assertEqual(user, state.user) + + def call_FUT( + self, + student_email=None, + auto_enroll=False, + email_students=False, + email_params=None + ): + from pocs.utils import enroll_email + if student_email is None: + student_email = self.user.email + before, after = enroll_email( + self.poc, student_email, auto_enroll, email_students, email_params + ) + return before, after + + def test_enroll_non_user_sending_email(self): + """enroll a non-user email and send an enrollment email to them + """ + # ensure no emails are in the outbox now + self.assertEqual(len(self.outbox), 0) + test_email = "nobody@nowhere.com" + before, after = self.call_FUT( + student_email=test_email, email_students=True + ) + + # there should be a future membership set for this email address now + self.check_membership(email=test_email, future=True) + for state in [before, after]: + self.check_enrollment_state(state, False, None, False) + # mail was sent and to the right person + self.assertEqual(len(self.outbox), 1) + msg = self.outbox[0] + self.assertTrue(test_email in msg.recipients()) + + def test_enroll_non_member_sending_email(self): + """register a non-member and send an enrollment email to them + """ + self.create_user() + # ensure no emails are in the outbox now + self.assertEqual(len(self.outbox), 0) + before, after = self.call_FUT(email_students=True) + + # there should be a membership set for this email address now + self.check_membership(email=self.user.email) + self.check_enrollment_state(before, False, self.user, True) + self.check_enrollment_state(after, True, self.user, True) + # mail was sent and to the right person + self.assertEqual(len(self.outbox), 1) + msg = self.outbox[0] + self.assertTrue(self.user.email in msg.recipients()) + + def test_enroll_member_sending_email(self): + """register a member and send an enrollment email to them + """ + self.register_user_in_poc() + # ensure no emails are in the outbox now + self.assertEqual(len(self.outbox), 0) + before, after = self.call_FUT(email_students=True) + + # there should be a membership set for this email address now + self.check_membership(email=self.user.email) + for state in [before, after]: + self.check_enrollment_state(state, True, self.user, True) + # mail was sent and to the right person + self.assertEqual(len(self.outbox), 1) + msg = self.outbox[0] + self.assertTrue(self.user.email in msg.recipients()) + + def test_enroll_non_user_no_email(self): + """register a non-user via email address but send no email + """ + # ensure no emails are in the outbox now + self.assertEqual(len(self.outbox), 0) + test_email = "nobody@nowhere.com" + before, after = self.call_FUT( + student_email=test_email, email_students=False + ) + + # there should be a future membership set for this email address now + self.check_membership(email=test_email, future=True) + for state in [before, after]: + self.check_enrollment_state(state, False, None, False) + # ensure there are still no emails in the outbox now + self.assertEqual(len(self.outbox), 0) + + + def test_enroll_non_member_no_email(self): + """register a non-member but send no email""" + self.create_user() + # ensure no emails are in the outbox now + self.assertEqual(len(self.outbox), 0) + before, after = self.call_FUT(email_students=False) + + # there should be a membership set for this email address now + self.check_membership(email=self.user.email) + self.check_enrollment_state(before, False, self.user, True) + self.check_enrollment_state(after, True, self.user, True) + # ensure there are still no emails in the outbox now + self.assertEqual(len(self.outbox), 0) + + def test_enroll_member_no_email(self): + """enroll a member but send no email + """ + self.register_user_in_poc() + # ensure no emails are in the outbox now + self.assertEqual(len(self.outbox), 0) + before, after = self.call_FUT(email_students=False) + + # there should be a membership set for this email address now + self.check_membership(email=self.user.email) + for state in [before, after]: + self.check_enrollment_state(state, True, self.user, True) + # ensure there are still no emails in the outbox now + self.assertEqual(len(self.outbox), 0) + + +# TODO: deal with changes in behavior for auto_enroll +class TestUnenrollEmail(ModuleStoreTestCase): + """Tests for the unenroll_email function from pocs.utils""" + def setUp(self): + course = CourseFactory.create() + coach = AdminFactory.create() + role = CoursePocCoachRole(course.id) + role.add_users(coach) + self.poc = PocFactory(course_id=course.id, coach=coach) + self.outbox = self.get_outbox() + + def tearDown(self): + for attr in ['user', 'email']: + if hasattr(self, attr): + delattr(self, attr) + + def get_outbox(self): + """Return the django mail outbox""" + from django.core import mail + return mail.outbox + + def create_user(self): + """provide a legitimate django user for testing + """ + if getattr(self, 'user', None) is None: + self.user = UserFactory() + + def make_poc_membership(self): + """create registration of self.user in self.poc + + registration will be inactive + """ + self.create_user() + PocMembershipFactory.create(poc=self.poc, student=self.user) + + def make_poc_future_membership(self): + """create future registration for email in self.poc""" + self.email = "nobody@nowhere.com" + PocFutureMembershipFactory.create( + poc=self.poc, email=self.email + ) + + def check_enrollment_state(self, state, in_poc, member, user): + """Verify an enrollment state object against provided arguments + + state.in_poc will always be a boolean + state.user will always be a boolean + state.member will be a Django user object or None + """ + self.assertEqual(in_poc, state.in_poc) + self.assertEqual(member, state.member) + self.assertEqual(user, state.user) + + def check_membership(self, future=False): + if future: + membership = PocFutureMembership.objects.filter( + poc=self.poc, email=self.email + ) + else: + membership = PocMembership.objects.filter( + poc=self.poc, student=self.user + ) + return membership.exists() + + def call_FUT(self, email_students=False): + from pocs.utils import unenroll_email + email = hasattr(self, 'user') and self.user.email or self.email + return unenroll_email(self.poc, email, email_students=email_students) + + def test_unenroll_future_member_with_email(self): + """unenroll a future member and send an email + """ + self.make_poc_future_membership() + # assert that a membership exists and that no emails have been sent + self.assertTrue(self.check_membership(future=True)) + self.assertEqual(len(self.outbox), 0) + # unenroll the student + before, after = self.call_FUT(email_students=True) + + # assert that membership is now gone + self.assertFalse(self.check_membership(future=True)) + # validate the before and after enrollment states + for state in [before, after]: + self.check_enrollment_state(state, False, None, False) + # check that mail was sent and to the right person + self.assertEqual(len(self.outbox), 1) + msg = self.outbox[0] + self.assertTrue(self.email in msg.recipients()) + + def test_unenroll_member_with_email(self): + """unenroll a current member and send an email""" + self.make_poc_membership() + # assert that a membership exists and that no emails have been sent + self.assertTrue(self.check_membership()) + self.assertEqual(len(self.outbox), 0) + # unenroll the student + before, after = self.call_FUT(email_students=True) + + # assert that membership is now gone + self.assertFalse(self.check_membership()) + # validate the before and after enrollment state + self.check_enrollment_state(after, False, self.user, True) + self.check_enrollment_state(before, True, self.user, True) + # check that mail was sent and to the right person + self.assertEqual(len(self.outbox), 1) + msg = self.outbox[0] + self.assertTrue(self.user.email in msg.recipients()) + + def test_unenroll_future_member_no_email(self): + """unenroll a future member but send no email + """ + self.make_poc_future_membership() + # assert that a membership exists and that no emails have been sent + self.assertTrue(self.check_membership(future=True)) + self.assertEqual(len(self.outbox), 0) + # unenroll the student + before, after = self.call_FUT() + + # assert that membership is now gone + self.assertFalse(self.check_membership(future=True)) + # validate the before and after enrollment states + for state in [before, after]: + self.check_enrollment_state(state, False, None, False) + # no email was sent to the student + self.assertEqual(len(self.outbox), 0) + + def test_unenroll_member_no_email(self): + """unenroll a current member but send no email + """ + self.make_poc_membership() + # assert that a membership exists and that no emails have been sent + self.assertTrue(self.check_membership()) + self.assertEqual(len(self.outbox), 0) + # unenroll the student + before, after = self.call_FUT() + + # assert that membership is now gone + self.assertFalse(self.check_membership()) + # validate the before and after enrollment state + self.check_enrollment_state(after, False, self.user, True) + self.check_enrollment_state(before, True, self.user, True) + # no email was sent to the student + self.assertEqual(len(self.outbox), 0) diff --git a/lms/djangoapps/pocs/tests/test_views.py b/lms/djangoapps/pocs/tests/test_views.py index feaa83107f..0f5e3e0ba2 100644 --- a/lms/djangoapps/pocs/tests/test_views.py +++ b/lms/djangoapps/pocs/tests/test_views.py @@ -8,12 +8,26 @@ from courseware.tests.helpers import LoginEnrollmentTestCase from django.core.urlresolvers import reverse from edxmako.shortcuts import render_to_response from student.roles import CoursePocCoachRole -from student.tests.factories import AdminFactory +from student.tests.factories import ( + AdminFactory, + CourseEnrollmentFactory, +) from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory - -from ..models import PersonalOnlineCourse +from xmodule.modulestore.tests.factories import ( + CourseFactory, + ItemFactory, +) +from ..models import ( + PersonalOnlineCourse, + PocMembership, + PocFutureMembership, +) from ..overrides import get_override_for_poc +from .factories import ( + PocFactory, + PocMembershipFactory, + PocFutureMembershipFactory, +) def intercept_renderer(path, context): @@ -65,6 +79,14 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): role = CoursePocCoachRole(self.course.id) role.add_users(self.coach) + def make_poc(self): + poc = PocFactory(course_id=self.course.id, coach=self.coach) + return poc + + def get_outbox(self): + from django.core import mail + return mail.outbox + def tearDown(self): """ Undo patches. @@ -158,6 +180,122 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): course_start = get_override_for_poc(poc, self.course, 'start') self.assertEqual(str(course_start)[:-9], u'2014-11-20 00:00') + def test_enroll_member_student(self): + self.make_coach() + poc = self.make_poc() + enrollment = CourseEnrollmentFactory(course_id=self.course.id) + student = enrollment.user + outbox = self.get_outbox() + self.assertEqual(len(outbox), 0) + + url = reverse( + 'poc_invite', + kwargs={'course_id': self.course.id.to_deprecated_string()} + ) + data = { + 'enrollment-button': 'Enroll', + 'student-ids': u','.join([student.email, ]), + } + response = self.client.post(url, data=data, follow=True) + self.assertEqual(response.status_code, 200) + # we were redirected to our current location + self.assertEqual(len(response.redirect_chain), 1) + self.assertTrue(302 in response.redirect_chain[0]) + self.assertEqual(len(outbox), 1) + self.assertTrue(student.email in outbox[0].recipients()) + # a PocMembership exists for this student + self.assertTrue( + PocMembership.objects.filter(poc=poc, student=student).exists() + ) + + def test_unenroll_member_student(self): + self.make_coach() + poc = self.make_poc() + enrollment = CourseEnrollmentFactory(course_id=self.course.id) + student = enrollment.user + outbox = self.get_outbox() + self.assertEqual(len(outbox), 0) + # student is member of POC: + PocMembershipFactory(poc=poc, student=student) + + url = reverse( + 'poc_invite', + kwargs={'course_id': self.course.id.to_deprecated_string()} + ) + data = { + 'enrollment-button': 'Unenroll', + 'student-ids': u','.join([student.email, ]), + } + response = self.client.post(url, data=data, follow=True) + self.assertEqual(response.status_code, 200) + # we were redirected to our current location + self.assertEqual(len(response.redirect_chain), 1) + self.assertTrue(302 in response.redirect_chain[0]) + self.assertEqual(len(outbox), 1) + self.assertTrue(student.email in outbox[0].recipients()) + # the membership for this student is gone + self.assertFalse( + PocMembership.objects.filter(poc=poc, student=student).exists() + ) + + def test_enroll_non_user_student(self): + test_email = "nobody@nowhere.com" + self.make_coach() + poc = self.make_poc() + outbox = self.get_outbox() + self.assertEqual(len(outbox), 0) + + url = reverse( + 'poc_invite', + kwargs={'course_id': self.course.id.to_deprecated_string()} + ) + data = { + 'enrollment-button': 'Enroll', + 'student-ids': u','.join([test_email, ]), + } + response = self.client.post(url, data=data, follow=True) + self.assertEqual(response.status_code, 200) + # we were redirected to our current location + self.assertEqual(len(response.redirect_chain), 1) + self.assertTrue(302 in response.redirect_chain[0]) + self.assertEqual(len(outbox), 1) + self.assertTrue(test_email in outbox[0].recipients()) + self.assertTrue( + PocFutureMembership.objects.filter( + poc=poc, email=test_email + ).exists() + ) + + def test_unenroll_non_user_student(self): + test_email = "nobody@nowhere.com" + self.make_coach() + poc = self.make_poc() + outbox = self.get_outbox() + PocFutureMembershipFactory(poc=poc, email=test_email) + self.assertEqual(len(outbox), 0) + + url = reverse( + 'poc_invite', + kwargs={'course_id': self.course.id.to_deprecated_string()} + ) + data = { + 'enrollment-button': 'Unenroll', + 'student-ids': u','.join([test_email, ]), + } + response = self.client.post(url, data=data, follow=True) + self.assertEqual(response.status_code, 200) + # we were redirected to our current location + self.assertEqual(len(response.redirect_chain), 1) + self.assertTrue(302 in response.redirect_chain[0]) + self.assertEqual(len(outbox), 1) + self.assertTrue(test_email in outbox[0].recipients()) + self.assertFalse( + PocFutureMembership.objects.filter( + poc=poc, email=test_email + ).exists() + ) + + def flatten(seq): """ diff --git a/lms/djangoapps/pocs/utils.py b/lms/djangoapps/pocs/utils.py index 6890bc18c6..1dbf068902 100644 --- a/lms/djangoapps/pocs/utils.py +++ b/lms/djangoapps/pocs/utils.py @@ -31,6 +31,7 @@ class EmailEnrollmentState(object): in_poc = poc_member.exists() full_name = user.profile.name else: + user = None in_poc = False full_name = None self.user = exists_user