Commit ae44f184 by Jeremy Bowman Committed by GitHub

Merge pull request #15951 from edx/jmbowman/ddt_cleanup

ddt usage cleanup
parents d6e89e7b fd6af6b0
...@@ -29,6 +29,11 @@ class CourseModeModelTest(TestCase): ...@@ -29,6 +29,11 @@ class CourseModeModelTest(TestCase):
""" """
Tests for the CourseMode model Tests for the CourseMode model
""" """
NOW = 'now'
DATES = {
NOW: datetime.now(),
None: None,
}
def setUp(self): def setUp(self):
super(CourseModeModelTest, self).setUp() super(CourseModeModelTest, self).setUp()
...@@ -317,10 +322,11 @@ class CourseModeModelTest(TestCase): ...@@ -317,10 +322,11 @@ class CourseModeModelTest(TestCase):
CourseMode.PROFESSIONAL, CourseMode.PROFESSIONAL,
CourseMode.NO_ID_PROFESSIONAL_MODE CourseMode.NO_ID_PROFESSIONAL_MODE
), ),
(datetime.now(), None), (NOW, None),
)) ))
@ddt.unpack @ddt.unpack
def test_invalid_mode_expiration(self, mode_slug, exp_dt): def test_invalid_mode_expiration(self, mode_slug, exp_dt_name):
exp_dt = self.DATES[exp_dt_name]
is_error_expected = CourseMode.is_professional_slug(mode_slug) and exp_dt is not None is_error_expected = CourseMode.is_professional_slug(mode_slug) and exp_dt is not None
try: try:
self.create_mode(mode_slug=mode_slug, mode_name=mode_slug.title(), expiration_datetime=exp_dt, min_price=10) self.create_mode(mode_slug=mode_slug, mode_name=mode_slug.title(), expiration_datetime=exp_dt, min_price=10)
......
...@@ -234,12 +234,16 @@ class CourseRole(RoleBase): ...@@ -234,12 +234,16 @@ class CourseRole(RoleBase):
def course_group_already_exists(self, course_key): def course_group_already_exists(self, course_key):
return CourseAccessRole.objects.filter(org=course_key.org, course_id=course_key).exists() return CourseAccessRole.objects.filter(org=course_key.org, course_id=course_key).exists()
def __repr__(self):
return '<{}: course_key={}>'.format(self.__class__.__name__, self.course_key)
class OrgRole(RoleBase): class OrgRole(RoleBase):
""" """
A named role in a particular org independent of course A named role in a particular org independent of course
""" """
pass def __repr__(self):
return '<{}>'.format(self.__class__.__name__)
@register_access_role @register_access_role
......
...@@ -33,8 +33,13 @@ from xmodule.modulestore.tests.factories import CourseFactory ...@@ -33,8 +33,13 @@ from xmodule.modulestore.tests.factories import CourseFactory
class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
"""Tests for per-course verification status on the dashboard. """ """Tests for per-course verification status on the dashboard. """
PAST = datetime.now(UTC) - timedelta(days=5) PAST = 'past'
FUTURE = datetime.now(UTC) + timedelta(days=5) FUTURE = 'future'
DATES = {
PAST: datetime.now(UTC) - timedelta(days=5),
FUTURE: datetime.now(UTC) + timedelta(days=5),
None: None,
}
URLCONF_MODULES = ['verify_student.urls'] URLCONF_MODULES = ['verify_student.urls']
...@@ -91,14 +96,14 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): ...@@ -91,14 +96,14 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_VERIFY) self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_VERIFY)
def test_need_to_verify_expiration(self): def test_need_to_verify_expiration(self):
self._setup_mode_and_enrollment(self.FUTURE, "verified") self._setup_mode_and_enrollment(self.DATES[self.FUTURE], "verified")
response = self.client.get(self.dashboard_url) response = self.client.get(self.dashboard_url)
self.assertContains(response, self.BANNER_ALT_MESSAGES[VERIFY_STATUS_NEED_TO_VERIFY]) self.assertContains(response, self.BANNER_ALT_MESSAGES[VERIFY_STATUS_NEED_TO_VERIFY])
self.assertContains(response, "You only have 4 days left to verify for this course.") self.assertContains(response, "You only have 4 days left to verify for this course.")
@ddt.data(None, FUTURE) @ddt.data(None, FUTURE)
def test_waiting_approval(self, expiration): def test_waiting_approval(self, expiration):
self._setup_mode_and_enrollment(expiration, "verified") self._setup_mode_and_enrollment(self.DATES[expiration], "verified")
# The student has submitted a photo verification # The student has submitted a photo verification
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
...@@ -110,7 +115,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): ...@@ -110,7 +115,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
@ddt.data(None, FUTURE) @ddt.data(None, FUTURE)
def test_fully_verified(self, expiration): def test_fully_verified(self, expiration):
self._setup_mode_and_enrollment(expiration, "verified") self._setup_mode_and_enrollment(self.DATES[expiration], "verified")
# The student has an approved verification # The student has an approved verification
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
...@@ -127,7 +132,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): ...@@ -127,7 +132,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
def test_missed_verification_deadline(self): def test_missed_verification_deadline(self):
# Expiration date in the past # Expiration date in the past
self._setup_mode_and_enrollment(self.PAST, "verified") self._setup_mode_and_enrollment(self.DATES[self.PAST], "verified")
# The student does NOT have an approved verification # The student does NOT have an approved verification
# so the status should show that the student missed the deadline. # so the status should show that the student missed the deadline.
...@@ -135,7 +140,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): ...@@ -135,7 +140,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
def test_missed_verification_deadline_verification_was_expired(self): def test_missed_verification_deadline_verification_was_expired(self):
# Expiration date in the past # Expiration date in the past
self._setup_mode_and_enrollment(self.PAST, "verified") self._setup_mode_and_enrollment(self.DATES[self.PAST], "verified")
# Create a verification, but the expiration date of the verification # Create a verification, but the expiration date of the verification
# occurred before the deadline. # occurred before the deadline.
...@@ -143,7 +148,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): ...@@ -143,7 +148,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
attempt.mark_ready() attempt.mark_ready()
attempt.submit() attempt.submit()
attempt.approve() attempt.approve()
attempt.created_at = self.PAST - timedelta(days=900) attempt.created_at = self.DATES[self.PAST] - timedelta(days=900)
attempt.save() attempt.save()
# The student didn't have an approved verification at the deadline, # The student didn't have an approved verification at the deadline,
...@@ -152,14 +157,14 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): ...@@ -152,14 +157,14 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
def test_missed_verification_deadline_but_later_verified(self): def test_missed_verification_deadline_but_later_verified(self):
# Expiration date in the past # Expiration date in the past
self._setup_mode_and_enrollment(self.PAST, "verified") self._setup_mode_and_enrollment(self.DATES[self.PAST], "verified")
# Successfully verify, but after the deadline has already passed # Successfully verify, but after the deadline has already passed
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
attempt.mark_ready() attempt.mark_ready()
attempt.submit() attempt.submit()
attempt.approve() attempt.approve()
attempt.created_at = self.PAST - timedelta(days=900) attempt.created_at = self.DATES[self.PAST] - timedelta(days=900)
attempt.save() attempt.save()
# The student didn't have an approved verification at the deadline, # The student didn't have an approved verification at the deadline,
...@@ -168,7 +173,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): ...@@ -168,7 +173,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
def test_verification_denied(self): def test_verification_denied(self):
# Expiration date in the future # Expiration date in the future
self._setup_mode_and_enrollment(self.FUTURE, "verified") self._setup_mode_and_enrollment(self.DATES[self.FUTURE], "verified")
# Create a verification with the specified status # Create a verification with the specified status
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
...@@ -182,7 +187,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): ...@@ -182,7 +187,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
def test_verification_error(self): def test_verification_error(self):
# Expiration date in the future # Expiration date in the future
self._setup_mode_and_enrollment(self.FUTURE, "verified") self._setup_mode_and_enrollment(self.DATES[self.FUTURE], "verified")
# Create a verification with the specified status # Create a verification with the specified status
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
...@@ -196,7 +201,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): ...@@ -196,7 +201,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
@override_settings(VERIFY_STUDENT={"DAYS_GOOD_FOR": 5, "EXPIRING_SOON_WINDOW": 10}) @override_settings(VERIFY_STUDENT={"DAYS_GOOD_FOR": 5, "EXPIRING_SOON_WINDOW": 10})
def test_verification_will_expire_by_deadline(self): def test_verification_will_expire_by_deadline(self):
# Expiration date in the future # Expiration date in the future
self._setup_mode_and_enrollment(self.FUTURE, "verified") self._setup_mode_and_enrollment(self.DATES[self.FUTURE], "verified")
# Create a verification attempt that: # Create a verification attempt that:
# 1) Is current (submitted in the last year) # 1) Is current (submitted in the last year)
...@@ -213,7 +218,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): ...@@ -213,7 +218,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
@override_settings(VERIFY_STUDENT={"DAYS_GOOD_FOR": 5, "EXPIRING_SOON_WINDOW": 10}) @override_settings(VERIFY_STUDENT={"DAYS_GOOD_FOR": 5, "EXPIRING_SOON_WINDOW": 10})
def test_reverification_submitted_with_current_approved_verificaiton(self): def test_reverification_submitted_with_current_approved_verificaiton(self):
# Expiration date in the future # Expiration date in the future
self._setup_mode_and_enrollment(self.FUTURE, "verified") self._setup_mode_and_enrollment(self.DATES[self.FUTURE], "verified")
# Create a verification attempt that is approved but expiring soon # Create a verification attempt that is approved but expiring soon
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
...@@ -236,7 +241,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): ...@@ -236,7 +241,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
def test_verification_occurred_after_deadline(self): def test_verification_occurred_after_deadline(self):
# Expiration date in the past # Expiration date in the past
self._setup_mode_and_enrollment(self.PAST, "verified") self._setup_mode_and_enrollment(self.DATES[self.PAST], "verified")
# The deadline has passed, and we've asked the student # The deadline has passed, and we've asked the student
# to reverify (through the support team). # to reverify (through the support team).
...@@ -250,7 +255,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): ...@@ -250,7 +255,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
def test_with_two_verifications(self): def test_with_two_verifications(self):
# checking if a user has two verification and but most recent verification course deadline is expired # checking if a user has two verification and but most recent verification course deadline is expired
self._setup_mode_and_enrollment(self.FUTURE, "verified") self._setup_mode_and_enrollment(self.DATES[self.FUTURE], "verified")
# The student has an approved verification # The student has an approved verification
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
...@@ -274,7 +279,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): ...@@ -274,7 +279,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
CourseModeFactory.create( CourseModeFactory.create(
course_id=course2.id, course_id=course2.id,
mode_slug="verified", mode_slug="verified",
expiration_datetime=self.PAST expiration_datetime=self.DATES[self.PAST]
) )
CourseEnrollmentFactory( CourseEnrollmentFactory(
course_id=course2.id, course_id=course2.id,
......
...@@ -297,19 +297,18 @@ class StudentDashboardTests(SharedModuleStoreTestCase): ...@@ -297,19 +297,18 @@ class StudentDashboardTests(SharedModuleStoreTestCase):
@patch.multiple('django.conf.settings', **MOCK_SETTINGS) @patch.multiple('django.conf.settings', **MOCK_SETTINGS)
@ddt.data( @ddt.data(
*itertools.product( *itertools.product(
[TOMORROW],
[True, False], [True, False],
[True, False], [True, False],
[ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split], [ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split],
) )
) )
@ddt.unpack @ddt.unpack
def test_sharing_icons_for_future_course(self, start_date, set_marketing, set_social_sharing, modulestore_type): def test_sharing_icons_for_future_course(self, set_marketing, set_social_sharing, modulestore_type):
""" """
Verify that the course sharing icons show up if course is starting in future and Verify that the course sharing icons show up if course is starting in future and
any of marketing or social sharing urls are set. any of marketing or social sharing urls are set.
""" """
self.course = CourseFactory.create(start=start_date, emit_signals=True, default_store=modulestore_type) self.course = CourseFactory.create(start=self.TOMORROW, emit_signals=True, default_store=modulestore_type)
self.course_enrollment = CourseEnrollmentFactory(course_id=self.course.id, user=self.user) self.course_enrollment = CourseEnrollmentFactory(course_id=self.course.id, user=self.user)
self.set_course_sharing_urls(set_marketing, set_social_sharing) self.set_course_sharing_urls(set_marketing, set_social_sharing)
......
...@@ -31,20 +31,26 @@ class TransactionManagersTestCase(TransactionTestCase): ...@@ -31,20 +31,26 @@ class TransactionManagersTestCase(TransactionTestCase):
To test do: "./manage.py lms --settings=test_with_mysql test util.tests.test_db" To test do: "./manage.py lms --settings=test_with_mysql test util.tests.test_db"
""" """
DECORATORS = {
'outer_atomic': outer_atomic(),
'outer_atomic_read_committed': outer_atomic(read_committed=True),
'commit_on_success': commit_on_success(),
'commit_on_success_read_committed': commit_on_success(read_committed=True),
}
@ddt.data( @ddt.data(
(outer_atomic(), IntegrityError, None, True), ('outer_atomic', IntegrityError, None, True),
(outer_atomic(read_committed=True), type(None), False, True), ('outer_atomic_read_committed', type(None), False, True),
(commit_on_success(), IntegrityError, None, True), ('commit_on_success', IntegrityError, None, True),
(commit_on_success(read_committed=True), type(None), False, True), ('commit_on_success_read_committed', type(None), False, True),
) )
@ddt.unpack @ddt.unpack
def test_concurrent_requests(self, transaction_decorator, exception_class, created_in_1, created_in_2): def test_concurrent_requests(self, transaction_decorator_name, exception_class, created_in_1, created_in_2):
""" """
Test that when isolation level is set to READ COMMITTED get_or_create() Test that when isolation level is set to READ COMMITTED get_or_create()
for the same row in concurrent requests does not raise an IntegrityError. for the same row in concurrent requests does not raise an IntegrityError.
""" """
transaction_decorator = self.DECORATORS[transaction_decorator_name]
if connection.vendor != 'mysql': if connection.vendor != 'mysql':
raise unittest.SkipTest('Only works on MySQL.') raise unittest.SkipTest('Only works on MySQL.')
......
""" """
General testing utilities. General testing utilities.
""" """
import functools
import sys import sys
from contextlib import contextmanager from contextlib import contextmanager
...@@ -124,3 +125,29 @@ class MockS3Mixin(object): ...@@ -124,3 +125,29 @@ class MockS3Mixin(object):
def tearDown(self): def tearDown(self):
self._mock_s3.stop() self._mock_s3.stop()
super(MockS3Mixin, self).tearDown() super(MockS3Mixin, self).tearDown()
class reprwrapper(object):
"""
Wrapper class for functions that need a normalized string representation.
"""
def __init__(self, func):
self._func = func
self.repr = 'Func: {}'.format(func.__name__)
functools.update_wrapper(self, func)
def __call__(self, *args, **kw):
return self._func(*args, **kw)
def __repr__(self):
return self.repr
def normalize_repr(func):
"""
Function decorator used to normalize its string representation.
Used to wrap functions used as ddt parameters, so pytest-xdist
doesn't complain about the sequence of discovered tests differing
between worker processes.
"""
return reprwrapper(func)
...@@ -207,42 +207,42 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase): ...@@ -207,42 +207,42 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase):
# Eligible and should stay that way # Eligible and should stay that way
( (
CertificateStatuses.downloadable, CertificateStatuses.downloadable,
datetime.now(pytz.UTC) - timedelta(days=2), timedelta(days=-2),
'Pass', 'Pass',
CertificateStatuses.generating CertificateStatuses.generating
), ),
# Ensure that certs in the wrong state can be fixed by regeneration # Ensure that certs in the wrong state can be fixed by regeneration
( (
CertificateStatuses.downloadable, CertificateStatuses.downloadable,
datetime.now(pytz.UTC) - timedelta(hours=1), timedelta(hours=-1),
'Pass', 'Pass',
CertificateStatuses.audit_passing CertificateStatuses.audit_passing
), ),
# Ineligible and should stay that way # Ineligible and should stay that way
( (
CertificateStatuses.audit_passing, CertificateStatuses.audit_passing,
datetime.now(pytz.UTC) - timedelta(hours=1), timedelta(hours=-1),
'Pass', 'Pass',
CertificateStatuses.audit_passing CertificateStatuses.audit_passing
), ),
# As above # As above
( (
CertificateStatuses.audit_notpassing, CertificateStatuses.audit_notpassing,
datetime.now(pytz.UTC) - timedelta(hours=1), timedelta(hours=-1),
'Pass', 'Pass',
CertificateStatuses.audit_passing CertificateStatuses.audit_passing
), ),
# As above # As above
( (
CertificateStatuses.audit_notpassing, CertificateStatuses.audit_notpassing,
datetime.now(pytz.UTC) - timedelta(hours=1), timedelta(hours=-1),
None, None,
CertificateStatuses.audit_notpassing CertificateStatuses.audit_notpassing
), ),
) )
@ddt.unpack @ddt.unpack
@override_settings(AUDIT_CERT_CUTOFF_DATE=datetime.now(pytz.UTC) - timedelta(days=1)) @override_settings(AUDIT_CERT_CUTOFF_DATE=datetime.now(pytz.UTC) - timedelta(days=1))
def test_regen_audit_certs_eligibility(self, status, created_date, grade, expected_status): def test_regen_audit_certs_eligibility(self, status, created_delta, grade, expected_status):
""" """
Test that existing audit certificates remain eligible even if cert Test that existing audit certificates remain eligible even if cert
generation is re-run. generation is re-run.
...@@ -254,6 +254,7 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase): ...@@ -254,6 +254,7 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase):
is_active=True, is_active=True,
mode=CourseMode.AUDIT, mode=CourseMode.AUDIT,
) )
created_date = datetime.now(pytz.UTC) + created_delta
with freezegun.freeze_time(created_date): with freezegun.freeze_time(created_date):
GeneratedCertificateFactory( GeneratedCertificateFactory(
user=self.user_2, user=self.user_2,
......
...@@ -106,6 +106,11 @@ class CourseListViewTests(CourseApiViewTestMixin, ModuleStoreTestCase): ...@@ -106,6 +106,11 @@ class CourseListViewTests(CourseApiViewTestMixin, ModuleStoreTestCase):
@ddt.ddt @ddt.ddt
class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase): class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase):
""" Tests for CourseRetrieveUpdateView. """ """ Tests for CourseRetrieveUpdateView. """
NOW = 'now'
DATES = {
NOW: datetime.now(),
None: None,
}
def setUp(self): def setUp(self):
super(CourseRetrieveUpdateViewTests, self).setUp() super(CourseRetrieveUpdateViewTests, self).setUp()
...@@ -276,12 +281,13 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase) ...@@ -276,12 +281,13 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase)
@ddt.data(*itertools.product( @ddt.data(*itertools.product(
('honor', 'audit', 'verified', 'professional', 'no-id-professional'), ('honor', 'audit', 'verified', 'professional', 'no-id-professional'),
(datetime.now(), None), (NOW, None),
)) ))
@ddt.unpack @ddt.unpack
def test_update_professional_expiration(self, mode_slug, expiration_datetime): def test_update_professional_expiration(self, mode_slug, expiration_datetime_name):
""" Verify that pushing a mode with a professional certificate and an expiration datetime """ Verify that pushing a mode with a professional certificate and an expiration datetime
will be rejected (this is not allowed). """ will be rejected (this is not allowed). """
expiration_datetime = self.DATES[expiration_datetime_name]
mode = self._serialize_course_mode( mode = self._serialize_course_mode(
CourseMode( CourseMode(
mode_slug=mode_slug, mode_slug=mode_slug,
......
...@@ -162,9 +162,14 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes ...@@ -162,9 +162,14 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes
""" """
Tests for the various access controls on the student dashboard Tests for the various access controls on the student dashboard
""" """
TOMORROW = datetime.datetime.now(pytz.utc) + datetime.timedelta(days=1) TOMORROW = 'tomorrow'
YESTERDAY = datetime.datetime.now(pytz.utc) - datetime.timedelta(days=1) YESTERDAY = 'yesterday'
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
DATES = {
TOMORROW: datetime.datetime.now(pytz.utc) + datetime.timedelta(days=1),
YESTERDAY: datetime.datetime.now(pytz.utc) - datetime.timedelta(days=1),
None: None,
}
def setUp(self): def setUp(self):
super(AccessTestCase, self).setUp() super(AccessTestCase, self).setUp()
...@@ -439,7 +444,7 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes ...@@ -439,7 +444,7 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes
mock_unit = Mock(location=self.course.location, user_partitions=[]) mock_unit = Mock(location=self.course.location, user_partitions=[])
mock_unit._class_tags = {} # Needed for detached check in _has_access_descriptor mock_unit._class_tags = {} # Needed for detached check in _has_access_descriptor
mock_unit.visible_to_staff_only = visible_to_staff_only mock_unit.visible_to_staff_only = visible_to_staff_only
mock_unit.start = start mock_unit.start = self.DATES[start]
mock_unit.merged_group_access = {} mock_unit.merged_group_access = {}
self.verify_access(mock_unit, expected_access, expected_error_type) self.verify_access(mock_unit, expected_access, expected_error_type)
...@@ -448,7 +453,7 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes ...@@ -448,7 +453,7 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes
mock_unit = Mock(user_partitions=[]) mock_unit = Mock(user_partitions=[])
mock_unit._class_tags = {} mock_unit._class_tags = {}
mock_unit.days_early_for_beta = 2 mock_unit.days_early_for_beta = 2
mock_unit.start = self.TOMORROW mock_unit.start = self.DATES[self.TOMORROW]
mock_unit.visible_to_staff_only = False mock_unit.visible_to_staff_only = False
mock_unit.merged_group_access = {} mock_unit.merged_group_access = {}
...@@ -465,7 +470,7 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes ...@@ -465,7 +470,7 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes
mock_unit = Mock(location=self.course.location, user_partitions=[]) mock_unit = Mock(location=self.course.location, user_partitions=[])
mock_unit._class_tags = {} # Needed for detached check in _has_access_descriptor mock_unit._class_tags = {} # Needed for detached check in _has_access_descriptor
mock_unit.visible_to_staff_only = False mock_unit.visible_to_staff_only = False
mock_unit.start = start mock_unit.start = self.DATES[start]
mock_unit.merged_group_access = {} mock_unit.merged_group_access = {}
self.verify_access(mock_unit, True) self.verify_access(mock_unit, True)
...@@ -486,7 +491,7 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes ...@@ -486,7 +491,7 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes
mock_unit = Mock(location=self.course.location, user_partitions=[]) mock_unit = Mock(location=self.course.location, user_partitions=[])
mock_unit._class_tags = {} # Needed for detached check in _has_access_descriptor mock_unit._class_tags = {} # Needed for detached check in _has_access_descriptor
mock_unit.visible_to_staff_only = False mock_unit.visible_to_staff_only = False
mock_unit.start = start mock_unit.start = self.DATES[start]
mock_unit.merged_group_access = {} mock_unit.merged_group_access = {}
self.verify_access(mock_unit, expected_access, expected_error_type) self.verify_access(mock_unit, expected_access, expected_error_type)
......
...@@ -49,6 +49,12 @@ TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT ...@@ -49,6 +49,12 @@ TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
class CoursesTest(ModuleStoreTestCase): class CoursesTest(ModuleStoreTestCase):
"""Test methods related to fetching courses.""" """Test methods related to fetching courses."""
ENABLED_SIGNALS = ['course_published'] ENABLED_SIGNALS = ['course_published']
GET_COURSE_WITH_ACCESS = 'get_course_with_access'
GET_COURSE_OVERVIEW_WITH_ACCESS = 'get_course_overview_with_access'
COURSE_ACCESS_FUNCS = {
GET_COURSE_WITH_ACCESS: get_course_with_access,
GET_COURSE_OVERVIEW_WITH_ACCESS: get_course_overview_with_access,
}
@override_settings(CMS_BASE=CMS_BASE_TEST) @override_settings(CMS_BASE=CMS_BASE_TEST)
def test_get_cms_course_block_link(self): def test_get_cms_course_block_link(self):
...@@ -64,8 +70,9 @@ class CoursesTest(ModuleStoreTestCase): ...@@ -64,8 +70,9 @@ class CoursesTest(ModuleStoreTestCase):
cms_url = u"//{}/course/{}".format(CMS_BASE_TEST, unicode(self.course.location)) cms_url = u"//{}/course/{}".format(CMS_BASE_TEST, unicode(self.course.location))
self.assertEqual(cms_url, get_cms_block_link(self.course, 'course')) self.assertEqual(cms_url, get_cms_block_link(self.course, 'course'))
@ddt.data(get_course_with_access, get_course_overview_with_access) @ddt.data(GET_COURSE_WITH_ACCESS, GET_COURSE_OVERVIEW_WITH_ACCESS)
def test_get_course_func_with_access_error(self, course_access_func): def test_get_course_func_with_access_error(self, course_access_func_name):
course_access_func = self.COURSE_ACCESS_FUNCS[course_access_func_name]
user = UserFactory.create() user = UserFactory.create()
course = CourseFactory.create(visible_to_staff_only=True) course = CourseFactory.create(visible_to_staff_only=True)
...@@ -76,11 +83,12 @@ class CoursesTest(ModuleStoreTestCase): ...@@ -76,11 +83,12 @@ class CoursesTest(ModuleStoreTestCase):
self.assertFalse(error.exception.access_response.has_access) self.assertFalse(error.exception.access_response.has_access)
@ddt.data( @ddt.data(
(get_course_with_access, 1), (GET_COURSE_WITH_ACCESS, 1),
(get_course_overview_with_access, 0), (GET_COURSE_OVERVIEW_WITH_ACCESS, 0),
) )
@ddt.unpack @ddt.unpack
def test_get_course_func_with_access(self, course_access_func, num_mongo_calls): def test_get_course_func_with_access(self, course_access_func_name, num_mongo_calls):
course_access_func = self.COURSE_ACCESS_FUNCS[course_access_func_name]
user = UserFactory.create() user = UserFactory.create()
course = CourseFactory.create(emit_signals=True) course = CourseFactory.create(emit_signals=True)
with check_mongo_calls(num_mongo_calls): with check_mongo_calls(num_mongo_calls):
......
...@@ -1570,11 +1570,11 @@ class TestStaffDebugInfo(SharedModuleStoreTestCase): ...@@ -1570,11 +1570,11 @@ class TestStaffDebugInfo(SharedModuleStoreTestCase):
PER_COURSE_ANONYMIZED_DESCRIPTORS = (LTIDescriptor, ) PER_COURSE_ANONYMIZED_DESCRIPTORS = (LTIDescriptor, )
# The "set" here is to work around the bug that load_classes returns duplicates for multiply-delcared classes. # The "set" here is to work around the bug that load_classes returns duplicates for multiply-declared classes.
PER_STUDENT_ANONYMIZED_DESCRIPTORS = set( PER_STUDENT_ANONYMIZED_DESCRIPTORS = sorted(set(
class_ for (name, class_) in XModuleDescriptor.load_classes() class_ for (name, class_) in XModuleDescriptor.load_classes()
if not issubclass(class_, PER_COURSE_ANONYMIZED_DESCRIPTORS) if not issubclass(class_, PER_COURSE_ANONYMIZED_DESCRIPTORS)
) ), key=str)
@attr(shard=1) @attr(shard=1)
......
...@@ -13,6 +13,7 @@ from mock import MagicMock, Mock, patch ...@@ -13,6 +13,7 @@ from mock import MagicMock, Mock, patch
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from webob import Request from webob import Request
from common.test.utils import normalize_repr
from openedx.core.djangoapps.contentserver.caching import del_cached_content from openedx.core.djangoapps.contentserver.caching import del_cached_content
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
...@@ -105,6 +106,7 @@ def _upload_file(subs_file, location, filename): ...@@ -105,6 +106,7 @@ def _upload_file(subs_file, location, filename):
del_cached_content(content.location) del_cached_content(content.location)
@normalize_repr
def attach_sub(item, filename): def attach_sub(item, filename):
""" """
Attach `en` transcript. Attach `en` transcript.
...@@ -112,6 +114,7 @@ def attach_sub(item, filename): ...@@ -112,6 +114,7 @@ def attach_sub(item, filename):
item.sub = filename item.sub = filename
@normalize_repr
def attach_bumper_transcript(item, filename, lang="en"): def attach_bumper_transcript(item, filename, lang="en"):
""" """
Attach bumper transcript. Attach bumper transcript.
......
...@@ -17,6 +17,7 @@ from path import Path as path ...@@ -17,6 +17,7 @@ from path import Path as path
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.tests.django_utils import TEST_DATA_MONGO_MODULESTORE, TEST_DATA_SPLIT_MODULESTORE from xmodule.modulestore.tests.django_utils import TEST_DATA_MONGO_MODULESTORE, TEST_DATA_SPLIT_MODULESTORE
from xmodule.tests.test_import import DummySystem from xmodule.tests.test_import import DummySystem
...@@ -29,6 +30,11 @@ from .helpers import BaseTestXmodule ...@@ -29,6 +30,11 @@ from .helpers import BaseTestXmodule
from .test_video_handlers import TestVideo from .test_video_handlers import TestVideo
from .test_video_xml import SOURCE_XML from .test_video_xml import SOURCE_XML
MODULESTORES = {
ModuleStoreEnum.Type.mongo: TEST_DATA_MONGO_MODULESTORE,
ModuleStoreEnum.Type.split: TEST_DATA_SPLIT_MODULESTORE,
}
@attr(shard=1) @attr(shard=1)
class TestVideoYouTube(TestVideo): class TestVideoYouTube(TestVideo):
...@@ -1162,14 +1168,14 @@ class TestEditorSavedMethod(BaseTestXmodule): ...@@ -1162,14 +1168,14 @@ class TestEditorSavedMethod(BaseTestXmodule):
self.test_dir = path(__file__).abspath().dirname().dirname().dirname().dirname().dirname() self.test_dir = path(__file__).abspath().dirname().dirname().dirname().dirname().dirname()
self.file_path = self.test_dir + '/common/test/data/uploads/' + self.file_name self.file_path = self.test_dir + '/common/test/data/uploads/' + self.file_name
@ddt.data(TEST_DATA_MONGO_MODULESTORE, TEST_DATA_SPLIT_MODULESTORE) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_editor_saved_when_html5_sub_not_exist(self, default_store): def test_editor_saved_when_html5_sub_not_exist(self, default_store):
""" """
When there is youtube_sub exist but no html5_sub present for When there is youtube_sub exist but no html5_sub present for
html5_sources, editor_saved function will generate new html5_sub html5_sources, editor_saved function will generate new html5_sub
for video. for video.
""" """
self.MODULESTORE = default_store # pylint: disable=invalid-name self.MODULESTORE = MODULESTORES[default_store] # pylint: disable=invalid-name
self.initialize_module(metadata=self.metadata) self.initialize_module(metadata=self.metadata)
item = self.store.get_item(self.item_descriptor.location) item = self.store.get_item(self.item_descriptor.location)
with open(self.file_path, "r") as myfile: with open(self.file_path, "r") as myfile:
...@@ -1184,13 +1190,13 @@ class TestEditorSavedMethod(BaseTestXmodule): ...@@ -1184,13 +1190,13 @@ class TestEditorSavedMethod(BaseTestXmodule):
self.assertIsInstance(Transcript.get_asset(item.location, 'subs_3_yD_cEKoCk.srt.sjson'), StaticContent) self.assertIsInstance(Transcript.get_asset(item.location, 'subs_3_yD_cEKoCk.srt.sjson'), StaticContent)
self.assertIsInstance(Transcript.get_asset(item.location, 'subs_video.srt.sjson'), StaticContent) self.assertIsInstance(Transcript.get_asset(item.location, 'subs_video.srt.sjson'), StaticContent)
@ddt.data(TEST_DATA_MONGO_MODULESTORE, TEST_DATA_SPLIT_MODULESTORE) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_editor_saved_when_youtube_and_html5_subs_exist(self, default_store): def test_editor_saved_when_youtube_and_html5_subs_exist(self, default_store):
""" """
When both youtube_sub and html5_sub already exist then no new When both youtube_sub and html5_sub already exist then no new
sub will be generated by editor_saved function. sub will be generated by editor_saved function.
""" """
self.MODULESTORE = default_store self.MODULESTORE = MODULESTORES[default_store]
self.initialize_module(metadata=self.metadata) self.initialize_module(metadata=self.metadata)
item = self.store.get_item(self.item_descriptor.location) item = self.store.get_item(self.item_descriptor.location)
with open(self.file_path, "r") as myfile: with open(self.file_path, "r") as myfile:
...@@ -1205,12 +1211,12 @@ class TestEditorSavedMethod(BaseTestXmodule): ...@@ -1205,12 +1211,12 @@ class TestEditorSavedMethod(BaseTestXmodule):
item.editor_saved(self.user, old_metadata, None) item.editor_saved(self.user, old_metadata, None)
self.assertFalse(manage_video_subtitles_save.called) self.assertFalse(manage_video_subtitles_save.called)
@ddt.data(TEST_DATA_MONGO_MODULESTORE, TEST_DATA_SPLIT_MODULESTORE) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_editor_saved_with_unstripped_video_id(self, default_store): def test_editor_saved_with_unstripped_video_id(self, default_store):
""" """
Verify editor saved when video id contains spaces/tabs. Verify editor saved when video id contains spaces/tabs.
""" """
self.MODULESTORE = default_store self.MODULESTORE = MODULESTORES[default_store]
stripped_video_id = unicode(uuid4()) stripped_video_id = unicode(uuid4())
unstripped_video_id = u'{video_id}{tabs}'.format(video_id=stripped_video_id, tabs=u'\t\t\t') unstripped_video_id = u'{video_id}{tabs}'.format(video_id=stripped_video_id, tabs=u'\t\t\t')
self.metadata.update({ self.metadata.update({
...@@ -1226,14 +1232,14 @@ class TestEditorSavedMethod(BaseTestXmodule): ...@@ -1226,14 +1232,14 @@ class TestEditorSavedMethod(BaseTestXmodule):
item.editor_saved(self.user, old_metadata, None) item.editor_saved(self.user, old_metadata, None)
self.assertEqual(item.edx_video_id, stripped_video_id) self.assertEqual(item.edx_video_id, stripped_video_id)
@ddt.data(TEST_DATA_MONGO_MODULESTORE, TEST_DATA_SPLIT_MODULESTORE) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
@patch('xmodule.video_module.video_module.edxval_api.get_url_for_profile', Mock(return_value='test_yt_id')) @patch('xmodule.video_module.video_module.edxval_api.get_url_for_profile', Mock(return_value='test_yt_id'))
def test_editor_saved_with_yt_val_profile(self, default_store): def test_editor_saved_with_yt_val_profile(self, default_store):
""" """
Verify editor saved overrides `youtube_id_1_0` when a youtube val profile is there Verify editor saved overrides `youtube_id_1_0` when a youtube val profile is there
for a given `edx_video_id`. for a given `edx_video_id`.
""" """
self.MODULESTORE = default_store self.MODULESTORE = MODULESTORES[default_store]
self.initialize_module(metadata=self.metadata) self.initialize_module(metadata=self.metadata)
item = self.store.get_item(self.item_descriptor.location) item = self.store.get_item(self.item_descriptor.location)
self.assertEqual(item.youtube_id_1_0, '3_yD_cEKoCk') self.assertEqual(item.youtube_id_1_0, '3_yD_cEKoCk')
......
...@@ -251,6 +251,11 @@ class ViewsTestCase(ModuleStoreTestCase): ...@@ -251,6 +251,11 @@ class ViewsTestCase(ModuleStoreTestCase):
""" """
Tests for views.py methods. Tests for views.py methods.
""" """
YESTERDAY = 'yesterday'
DATES = {
YESTERDAY: datetime.now(UTC) - timedelta(days=1),
None: None,
}
def setUp(self): def setUp(self):
super(ViewsTestCase, self).setUp() super(ViewsTestCase, self).setUp()
...@@ -751,7 +756,7 @@ class ViewsTestCase(ModuleStoreTestCase): ...@@ -751,7 +756,7 @@ class ViewsTestCase(ModuleStoreTestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIn('Financial Assistance Application', response.content) self.assertIn('Financial Assistance Application', response.content)
@ddt.data(([CourseMode.AUDIT, CourseMode.VERIFIED], CourseMode.AUDIT, True, datetime.now(UTC) - timedelta(days=1)), @ddt.data(([CourseMode.AUDIT, CourseMode.VERIFIED], CourseMode.AUDIT, True, YESTERDAY),
([CourseMode.AUDIT, CourseMode.VERIFIED], CourseMode.VERIFIED, True, None), ([CourseMode.AUDIT, CourseMode.VERIFIED], CourseMode.VERIFIED, True, None),
([CourseMode.AUDIT, CourseMode.VERIFIED], CourseMode.AUDIT, False, None), ([CourseMode.AUDIT, CourseMode.VERIFIED], CourseMode.AUDIT, False, None),
([CourseMode.AUDIT], CourseMode.AUDIT, False, None)) ([CourseMode.AUDIT], CourseMode.AUDIT, False, None))
...@@ -770,7 +775,7 @@ class ViewsTestCase(ModuleStoreTestCase): ...@@ -770,7 +775,7 @@ class ViewsTestCase(ModuleStoreTestCase):
# Create Course Modes # Create Course Modes
for mode in course_modes: for mode in course_modes:
CourseModeFactory.create(mode_slug=mode, course_id=course.id, expiration_datetime=expiration) CourseModeFactory.create(mode_slug=mode, course_id=course.id, expiration_datetime=self.DATES[expiration])
# Enroll user in the course # Enroll user in the course
CourseEnrollmentFactory(course_id=course.id, user=self.user, mode=enrollment_mode) CourseEnrollmentFactory(course_id=course.id, user=self.user, mode=enrollment_mode)
...@@ -1705,10 +1710,16 @@ class ProgressPageShowCorrectnessTests(ProgressPageBaseTests): ...@@ -1705,10 +1710,16 @@ class ProgressPageShowCorrectnessTests(ProgressPageBaseTests):
# Constants used in the test data # Constants used in the test data
NOW = datetime.now(UTC) NOW = datetime.now(UTC)
DAY_DELTA = timedelta(days=1) DAY_DELTA = timedelta(days=1)
YESTERDAY = NOW - DAY_DELTA YESTERDAY = 'yesterday'
TODAY = NOW TODAY = 'today'
TOMORROW = NOW + DAY_DELTA TOMORROW = 'tomorrow'
GRADER_TYPE = 'Homework' GRADER_TYPE = 'Homework'
DATES = {
YESTERDAY: NOW - DAY_DELTA,
TODAY: NOW,
TOMORROW: NOW + DAY_DELTA,
None: None,
}
def setUp(self): def setUp(self):
super(ProgressPageShowCorrectnessTests, self).setUp() super(ProgressPageShowCorrectnessTests, self).setUp()
...@@ -1853,12 +1864,12 @@ class ProgressPageShowCorrectnessTests(ProgressPageBaseTests): ...@@ -1853,12 +1864,12 @@ class ProgressPageShowCorrectnessTests(ProgressPageBaseTests):
(ShowCorrectness.PAST_DUE, TOMORROW, True), (ShowCorrectness.PAST_DUE, TOMORROW, True),
) )
@ddt.unpack @ddt.unpack
def test_progress_page_no_problem_scores(self, show_correctness, due_date, graded): def test_progress_page_no_problem_scores(self, show_correctness, due_date_name, graded):
""" """
Test that "no problem scores are present" for a course with no problems, Test that "no problem scores are present" for a course with no problems,
regardless of the various show correctness settings. regardless of the various show correctness settings.
""" """
self.setup_course(show_correctness=show_correctness, due_date=due_date, graded=graded) self.setup_course(show_correctness=show_correctness, due_date=self.DATES[due_date_name], graded=graded)
resp = self._get_progress_page() resp = self._get_progress_page()
# Test that no problem scores are present # Test that no problem scores are present
...@@ -1893,11 +1904,12 @@ class ProgressPageShowCorrectnessTests(ProgressPageBaseTests): ...@@ -1893,11 +1904,12 @@ class ProgressPageShowCorrectnessTests(ProgressPageBaseTests):
(ShowCorrectness.PAST_DUE, TOMORROW, True, False), (ShowCorrectness.PAST_DUE, TOMORROW, True, False),
) )
@ddt.unpack @ddt.unpack
def test_progress_page_hide_scores_from_learner(self, show_correctness, due_date, graded, show_grades): def test_progress_page_hide_scores_from_learner(self, show_correctness, due_date_name, graded, show_grades):
""" """
Test that problem scores are hidden on progress page when correctness is not available to the learner, and that Test that problem scores are hidden on progress page when correctness is not available to the learner, and that
they are visible when it is. they are visible when it is.
""" """
due_date = self.DATES[due_date_name]
self.setup_course(show_correctness=show_correctness, due_date=due_date, graded=graded) self.setup_course(show_correctness=show_correctness, due_date=due_date, graded=graded)
self.add_problem() self.add_problem()
...@@ -1944,10 +1956,11 @@ class ProgressPageShowCorrectnessTests(ProgressPageBaseTests): ...@@ -1944,10 +1956,11 @@ class ProgressPageShowCorrectnessTests(ProgressPageBaseTests):
(ShowCorrectness.PAST_DUE, TOMORROW, True, True), (ShowCorrectness.PAST_DUE, TOMORROW, True, True),
) )
@ddt.unpack @ddt.unpack
def test_progress_page_hide_scores_from_staff(self, show_correctness, due_date, graded, show_grades): def test_progress_page_hide_scores_from_staff(self, show_correctness, due_date_name, graded, show_grades):
""" """
Test that problem scores are hidden from staff viewing a learner's progress page only if show_correctness=never. Test that problem scores are hidden from staff viewing a learner's progress page only if show_correctness=never.
""" """
due_date = self.DATES[due_date_name]
self.setup_course(show_correctness=show_correctness, due_date=due_date, graded=graded) self.setup_course(show_correctness=show_correctness, due_date=due_date, graded=graded)
self.add_problem() self.add_problem()
......
...@@ -19,6 +19,35 @@ from xmodule.graders import ProblemScore ...@@ -19,6 +19,35 @@ from xmodule.graders import ProblemScore
NOW = now() NOW = now()
def submission_value_repr(self):
"""
String representation for the SubmissionValue namedtuple which excludes
the "created_at" attribute that changes with each execution. Needed for
consistency of ddt-generated test methods across pytest-xdist workers.
"""
return '<SubmissionValue exists={}>'.format(self.exists)
def csm_value_repr(self):
"""
String representation for the CSMValue namedtuple which excludes
the "created" attribute that changes with each execution. Needed for
consistency of ddt-generated test methods across pytest-xdist workers.
"""
return '<CSMValue exists={} raw_earned={}>'.format(self.exists, self.raw_earned)
def expected_result_repr(self):
"""
String representation for the ExpectedResult namedtuple which excludes
the "first_attempted" attribute that changes with each execution. Needed
for consistency of ddt-generated test methods across pytest-xdist workers.
"""
included = ('raw_earned', 'raw_possible', 'weighted_earned', 'weighted_possible', 'weight', 'graded')
attributes = ['{}={}'.format(name, getattr(self, name)) for name in included]
return '<ExpectedResult {}>'.format(' '.join(attributes))
class TestScoredBlockTypes(TestCase): class TestScoredBlockTypes(TestCase):
""" """
Tests for the possibly_scored function. Tests for the possibly_scored function.
...@@ -52,13 +81,16 @@ class TestGetScore(TestCase): ...@@ -52,13 +81,16 @@ class TestGetScore(TestCase):
location = 'test_location' location = 'test_location'
SubmissionValue = namedtuple('SubmissionValue', 'exists, points_earned, points_possible, created_at') SubmissionValue = namedtuple('SubmissionValue', 'exists, points_earned, points_possible, created_at')
SubmissionValue.__repr__ = submission_value_repr
CSMValue = namedtuple('CSMValue', 'exists, raw_earned, raw_possible, created') CSMValue = namedtuple('CSMValue', 'exists, raw_earned, raw_possible, created')
CSMValue.__repr__ = csm_value_repr
PersistedBlockValue = namedtuple('PersistedBlockValue', 'exists, raw_possible, weight, graded') PersistedBlockValue = namedtuple('PersistedBlockValue', 'exists, raw_possible, weight, graded')
ContentBlockValue = namedtuple('ContentBlockValue', 'raw_possible, weight, explicit_graded') ContentBlockValue = namedtuple('ContentBlockValue', 'raw_possible, weight, explicit_graded')
ExpectedResult = namedtuple( ExpectedResult = namedtuple(
'ExpectedResult', 'ExpectedResult',
'raw_earned, raw_possible, weighted_earned, weighted_possible, weight, graded, first_attempted' 'raw_earned, raw_possible, weighted_earned, weighted_possible, weight, graded, first_attempted'
) )
ExpectedResult.__repr__ = expected_result_repr
def _create_submissions_scores(self, submission_value): def _create_submissions_scores(self, submission_value):
""" """
......
...@@ -16,7 +16,6 @@ from util.date_utils import to_timestamp ...@@ -16,7 +16,6 @@ from util.date_utils import to_timestamp
from ..constants import ScoreDatabaseTableEnum from ..constants import ScoreDatabaseTableEnum
from ..signals.handlers import ( from ..signals.handlers import (
disconnect_submissions_signal_receiver, disconnect_submissions_signal_receiver,
enqueue_subsection_update,
problem_raw_score_changed_handler, problem_raw_score_changed_handler,
submissions_score_reset_handler, submissions_score_reset_handler,
submissions_score_set_handler submissions_score_set_handler
...@@ -28,20 +27,30 @@ UUID_REGEX = re.compile(ur'%(hex)s{8}-%(hex)s{4}-%(hex)s{4}-%(hex)s{4}-%(hex)s{1 ...@@ -28,20 +27,30 @@ UUID_REGEX = re.compile(ur'%(hex)s{8}-%(hex)s{4}-%(hex)s{4}-%(hex)s{4}-%(hex)s{1
FROZEN_NOW_DATETIME = datetime.now().replace(tzinfo=pytz.UTC) FROZEN_NOW_DATETIME = datetime.now().replace(tzinfo=pytz.UTC)
FROZEN_NOW_TIMESTAMP = to_timestamp(FROZEN_NOW_DATETIME) FROZEN_NOW_TIMESTAMP = to_timestamp(FROZEN_NOW_DATETIME)
SUBMISSION_SET_KWARGS = { SUBMISSIONS_SCORE_SET_HANDLER = 'submissions_score_set_handler'
'points_possible': 10, SUBMISSIONS_SCORE_RESET_HANDLER = 'submissions_score_reset_handler'
'points_earned': 5, HANDLERS = {
'anonymous_user_id': 'anonymous_id', SUBMISSIONS_SCORE_SET_HANDLER: submissions_score_set_handler,
'course_id': 'CourseID', SUBMISSIONS_SCORE_RESET_HANDLER: submissions_score_reset_handler,
'item_id': 'i4x://org/course/usage/123456',
'created_at': FROZEN_NOW_TIMESTAMP,
} }
SUBMISSION_RESET_KWARGS = { SUBMISSION_SET_KWARGS = 'submission_set_kwargs'
'anonymous_user_id': 'anonymous_id', SUBMISSION_RESET_KWARGS = 'submission_reset_kwargs'
'course_id': 'CourseID', SUBMISSION_KWARGS = {
'item_id': 'i4x://org/course/usage/123456', SUBMISSION_SET_KWARGS: {
'created_at': FROZEN_NOW_TIMESTAMP, 'points_possible': 10,
'points_earned': 5,
'anonymous_user_id': 'anonymous_id',
'course_id': 'CourseID',
'item_id': 'i4x://org/course/usage/123456',
'created_at': FROZEN_NOW_TIMESTAMP,
},
SUBMISSION_RESET_KWARGS: {
'anonymous_user_id': 'anonymous_id',
'course_id': 'CourseID',
'item_id': 'i4x://org/course/usage/123456',
'created_at': FROZEN_NOW_TIMESTAMP,
},
} }
PROBLEM_RAW_SCORE_CHANGED_KWARGS = { PROBLEM_RAW_SCORE_CHANGED_KWARGS = {
...@@ -82,6 +91,10 @@ class ScoreChangedSignalRelayTest(TestCase): ...@@ -82,6 +91,10 @@ class ScoreChangedSignalRelayTest(TestCase):
This ensures that listeners in the LMS only have to handle one type This ensures that listeners in the LMS only have to handle one type
of signal for all scoring events regardless of their origin. of signal for all scoring events regardless of their origin.
""" """
SIGNALS = {
'score_set': score_set,
'score_reset': score_reset,
}
def setUp(self): def setUp(self):
""" """
...@@ -110,11 +123,11 @@ class ScoreChangedSignalRelayTest(TestCase): ...@@ -110,11 +123,11 @@ class ScoreChangedSignalRelayTest(TestCase):
return mock return mock
@ddt.data( @ddt.data(
[submissions_score_set_handler, SUBMISSION_SET_KWARGS, 5, 10], [SUBMISSIONS_SCORE_SET_HANDLER, SUBMISSION_SET_KWARGS, 5, 10],
[submissions_score_reset_handler, SUBMISSION_RESET_KWARGS, 0, 0], [SUBMISSIONS_SCORE_RESET_HANDLER, SUBMISSION_RESET_KWARGS, 0, 0],
) )
@ddt.unpack @ddt.unpack
def test_score_set_signal_handler(self, handler, kwargs, earned, possible): def test_score_set_signal_handler(self, handler_name, kwargs, earned, possible):
""" """
Ensure that on receipt of a score_(re)set signal from the Submissions API, Ensure that on receipt of a score_(re)set signal from the Submissions API,
the signal handler correctly converts it to a PROBLEM_WEIGHTED_SCORE_CHANGED the signal handler correctly converts it to a PROBLEM_WEIGHTED_SCORE_CHANGED
...@@ -122,7 +135,9 @@ class ScoreChangedSignalRelayTest(TestCase): ...@@ -122,7 +135,9 @@ class ScoreChangedSignalRelayTest(TestCase):
Also ensures that the handler calls user_by_anonymous_id correctly. Also ensures that the handler calls user_by_anonymous_id correctly.
""" """
handler(None, **kwargs) local_kwargs = SUBMISSION_KWARGS[kwargs].copy()
handler = HANDLERS[handler_name]
handler(None, **local_kwargs)
expected_set_kwargs = { expected_set_kwargs = {
'sender': None, 'sender': None,
'weighted_possible': possible, 'weighted_possible': possible,
...@@ -134,35 +149,36 @@ class ScoreChangedSignalRelayTest(TestCase): ...@@ -134,35 +149,36 @@ class ScoreChangedSignalRelayTest(TestCase):
'modified': FROZEN_NOW_TIMESTAMP, 'modified': FROZEN_NOW_TIMESTAMP,
'score_db_table': 'submissions', 'score_db_table': 'submissions',
} }
if handler == submissions_score_reset_handler: if kwargs == SUBMISSION_RESET_KWARGS:
expected_set_kwargs['score_deleted'] = True expected_set_kwargs['score_deleted'] = True
self.signal_mock.assert_called_once_with(**expected_set_kwargs) self.signal_mock.assert_called_once_with(**expected_set_kwargs)
self.get_user_mock.assert_called_once_with(kwargs['anonymous_user_id']) self.get_user_mock.assert_called_once_with(local_kwargs['anonymous_user_id'])
def test_tnl_6599_zero_possible_bug(self): def test_tnl_6599_zero_possible_bug(self):
""" """
Ensure that, if coming from the submissions API, signals indicating a Ensure that, if coming from the submissions API, signals indicating a
a possible score of 0 are swallowed for reasons outlined in TNL-6559. a possible score of 0 are swallowed for reasons outlined in TNL-6559.
""" """
local_kwargs = SUBMISSION_SET_KWARGS.copy() local_kwargs = SUBMISSION_KWARGS[SUBMISSION_SET_KWARGS].copy()
local_kwargs['points_earned'] = 0 local_kwargs['points_earned'] = 0
local_kwargs['points_possible'] = 0 local_kwargs['points_possible'] = 0
submissions_score_set_handler(None, **local_kwargs) submissions_score_set_handler(None, **local_kwargs)
self.signal_mock.assert_not_called() self.signal_mock.assert_not_called()
@ddt.data( @ddt.data(
[submissions_score_set_handler, SUBMISSION_SET_KWARGS], [SUBMISSIONS_SCORE_SET_HANDLER, SUBMISSION_SET_KWARGS],
[submissions_score_reset_handler, SUBMISSION_RESET_KWARGS] [SUBMISSIONS_SCORE_RESET_HANDLER, SUBMISSION_RESET_KWARGS]
) )
@ddt.unpack @ddt.unpack
def test_score_set_missing_kwarg(self, handler, kwargs): def test_score_set_missing_kwarg(self, handler_name, kwargs):
""" """
Ensure that, on receipt of a score_(re)set signal from the Submissions API Ensure that, on receipt of a score_(re)set signal from the Submissions API
that does not have the correct kwargs, the courseware model does not that does not have the correct kwargs, the courseware model does not
generate a signal. generate a signal.
""" """
for missing in kwargs: handler = HANDLERS[handler_name]
local_kwargs = kwargs.copy() for missing in SUBMISSION_KWARGS[kwargs]:
local_kwargs = SUBMISSION_KWARGS[kwargs].copy()
del local_kwargs[missing] del local_kwargs[missing]
with self.assertRaises(KeyError): with self.assertRaises(KeyError):
...@@ -170,18 +186,19 @@ class ScoreChangedSignalRelayTest(TestCase): ...@@ -170,18 +186,19 @@ class ScoreChangedSignalRelayTest(TestCase):
self.signal_mock.assert_not_called() self.signal_mock.assert_not_called()
@ddt.data( @ddt.data(
[submissions_score_set_handler, SUBMISSION_SET_KWARGS], [SUBMISSIONS_SCORE_SET_HANDLER, SUBMISSION_SET_KWARGS],
[submissions_score_reset_handler, SUBMISSION_RESET_KWARGS] [SUBMISSIONS_SCORE_RESET_HANDLER, SUBMISSION_RESET_KWARGS]
) )
@ddt.unpack @ddt.unpack
def test_score_set_bad_user(self, handler, kwargs): def test_score_set_bad_user(self, handler_name, kwargs):
""" """
Ensure that, on receipt of a score_(re)set signal from the Submissions API Ensure that, on receipt of a score_(re)set signal from the Submissions API
that has an invalid user ID, the courseware model does not generate a that has an invalid user ID, the courseware model does not generate a
signal. signal.
""" """
handler = HANDLERS[handler_name]
self.get_user_mock = self.setup_patch('lms.djangoapps.grades.signals.handlers.user_by_anonymous_id', None) self.get_user_mock = self.setup_patch('lms.djangoapps.grades.signals.handlers.user_by_anonymous_id', None)
handler(None, **kwargs) handler(None, **SUBMISSION_KWARGS[kwargs])
self.signal_mock.assert_not_called() self.signal_mock.assert_not_called()
def test_raw_score_changed_signal_handler(self): def test_raw_score_changed_signal_handler(self):
...@@ -198,14 +215,18 @@ class ScoreChangedSignalRelayTest(TestCase): ...@@ -198,14 +215,18 @@ class ScoreChangedSignalRelayTest(TestCase):
self.signal_mock.assert_called_with(**expected_set_kwargs) self.signal_mock.assert_called_with(**expected_set_kwargs)
@ddt.data( @ddt.data(
[score_set, 'lms.djangoapps.grades.signals.handlers.submissions_score_set_handler', SUBMISSION_SET_KWARGS], ['score_set', 'lms.djangoapps.grades.signals.handlers.submissions_score_set_handler',
[score_reset, 'lms.djangoapps.grades.signals.handlers.submissions_score_reset_handler', SUBMISSION_RESET_KWARGS] SUBMISSION_SET_KWARGS],
['score_reset', 'lms.djangoapps.grades.signals.handlers.submissions_score_reset_handler',
SUBMISSION_RESET_KWARGS]
) )
@ddt.unpack @ddt.unpack
def test_disconnect_manager(self, signal, handler, kwargs): def test_disconnect_manager(self, signal_name, handler, kwargs):
""" """
Tests to confirm the disconnect_submissions_signal_receiver context manager is working correctly. Tests to confirm the disconnect_submissions_signal_receiver context manager is working correctly.
""" """
signal = self.SIGNALS[signal_name]
kwargs = SUBMISSION_KWARGS[kwargs].copy()
handler_mock = self.setup_patch(handler, None) handler_mock = self.setup_patch(handler, None)
# Receiver connected before we start # Receiver connected before we start
......
...@@ -7,6 +7,7 @@ from nose.plugins.attrib import attr ...@@ -7,6 +7,7 @@ from nose.plugins.attrib import attr
from bulk_email.models import SEND_TO_LEARNERS, SEND_TO_MYSELF, SEND_TO_STAFF, CourseEmail from bulk_email.models import SEND_TO_LEARNERS, SEND_TO_MYSELF, SEND_TO_STAFF, CourseEmail
from certificates.models import CertificateGenerationHistory, CertificateStatuses from certificates.models import CertificateGenerationHistory, CertificateStatuses
from common.test.utils import normalize_repr
from courseware.tests.factories import UserFactory from courseware.tests.factories import UserFactory
from lms.djangoapps.instructor_task.api import ( from lms.djangoapps.instructor_task.api import (
SpecificStudentIdMissingError, SpecificStudentIdMissingError,
...@@ -147,21 +148,29 @@ class InstructorTaskModuleSubmitTest(InstructorTaskModuleTestCase): ...@@ -147,21 +148,29 @@ class InstructorTaskModuleSubmitTest(InstructorTaskModuleTestCase):
self._test_submit_with_long_url(submit_delete_problem_state_for_all_students) self._test_submit_with_long_url(submit_delete_problem_state_for_all_students)
@ddt.data( @ddt.data(
(submit_rescore_problem_for_all_students, 'rescore_problem'), (normalize_repr(submit_rescore_problem_for_all_students), 'rescore_problem'),
(submit_rescore_problem_for_all_students, 'rescore_problem_if_higher', {'only_if_higher': True}),
(submit_rescore_problem_for_student, 'rescore_problem', {'student': True}),
(submit_rescore_problem_for_student, 'rescore_problem_if_higher', {'student': True, 'only_if_higher': True}),
(submit_reset_problem_attempts_for_all_students, 'reset_problem_attempts'),
(submit_delete_problem_state_for_all_students, 'delete_problem_state'),
(submit_rescore_entrance_exam_for_student, 'rescore_problem', {'student': True}),
( (
submit_rescore_entrance_exam_for_student, normalize_repr(submit_rescore_problem_for_all_students),
'rescore_problem_if_higher',
{'only_if_higher': True}
),
(normalize_repr(submit_rescore_problem_for_student), 'rescore_problem', {'student': True}),
(
normalize_repr(submit_rescore_problem_for_student),
'rescore_problem_if_higher',
{'student': True, 'only_if_higher': True}
),
(normalize_repr(submit_reset_problem_attempts_for_all_students), 'reset_problem_attempts'),
(normalize_repr(submit_delete_problem_state_for_all_students), 'delete_problem_state'),
(normalize_repr(submit_rescore_entrance_exam_for_student), 'rescore_problem', {'student': True}),
(
normalize_repr(submit_rescore_entrance_exam_for_student),
'rescore_problem_if_higher', 'rescore_problem_if_higher',
{'student': True, 'only_if_higher': True}, {'student': True, 'only_if_higher': True},
), ),
(submit_reset_problem_attempts_in_entrance_exam, 'reset_problem_attempts', {'student': True}), (normalize_repr(submit_reset_problem_attempts_in_entrance_exam), 'reset_problem_attempts', {'student': True}),
(submit_delete_entrance_exam_state_for_student, 'delete_problem_state', {'student': True}), (normalize_repr(submit_delete_entrance_exam_state_for_student), 'delete_problem_state', {'student': True}),
(submit_override_score, 'override_problem_score', {'student': True, 'score': 0}) (normalize_repr(submit_override_score), 'override_problem_score', {'student': True, 'score': 0})
) )
@ddt.unpack @ddt.unpack
def test_submit_task(self, task_function, expected_task_type, params=None): def test_submit_task(self, task_function, expected_task_type, params=None):
......
...@@ -84,6 +84,11 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest ...@@ -84,6 +84,11 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest
LAST_WEEK = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=7) LAST_WEEK = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=7)
ADVERTISED_START = "Spring 2016" ADVERTISED_START = "Spring 2016"
ENABLED_SIGNALS = ['course_published'] ENABLED_SIGNALS = ['course_published']
DATES = {
'next_week': NEXT_WEEK,
'last_week': LAST_WEEK,
'default_start_date': DEFAULT_START_DATE,
}
@patch.dict(settings.FEATURES, {"ENABLE_DISCUSSION_SERVICE": True}) @patch.dict(settings.FEATURES, {"ENABLE_DISCUSSION_SERVICE": True})
def setUp(self, *args, **kwargs): def setUp(self, *args, **kwargs):
...@@ -175,12 +180,12 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest ...@@ -175,12 +180,12 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest
self.assertFalse(result['has_access']) self.assertFalse(result['has_access'])
@ddt.data( @ddt.data(
(NEXT_WEEK, ADVERTISED_START, ADVERTISED_START, "string"), ('next_week', ADVERTISED_START, ADVERTISED_START, "string"),
(NEXT_WEEK, None, defaultfilters.date(NEXT_WEEK, "DATE_FORMAT"), "timestamp"), ('next_week', None, defaultfilters.date(NEXT_WEEK, "DATE_FORMAT"), "timestamp"),
(NEXT_WEEK, '', defaultfilters.date(NEXT_WEEK, "DATE_FORMAT"), "timestamp"), ('next_week', '', defaultfilters.date(NEXT_WEEK, "DATE_FORMAT"), "timestamp"),
(DEFAULT_START_DATE, ADVERTISED_START, ADVERTISED_START, "string"), ('default_start_date', ADVERTISED_START, ADVERTISED_START, "string"),
(DEFAULT_START_DATE, '', None, "empty"), ('default_start_date', '', None, "empty"),
(DEFAULT_START_DATE, None, None, "empty"), ('default_start_date', None, None, "empty"),
) )
@ddt.unpack @ddt.unpack
@patch.dict(settings.FEATURES, {'DISABLE_START_DATES': False, 'ENABLE_MKTG_SITE': True}) @patch.dict(settings.FEATURES, {'DISABLE_START_DATES': False, 'ENABLE_MKTG_SITE': True})
...@@ -190,7 +195,7 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest ...@@ -190,7 +195,7 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest
case the course has not started case the course has not started
""" """
self.login() self.login()
course = CourseFactory.create(start=start, advertised_start=advertised_start, mobile_available=True) course = CourseFactory.create(start=self.DATES[start], advertised_start=advertised_start, mobile_available=True)
self.enroll(course.id) self.enroll(course.id)
response = self.api_response() response = self.api_response()
......
...@@ -118,17 +118,17 @@ class TeamMembershipTest(SharedModuleStoreTestCase): ...@@ -118,17 +118,17 @@ class TeamMembershipTest(SharedModuleStoreTestCase):
class TeamSignalsTest(EventTestMixin, SharedModuleStoreTestCase): class TeamSignalsTest(EventTestMixin, SharedModuleStoreTestCase):
"""Tests for handling of team-related signals.""" """Tests for handling of team-related signals."""
SIGNALS_LIST = ( SIGNALS = {
thread_created, 'thread_created': thread_created,
thread_edited, 'thread_edited': thread_edited,
thread_deleted, 'thread_deleted': thread_deleted,
thread_voted, 'thread_voted': thread_voted,
comment_created, 'comment_created': comment_created,
comment_edited, 'comment_edited': comment_edited,
comment_deleted, 'comment_deleted': comment_deleted,
comment_voted, 'comment_voted': comment_voted,
comment_endorsed 'comment_endorsed': comment_endorsed,
) }
DISCUSSION_TOPIC_ID = 'test_topic' DISCUSSION_TOPIC_ID = 'test_topic'
...@@ -180,30 +180,33 @@ class TeamSignalsTest(EventTestMixin, SharedModuleStoreTestCase): ...@@ -180,30 +180,33 @@ class TeamSignalsTest(EventTestMixin, SharedModuleStoreTestCase):
@ddt.data( @ddt.data(
*itertools.product( *itertools.product(
SIGNALS_LIST, SIGNALS.keys(),
(('user', True), ('moderator', False)) (('user', True), ('moderator', False))
) )
) )
@ddt.unpack @ddt.unpack
def test_signals(self, signal, (user, should_update)): def test_signals(self, signal_name, (user, should_update)):
"""Test that `last_activity_at` is correctly updated when team-related """Test that `last_activity_at` is correctly updated when team-related
signals are sent. signals are sent.
""" """
with self.assert_last_activity_updated(should_update): with self.assert_last_activity_updated(should_update):
user = getattr(self, user) user = getattr(self, user)
signal = self.SIGNALS[signal_name]
signal.send(sender=None, user=user, post=self.mock_comment()) signal.send(sender=None, user=user, post=self.mock_comment())
@ddt.data(thread_voted, comment_voted) @ddt.data('thread_voted', 'comment_voted')
def test_vote_others_post(self, signal): def test_vote_others_post(self, signal_name):
"""Test that voting on another user's post correctly fires a """Test that voting on another user's post correctly fires a
signal.""" signal."""
with self.assert_last_activity_updated(True): with self.assert_last_activity_updated(True):
signal = self.SIGNALS[signal_name]
signal.send(sender=None, user=self.user, post=self.mock_comment(user=self.moderator)) signal.send(sender=None, user=self.user, post=self.mock_comment(user=self.moderator))
@ddt.data(*SIGNALS_LIST) @ddt.data(*SIGNALS.keys())
def test_signals_course_context(self, signal): def test_signals_course_context(self, signal_name):
"""Test that `last_activity_at` is not updated when activity takes """Test that `last_activity_at` is not updated when activity takes
place in discussions outside of a team. place in discussions outside of a team.
""" """
with self.assert_last_activity_updated(False): with self.assert_last_activity_updated(False):
signal = self.SIGNALS[signal_name]
signal.send(sender=None, user=self.user, post=self.mock_comment(context='course')) signal.send(sender=None, user=self.user, post=self.mock_comment(context='course'))
...@@ -89,8 +89,15 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): ...@@ -89,8 +89,15 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
PASSWORD = "test_password" PASSWORD = "test_password"
NOW = datetime.now(pytz.UTC) NOW = datetime.now(pytz.UTC)
YESTERDAY = NOW - timedelta(days=1) YESTERDAY = 'yesterday'
TOMORROW = NOW + timedelta(days=1) TOMORROW = 'tomorrow'
NEXT_YEAR = 'next_year'
DATES = {
YESTERDAY: NOW - timedelta(days=1),
TOMORROW: NOW + timedelta(days=1),
NEXT_YEAR: NOW + timedelta(days=360),
None: None,
}
URLCONF_MODULES = ['openedx.core.djangoapps.embargo'] URLCONF_MODULES = ['openedx.core.djangoapps.embargo']
...@@ -492,7 +499,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): ...@@ -492,7 +499,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
) )
@ddt.unpack @ddt.unpack
def test_payment_confirmation_course_details(self, course_start, show_courseware_url): def test_payment_confirmation_course_details(self, course_start, show_courseware_url):
course = self._create_course("verified", course_start=course_start) course = self._create_course("verified", course_start=self.DATES[course_start])
self._enroll(course.id, "verified") self._enroll(course.id, "verified")
response = self._get_page('verify_student_payment_confirmation', course.id) response = self._get_page('verify_student_payment_confirmation', course.id)
...@@ -753,9 +760,10 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): ...@@ -753,9 +760,10 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
self.assertContains(response, "verification deadline") self.assertContains(response, "verification deadline")
self.assertContains(response, deadline) self.assertContains(response, deadline)
@ddt.data(datetime.now(tz=pytz.UTC) + timedelta(days=360), None) @ddt.data(NEXT_YEAR, None)
def test_course_mode_expired_verification_deadline_in_future(self, verification_deadline): def test_course_mode_expired_verification_deadline_in_future(self, verification_deadline):
"""Verify that student can not upgrade in expired course mode.""" """Verify that student can not upgrade in expired course mode."""
verification_deadline = self.DATES[verification_deadline]
course_modes = ("verified", "credit") course_modes = ("verified", "credit")
course = self._create_course(*course_modes) course = self._create_course(*course_modes)
......
...@@ -183,6 +183,9 @@ class MockTransformer(BlockStructureTransformer): ...@@ -183,6 +183,9 @@ class MockTransformer(BlockStructureTransformer):
def transform(self, usage_info, block_structure): def transform(self, usage_info, block_structure):
pass pass
def __repr__(self):
return self.name()
class MockFilteringTransformer(FilteringTransformerMixin, BlockStructureTransformer): class MockFilteringTransformer(FilteringTransformerMixin, BlockStructureTransformer):
""" """
......
...@@ -46,10 +46,18 @@ class CourseOverviewTestCase(ModuleStoreTestCase): ...@@ -46,10 +46,18 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
""" """
TODAY = timezone.now() TODAY = timezone.now()
LAST_MONTH = TODAY - datetime.timedelta(days=30) LAST_MONTH = 'last_month'
LAST_WEEK = TODAY - datetime.timedelta(days=7) LAST_WEEK = 'last_week'
NEXT_WEEK = TODAY + datetime.timedelta(days=7) NEXT_WEEK = 'next_week'
NEXT_MONTH = TODAY + datetime.timedelta(days=30) NEXT_MONTH = 'next_month'
DATES = {
'default_start_date': DEFAULT_START_DATE,
LAST_MONTH: TODAY - datetime.timedelta(days=30),
LAST_WEEK: TODAY - datetime.timedelta(days=7),
NEXT_WEEK: TODAY + datetime.timedelta(days=7),
NEXT_MONTH: TODAY + datetime.timedelta(days=30),
None: None,
}
COURSE_OVERVIEW_TABS = {'courseware', 'info', 'textbooks', 'discussion', 'wiki', 'progress'} COURSE_OVERVIEW_TABS = {'courseware', 'info', 'textbooks', 'discussion', 'wiki', 'progress'}
...@@ -229,7 +237,7 @@ class CourseOverviewTestCase(ModuleStoreTestCase): ...@@ -229,7 +237,7 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
}, },
{ {
# # Don't set display name # # Don't set display name
"start": DEFAULT_START_DATE, # Default start and end dates "start": 'default_start_date', # Default start and end dates
"end": None, "end": None,
"advertised_start": None, # No advertised start "advertised_start": None, # No advertised start
"pre_requisite_courses": [], # No pre-requisites "pre_requisite_courses": [], # No pre-requisites
...@@ -251,10 +259,15 @@ class CourseOverviewTestCase(ModuleStoreTestCase): ...@@ -251,10 +259,15 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
modulestore_type (ModuleStoreEnum.Type): type of store to create the modulestore_type (ModuleStoreEnum.Type): type of store to create the
course in. course in.
""" """
kwargs = course_kwargs.copy()
kwargs['start'] = self.DATES[course_kwargs['start']]
kwargs['end'] = self.DATES[course_kwargs['end']]
if 'announcement' in course_kwargs:
kwargs['announcement'] = self.DATES[course_kwargs['announcement']]
# Note: We specify a value for 'run' here because, for some reason, # Note: We specify a value for 'run' here because, for some reason,
# .create raises an InvalidKeyError if we don't (even though my # .create raises an InvalidKeyError if we don't (even though my
# other test functions don't specify a run but work fine). # other test functions don't specify a run but work fine).
course = CourseFactory.create(default_store=modulestore_type, run="TestRun", **course_kwargs) course = CourseFactory.create(default_store=modulestore_type, run="TestRun", **kwargs)
self.check_course_overview_against_course(course) self.check_course_overview_against_course(course)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
......
...@@ -35,6 +35,12 @@ class TestMinGradedRequirementStatus(ModuleStoreTestCase): ...@@ -35,6 +35,12 @@ class TestMinGradedRequirementStatus(ModuleStoreTestCase):
VALID_DUE_DATE = datetime.now(pytz.UTC) + timedelta(days=20) VALID_DUE_DATE = datetime.now(pytz.UTC) + timedelta(days=20)
EXPIRED_DUE_DATE = datetime.now(pytz.UTC) - timedelta(days=20) EXPIRED_DUE_DATE = datetime.now(pytz.UTC) - timedelta(days=20)
DATES = {
'valid': VALID_DUE_DATE,
'expired': EXPIRED_DUE_DATE,
None: None,
}
def setUp(self): def setUp(self):
super(TestMinGradedRequirementStatus, self).setUp() super(TestMinGradedRequirementStatus, self).setUp()
self.course = CourseFactory.create( self.course = CourseFactory.create(
...@@ -85,13 +91,13 @@ class TestMinGradedRequirementStatus(ModuleStoreTestCase): ...@@ -85,13 +91,13 @@ class TestMinGradedRequirementStatus(ModuleStoreTestCase):
self.assertEqual(req_status[0]['reason'], expected_reason) self.assertEqual(req_status[0]['reason'], expected_reason)
@ddt.data( @ddt.data(
(0.6, VALID_DUE_DATE), (0.6, 'valid'),
(0.52, None), (0.52, None),
) )
@ddt.unpack @ddt.unpack
def test_min_grade_requirement_with_valid_grade(self, grade, due_date): def test_min_grade_requirement_with_valid_grade(self, grade, due_date_name):
"""Test with valid grades submitted before deadline""" """Test with valid grades submitted before deadline"""
self.assert_requirement_status(grade, due_date, 'satisfied') self.assert_requirement_status(grade, self.DATES[due_date_name], 'satisfied')
def test_grade_changed(self): def test_grade_changed(self):
""" Verify successive calls to update a satisfied grade requirement are recorded. """ """ Verify successive calls to update a satisfied grade requirement are recorded. """
...@@ -106,12 +112,12 @@ class TestMinGradedRequirementStatus(ModuleStoreTestCase): ...@@ -106,12 +112,12 @@ class TestMinGradedRequirementStatus(ModuleStoreTestCase):
@ddt.data( @ddt.data(
(0.50, None), (0.50, None),
(0.51, None), (0.51, None),
(0.40, VALID_DUE_DATE), (0.40, 'valid'),
) )
@ddt.unpack @ddt.unpack
def test_min_grade_requirement_failed_grade_valid_deadline(self, grade, due_date): def test_min_grade_requirement_failed_grade_valid_deadline(self, grade, due_date_name):
"""Test with failed grades and deadline is still open or not defined.""" """Test with failed grades and deadline is still open or not defined."""
self.assert_requirement_status(grade, due_date, None) self.assert_requirement_status(grade, self.DATES[due_date_name], None)
def test_min_grade_requirement_failed_grade_expired_deadline(self): def test_min_grade_requirement_failed_grade_expired_deadline(self):
"""Test with failed grades and deadline expire""" """Test with failed grades and deadline expire"""
......
...@@ -29,6 +29,10 @@ COMMAND_MODULE = 'openedx.core.djangoapps.programs.management.commands.backpopul ...@@ -29,6 +29,10 @@ COMMAND_MODULE = 'openedx.core.djangoapps.programs.management.commands.backpopul
class BackpopulateProgramCredentialsTests(CatalogIntegrationMixin, CredentialsApiConfigMixin, TestCase): class BackpopulateProgramCredentialsTests(CatalogIntegrationMixin, CredentialsApiConfigMixin, TestCase):
"""Tests for the backpopulate_program_credentials management command.""" """Tests for the backpopulate_program_credentials management command."""
course_run_key, alternate_course_run_key = (generate_course_run_key() for __ in range(2)) course_run_key, alternate_course_run_key = (generate_course_run_key() for __ in range(2))
# Constants for the _get_programs_data hierarchy types used in test_flatten()
SEPARATE_PROGRAMS = 'separate_programs'
SEPARATE_COURSES = 'separate_courses'
SAME_COURSE = 'same_course'
def setUp(self): def setUp(self):
super(BackpopulateProgramCredentialsTests, self).setUp() super(BackpopulateProgramCredentialsTests, self).setUp()
...@@ -44,6 +48,54 @@ class BackpopulateProgramCredentialsTests(CatalogIntegrationMixin, CredentialsAp ...@@ -44,6 +48,54 @@ class BackpopulateProgramCredentialsTests(CatalogIntegrationMixin, CredentialsAp
catalog_integration = self.create_catalog_integration() catalog_integration = self.create_catalog_integration()
UserFactory(username=catalog_integration.service_username) UserFactory(username=catalog_integration.service_username)
def _get_programs_data(self, hierarchy_type):
"""
Generate a mock response for get_programs() with the given type of
course hierarchy. Dramatically simplifies (and makes consistent
between test runs) the ddt-generated test_flatten methods.
"""
if hierarchy_type == self.SEPARATE_PROGRAMS:
return [
ProgramFactory(
courses=[
CourseFactory(course_runs=[
CourseRunFactory(key=self.course_run_key),
]),
]
),
ProgramFactory(
courses=[
CourseFactory(course_runs=[
CourseRunFactory(key=self.alternate_course_run_key),
]),
]
),
]
elif hierarchy_type == self.SEPARATE_COURSES:
return [
ProgramFactory(
courses=[
CourseFactory(course_runs=[
CourseRunFactory(key=self.course_run_key),
]),
CourseFactory(course_runs=[
CourseRunFactory(key=self.alternate_course_run_key),
]),
]
),
]
else: # SAME_COURSE
return [
ProgramFactory(
courses=[
CourseFactory(course_runs=[
CourseRunFactory(key=self.course_run_key),
CourseRunFactory(key=self.alternate_course_run_key),
]),
]
),
]
@ddt.data(True, False) @ddt.data(True, False)
def test_handle(self, commit, mock_task, mock_get_programs): def test_handle(self, commit, mock_task, mock_get_programs):
""" """
...@@ -112,49 +164,10 @@ class BackpopulateProgramCredentialsTests(CatalogIntegrationMixin, CredentialsAp ...@@ -112,49 +164,10 @@ class BackpopulateProgramCredentialsTests(CatalogIntegrationMixin, CredentialsAp
# The task should be called for both users since professional and no-id-professional are equivalent. # The task should be called for both users since professional and no-id-professional are equivalent.
mock_task.assert_has_calls([mock.call(self.alice.username), mock.call(self.bob.username)]) mock_task.assert_has_calls([mock.call(self.alice.username), mock.call(self.bob.username)])
@ddt.data( @ddt.data(SEPARATE_PROGRAMS, SEPARATE_COURSES, SAME_COURSE)
[ def test_handle_flatten(self, hierarchy_type, mock_task, mock_get_programs):
ProgramFactory(
courses=[
CourseFactory(course_runs=[
CourseRunFactory(key=course_run_key),
]),
]
),
ProgramFactory(
courses=[
CourseFactory(course_runs=[
CourseRunFactory(key=alternate_course_run_key),
]),
]
),
],
[
ProgramFactory(
courses=[
CourseFactory(course_runs=[
CourseRunFactory(key=course_run_key),
]),
CourseFactory(course_runs=[
CourseRunFactory(key=alternate_course_run_key),
]),
]
),
],
[
ProgramFactory(
courses=[
CourseFactory(course_runs=[
CourseRunFactory(key=course_run_key),
CourseRunFactory(key=alternate_course_run_key),
]),
]
),
],
)
def test_handle_flatten(self, data, mock_task, mock_get_programs):
"""Verify that program structures are flattened correctly.""" """Verify that program structures are flattened correctly."""
mock_get_programs.return_value = data mock_get_programs.return_value = self._get_programs_data(hierarchy_type)
GeneratedCertificateFactory( GeneratedCertificateFactory(
user=self.alice, user=self.alice,
......
...@@ -53,11 +53,11 @@ class RegistrationValidationViewTests(test_utils.ApiTestCase): ...@@ -53,11 +53,11 @@ class RegistrationValidationViewTests(test_utils.ApiTestCase):
) )
@ddt.data( @ddt.data(
['name', (name for name in testutils.VALID_NAMES)], ['name', [name for name in testutils.VALID_NAMES]],
['email', (email for email in testutils.VALID_EMAILS)], ['email', [email for email in testutils.VALID_EMAILS]],
['password', (password for password in testutils.VALID_PASSWORDS)], ['password', [password for password in testutils.VALID_PASSWORDS]],
['username', (username for username in testutils.VALID_USERNAMES)], ['username', [username for username in testutils.VALID_USERNAMES]],
['country', (country for country in testutils.VALID_COUNTRIES)] ['country', [country for country in testutils.VALID_COUNTRIES]]
) )
@ddt.unpack @ddt.unpack
def test_positive_validation_decision(self, form_field_name, user_data): def test_positive_validation_decision(self, form_field_name, user_data):
...@@ -71,11 +71,11 @@ class RegistrationValidationViewTests(test_utils.ApiTestCase): ...@@ -71,11 +71,11 @@ class RegistrationValidationViewTests(test_utils.ApiTestCase):
@ddt.data( @ddt.data(
# Skip None type for invalidity checks. # Skip None type for invalidity checks.
['name', (name for name in testutils.INVALID_NAMES[1:])], ['name', [name for name in testutils.INVALID_NAMES[1:]]],
['email', (email for email in testutils.INVALID_EMAILS[1:])], ['email', [email for email in testutils.INVALID_EMAILS[1:]]],
['password', (password for password in testutils.INVALID_PASSWORDS[1:])], ['password', [password for password in testutils.INVALID_PASSWORDS[1:]]],
['username', (username for username in testutils.INVALID_USERNAMES[1:])], ['username', [username for username in testutils.INVALID_USERNAMES[1:]]],
['country', (country for country in testutils.INVALID_COUNTRIES[1:])] ['country', [country for country in testutils.INVALID_COUNTRIES[1:]]]
) )
@ddt.unpack @ddt.unpack
def test_negative_validation_decision(self, form_field_name, user_data): def test_negative_validation_decision(self, form_field_name, user_data):
......
...@@ -6,6 +6,8 @@ import ddt ...@@ -6,6 +6,8 @@ import ddt
from django.contrib.messages.middleware import MessageMiddleware from django.contrib.messages.middleware import MessageMiddleware
from django.test import RequestFactory, TestCase from django.test import RequestFactory, TestCase
from common.test.utils import normalize_repr
from openedx.core.djangolib.markup import HTML, Text from openedx.core.djangolib.markup import HTML, Text
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
...@@ -60,10 +62,10 @@ class UserMessagesTestCase(TestCase): ...@@ -60,10 +62,10 @@ class UserMessagesTestCase(TestCase):
self.assertEquals(messages[0].icon_class, expected_icon_class) self.assertEquals(messages[0].icon_class, expected_icon_class)
@ddt.data( @ddt.data(
(PageLevelMessages.register_error_message, UserMessageType.ERROR), (normalize_repr(PageLevelMessages.register_error_message), UserMessageType.ERROR),
(PageLevelMessages.register_info_message, UserMessageType.INFO), (normalize_repr(PageLevelMessages.register_info_message), UserMessageType.INFO),
(PageLevelMessages.register_success_message, UserMessageType.SUCCESS), (normalize_repr(PageLevelMessages.register_success_message), UserMessageType.SUCCESS),
(PageLevelMessages.register_warning_message, UserMessageType.WARNING), (normalize_repr(PageLevelMessages.register_warning_message), UserMessageType.WARNING),
) )
@ddt.unpack @ddt.unpack
def test_message_type(self, register_message_function, expected_message_type): def test_message_type(self, register_message_function, expected_message_type):
......
...@@ -16,7 +16,16 @@ from xblock.fields import ScopeIds, UNIQUE_ID, NO_CACHE_VALUE ...@@ -16,7 +16,16 @@ from xblock.fields import ScopeIds, UNIQUE_ID, NO_CACHE_VALUE
from xblock.runtime import Runtime from xblock.runtime import Runtime
def attribute_pair_repr(self):
"""
Custom string representation for the AttributePair namedtuple which is
consistent between test runs.
"""
return '<AttributePair name={}>'.format(self.name)
AttributePair = namedtuple("AttributePair", ["name", "value"]) AttributePair = namedtuple("AttributePair", ["name", "value"])
AttributePair.__repr__ = attribute_pair_repr
ID_ATTR_NAMES = ("discussion_id", "id",) ID_ATTR_NAMES = ("discussion_id", "id",)
......
...@@ -20,8 +20,6 @@ from xmodule.course_module import DEFAULT_START_DATE ...@@ -20,8 +20,6 @@ from xmodule.course_module import DEFAULT_START_DATE
from .test_course_home import course_home_url from .test_course_home import course_home_url
TEST_PASSWORD = 'test' TEST_PASSWORD = 'test'
FUTURE_DAY = datetime.datetime.now() + datetime.timedelta(days=30)
PAST_DAY = datetime.datetime.now() - datetime.timedelta(days=30)
class TestCourseOutlinePage(SharedModuleStoreTestCase): class TestCourseOutlinePage(SharedModuleStoreTestCase):
...@@ -343,14 +341,22 @@ class TestEmptyCourseOutlinePage(SharedModuleStoreTestCase): ...@@ -343,14 +341,22 @@ class TestEmptyCourseOutlinePage(SharedModuleStoreTestCase):
""" """
Test the new course outline view. Test the new course outline view.
""" """
FUTURE_DAY = 'future_day'
PAST_DAY = 'past_day'
DATES = {
'default_start_date': DEFAULT_START_DATE,
FUTURE_DAY: datetime.datetime.now() + datetime.timedelta(days=30),
PAST_DAY: datetime.datetime.now() - datetime.timedelta(days=30),
}
@ddt.data( @ddt.data(
(FUTURE_DAY, 'This course has not started yet, and will launch on'), (FUTURE_DAY, 'This course has not started yet, and will launch on'),
(PAST_DAY, "We're still working on course content."), (PAST_DAY, "We're still working on course content."),
(DEFAULT_START_DATE, 'This course has not started yet.'), ('default_start_date', 'This course has not started yet.'),
) )
@ddt.unpack @ddt.unpack
def test_empty_course_rendering(self, start_date, expected_text): def test_empty_course_rendering(self, start_date_name, expected_text):
course = CourseFactory.create(start=start_date) course = CourseFactory.create(start=self.DATES[start_date_name])
test_user = UserFactory(password=TEST_PASSWORD) test_user = UserFactory(password=TEST_PASSWORD)
CourseEnrollment.enroll(test_user, course.id) CourseEnrollment.enroll(test_user, course.id)
self.client.login(username=test_user.username, password=TEST_PASSWORD) self.client.login(username=test_user.username, password=TEST_PASSWORD)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment