Commit 2f581448 by Nimisha Asthagiri

Refactor Courseware Index

MA-2189
parent b1313671
......@@ -298,7 +298,7 @@ class DashboardTest(ModuleStoreTestCase):
self.assertIsNone(course_mode_info['days_for_upsell'])
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@patch('courseware.views.log.warning')
@patch('courseware.index.log.warning')
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True})
def test_blocked_course_scenario(self, log_warning):
......@@ -349,7 +349,10 @@ class DashboardTest(ModuleStoreTestCase):
# Direct link to course redirect to user dashboard
self.client.get(reverse('courseware', kwargs={"course_id": self.course.id.to_deprecated_string()}))
log_warning.assert_called_with(
u'User %s cannot access the course %s because payment has not yet been received', self.user, self.course.id.to_deprecated_string())
u'User %s cannot access the course %s because payment has not yet been received',
self.user,
unicode(self.course.id),
)
# Now re-validating the invoice
invoice = shoppingcart.models.Invoice.objects.get(id=sale_invoice_1.id)
......
......@@ -25,7 +25,7 @@ def course_has_entrance_exam(course):
return True
def user_can_skip_entrance_exam(request, user, course):
def user_can_skip_entrance_exam(user, course):
"""
Checks all of the various override conditions for a user to skip an entrance exam
Begin by short-circuiting if the course does not have an entrance exam
......@@ -38,7 +38,7 @@ def user_can_skip_entrance_exam(request, user, course):
return True
if EntranceExamConfiguration.user_can_skip_entrance_exam(user, course.id):
return True
if not get_entrance_exam_content(request, course):
if not get_entrance_exam_content(user, course):
return True
return False
......@@ -66,7 +66,7 @@ def user_must_complete_entrance_exam(request, user, course):
whether or not the user is allowed to clear the Entrance Exam gate and access the rest of the course.
"""
# First, let's see if the user is allowed to skip
if user_can_skip_entrance_exam(request, user, course):
if user_can_skip_entrance_exam(user, course):
return False
# If they can't actually skip the exam, we'll need to see if they've already passed it
if user_has_passed_entrance_exam(request, course):
......@@ -157,11 +157,11 @@ def get_entrance_exam_score(request, course):
return _calculate_entrance_exam_score(request.user, course, exam_modules)
def get_entrance_exam_content(request, course):
def get_entrance_exam_content(user, course):
"""
Get the entrance exam content information (ie, chapter module)
"""
required_content = get_required_content(course, request.user)
required_content = get_required_content(course, user)
exam_module = None
for content in required_content:
......
"""
Exception classes used in lms/courseware.
"""
class Redirect(Exception):
"""
Exception class that requires redirecting to a URL.
"""
def __init__(self, url):
super(Redirect, self).__init__()
self.url = url
......@@ -123,13 +123,20 @@ def toc_for_course(user, request, course, active_chapter, active_section, field_
Create a table of contents from the module store
Return format:
[ {'display_name': name, 'url_name': url_name,
'sections': SECTIONS, 'active': bool}, ... ]
{ 'chapters': [
{'display_name': name, 'url_name': url_name, 'sections': SECTIONS, 'active': bool},
],
'previous_of_active_section': {..},
'next_of_active_section': {..}
}
where SECTIONS is a list
[ {'display_name': name, 'url_name': url_name,
'format': format, 'due': due, 'active' : bool, 'graded': bool}, ...]
where previous_of_active_section and next_of_active_section have information on the
next/previous sections of the active section.
active is set for the section and chapter corresponding to the passed
parameters, which are expected to be url_names of the chapter+section.
Everything else comes from the xml, or defaults to "".
......@@ -139,7 +146,7 @@ def toc_for_course(user, request, course, active_chapter, active_section, field_
NOTE: assumes that if we got this far, user has access to course. Returns
None if this is not the case.
field_data_cache must include data from the course module and 2 levels of its descendents
field_data_cache must include data from the course module and 2 levels of its descendants
'''
with modulestore().bulk_operations(course.id):
......@@ -221,7 +228,11 @@ def toc_for_course(user, request, course, active_chapter, active_section, field_
'sections': sections,
'active': chapter.url_name == active_chapter
})
return toc_chapters, previous_of_active_section, next_of_active_section
return {
'chapters': toc_chapters,
'previous_of_active_section': previous_of_active_section,
'next_of_active_section': next_of_active_section,
}
def _add_timed_exam_info(user, course, section, section_context):
......
......@@ -62,7 +62,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
parent=self.course,
display_name='Overview'
)
ItemFactory.create(
self.welcome = ItemFactory.create(
parent=self.chapter,
display_name='Welcome'
)
......@@ -250,7 +250,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
kwargs={
'course_id': unicode(self.course.id),
'chapter': self.chapter.location.name,
'section': self.chapter_subsection.location.name
'section': self.welcome.location.name
})
resp = self.client.get(url)
self.assertRedirects(resp, expected_url, status_code=302, target_status_code=200)
......@@ -278,14 +278,14 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
"""
test get entrance exam content method
"""
exam_chapter = get_entrance_exam_content(self.request, self.course)
exam_chapter = get_entrance_exam_content(self.request.user, self.course)
self.assertEqual(exam_chapter.url_name, self.entrance_exam.url_name)
self.assertFalse(user_has_passed_entrance_exam(self.request, self.course))
answer_entrance_exam_problem(self.course, self.request, self.problem_1)
answer_entrance_exam_problem(self.course, self.request, self.problem_2)
exam_chapter = get_entrance_exam_content(self.request, self.course)
exam_chapter = get_entrance_exam_content(self.request.user, self.course)
self.assertEqual(exam_chapter, None)
self.assertTrue(user_has_passed_entrance_exam(self.request, self.course))
......@@ -314,7 +314,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
kwargs={
'course_id': unicode(self.course.id),
'chapter': self.entrance_exam.location.name,
'section': self.exam_1.location.name
'section': self.exam_1.location.name,
}
)
resp = self.client.get(url)
......@@ -457,11 +457,13 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
kwargs={'course_id': unicode(self.course.id), 'chapter': self.chapter.url_name}
)
response = self.client.get(url)
redirect_url = reverse('courseware', args=[unicode(self.course.id)])
self.assertRedirects(response, redirect_url, status_code=302, target_status_code=302)
response = self.client.get(redirect_url)
exam_url = response.get('Location')
self.assertRedirects(response, exam_url)
expected_url = reverse('courseware_section',
kwargs={
'course_id': unicode(self.course.id),
'chapter': self.entrance_exam.location.name,
'section': self.exam_1.location.name
})
self.assertRedirects(response, expected_url, status_code=302, target_status_code=200)
@patch('courseware.entrance_exams.user_has_passed_entrance_exam', Mock(return_value=False))
def test_courseinfo_page_access_without_passing_entrance_exam(self):
......@@ -516,7 +518,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
"""
Test can_skip_entrance_exam method with anonymous user
"""
self.assertFalse(user_can_skip_entrance_exam(self.request, self.anonymous_user, self.course))
self.assertFalse(user_can_skip_entrance_exam(self.anonymous_user, self.course))
def test_has_passed_entrance_exam_with_anonymous_user(self):
"""
......@@ -583,7 +585,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
self.request.user,
self.entrance_exam
)
toc, __, __ = toc_for_course(
toc = toc_for_course(
self.request.user,
self.request,
self.course,
......@@ -591,7 +593,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
self.exam_1.url_name,
self.field_data_cache
)
return toc
return toc['chapters']
def answer_entrance_exam_problem(course, request, problem, user=None):
......
......@@ -668,13 +668,13 @@ class TestTOC(ModuleStoreTestCase):
course = self.store.get_course(self.toy_course.id, depth=2)
with check_mongo_calls(toc_finds):
actual, prev_sequential, next_sequential = render.toc_for_course(
actual = render.toc_for_course(
self.request.user, self.request, course, self.chapter, None, self.field_data_cache
)
for toc_section in expected:
self.assertIn(toc_section, actual)
self.assertIsNone(prev_sequential)
self.assertIsNone(next_sequential)
self.assertIn(toc_section, actual['chapters'])
self.assertIsNone(actual['previous_of_active_section'])
self.assertIsNone(actual['next_of_active_section'])
# Mongo makes 3 queries to load the course to depth 2:
# - 1 for the course
......@@ -709,13 +709,13 @@ class TestTOC(ModuleStoreTestCase):
'url_name': 'secret:magic', 'display_name': 'secret:magic', 'display_id': 'secretmagic'}])
with check_mongo_calls(toc_finds):
actual, prev_sequential, next_sequential = render.toc_for_course(
actual = render.toc_for_course(
self.request.user, self.request, self.toy_course, self.chapter, section, self.field_data_cache
)
for toc_section in expected:
self.assertIn(toc_section, actual)
self.assertEquals(prev_sequential['url_name'], 'Toy_Videos')
self.assertEquals(next_sequential['url_name'], 'video_123456789012')
self.assertIn(toc_section, actual['chapters'])
self.assertEquals(actual['previous_of_active_section']['url_name'], 'Toy_Videos')
self.assertEquals(actual['next_of_active_section']['url_name'], 'video_123456789012')
@attr('shard_1')
......@@ -856,7 +856,7 @@ class TestProctoringRendering(SharedModuleStoreTestCase):
"""
self._setup_test_data(enrollment_mode, is_practice_exam, attempt_status)
actual, prev_sequential, next_sequential = render.toc_for_course(
actual = render.toc_for_course(
self.request.user,
self.request,
self.toy_course,
......@@ -864,15 +864,15 @@ class TestProctoringRendering(SharedModuleStoreTestCase):
'Toy_Videos',
self.field_data_cache
)
section_actual = self._find_section(actual, 'Overview', 'Toy_Videos')
section_actual = self._find_section(actual['chapters'], 'Overview', 'Toy_Videos')
if expected:
self.assertIn(expected, [section_actual['proctoring']])
else:
# we expect there not to be a 'proctoring' key in the dict
self.assertNotIn('proctoring', section_actual)
self.assertIsNone(prev_sequential)
self.assertEquals(next_sequential['url_name'], u"Welcome")
self.assertIsNone(actual['previous_of_active_section'])
self.assertEquals(actual['next_of_active_section']['url_name'], u"Welcome")
@ddt.data(
(
......@@ -1114,7 +1114,7 @@ class TestGatedSubsectionRendering(SharedModuleStoreTestCase, MilestonesTestCase
"""
Test generation of TOC for a course with a gated subsection
"""
actual, prev_sequential, next_sequential = render.toc_for_course(
actual = render.toc_for_course(
self.request.user,
self.request,
self.course,
......@@ -1122,11 +1122,11 @@ class TestGatedSubsectionRendering(SharedModuleStoreTestCase, MilestonesTestCase
self.open_seq.display_name,
self.field_data_cache
)
self.assertIsNotNone(self._find_sequential(actual, 'Chapter', 'Open_Sequential'))
self.assertIsNone(self._find_sequential(actual, 'Chapter', 'Gated_Sequential'))
self.assertIsNone(self._find_sequential(actual, 'Non-existant_Chapter', 'Non-existant_Sequential'))
self.assertIsNone(prev_sequential)
self.assertIsNone(next_sequential)
self.assertIsNotNone(self._find_sequential(actual['chapters'], 'Chapter', 'Open_Sequential'))
self.assertIsNone(self._find_sequential(actual['chapters'], 'Chapter', 'Gated_Sequential'))
self.assertIsNone(self._find_sequential(actual['chapters'], 'Non-existent_Chapter', 'Non-existent_Sequential'))
self.assertIsNone(actual['previous_of_active_section'])
self.assertIsNone(actual['next_of_active_section'])
@attr('shard_1')
......
......@@ -44,7 +44,7 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
cls.section9 = ItemFactory.create(parent=cls.chapter9,
display_name='factory_section')
cls.unit0 = ItemFactory.create(parent=cls.section0,
display_name='New Unit')
display_name='New Unit 0')
cls.chapterchrome = ItemFactory.create(parent=cls.course,
display_name='Chrome')
......@@ -119,6 +119,7 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
'section': displayname,
}))
self.assertEquals('course-tabs' in response.content, tabs)
self.assertEquals('course-navigation' in response.content, accordion)
self.assertTabInactive('progress', response)
self.assertTabActive('courseware', response)
......@@ -165,7 +166,6 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
resp = self.client.get(reverse('courseware',
kwargs={'course_id': self.course.id.to_deprecated_string()}))
self.assertRedirects(resp, reverse(
'courseware_section', kwargs={'course_id': self.course.id.to_deprecated_string(),
'chapter': 'Overview',
......@@ -174,30 +174,26 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
def test_redirects_second_time(self):
"""
Verify the accordion remembers we've already visited the Welcome section
and redirects correpondingly.
and redirects correspondingly.
"""
email, password = self.STUDENT_INFO[0]
self.login(email, password)
self.enroll(self.course, True)
self.enroll(self.test_course, True)
self.client.get(reverse('courseware_section', kwargs={
'course_id': self.course.id.to_deprecated_string(),
'chapter': 'Overview',
'section': 'Welcome',
}))
resp = self.client.get(reverse('courseware',
kwargs={'course_id': self.course.id.to_deprecated_string()}))
redirect_url = reverse(
'courseware_chapter',
section_url = reverse(
'courseware_section',
kwargs={
'course_id': self.course.id.to_deprecated_string(),
'chapter': 'Overview'
}
'chapter': 'Overview',
'section': 'Welcome',
},
)
self.assertRedirects(resp, redirect_url)
self.client.get(section_url)
resp = self.client.get(
reverse('courseware', kwargs={'course_id': self.course.id.to_deprecated_string()}),
)
self.assertRedirects(resp, section_url)
def test_accordion_state(self):
"""
......@@ -209,15 +205,15 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
self.enroll(self.test_course, True)
# Now we directly navigate to a section in a chapter other than 'Overview'.
url = reverse(
section_url = reverse(
'courseware_section',
kwargs={
'course_id': self.course.id.to_deprecated_string(),
'chapter': 'factory_chapter',
'section': 'factory_section'
'section': 'factory_section',
}
)
self.assert_request_status_code(200, url)
self.assert_request_status_code(200, section_url)
# And now hitting the courseware tab should redirect to 'factory_chapter'
url = reverse(
......@@ -225,15 +221,7 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
kwargs={'course_id': self.course.id.to_deprecated_string()}
)
resp = self.client.get(url)
redirect_url = reverse(
'courseware_chapter',
kwargs={
'course_id': self.course.id.to_deprecated_string(),
'chapter': 'factory_chapter',
}
)
self.assertRedirects(resp, redirect_url)
self.assertRedirects(resp, section_url)
def test_incomplete_course(self):
email = self.staff_user.email
......@@ -247,7 +235,8 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
'courseware',
kwargs={'course_id': test_course_id}
)
self.assert_request_status_code(200, url)
response = self.assert_request_status_code(200, url)
self.assertIn("No content has been added to this course", response.content)
section = ItemFactory.create(
parent_location=self.test_course.location,
......@@ -257,21 +246,25 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
'courseware',
kwargs={'course_id': test_course_id}
)
self.assert_request_status_code(200, url)
response = self.assert_request_status_code(200, url)
self.assertNotIn("No content has been added to this course", response.content)
self.assertIn("New Section", response.content)
subsection = ItemFactory.create(
parent_location=section.location,
display_name='New Subsection'
display_name='New Subsection',
)
url = reverse(
'courseware',
kwargs={'course_id': test_course_id}
)
self.assert_request_status_code(200, url)
response = self.assert_request_status_code(200, url)
self.assertIn("New Subsection", response.content)
self.assertNotIn("sequence-nav", response.content)
ItemFactory.create(
parent_location=subsection.location,
display_name='New Unit'
display_name='New Unit',
)
url = reverse(
'courseware',
......
......@@ -34,6 +34,7 @@ from certificates.tests.factories import GeneratedCertificateFactory
from commerce.models import CommerceConfiguration
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from courseware.index import render_accordion, CoursewareIndex
from courseware.model_data import set_score
from courseware.module_render import toc_for_course
from courseware.testutils import RenderXBlockTestMixin
......@@ -255,7 +256,7 @@ class ViewsTestCase(ModuleStoreTestCase):
self._verify_index_response(expected_response_code=404, chapter_name='non-existent')
def test_index_nonexistent_chapter_masquerade(self):
with patch('courseware.views.setup_masquerade') as patch_masquerade:
with patch('courseware.index.setup_masquerade') as patch_masquerade:
masquerade = MagicMock(role='student')
patch_masquerade.return_value = (masquerade, self.user)
self._verify_index_response(expected_response_code=302, chapter_name='non-existent')
......@@ -264,7 +265,7 @@ class ViewsTestCase(ModuleStoreTestCase):
self._verify_index_response(expected_response_code=404, section_name='non-existent')
def test_index_nonexistent_section_masquerade(self):
with patch('courseware.views.setup_masquerade') as patch_masquerade:
with patch('courseware.index.setup_masquerade') as patch_masquerade:
masquerade = MagicMock(role='student')
patch_masquerade.return_value = (masquerade, self.user)
self._verify_index_response(expected_response_code=302, section_name='non-existent')
......@@ -416,14 +417,6 @@ class ViewsTestCase(ModuleStoreTestCase):
get_redirect_url(self.course_key, self.section.location),
)
def test_redirect_to_course_position(self):
mock_module = MagicMock()
mock_module.descriptor.id = 'Underwater Basketweaving'
mock_module.position = 3
mock_module.get_display_items.return_value = []
self.assertRaises(Http404, views.redirect_to_course_position,
mock_module, views.CONTENT_DEPTH)
def test_invalid_course_id(self):
response = self.client.get('/courses/MITx/3.091X/')
self.assertEqual(response.status_code, 404)
......@@ -462,15 +455,6 @@ class ViewsTestCase(ModuleStoreTestCase):
response = self.client.get(request_url)
self.assertEqual(response.status_code, 404)
def test_registered_for_course(self):
self.assertFalse(views.registered_for_course('Basketweaving', None))
mock_user = MagicMock()
mock_user.is_authenticated.return_value = False
self.assertFalse(views.registered_for_course('dummy', mock_user))
mock_course = MagicMock()
mock_course.id = self.course_key
self.assertTrue(views.registered_for_course(mock_course, self.user))
@override_settings(PAID_COURSE_REGISTRATION_CURRENCY=["USD", "$"])
def test_get_cosmetic_display_price(self):
"""
......@@ -917,10 +901,10 @@ class TestAccordionDueDate(BaseDueDateTests):
def get_text(self, course):
""" Returns the HTML for the accordion """
table_of_contents, __, __ = toc_for_course(
table_of_contents = toc_for_course(
self.request.user, self.request, course, unicode(course.get_children()[0].scope_ids.usage_id), None, None
)
return views.render_accordion(self.request, course, table_of_contents)
return render_accordion(self.request, course, table_of_contents['chapters'])
@attr('shard_1')
......@@ -1497,7 +1481,9 @@ class TestIndexView(ModuleStoreTestCase):
mako_middleware_process_request(request)
# Trigger the assertions embedded in the ViewCheckerBlocks
response = views.index(request, unicode(course.id), chapter=chapter.url_name, section=section.url_name)
response = CoursewareIndex.as_view()(
request, unicode(course.id), chapter=chapter.url_name, section=section.url_name
)
self.assertEquals(response.content.count("ViewCheckerPassed"), 3)
@XBlock.register_temp_plugin(ActivateIDCheckerBlock, 'id_checker')
......@@ -1525,7 +1511,9 @@ class TestIndexView(ModuleStoreTestCase):
request.user = user
mako_middleware_process_request(request)
response = views.index(request, unicode(course.id), chapter=chapter.url_name, section=section.url_name)
response = CoursewareIndex.as_view()(
request, unicode(course.id), chapter=chapter.url_name, section=section.url_name
)
self.assertIn("Activate Block ID: test_block_id", response.content)
......@@ -1546,7 +1534,9 @@ class TestIndexViewWithGating(ModuleStoreTestCase, MilestonesTestCaseMixin):
self.store.update_item(self.course, 0)
self.chapter = ItemFactory.create(parent=self.course, category="chapter", display_name="Chapter")
self.open_seq = ItemFactory.create(parent=self.chapter, category='sequential', display_name="Open Sequential")
ItemFactory.create(parent=self.open_seq, category='problem', display_name="Problem 1")
self.gated_seq = ItemFactory.create(parent=self.chapter, category='sequential', display_name="Gated Sequential")
ItemFactory.create(parent=self.gated_seq, category='problem', display_name="Problem 2")
gating_api.add_prerequisite(self.course.id, self.open_seq.location)
gating_api.set_required_content(self.course.id, self.gated_seq.location, self.open_seq.location, 100)
......@@ -1570,7 +1560,7 @@ class TestIndexViewWithGating(ModuleStoreTestCase, MilestonesTestCaseMixin):
mako_middleware_process_request(request)
with self.assertRaises(Http404):
__ = views.index(
CoursewareIndex.as_view()(
request,
unicode(self.course.id),
chapter=self.chapter.url_name,
......
......@@ -15,7 +15,8 @@ from opaque_keys import InvalidKeyError
from courseware.access import is_mobile_available_for_user
from courseware.model_data import FieldDataCache
from courseware.module_render import get_module_for_descriptor
from courseware.views import get_current_child, save_positions_recursively_up
from courseware.index import save_positions_recursively_up
from courseware.views import get_current_child
from student.models import CourseEnrollment, User
from xblock.fields import Scope
......
......@@ -12,6 +12,7 @@ from microsite_configuration import microsite
import auth_exchange.views
from config_models.views import ConfigurationModelCurrentAPIView
from courseware.index import CoursewareIndex
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
......@@ -462,28 +463,28 @@ urlpatterns += (
r'^courses/{}/courseware/?$'.format(
settings.COURSE_ID_PATTERN,
),
'courseware.views.index',
CoursewareIndex.as_view(),
name='courseware',
),
url(
r'^courses/{}/courseware/(?P<chapter>[^/]*)/$'.format(
settings.COURSE_ID_PATTERN,
),
'courseware.views.index',
CoursewareIndex.as_view(),
name='courseware_chapter',
),
url(
r'^courses/{}/courseware/(?P<chapter>[^/]*)/(?P<section>[^/]*)/$'.format(
settings.COURSE_ID_PATTERN,
),
'courseware.views.index',
CoursewareIndex.as_view(),
name='courseware_section',
),
url(
r'^courses/{}/courseware/(?P<chapter>[^/]*)/(?P<section>[^/]*)/(?P<position>[^/]*)/?$'.format(
settings.COURSE_ID_PATTERN,
),
'courseware.views.index',
CoursewareIndex.as_view(),
name='courseware_position',
),
......
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