Merge pull request #15951 from edx/jmbowman/ddt_cleanup
ddt usage cleanup
This commit is contained in:
@@ -29,6 +29,11 @@ class CourseModeModelTest(TestCase):
|
||||
"""
|
||||
Tests for the CourseMode model
|
||||
"""
|
||||
NOW = 'now'
|
||||
DATES = {
|
||||
NOW: datetime.now(),
|
||||
None: None,
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(CourseModeModelTest, self).setUp()
|
||||
@@ -317,10 +322,11 @@ class CourseModeModelTest(TestCase):
|
||||
CourseMode.PROFESSIONAL,
|
||||
CourseMode.NO_ID_PROFESSIONAL_MODE
|
||||
),
|
||||
(datetime.now(), None),
|
||||
(NOW, None),
|
||||
))
|
||||
@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
|
||||
try:
|
||||
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):
|
||||
def course_group_already_exists(self, course_key):
|
||||
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):
|
||||
"""
|
||||
A named role in a particular org independent of course
|
||||
"""
|
||||
pass
|
||||
def __repr__(self):
|
||||
return '<{}>'.format(self.__class__.__name__)
|
||||
|
||||
|
||||
@register_access_role
|
||||
|
||||
@@ -33,8 +33,13 @@ from xmodule.modulestore.tests.factories import CourseFactory
|
||||
class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
|
||||
"""Tests for per-course verification status on the dashboard. """
|
||||
|
||||
PAST = datetime.now(UTC) - timedelta(days=5)
|
||||
FUTURE = datetime.now(UTC) + timedelta(days=5)
|
||||
PAST = 'past'
|
||||
FUTURE = 'future'
|
||||
DATES = {
|
||||
PAST: datetime.now(UTC) - timedelta(days=5),
|
||||
FUTURE: datetime.now(UTC) + timedelta(days=5),
|
||||
None: None,
|
||||
}
|
||||
|
||||
URLCONF_MODULES = ['verify_student.urls']
|
||||
|
||||
@@ -91,14 +96,14 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
|
||||
self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_VERIFY)
|
||||
|
||||
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)
|
||||
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.")
|
||||
|
||||
@ddt.data(None, FUTURE)
|
||||
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
|
||||
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
|
||||
@@ -110,7 +115,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
|
||||
|
||||
@ddt.data(None, FUTURE)
|
||||
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
|
||||
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
|
||||
@@ -127,7 +132,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
|
||||
|
||||
def test_missed_verification_deadline(self):
|
||||
# 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
|
||||
# so the status should show that the student missed the deadline.
|
||||
@@ -135,7 +140,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
|
||||
|
||||
def test_missed_verification_deadline_verification_was_expired(self):
|
||||
# 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
|
||||
# occurred before the deadline.
|
||||
@@ -143,7 +148,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
|
||||
attempt.mark_ready()
|
||||
attempt.submit()
|
||||
attempt.approve()
|
||||
attempt.created_at = self.PAST - timedelta(days=900)
|
||||
attempt.created_at = self.DATES[self.PAST] - timedelta(days=900)
|
||||
attempt.save()
|
||||
|
||||
# The student didn't have an approved verification at the deadline,
|
||||
@@ -152,14 +157,14 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
|
||||
|
||||
def test_missed_verification_deadline_but_later_verified(self):
|
||||
# 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
|
||||
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
|
||||
attempt.mark_ready()
|
||||
attempt.submit()
|
||||
attempt.approve()
|
||||
attempt.created_at = self.PAST - timedelta(days=900)
|
||||
attempt.created_at = self.DATES[self.PAST] - timedelta(days=900)
|
||||
attempt.save()
|
||||
|
||||
# The student didn't have an approved verification at the deadline,
|
||||
@@ -168,7 +173,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
|
||||
|
||||
def test_verification_denied(self):
|
||||
# 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
|
||||
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
|
||||
@@ -182,7 +187,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
|
||||
|
||||
def test_verification_error(self):
|
||||
# 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
|
||||
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
|
||||
@@ -196,7 +201,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
|
||||
@override_settings(VERIFY_STUDENT={"DAYS_GOOD_FOR": 5, "EXPIRING_SOON_WINDOW": 10})
|
||||
def test_verification_will_expire_by_deadline(self):
|
||||
# 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:
|
||||
# 1) Is current (submitted in the last year)
|
||||
@@ -213,7 +218,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
|
||||
@override_settings(VERIFY_STUDENT={"DAYS_GOOD_FOR": 5, "EXPIRING_SOON_WINDOW": 10})
|
||||
def test_reverification_submitted_with_current_approved_verificaiton(self):
|
||||
# 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
|
||||
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
|
||||
@@ -236,7 +241,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
|
||||
|
||||
def test_verification_occurred_after_deadline(self):
|
||||
# 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
|
||||
# to reverify (through the support team).
|
||||
@@ -250,7 +255,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
|
||||
def test_with_two_verifications(self):
|
||||
# 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
|
||||
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
|
||||
@@ -274,7 +279,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
|
||||
CourseModeFactory.create(
|
||||
course_id=course2.id,
|
||||
mode_slug="verified",
|
||||
expiration_datetime=self.PAST
|
||||
expiration_datetime=self.DATES[self.PAST]
|
||||
)
|
||||
CourseEnrollmentFactory(
|
||||
course_id=course2.id,
|
||||
|
||||
@@ -297,19 +297,18 @@ class StudentDashboardTests(SharedModuleStoreTestCase):
|
||||
@patch.multiple('django.conf.settings', **MOCK_SETTINGS)
|
||||
@ddt.data(
|
||||
*itertools.product(
|
||||
[TOMORROW],
|
||||
[True, False],
|
||||
[True, False],
|
||||
[ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split],
|
||||
)
|
||||
)
|
||||
@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
|
||||
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.set_course_sharing_urls(set_marketing, set_social_sharing)
|
||||
|
||||
|
||||
@@ -31,20 +31,26 @@ class TransactionManagersTestCase(TransactionTestCase):
|
||||
|
||||
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(
|
||||
(outer_atomic(), IntegrityError, None, True),
|
||||
(outer_atomic(read_committed=True), type(None), False, True),
|
||||
(commit_on_success(), IntegrityError, None, True),
|
||||
(commit_on_success(read_committed=True), type(None), False, True),
|
||||
('outer_atomic', IntegrityError, None, True),
|
||||
('outer_atomic_read_committed', type(None), False, True),
|
||||
('commit_on_success', IntegrityError, None, True),
|
||||
('commit_on_success_read_committed', type(None), False, True),
|
||||
)
|
||||
@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()
|
||||
for the same row in concurrent requests does not raise an IntegrityError.
|
||||
"""
|
||||
|
||||
transaction_decorator = self.DECORATORS[transaction_decorator_name]
|
||||
if connection.vendor != 'mysql':
|
||||
raise unittest.SkipTest('Only works on MySQL.')
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
General testing utilities.
|
||||
"""
|
||||
import functools
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
|
||||
@@ -124,3 +125,29 @@ class MockS3Mixin(object):
|
||||
def tearDown(self):
|
||||
self._mock_s3.stop()
|
||||
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):
|
||||
# Eligible and should stay that way
|
||||
(
|
||||
CertificateStatuses.downloadable,
|
||||
datetime.now(pytz.UTC) - timedelta(days=2),
|
||||
timedelta(days=-2),
|
||||
'Pass',
|
||||
CertificateStatuses.generating
|
||||
),
|
||||
# Ensure that certs in the wrong state can be fixed by regeneration
|
||||
(
|
||||
CertificateStatuses.downloadable,
|
||||
datetime.now(pytz.UTC) - timedelta(hours=1),
|
||||
timedelta(hours=-1),
|
||||
'Pass',
|
||||
CertificateStatuses.audit_passing
|
||||
),
|
||||
# Ineligible and should stay that way
|
||||
(
|
||||
CertificateStatuses.audit_passing,
|
||||
datetime.now(pytz.UTC) - timedelta(hours=1),
|
||||
timedelta(hours=-1),
|
||||
'Pass',
|
||||
CertificateStatuses.audit_passing
|
||||
),
|
||||
# As above
|
||||
(
|
||||
CertificateStatuses.audit_notpassing,
|
||||
datetime.now(pytz.UTC) - timedelta(hours=1),
|
||||
timedelta(hours=-1),
|
||||
'Pass',
|
||||
CertificateStatuses.audit_passing
|
||||
),
|
||||
# As above
|
||||
(
|
||||
CertificateStatuses.audit_notpassing,
|
||||
datetime.now(pytz.UTC) - timedelta(hours=1),
|
||||
timedelta(hours=-1),
|
||||
None,
|
||||
CertificateStatuses.audit_notpassing
|
||||
),
|
||||
)
|
||||
@ddt.unpack
|
||||
@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
|
||||
generation is re-run.
|
||||
@@ -254,6 +254,7 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase):
|
||||
is_active=True,
|
||||
mode=CourseMode.AUDIT,
|
||||
)
|
||||
created_date = datetime.now(pytz.UTC) + created_delta
|
||||
with freezegun.freeze_time(created_date):
|
||||
GeneratedCertificateFactory(
|
||||
user=self.user_2,
|
||||
|
||||
@@ -106,6 +106,11 @@ class CourseListViewTests(CourseApiViewTestMixin, ModuleStoreTestCase):
|
||||
@ddt.ddt
|
||||
class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase):
|
||||
""" Tests for CourseRetrieveUpdateView. """
|
||||
NOW = 'now'
|
||||
DATES = {
|
||||
NOW: datetime.now(),
|
||||
None: None,
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(CourseRetrieveUpdateViewTests, self).setUp()
|
||||
@@ -276,12 +281,13 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase)
|
||||
|
||||
@ddt.data(*itertools.product(
|
||||
('honor', 'audit', 'verified', 'professional', 'no-id-professional'),
|
||||
(datetime.now(), None),
|
||||
(NOW, None),
|
||||
))
|
||||
@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
|
||||
will be rejected (this is not allowed). """
|
||||
expiration_datetime = self.DATES[expiration_datetime_name]
|
||||
mode = self._serialize_course_mode(
|
||||
CourseMode(
|
||||
mode_slug=mode_slug,
|
||||
|
||||
@@ -162,9 +162,14 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes
|
||||
"""
|
||||
Tests for the various access controls on the student dashboard
|
||||
"""
|
||||
TOMORROW = datetime.datetime.now(pytz.utc) + datetime.timedelta(days=1)
|
||||
YESTERDAY = datetime.datetime.now(pytz.utc) - datetime.timedelta(days=1)
|
||||
TOMORROW = 'tomorrow'
|
||||
YESTERDAY = 'yesterday'
|
||||
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):
|
||||
super(AccessTestCase, self).setUp()
|
||||
@@ -439,7 +444,7 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes
|
||||
mock_unit = Mock(location=self.course.location, user_partitions=[])
|
||||
mock_unit._class_tags = {} # Needed for detached check in _has_access_descriptor
|
||||
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 = {}
|
||||
|
||||
self.verify_access(mock_unit, expected_access, expected_error_type)
|
||||
@@ -448,7 +453,7 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes
|
||||
mock_unit = Mock(user_partitions=[])
|
||||
mock_unit._class_tags = {}
|
||||
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.merged_group_access = {}
|
||||
|
||||
@@ -465,7 +470,7 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes
|
||||
mock_unit = Mock(location=self.course.location, user_partitions=[])
|
||||
mock_unit._class_tags = {} # Needed for detached check in _has_access_descriptor
|
||||
mock_unit.visible_to_staff_only = False
|
||||
mock_unit.start = start
|
||||
mock_unit.start = self.DATES[start]
|
||||
mock_unit.merged_group_access = {}
|
||||
|
||||
self.verify_access(mock_unit, True)
|
||||
@@ -486,7 +491,7 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes
|
||||
mock_unit = Mock(location=self.course.location, user_partitions=[])
|
||||
mock_unit._class_tags = {} # Needed for detached check in _has_access_descriptor
|
||||
mock_unit.visible_to_staff_only = False
|
||||
mock_unit.start = start
|
||||
mock_unit.start = self.DATES[start]
|
||||
mock_unit.merged_group_access = {}
|
||||
|
||||
self.verify_access(mock_unit, expected_access, expected_error_type)
|
||||
|
||||
@@ -49,6 +49,12 @@ TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
|
||||
class CoursesTest(ModuleStoreTestCase):
|
||||
"""Test methods related to fetching courses."""
|
||||
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)
|
||||
def test_get_cms_course_block_link(self):
|
||||
@@ -64,8 +70,9 @@ class CoursesTest(ModuleStoreTestCase):
|
||||
cms_url = u"//{}/course/{}".format(CMS_BASE_TEST, unicode(self.course.location))
|
||||
self.assertEqual(cms_url, get_cms_block_link(self.course, 'course'))
|
||||
|
||||
@ddt.data(get_course_with_access, get_course_overview_with_access)
|
||||
def test_get_course_func_with_access_error(self, course_access_func):
|
||||
@ddt.data(GET_COURSE_WITH_ACCESS, GET_COURSE_OVERVIEW_WITH_ACCESS)
|
||||
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()
|
||||
course = CourseFactory.create(visible_to_staff_only=True)
|
||||
|
||||
@@ -76,11 +83,12 @@ class CoursesTest(ModuleStoreTestCase):
|
||||
self.assertFalse(error.exception.access_response.has_access)
|
||||
|
||||
@ddt.data(
|
||||
(get_course_with_access, 1),
|
||||
(get_course_overview_with_access, 0),
|
||||
(GET_COURSE_WITH_ACCESS, 1),
|
||||
(GET_COURSE_OVERVIEW_WITH_ACCESS, 0),
|
||||
)
|
||||
@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()
|
||||
course = CourseFactory.create(emit_signals=True)
|
||||
with check_mongo_calls(num_mongo_calls):
|
||||
|
||||
@@ -1570,11 +1570,11 @@ class TestStaffDebugInfo(SharedModuleStoreTestCase):
|
||||
|
||||
PER_COURSE_ANONYMIZED_DESCRIPTORS = (LTIDescriptor, )
|
||||
|
||||
# The "set" here is to work around the bug that load_classes returns duplicates for multiply-delcared classes.
|
||||
PER_STUDENT_ANONYMIZED_DESCRIPTORS = set(
|
||||
# The "set" here is to work around the bug that load_classes returns duplicates for multiply-declared classes.
|
||||
PER_STUDENT_ANONYMIZED_DESCRIPTORS = sorted(set(
|
||||
class_ for (name, class_) in XModuleDescriptor.load_classes()
|
||||
if not issubclass(class_, PER_COURSE_ANONYMIZED_DESCRIPTORS)
|
||||
)
|
||||
), key=str)
|
||||
|
||||
|
||||
@attr(shard=1)
|
||||
|
||||
@@ -13,6 +13,7 @@ from mock import MagicMock, Mock, patch
|
||||
from nose.plugins.attrib import attr
|
||||
from webob import Request
|
||||
|
||||
from common.test.utils import normalize_repr
|
||||
from openedx.core.djangoapps.contentserver.caching import del_cached_content
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.contentstore.django import contentstore
|
||||
@@ -105,6 +106,7 @@ def _upload_file(subs_file, location, filename):
|
||||
del_cached_content(content.location)
|
||||
|
||||
|
||||
@normalize_repr
|
||||
def attach_sub(item, filename):
|
||||
"""
|
||||
Attach `en` transcript.
|
||||
@@ -112,6 +114,7 @@ def attach_sub(item, filename):
|
||||
item.sub = filename
|
||||
|
||||
|
||||
@normalize_repr
|
||||
def attach_bumper_transcript(item, filename, lang="en"):
|
||||
"""
|
||||
Attach bumper transcript.
|
||||
|
||||
@@ -17,6 +17,7 @@ from path import Path as path
|
||||
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MONGO_MODULESTORE, TEST_DATA_SPLIT_MODULESTORE
|
||||
from xmodule.tests.test_import import DummySystem
|
||||
@@ -29,6 +30,11 @@ from .helpers import BaseTestXmodule
|
||||
from .test_video_handlers import TestVideo
|
||||
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)
|
||||
class TestVideoYouTube(TestVideo):
|
||||
@@ -1162,14 +1168,14 @@ class TestEditorSavedMethod(BaseTestXmodule):
|
||||
self.test_dir = path(__file__).abspath().dirname().dirname().dirname().dirname().dirname()
|
||||
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):
|
||||
"""
|
||||
When there is youtube_sub exist but no html5_sub present for
|
||||
html5_sources, editor_saved function will generate new html5_sub
|
||||
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)
|
||||
item = self.store.get_item(self.item_descriptor.location)
|
||||
with open(self.file_path, "r") as myfile:
|
||||
@@ -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_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):
|
||||
"""
|
||||
When both youtube_sub and html5_sub already exist then no new
|
||||
sub will be generated by editor_saved function.
|
||||
"""
|
||||
self.MODULESTORE = default_store
|
||||
self.MODULESTORE = MODULESTORES[default_store]
|
||||
self.initialize_module(metadata=self.metadata)
|
||||
item = self.store.get_item(self.item_descriptor.location)
|
||||
with open(self.file_path, "r") as myfile:
|
||||
@@ -1205,12 +1211,12 @@ class TestEditorSavedMethod(BaseTestXmodule):
|
||||
item.editor_saved(self.user, old_metadata, None)
|
||||
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):
|
||||
"""
|
||||
Verify editor saved when video id contains spaces/tabs.
|
||||
"""
|
||||
self.MODULESTORE = default_store
|
||||
self.MODULESTORE = MODULESTORES[default_store]
|
||||
stripped_video_id = unicode(uuid4())
|
||||
unstripped_video_id = u'{video_id}{tabs}'.format(video_id=stripped_video_id, tabs=u'\t\t\t')
|
||||
self.metadata.update({
|
||||
@@ -1226,14 +1232,14 @@ class TestEditorSavedMethod(BaseTestXmodule):
|
||||
item.editor_saved(self.user, old_metadata, None)
|
||||
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'))
|
||||
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
|
||||
for a given `edx_video_id`.
|
||||
"""
|
||||
self.MODULESTORE = default_store
|
||||
self.MODULESTORE = MODULESTORES[default_store]
|
||||
self.initialize_module(metadata=self.metadata)
|
||||
item = self.store.get_item(self.item_descriptor.location)
|
||||
self.assertEqual(item.youtube_id_1_0, '3_yD_cEKoCk')
|
||||
|
||||
@@ -251,6 +251,11 @@ class ViewsTestCase(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for views.py methods.
|
||||
"""
|
||||
YESTERDAY = 'yesterday'
|
||||
DATES = {
|
||||
YESTERDAY: datetime.now(UTC) - timedelta(days=1),
|
||||
None: None,
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(ViewsTestCase, self).setUp()
|
||||
@@ -751,7 +756,7 @@ class ViewsTestCase(ModuleStoreTestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
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.AUDIT, False, None),
|
||||
([CourseMode.AUDIT], CourseMode.AUDIT, False, None))
|
||||
@@ -770,7 +775,7 @@ class ViewsTestCase(ModuleStoreTestCase):
|
||||
|
||||
# Create 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
|
||||
CourseEnrollmentFactory(course_id=course.id, user=self.user, mode=enrollment_mode)
|
||||
@@ -1705,10 +1710,16 @@ class ProgressPageShowCorrectnessTests(ProgressPageBaseTests):
|
||||
# Constants used in the test data
|
||||
NOW = datetime.now(UTC)
|
||||
DAY_DELTA = timedelta(days=1)
|
||||
YESTERDAY = NOW - DAY_DELTA
|
||||
TODAY = NOW
|
||||
TOMORROW = NOW + DAY_DELTA
|
||||
YESTERDAY = 'yesterday'
|
||||
TODAY = 'today'
|
||||
TOMORROW = 'tomorrow'
|
||||
GRADER_TYPE = 'Homework'
|
||||
DATES = {
|
||||
YESTERDAY: NOW - DAY_DELTA,
|
||||
TODAY: NOW,
|
||||
TOMORROW: NOW + DAY_DELTA,
|
||||
None: None,
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(ProgressPageShowCorrectnessTests, self).setUp()
|
||||
@@ -1853,12 +1864,12 @@ class ProgressPageShowCorrectnessTests(ProgressPageBaseTests):
|
||||
(ShowCorrectness.PAST_DUE, TOMORROW, True),
|
||||
)
|
||||
@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,
|
||||
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()
|
||||
|
||||
# Test that no problem scores are present
|
||||
@@ -1893,11 +1904,12 @@ class ProgressPageShowCorrectnessTests(ProgressPageBaseTests):
|
||||
(ShowCorrectness.PAST_DUE, TOMORROW, True, False),
|
||||
)
|
||||
@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
|
||||
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.add_problem()
|
||||
|
||||
@@ -1944,10 +1956,11 @@ class ProgressPageShowCorrectnessTests(ProgressPageBaseTests):
|
||||
(ShowCorrectness.PAST_DUE, TOMORROW, True, True),
|
||||
)
|
||||
@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.
|
||||
"""
|
||||
due_date = self.DATES[due_date_name]
|
||||
self.setup_course(show_correctness=show_correctness, due_date=due_date, graded=graded)
|
||||
self.add_problem()
|
||||
|
||||
|
||||
@@ -19,6 +19,35 @@ from xmodule.graders import ProblemScore
|
||||
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):
|
||||
"""
|
||||
Tests for the possibly_scored function.
|
||||
@@ -52,13 +81,16 @@ class TestGetScore(TestCase):
|
||||
location = 'test_location'
|
||||
|
||||
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.__repr__ = csm_value_repr
|
||||
PersistedBlockValue = namedtuple('PersistedBlockValue', 'exists, raw_possible, weight, graded')
|
||||
ContentBlockValue = namedtuple('ContentBlockValue', 'raw_possible, weight, explicit_graded')
|
||||
ExpectedResult = namedtuple(
|
||||
'ExpectedResult',
|
||||
'raw_earned, raw_possible, weighted_earned, weighted_possible, weight, graded, first_attempted'
|
||||
)
|
||||
ExpectedResult.__repr__ = expected_result_repr
|
||||
|
||||
def _create_submissions_scores(self, submission_value):
|
||||
"""
|
||||
|
||||
@@ -16,7 +16,6 @@ from util.date_utils import to_timestamp
|
||||
from ..constants import ScoreDatabaseTableEnum
|
||||
from ..signals.handlers import (
|
||||
disconnect_submissions_signal_receiver,
|
||||
enqueue_subsection_update,
|
||||
problem_raw_score_changed_handler,
|
||||
submissions_score_reset_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
|
||||
FROZEN_NOW_DATETIME = datetime.now().replace(tzinfo=pytz.UTC)
|
||||
FROZEN_NOW_TIMESTAMP = to_timestamp(FROZEN_NOW_DATETIME)
|
||||
|
||||
SUBMISSION_SET_KWARGS = {
|
||||
'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,
|
||||
SUBMISSIONS_SCORE_SET_HANDLER = 'submissions_score_set_handler'
|
||||
SUBMISSIONS_SCORE_RESET_HANDLER = 'submissions_score_reset_handler'
|
||||
HANDLERS = {
|
||||
SUBMISSIONS_SCORE_SET_HANDLER: submissions_score_set_handler,
|
||||
SUBMISSIONS_SCORE_RESET_HANDLER: submissions_score_reset_handler,
|
||||
}
|
||||
|
||||
SUBMISSION_RESET_KWARGS = {
|
||||
'anonymous_user_id': 'anonymous_id',
|
||||
'course_id': 'CourseID',
|
||||
'item_id': 'i4x://org/course/usage/123456',
|
||||
'created_at': FROZEN_NOW_TIMESTAMP,
|
||||
SUBMISSION_SET_KWARGS = 'submission_set_kwargs'
|
||||
SUBMISSION_RESET_KWARGS = 'submission_reset_kwargs'
|
||||
SUBMISSION_KWARGS = {
|
||||
SUBMISSION_SET_KWARGS: {
|
||||
'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 = {
|
||||
@@ -82,6 +91,10 @@ class ScoreChangedSignalRelayTest(TestCase):
|
||||
This ensures that listeners in the LMS only have to handle one type
|
||||
of signal for all scoring events regardless of their origin.
|
||||
"""
|
||||
SIGNALS = {
|
||||
'score_set': score_set,
|
||||
'score_reset': score_reset,
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
@@ -110,11 +123,11 @@ class ScoreChangedSignalRelayTest(TestCase):
|
||||
return mock
|
||||
|
||||
@ddt.data(
|
||||
[submissions_score_set_handler, SUBMISSION_SET_KWARGS, 5, 10],
|
||||
[submissions_score_reset_handler, SUBMISSION_RESET_KWARGS, 0, 0],
|
||||
[SUBMISSIONS_SCORE_SET_HANDLER, SUBMISSION_SET_KWARGS, 5, 10],
|
||||
[SUBMISSIONS_SCORE_RESET_HANDLER, SUBMISSION_RESET_KWARGS, 0, 0],
|
||||
)
|
||||
@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,
|
||||
the signal handler correctly converts it to a PROBLEM_WEIGHTED_SCORE_CHANGED
|
||||
@@ -122,7 +135,9 @@ class ScoreChangedSignalRelayTest(TestCase):
|
||||
|
||||
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 = {
|
||||
'sender': None,
|
||||
'weighted_possible': possible,
|
||||
@@ -134,35 +149,36 @@ class ScoreChangedSignalRelayTest(TestCase):
|
||||
'modified': FROZEN_NOW_TIMESTAMP,
|
||||
'score_db_table': 'submissions',
|
||||
}
|
||||
if handler == submissions_score_reset_handler:
|
||||
if kwargs == SUBMISSION_RESET_KWARGS:
|
||||
expected_set_kwargs['score_deleted'] = True
|
||||
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):
|
||||
"""
|
||||
Ensure that, if coming from the submissions API, signals indicating a
|
||||
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_possible'] = 0
|
||||
submissions_score_set_handler(None, **local_kwargs)
|
||||
self.signal_mock.assert_not_called()
|
||||
|
||||
@ddt.data(
|
||||
[submissions_score_set_handler, SUBMISSION_SET_KWARGS],
|
||||
[submissions_score_reset_handler, SUBMISSION_RESET_KWARGS]
|
||||
[SUBMISSIONS_SCORE_SET_HANDLER, SUBMISSION_SET_KWARGS],
|
||||
[SUBMISSIONS_SCORE_RESET_HANDLER, SUBMISSION_RESET_KWARGS]
|
||||
)
|
||||
@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
|
||||
that does not have the correct kwargs, the courseware model does not
|
||||
generate a signal.
|
||||
"""
|
||||
for missing in kwargs:
|
||||
local_kwargs = kwargs.copy()
|
||||
handler = HANDLERS[handler_name]
|
||||
for missing in SUBMISSION_KWARGS[kwargs]:
|
||||
local_kwargs = SUBMISSION_KWARGS[kwargs].copy()
|
||||
del local_kwargs[missing]
|
||||
|
||||
with self.assertRaises(KeyError):
|
||||
@@ -170,18 +186,19 @@ class ScoreChangedSignalRelayTest(TestCase):
|
||||
self.signal_mock.assert_not_called()
|
||||
|
||||
@ddt.data(
|
||||
[submissions_score_set_handler, SUBMISSION_SET_KWARGS],
|
||||
[submissions_score_reset_handler, SUBMISSION_RESET_KWARGS]
|
||||
[SUBMISSIONS_SCORE_SET_HANDLER, SUBMISSION_SET_KWARGS],
|
||||
[SUBMISSIONS_SCORE_RESET_HANDLER, SUBMISSION_RESET_KWARGS]
|
||||
)
|
||||
@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
|
||||
that has an invalid user ID, the courseware model does not generate a
|
||||
signal.
|
||||
"""
|
||||
handler = HANDLERS[handler_name]
|
||||
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()
|
||||
|
||||
def test_raw_score_changed_signal_handler(self):
|
||||
@@ -198,14 +215,18 @@ class ScoreChangedSignalRelayTest(TestCase):
|
||||
self.signal_mock.assert_called_with(**expected_set_kwargs)
|
||||
|
||||
@ddt.data(
|
||||
[score_set, 'lms.djangoapps.grades.signals.handlers.submissions_score_set_handler', SUBMISSION_SET_KWARGS],
|
||||
[score_reset, 'lms.djangoapps.grades.signals.handlers.submissions_score_reset_handler', SUBMISSION_RESET_KWARGS]
|
||||
['score_set', 'lms.djangoapps.grades.signals.handlers.submissions_score_set_handler',
|
||||
SUBMISSION_SET_KWARGS],
|
||||
['score_reset', 'lms.djangoapps.grades.signals.handlers.submissions_score_reset_handler',
|
||||
SUBMISSION_RESET_KWARGS]
|
||||
)
|
||||
@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.
|
||||
"""
|
||||
signal = self.SIGNALS[signal_name]
|
||||
kwargs = SUBMISSION_KWARGS[kwargs].copy()
|
||||
handler_mock = self.setup_patch(handler, None)
|
||||
|
||||
# Receiver connected before we start
|
||||
|
||||
@@ -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 certificates.models import CertificateGenerationHistory, CertificateStatuses
|
||||
from common.test.utils import normalize_repr
|
||||
from courseware.tests.factories import UserFactory
|
||||
from lms.djangoapps.instructor_task.api import (
|
||||
SpecificStudentIdMissingError,
|
||||
@@ -147,21 +148,29 @@ class InstructorTaskModuleSubmitTest(InstructorTaskModuleTestCase):
|
||||
self._test_submit_with_long_url(submit_delete_problem_state_for_all_students)
|
||||
|
||||
@ddt.data(
|
||||
(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}),
|
||||
(normalize_repr(submit_rescore_problem_for_all_students), 'rescore_problem'),
|
||||
(
|
||||
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',
|
||||
{'student': True, 'only_if_higher': True},
|
||||
),
|
||||
(submit_reset_problem_attempts_in_entrance_exam, 'reset_problem_attempts', {'student': True}),
|
||||
(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_reset_problem_attempts_in_entrance_exam), 'reset_problem_attempts', {'student': True}),
|
||||
(normalize_repr(submit_delete_entrance_exam_state_for_student), 'delete_problem_state', {'student': True}),
|
||||
(normalize_repr(submit_override_score), 'override_problem_score', {'student': True, 'score': 0})
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_submit_task(self, task_function, expected_task_type, params=None):
|
||||
|
||||
@@ -84,6 +84,11 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest
|
||||
LAST_WEEK = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=7)
|
||||
ADVERTISED_START = "Spring 2016"
|
||||
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})
|
||||
def setUp(self, *args, **kwargs):
|
||||
@@ -175,12 +180,12 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest
|
||||
self.assertFalse(result['has_access'])
|
||||
|
||||
@ddt.data(
|
||||
(NEXT_WEEK, ADVERTISED_START, ADVERTISED_START, "string"),
|
||||
(NEXT_WEEK, None, 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, '', None, "empty"),
|
||||
(DEFAULT_START_DATE, None, None, "empty"),
|
||||
('next_week', ADVERTISED_START, ADVERTISED_START, "string"),
|
||||
('next_week', None, 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', '', None, "empty"),
|
||||
('default_start_date', None, None, "empty"),
|
||||
)
|
||||
@ddt.unpack
|
||||
@patch.dict(settings.FEATURES, {'DISABLE_START_DATES': False, 'ENABLE_MKTG_SITE': True})
|
||||
@@ -190,7 +195,7 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest
|
||||
case the course has not started
|
||||
"""
|
||||
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)
|
||||
|
||||
response = self.api_response()
|
||||
|
||||
@@ -118,17 +118,17 @@ class TeamMembershipTest(SharedModuleStoreTestCase):
|
||||
class TeamSignalsTest(EventTestMixin, SharedModuleStoreTestCase):
|
||||
"""Tests for handling of team-related signals."""
|
||||
|
||||
SIGNALS_LIST = (
|
||||
thread_created,
|
||||
thread_edited,
|
||||
thread_deleted,
|
||||
thread_voted,
|
||||
comment_created,
|
||||
comment_edited,
|
||||
comment_deleted,
|
||||
comment_voted,
|
||||
comment_endorsed
|
||||
)
|
||||
SIGNALS = {
|
||||
'thread_created': thread_created,
|
||||
'thread_edited': thread_edited,
|
||||
'thread_deleted': thread_deleted,
|
||||
'thread_voted': thread_voted,
|
||||
'comment_created': comment_created,
|
||||
'comment_edited': comment_edited,
|
||||
'comment_deleted': comment_deleted,
|
||||
'comment_voted': comment_voted,
|
||||
'comment_endorsed': comment_endorsed,
|
||||
}
|
||||
|
||||
DISCUSSION_TOPIC_ID = 'test_topic'
|
||||
|
||||
@@ -180,30 +180,33 @@ class TeamSignalsTest(EventTestMixin, SharedModuleStoreTestCase):
|
||||
|
||||
@ddt.data(
|
||||
*itertools.product(
|
||||
SIGNALS_LIST,
|
||||
SIGNALS.keys(),
|
||||
(('user', True), ('moderator', False))
|
||||
)
|
||||
)
|
||||
@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
|
||||
signals are sent.
|
||||
"""
|
||||
with self.assert_last_activity_updated(should_update):
|
||||
user = getattr(self, user)
|
||||
signal = self.SIGNALS[signal_name]
|
||||
signal.send(sender=None, user=user, post=self.mock_comment())
|
||||
|
||||
@ddt.data(thread_voted, comment_voted)
|
||||
def test_vote_others_post(self, signal):
|
||||
@ddt.data('thread_voted', 'comment_voted')
|
||||
def test_vote_others_post(self, signal_name):
|
||||
"""Test that voting on another user's post correctly fires a
|
||||
signal."""
|
||||
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))
|
||||
|
||||
@ddt.data(*SIGNALS_LIST)
|
||||
def test_signals_course_context(self, signal):
|
||||
@ddt.data(*SIGNALS.keys())
|
||||
def test_signals_course_context(self, signal_name):
|
||||
"""Test that `last_activity_at` is not updated when activity takes
|
||||
place in discussions outside of a team.
|
||||
"""
|
||||
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'))
|
||||
|
||||
@@ -89,8 +89,15 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
|
||||
PASSWORD = "test_password"
|
||||
|
||||
NOW = datetime.now(pytz.UTC)
|
||||
YESTERDAY = NOW - timedelta(days=1)
|
||||
TOMORROW = NOW + timedelta(days=1)
|
||||
YESTERDAY = 'yesterday'
|
||||
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']
|
||||
|
||||
@@ -492,7 +499,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
|
||||
)
|
||||
@ddt.unpack
|
||||
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")
|
||||
response = self._get_page('verify_student_payment_confirmation', course.id)
|
||||
|
||||
@@ -753,9 +760,10 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
|
||||
self.assertContains(response, "verification 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):
|
||||
"""Verify that student can not upgrade in expired course mode."""
|
||||
verification_deadline = self.DATES[verification_deadline]
|
||||
course_modes = ("verified", "credit")
|
||||
course = self._create_course(*course_modes)
|
||||
|
||||
|
||||
@@ -183,6 +183,9 @@ class MockTransformer(BlockStructureTransformer):
|
||||
def transform(self, usage_info, block_structure):
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
return self.name()
|
||||
|
||||
|
||||
class MockFilteringTransformer(FilteringTransformerMixin, BlockStructureTransformer):
|
||||
"""
|
||||
|
||||
@@ -46,10 +46,18 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
|
||||
"""
|
||||
|
||||
TODAY = timezone.now()
|
||||
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)
|
||||
LAST_MONTH = 'last_month'
|
||||
LAST_WEEK = 'last_week'
|
||||
NEXT_WEEK = 'next_week'
|
||||
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'}
|
||||
|
||||
@@ -229,7 +237,7 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
|
||||
},
|
||||
{
|
||||
# # 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,
|
||||
"advertised_start": None, # No advertised start
|
||||
"pre_requisite_courses": [], # No pre-requisites
|
||||
@@ -251,10 +259,15 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
|
||||
modulestore_type (ModuleStoreEnum.Type): type of store to create the
|
||||
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,
|
||||
# .create raises an InvalidKeyError if we don't (even though my
|
||||
# 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)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
|
||||
@@ -35,6 +35,12 @@ class TestMinGradedRequirementStatus(ModuleStoreTestCase):
|
||||
VALID_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):
|
||||
super(TestMinGradedRequirementStatus, self).setUp()
|
||||
self.course = CourseFactory.create(
|
||||
@@ -85,13 +91,13 @@ class TestMinGradedRequirementStatus(ModuleStoreTestCase):
|
||||
self.assertEqual(req_status[0]['reason'], expected_reason)
|
||||
|
||||
@ddt.data(
|
||||
(0.6, VALID_DUE_DATE),
|
||||
(0.6, 'valid'),
|
||||
(0.52, None),
|
||||
)
|
||||
@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"""
|
||||
self.assert_requirement_status(grade, due_date, 'satisfied')
|
||||
self.assert_requirement_status(grade, self.DATES[due_date_name], 'satisfied')
|
||||
|
||||
def test_grade_changed(self):
|
||||
""" Verify successive calls to update a satisfied grade requirement are recorded. """
|
||||
@@ -106,12 +112,12 @@ class TestMinGradedRequirementStatus(ModuleStoreTestCase):
|
||||
@ddt.data(
|
||||
(0.50, None),
|
||||
(0.51, None),
|
||||
(0.40, VALID_DUE_DATE),
|
||||
(0.40, 'valid'),
|
||||
)
|
||||
@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."""
|
||||
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):
|
||||
"""Test with failed grades and deadline expire"""
|
||||
|
||||
@@ -29,6 +29,10 @@ COMMAND_MODULE = 'openedx.core.djangoapps.programs.management.commands.backpopul
|
||||
class BackpopulateProgramCredentialsTests(CatalogIntegrationMixin, CredentialsApiConfigMixin, TestCase):
|
||||
"""Tests for the backpopulate_program_credentials management command."""
|
||||
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):
|
||||
super(BackpopulateProgramCredentialsTests, self).setUp()
|
||||
@@ -44,6 +48,54 @@ class BackpopulateProgramCredentialsTests(CatalogIntegrationMixin, CredentialsAp
|
||||
catalog_integration = self.create_catalog_integration()
|
||||
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)
|
||||
def test_handle(self, commit, mock_task, mock_get_programs):
|
||||
"""
|
||||
@@ -112,49 +164,10 @@ class BackpopulateProgramCredentialsTests(CatalogIntegrationMixin, CredentialsAp
|
||||
# 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)])
|
||||
|
||||
@ddt.data(
|
||||
[
|
||||
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):
|
||||
@ddt.data(SEPARATE_PROGRAMS, SEPARATE_COURSES, SAME_COURSE)
|
||||
def test_handle_flatten(self, hierarchy_type, mock_task, mock_get_programs):
|
||||
"""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(
|
||||
user=self.alice,
|
||||
|
||||
@@ -53,11 +53,11 @@ class RegistrationValidationViewTests(test_utils.ApiTestCase):
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
['name', (name for name in testutils.VALID_NAMES)],
|
||||
['email', (email for email in testutils.VALID_EMAILS)],
|
||||
['password', (password for password in testutils.VALID_PASSWORDS)],
|
||||
['username', (username for username in testutils.VALID_USERNAMES)],
|
||||
['country', (country for country in testutils.VALID_COUNTRIES)]
|
||||
['name', [name for name in testutils.VALID_NAMES]],
|
||||
['email', [email for email in testutils.VALID_EMAILS]],
|
||||
['password', [password for password in testutils.VALID_PASSWORDS]],
|
||||
['username', [username for username in testutils.VALID_USERNAMES]],
|
||||
['country', [country for country in testutils.VALID_COUNTRIES]]
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_positive_validation_decision(self, form_field_name, user_data):
|
||||
@@ -71,11 +71,11 @@ class RegistrationValidationViewTests(test_utils.ApiTestCase):
|
||||
|
||||
@ddt.data(
|
||||
# Skip None type for invalidity checks.
|
||||
['name', (name for name in testutils.INVALID_NAMES[1:])],
|
||||
['email', (email for email in testutils.INVALID_EMAILS[1:])],
|
||||
['password', (password for password in testutils.INVALID_PASSWORDS[1:])],
|
||||
['username', (username for username in testutils.INVALID_USERNAMES[1:])],
|
||||
['country', (country for country in testutils.INVALID_COUNTRIES[1:])]
|
||||
['name', [name for name in testutils.INVALID_NAMES[1:]]],
|
||||
['email', [email for email in testutils.INVALID_EMAILS[1:]]],
|
||||
['password', [password for password in testutils.INVALID_PASSWORDS[1:]]],
|
||||
['username', [username for username in testutils.INVALID_USERNAMES[1:]]],
|
||||
['country', [country for country in testutils.INVALID_COUNTRIES[1:]]]
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_negative_validation_decision(self, form_field_name, user_data):
|
||||
|
||||
@@ -6,6 +6,8 @@ import ddt
|
||||
|
||||
from django.contrib.messages.middleware import MessageMiddleware
|
||||
from django.test import RequestFactory, TestCase
|
||||
|
||||
from common.test.utils import normalize_repr
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
@@ -60,10 +62,10 @@ class UserMessagesTestCase(TestCase):
|
||||
self.assertEquals(messages[0].icon_class, expected_icon_class)
|
||||
|
||||
@ddt.data(
|
||||
(PageLevelMessages.register_error_message, UserMessageType.ERROR),
|
||||
(PageLevelMessages.register_info_message, UserMessageType.INFO),
|
||||
(PageLevelMessages.register_success_message, UserMessageType.SUCCESS),
|
||||
(PageLevelMessages.register_warning_message, UserMessageType.WARNING),
|
||||
(normalize_repr(PageLevelMessages.register_error_message), UserMessageType.ERROR),
|
||||
(normalize_repr(PageLevelMessages.register_info_message), UserMessageType.INFO),
|
||||
(normalize_repr(PageLevelMessages.register_success_message), UserMessageType.SUCCESS),
|
||||
(normalize_repr(PageLevelMessages.register_warning_message), UserMessageType.WARNING),
|
||||
)
|
||||
@ddt.unpack
|
||||
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
|
||||
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.__repr__ = attribute_pair_repr
|
||||
|
||||
|
||||
ID_ATTR_NAMES = ("discussion_id", "id",)
|
||||
|
||||
@@ -20,8 +20,6 @@ from xmodule.course_module import DEFAULT_START_DATE
|
||||
from .test_course_home import course_home_url
|
||||
|
||||
TEST_PASSWORD = 'test'
|
||||
FUTURE_DAY = datetime.datetime.now() + datetime.timedelta(days=30)
|
||||
PAST_DAY = datetime.datetime.now() - datetime.timedelta(days=30)
|
||||
|
||||
|
||||
class TestCourseOutlinePage(SharedModuleStoreTestCase):
|
||||
@@ -343,14 +341,22 @@ class TestEmptyCourseOutlinePage(SharedModuleStoreTestCase):
|
||||
"""
|
||||
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(
|
||||
(FUTURE_DAY, 'This course has not started yet, and will launch on'),
|
||||
(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
|
||||
def test_empty_course_rendering(self, start_date, expected_text):
|
||||
course = CourseFactory.create(start=start_date)
|
||||
def test_empty_course_rendering(self, start_date_name, expected_text):
|
||||
course = CourseFactory.create(start=self.DATES[start_date_name])
|
||||
test_user = UserFactory(password=TEST_PASSWORD)
|
||||
CourseEnrollment.enroll(test_user, course.id)
|
||||
self.client.login(username=test_user.username, password=TEST_PASSWORD)
|
||||
|
||||
Reference in New Issue
Block a user