318 lines
12 KiB
Python
318 lines
12 KiB
Python
"""
|
|
tests for the models
|
|
"""
|
|
from datetime import datetime, timedelta
|
|
from django.utils.timezone import UTC
|
|
from mock import patch
|
|
from nose.plugins.attrib import attr
|
|
from student.models import CourseEnrollment # pylint: disable=import-error
|
|
from student.roles import CourseCcxCoachRole # pylint: disable=import-error
|
|
from student.tests.factories import ( # pylint: disable=import-error
|
|
AdminFactory,
|
|
CourseEnrollmentFactory,
|
|
UserFactory,
|
|
)
|
|
from util.tests.test_date_utils import fake_ugettext
|
|
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
|
from xmodule.modulestore.tests.factories import (
|
|
CourseFactory,
|
|
check_mongo_calls
|
|
)
|
|
|
|
from .factories import (
|
|
CcxFactory,
|
|
CcxFutureMembershipFactory,
|
|
)
|
|
from ..models import (
|
|
CcxMembership,
|
|
CcxFutureMembership,
|
|
)
|
|
from ..overrides import override_field_for_ccx
|
|
|
|
|
|
@attr('shard_1')
|
|
class TestCcxMembership(ModuleStoreTestCase):
|
|
"""Unit tests for the CcxMembership model
|
|
"""
|
|
|
|
def setUp(self):
|
|
"""common setup for all tests"""
|
|
super(TestCcxMembership, self).setUp()
|
|
self.course = course = CourseFactory.create()
|
|
coach = AdminFactory.create()
|
|
role = CourseCcxCoachRole(course.id)
|
|
role.add_users(coach)
|
|
self.ccx = CcxFactory(course_id=course.id, coach=coach)
|
|
enrollment = CourseEnrollmentFactory.create(course_id=course.id)
|
|
self.enrolled_user = enrollment.user
|
|
self.unenrolled_user = UserFactory.create()
|
|
|
|
def create_future_enrollment(self, user, auto_enroll=True):
|
|
"""
|
|
utility method to create future enrollment
|
|
"""
|
|
pfm = CcxFutureMembershipFactory.create(
|
|
ccx=self.ccx,
|
|
email=user.email,
|
|
auto_enroll=auto_enroll
|
|
)
|
|
return pfm
|
|
|
|
def has_course_enrollment(self, user):
|
|
"""
|
|
utility method to create future enrollment
|
|
"""
|
|
enrollment = CourseEnrollment.objects.filter(
|
|
user=user, course_id=self.course.id
|
|
)
|
|
return enrollment.exists()
|
|
|
|
def has_ccx_membership(self, user):
|
|
"""
|
|
verify ccx membership
|
|
"""
|
|
membership = CcxMembership.objects.filter(
|
|
student=user, ccx=self.ccx, active=True
|
|
)
|
|
return membership.exists()
|
|
|
|
def has_ccx_future_membership(self, user):
|
|
"""
|
|
verify future ccx membership
|
|
"""
|
|
future_membership = CcxFutureMembership.objects.filter(
|
|
email=user.email, ccx=self.ccx
|
|
)
|
|
return future_membership.exists()
|
|
|
|
def call_mut(self, student, future_membership):
|
|
"""
|
|
Call the method undser test
|
|
"""
|
|
CcxMembership.auto_enroll(student, future_membership)
|
|
|
|
def test_ccx_auto_enroll_unregistered_user(self):
|
|
"""verify auto_enroll works when user is not enrolled in the MOOC
|
|
|
|
n.b. After auto_enroll, user will have both a MOOC enrollment and a
|
|
CCX membership
|
|
"""
|
|
user = self.unenrolled_user
|
|
pfm = self.create_future_enrollment(user)
|
|
self.assertTrue(self.has_ccx_future_membership(user))
|
|
self.assertFalse(self.has_course_enrollment(user))
|
|
# auto_enroll user
|
|
self.call_mut(user, pfm)
|
|
|
|
self.assertTrue(self.has_course_enrollment(user))
|
|
self.assertTrue(self.has_ccx_membership(user))
|
|
self.assertFalse(self.has_ccx_future_membership(user))
|
|
|
|
def test_ccx_auto_enroll_registered_user(self):
|
|
"""verify auto_enroll works when user is enrolled in the MOOC
|
|
"""
|
|
user = self.enrolled_user
|
|
pfm = self.create_future_enrollment(user)
|
|
self.assertTrue(self.has_ccx_future_membership(user))
|
|
self.assertTrue(self.has_course_enrollment(user))
|
|
|
|
self.call_mut(user, pfm)
|
|
|
|
self.assertTrue(self.has_course_enrollment(user))
|
|
self.assertTrue(self.has_ccx_membership(user))
|
|
self.assertFalse(self.has_ccx_future_membership(user))
|
|
|
|
def test_future_membership_disallows_auto_enroll(self):
|
|
"""verify that the CcxFutureMembership can veto auto_enroll
|
|
"""
|
|
user = self.unenrolled_user
|
|
pfm = self.create_future_enrollment(user, auto_enroll=False)
|
|
self.assertTrue(self.has_ccx_future_membership(user))
|
|
self.assertFalse(self.has_course_enrollment(user))
|
|
|
|
self.assertRaises(ValueError, self.call_mut, user, pfm)
|
|
|
|
self.assertFalse(self.has_course_enrollment(user))
|
|
self.assertFalse(self.has_ccx_membership(user))
|
|
self.assertTrue(self.has_ccx_future_membership(user))
|
|
|
|
|
|
@attr('shard_1')
|
|
class TestCCX(ModuleStoreTestCase):
|
|
"""Unit tests for the CustomCourseForEdX model
|
|
"""
|
|
|
|
def setUp(self):
|
|
"""common setup for all tests"""
|
|
super(TestCCX, self).setUp()
|
|
self.course = course = CourseFactory.create()
|
|
coach = AdminFactory.create()
|
|
role = CourseCcxCoachRole(course.id)
|
|
role.add_users(coach)
|
|
self.ccx = CcxFactory(course_id=course.id, coach=coach)
|
|
|
|
def set_ccx_override(self, field, value):
|
|
"""Create a field override for the test CCX on <field> with <value>"""
|
|
override_field_for_ccx(self.ccx, self.course, field, value)
|
|
|
|
def test_ccx_course_is_correct_course(self):
|
|
"""verify that the course property of a ccx returns the right course"""
|
|
expected = self.course
|
|
actual = self.ccx.course
|
|
self.assertEqual(expected, actual)
|
|
|
|
def test_ccx_course_caching(self):
|
|
"""verify that caching the propery works to limit queries"""
|
|
with check_mongo_calls(1):
|
|
# these statements are used entirely to demonstrate the
|
|
# instance-level caching of these values on CCX objects. The
|
|
# check_mongo_calls context is the point here.
|
|
self.ccx.course # pylint: disable=pointless-statement
|
|
with check_mongo_calls(0):
|
|
self.ccx.course # pylint: disable=pointless-statement
|
|
|
|
def test_ccx_start_is_correct(self):
|
|
"""verify that the start datetime for a ccx is correctly retrieved
|
|
|
|
Note that after setting the start field override microseconds are
|
|
truncated, so we can't do a direct comparison between before and after.
|
|
For this reason we test the difference between and make sure it is less
|
|
than one second.
|
|
"""
|
|
expected = datetime.now(UTC())
|
|
self.set_ccx_override('start', expected)
|
|
actual = self.ccx.start # pylint: disable=no-member
|
|
diff = expected - actual
|
|
self.assertTrue(abs(diff.total_seconds()) < 1)
|
|
|
|
def test_ccx_start_caching(self):
|
|
"""verify that caching the start property works to limit queries"""
|
|
now = datetime.now(UTC())
|
|
self.set_ccx_override('start', now)
|
|
with check_mongo_calls(1):
|
|
# these statements are used entirely to demonstrate the
|
|
# instance-level caching of these values on CCX objects. The
|
|
# check_mongo_calls context is the point here.
|
|
self.ccx.start # pylint: disable=pointless-statement, no-member
|
|
with check_mongo_calls(0):
|
|
self.ccx.start # pylint: disable=pointless-statement, no-member
|
|
|
|
def test_ccx_due_without_override(self):
|
|
"""verify that due returns None when the field has not been set"""
|
|
actual = self.ccx.due # pylint: disable=no-member
|
|
self.assertIsNone(actual)
|
|
|
|
def test_ccx_due_is_correct(self):
|
|
"""verify that the due datetime for a ccx is correctly retrieved"""
|
|
expected = datetime.now(UTC())
|
|
self.set_ccx_override('due', expected)
|
|
actual = self.ccx.due # pylint: disable=no-member
|
|
diff = expected - actual
|
|
self.assertTrue(abs(diff.total_seconds()) < 1)
|
|
|
|
def test_ccx_due_caching(self):
|
|
"""verify that caching the due property works to limit queries"""
|
|
expected = datetime.now(UTC())
|
|
self.set_ccx_override('due', expected)
|
|
with check_mongo_calls(1):
|
|
# these statements are used entirely to demonstrate the
|
|
# instance-level caching of these values on CCX objects. The
|
|
# check_mongo_calls context is the point here.
|
|
self.ccx.due # pylint: disable=pointless-statement, no-member
|
|
with check_mongo_calls(0):
|
|
self.ccx.due # pylint: disable=pointless-statement, no-member
|
|
|
|
def test_ccx_has_started(self):
|
|
"""verify that a ccx marked as starting yesterday has started"""
|
|
now = datetime.now(UTC())
|
|
delta = timedelta(1)
|
|
then = now - delta
|
|
self.set_ccx_override('start', then)
|
|
self.assertTrue(self.ccx.has_started()) # pylint: disable=no-member
|
|
|
|
def test_ccx_has_not_started(self):
|
|
"""verify that a ccx marked as starting tomorrow has not started"""
|
|
now = datetime.now(UTC())
|
|
delta = timedelta(1)
|
|
then = now + delta
|
|
self.set_ccx_override('start', then)
|
|
self.assertFalse(self.ccx.has_started()) # pylint: disable=no-member
|
|
|
|
def test_ccx_has_ended(self):
|
|
"""verify that a ccx that has a due date in the past has ended"""
|
|
now = datetime.now(UTC())
|
|
delta = timedelta(1)
|
|
then = now - delta
|
|
self.set_ccx_override('due', then)
|
|
self.assertTrue(self.ccx.has_ended()) # pylint: disable=no-member
|
|
|
|
def test_ccx_has_not_ended(self):
|
|
"""verify that a ccx that has a due date in the future has not eneded
|
|
"""
|
|
now = datetime.now(UTC())
|
|
delta = timedelta(1)
|
|
then = now + delta
|
|
self.set_ccx_override('due', then)
|
|
self.assertFalse(self.ccx.has_ended()) # pylint: disable=no-member
|
|
|
|
def test_ccx_without_due_date_has_not_ended(self):
|
|
"""verify that a ccx without a due date has not ended"""
|
|
self.assertFalse(self.ccx.has_ended()) # pylint: disable=no-member
|
|
|
|
# ensure that the expected localized format will be found by the i18n
|
|
# service
|
|
@patch('util.date_utils.ugettext', fake_ugettext(translations={
|
|
"SHORT_DATE_FORMAT": "%b %d, %Y",
|
|
}))
|
|
def test_start_datetime_short_date(self):
|
|
"""verify that the start date for a ccx formats properly by default"""
|
|
start = datetime(2015, 1, 1, 12, 0, 0, tzinfo=UTC())
|
|
expected = "Jan 01, 2015"
|
|
self.set_ccx_override('start', start)
|
|
actual = self.ccx.start_datetime_text() # pylint: disable=no-member
|
|
self.assertEqual(expected, actual)
|
|
|
|
@patch('util.date_utils.ugettext', fake_ugettext(translations={
|
|
"DATE_TIME_FORMAT": "%b %d, %Y at %H:%M",
|
|
}))
|
|
def test_start_datetime_date_time_format(self):
|
|
"""verify that the DATE_TIME format also works as expected"""
|
|
start = datetime(2015, 1, 1, 12, 0, 0, tzinfo=UTC())
|
|
expected = "Jan 01, 2015 at 12:00 UTC"
|
|
self.set_ccx_override('start', start)
|
|
actual = self.ccx.start_datetime_text('DATE_TIME') # pylint: disable=no-member
|
|
self.assertEqual(expected, actual)
|
|
|
|
@patch('util.date_utils.ugettext', fake_ugettext(translations={
|
|
"SHORT_DATE_FORMAT": "%b %d, %Y",
|
|
}))
|
|
def test_end_datetime_short_date(self):
|
|
"""verify that the end date for a ccx formats properly by default"""
|
|
end = datetime(2015, 1, 1, 12, 0, 0, tzinfo=UTC())
|
|
expected = "Jan 01, 2015"
|
|
self.set_ccx_override('due', end)
|
|
actual = self.ccx.end_datetime_text() # pylint: disable=no-member
|
|
self.assertEqual(expected, actual)
|
|
|
|
@patch('util.date_utils.ugettext', fake_ugettext(translations={
|
|
"DATE_TIME_FORMAT": "%b %d, %Y at %H:%M",
|
|
}))
|
|
def test_end_datetime_date_time_format(self):
|
|
"""verify that the DATE_TIME format also works as expected"""
|
|
end = datetime(2015, 1, 1, 12, 0, 0, tzinfo=UTC())
|
|
expected = "Jan 01, 2015 at 12:00 UTC"
|
|
self.set_ccx_override('due', end)
|
|
actual = self.ccx.end_datetime_text('DATE_TIME') # pylint: disable=no-member
|
|
self.assertEqual(expected, actual)
|
|
|
|
@patch('util.date_utils.ugettext', fake_ugettext(translations={
|
|
"DATE_TIME_FORMAT": "%b %d, %Y at %H:%M",
|
|
}))
|
|
def test_end_datetime_no_due_date(self):
|
|
"""verify that without a due date, the end date is an empty string"""
|
|
expected = ''
|
|
actual = self.ccx.end_datetime_text() # pylint: disable=no-member
|
|
self.assertEqual(expected, actual)
|
|
actual = self.ccx.end_datetime_text('DATE_TIME') # pylint: disable=no-member
|
|
self.assertEqual(expected, actual)
|