Commit fd6af6b0 by Jeremy Bowman

ddt usage cleanup

parent 01e41243
......@@ -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.')
......
"""
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)
......
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