Commit 0bd2286e by Johnny Brown

show a friendly message for an empty course rather than an error page

parent 85f1997f
...@@ -151,3 +151,4 @@ Steven Burch <stv@stanford.edu> ...@@ -151,3 +151,4 @@ Steven Burch <stv@stanford.edu>
Waqas Khalid <wkhalid@edx.org> Waqas Khalid <wkhalid@edx.org>
Muhammad Ammar <mammar@edx.org> Muhammad Ammar <mammar@edx.org>
Abdallah Nassif <abdoosh00@gmail.com> Abdallah Nassif <abdoosh00@gmail.com>
Johnny Brown <johnnybrown7@gmail.com>
...@@ -506,6 +506,42 @@ class SplitModuleTest(unittest.TestCase): ...@@ -506,6 +506,42 @@ class SplitModuleTest(unittest.TestCase):
return element return element
class TestHasChildrenAtDepth(SplitModuleTest):
"""Test the has_children_at_depth method of XModuleMixin. """
def test_has_children_at_depth(self):
course_locator = CourseLocator(
org='testx', offering='GreekHero', branch='draft'
)
block_locator = BlockUsageLocator(
course_locator, 'course', 'head12345'
)
block = modulestore().get_item(block_locator)
self.assertRaises(
ValueError, block.has_children_at_depth, -1,
)
self.assertTrue(block.has_children_at_depth(0))
self.assertTrue(block.has_children_at_depth(1))
self.assertFalse(block.has_children_at_depth(2))
ch1 = modulestore().get_item(
BlockUsageLocator(course_locator, 'chapter', block_id='chapter1')
)
self.assertFalse(ch1.has_children_at_depth(0))
ch2 = modulestore().get_item(
BlockUsageLocator(course_locator, 'chapter', block_id='chapter2')
)
self.assertFalse(ch2.has_children_at_depth(0))
ch3 = modulestore().get_item(
BlockUsageLocator(course_locator, 'chapter', block_id='chapter3')
)
self.assertTrue(ch3.has_children_at_depth(0))
self.assertFalse(ch3.has_children_at_depth(1))
class SplitModuleCourseTests(SplitModuleTest): class SplitModuleCourseTests(SplitModuleTest):
''' '''
Course CRUD operation tests Course CRUD operation tests
......
...@@ -209,6 +209,29 @@ class XModuleMixin(XBlockMixin): ...@@ -209,6 +209,29 @@ class XModuleMixin(XBlockMixin):
result[field.name] = field.read_json(self) result[field.name] = field.read_json(self)
return result return result
def has_children_at_depth(self, depth):
"""
Returns true if self has children at the given depth. depth==0 returns
false if self is a leaf, true otherwise.
SELF
|
[child at depth 0]
/ \
[depth 1] [depth 1]
/ \
[depth 2] [depth 2]
So the example above would return True for `has_children_at_depth(2)`, and False
for depth > 2
"""
if depth < 0:
raise ValueError("negative depth argument is invalid")
elif depth == 0:
return bool(self.get_children())
else:
return any(child.has_children_at_depth(depth - 1) for child in self.get_children())
def get_content_titles(self): def get_content_titles(self):
""" """
Returns list of content titles for all of self's children. Returns list of content titles for all of self's children.
......
...@@ -13,6 +13,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase ...@@ -13,6 +13,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from courseware.tests.helpers import LoginEnrollmentTestCase, check_for_get_code from courseware.tests.helpers import LoginEnrollmentTestCase, check_for_get_code
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from courseware.tests.factories import GlobalStaffFactory
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
...@@ -35,6 +36,8 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -35,6 +36,8 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase):
display_name='Welcome') display_name='Welcome')
self.section9 = ItemFactory.create(parent_location=self.chapter9.location, self.section9 = ItemFactory.create(parent_location=self.chapter9.location,
display_name='factory_section') display_name='factory_section')
self.unit0 = ItemFactory.create(parent_location=self.section0.location,
display_name='New Unit')
# Create student accounts and activate them. # Create student accounts and activate them.
for i in range(len(self.STUDENT_INFO)): for i in range(len(self.STUDENT_INFO)):
...@@ -43,6 +46,8 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -43,6 +46,8 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.create_account(username, email, password) self.create_account(username, email, password)
self.activate_user(email) self.activate_user(email)
self.staff_user = GlobalStaffFactory()
@override_settings(SESSION_INACTIVITY_TIMEOUT_IN_SECONDS=1) @override_settings(SESSION_INACTIVITY_TIMEOUT_IN_SECONDS=1)
def test_inactive_session_timeout(self): def test_inactive_session_timeout(self):
""" """
...@@ -135,3 +140,55 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -135,3 +140,55 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.assertRedirects(resp, reverse('courseware_chapter', self.assertRedirects(resp, reverse('courseware_chapter',
kwargs={'course_id': self.course.id.to_deprecated_string(), kwargs={'course_id': self.course.id.to_deprecated_string(),
'chapter': 'factory_chapter'})) 'chapter': 'factory_chapter'}))
def test_incomplete_course(self):
email = self.staff_user.email
password = "test"
self.login(email, password)
self.enroll(self.test_course, True)
test_course_id = self.test_course.id.to_deprecated_string()
check_for_get_code(
self, 200,
reverse(
'courseware',
kwargs={'course_id': test_course_id}
)
)
section = ItemFactory.create(
parent_location=self.test_course.location,
display_name='New Section'
)
check_for_get_code(
self, 200,
reverse(
'courseware',
kwargs={'course_id': test_course_id}
)
)
subsection = ItemFactory.create(
parent_location=section.location,
display_name='New Subsection'
)
check_for_get_code(
self, 200,
reverse(
'courseware',
kwargs={'course_id': test_course_id}
)
)
ItemFactory.create(
parent_location=subsection.location,
display_name='New Unit'
)
check_for_get_code(
self, 302,
reverse(
'courseware',
kwargs={'course_id': test_course_id}
)
)
...@@ -112,23 +112,39 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -112,23 +112,39 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
def setUp(self): def setUp(self):
self.course = CourseFactory.create(number='999', display_name='Robot_Super_Course') self.course = CourseFactory.create(number='999', display_name='Robot_Super_Course')
self.overview_chapter = ItemFactory.create(display_name='Overview')
self.courseware_chapter = ItemFactory.create(display_name='courseware') self.courseware_chapter = ItemFactory.create(display_name='courseware')
self.overview_chapter = ItemFactory.create(
parent_location=self.course.location,
display_name='Super Overview'
)
self.welcome_section = ItemFactory.create(
parent_location=self.overview_chapter.location,
display_name='Super Welcome'
)
self.welcome_unit = ItemFactory.create(
parent_location=self.welcome_section.location,
display_name='Super Unit'
)
self.course = modulestore().get_course(self.course.id) self.course = modulestore().get_course(self.course.id)
self.test_course = CourseFactory.create(number='666', display_name='Robot_Sub_Course') self.test_course = CourseFactory.create(number='666', display_name='Robot_Sub_Course')
self.other_org_course = CourseFactory.create(org='Other_Org_Course') self.other_org_course = CourseFactory.create(org='Other_Org_Course')
self.sub_courseware_chapter = ItemFactory.create( self.sub_courseware_chapter = ItemFactory.create(
parent_location=self.test_course.location, display_name='courseware' parent_location=self.test_course.location,
display_name='courseware'
) )
self.sub_overview_chapter = ItemFactory.create( self.sub_overview_chapter = ItemFactory.create(
parent_location=self.sub_courseware_chapter.location, parent_location=self.sub_courseware_chapter.location,
display_name='Overview' display_name='Overview'
) )
self.welcome_section = ItemFactory.create( self.sub_welcome_section = ItemFactory.create(
parent_location=self.overview_chapter.location, parent_location=self.sub_overview_chapter.location,
display_name='Welcome' display_name='Welcome'
) )
self.sub_welcome_unit = ItemFactory.create(
parent_location=self.sub_welcome_section.location,
display_name='New Unit'
)
self.test_course = modulestore().get_course(self.test_course.id) self.test_course = modulestore().get_course(self.test_course.id)
self.global_staff_user = GlobalStaffFactory() self.global_staff_user = GlobalStaffFactory()
...@@ -151,9 +167,13 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -151,9 +167,13 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.login(self.unenrolled_user) self.login(self.unenrolled_user)
response = self.client.get(reverse('courseware', response = self.client.get(reverse('courseware',
kwargs={'course_id': self.course.id.to_deprecated_string()})) kwargs={'course_id': self.course.id.to_deprecated_string()}))
self.assertRedirects(response, self.assertRedirects(
reverse('about_course', response,
args=[self.course.id.to_deprecated_string()])) reverse(
'about_course',
args=[self.course.id.to_deprecated_string()]
)
)
def test_redirection_enrolled(self): def test_redirection_enrolled(self):
""" """
...@@ -162,14 +182,22 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -162,14 +182,22 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
""" """
self.login(self.enrolled_user) self.login(self.enrolled_user)
response = self.client.get(reverse('courseware', response = self.client.get(
kwargs={'course_id': self.course.id.to_deprecated_string()})) reverse(
'courseware',
kwargs={'course_id': self.course.id.to_deprecated_string()}
)
)
self.assertRedirects(response, self.assertRedirects(
reverse('courseware_section', response,
reverse(
'courseware_section',
kwargs={'course_id': self.course.id.to_deprecated_string(), kwargs={'course_id': self.course.id.to_deprecated_string(),
'chapter': 'Overview', 'chapter': self.overview_chapter.url_name,
'section': 'Welcome'})) 'section': self.welcome_section.url_name}
)
)
def test_instructor_page_access_nonstaff(self): def test_instructor_page_access_nonstaff(self):
""" """
...@@ -314,6 +342,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -314,6 +342,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
""" """
now = datetime.datetime.now(pytz.UTC) now = datetime.datetime.now(pytz.UTC)
tomorrow = now + datetime.timedelta(days=1) tomorrow = now + datetime.timedelta(days=1)
self.course.start = tomorrow self.course.start = tomorrow
self.test_course.start = tomorrow self.test_course.start = tomorrow
self.course = self.update_course(self.course) self.course = self.update_course(self.course)
......
...@@ -145,7 +145,7 @@ class ViewsTestCase(TestCase): ...@@ -145,7 +145,7 @@ class ViewsTestCase(TestCase):
mock_module.position = 3 mock_module.position = 3
mock_module.get_display_items.return_value = [] mock_module.get_display_items.return_value = []
self.assertRaises(Http404, views.redirect_to_course_position, self.assertRaises(Http404, views.redirect_to_course_position,
mock_module) mock_module, views.CONTENT_DEPTH)
def test_registered_for_course(self): def test_registered_for_course(self):
self.assertFalse(views.registered_for_course('Basketweaving', None)) self.assertFalse(views.registered_for_course('Basketweaving', None))
......
...@@ -52,6 +52,7 @@ log = logging.getLogger("edx.courseware") ...@@ -52,6 +52,7 @@ log = logging.getLogger("edx.courseware")
template_imports = {'urllib': urllib} template_imports = {'urllib': urllib}
CONTENT_DEPTH = 2
def user_groups(user): def user_groups(user):
""" """
...@@ -113,17 +114,36 @@ def render_accordion(request, course, chapter, section, field_data_cache): ...@@ -113,17 +114,36 @@ def render_accordion(request, course, chapter, section, field_data_cache):
return render_to_string('courseware/accordion.html', context) return render_to_string('courseware/accordion.html', context)
def get_current_child(xmodule): def get_current_child(xmodule, min_depth=None):
""" """
Get the xmodule.position's display item of an xmodule that has a position and Get the xmodule.position's display item of an xmodule that has a position and
children. If xmodule has no position or is out of bounds, return the first child. children. If xmodule has no position or is out of bounds, return the first
child with children extending down to content_depth.
For example, if chapter_one has no position set, with two child sections,
section-A having no children and section-B having a discussion unit,
`get_current_child(chapter, min_depth=1)` will return section-B.
Returns None only if there are no children at all. Returns None only if there are no children at all.
""" """
def _get_default_child_module(child_modules):
"""Returns the first child of xmodule, subject to min_depth."""
if not child_modules:
default_child = None
elif not min_depth > 0:
default_child = child_modules[0]
else:
content_children = [child for child in child_modules if
child.has_children_at_depth(min_depth - 1)]
default_child = content_children[0] if content_children else None
return default_child
if not hasattr(xmodule, 'position'): if not hasattr(xmodule, 'position'):
return None return None
if xmodule.position is None: if xmodule.position is None:
pos = 0 return _get_default_child_module(xmodule.get_display_items())
else: else:
# position is 1-indexed. # position is 1-indexed.
pos = xmodule.position - 1 pos = xmodule.position - 1
...@@ -132,14 +152,15 @@ def get_current_child(xmodule): ...@@ -132,14 +152,15 @@ def get_current_child(xmodule):
if 0 <= pos < len(children): if 0 <= pos < len(children):
child = children[pos] child = children[pos]
elif len(children) > 0: elif len(children) > 0:
# Something is wrong. Default to first child # module has a set position, but the position is out of range.
child = children[0] # return default child.
child = _get_default_child_module(children)
else: else:
child = None child = None
return child return child
def redirect_to_course_position(course_module): def redirect_to_course_position(course_module, content_depth):
""" """
Return a redirect to the user's current place in the course. Return a redirect to the user's current place in the course.
...@@ -153,7 +174,7 @@ def redirect_to_course_position(course_module): ...@@ -153,7 +174,7 @@ def redirect_to_course_position(course_module):
""" """
urlargs = {'course_id': course_module.id.to_deprecated_string()} urlargs = {'course_id': course_module.id.to_deprecated_string()}
chapter = get_current_child(course_module) chapter = get_current_child(course_module, min_depth=content_depth)
if chapter is None: if chapter is None:
# oops. Something bad has happened. # oops. Something bad has happened.
raise Http404("No chapter found when loading current position in course") raise Http404("No chapter found when loading current position in course")
...@@ -163,7 +184,7 @@ def redirect_to_course_position(course_module): ...@@ -163,7 +184,7 @@ def redirect_to_course_position(course_module):
return redirect(reverse('courseware_chapter', kwargs=urlargs)) return redirect(reverse('courseware_chapter', kwargs=urlargs))
# Relying on default of returning first child # Relying on default of returning first child
section = get_current_child(chapter) section = get_current_child(chapter, min_depth=content_depth - 1)
if section is None: if section is None:
raise Http404("No section found when loading current position in course") raise Http404("No section found when loading current position in course")
...@@ -266,9 +287,6 @@ def index(request, course_id, chapter=None, section=None, ...@@ -266,9 +287,6 @@ def index(request, course_id, chapter=None, section=None,
studio_url = get_studio_url(course_key, 'course') studio_url = get_studio_url(course_key, 'course')
if chapter is None:
return redirect_to_course_position(course_module)
context = { context = {
'csrf': csrf(request)['csrf_token'], 'csrf': csrf(request)['csrf_token'],
'accordion': render_accordion(request, course, chapter, section, field_data_cache), 'accordion': render_accordion(request, course, chapter, section, field_data_cache),
...@@ -283,6 +301,15 @@ def index(request, course_id, chapter=None, section=None, ...@@ -283,6 +301,15 @@ def index(request, course_id, chapter=None, section=None,
'reverifications': fetch_reverify_banner_info(request, course_key), 'reverifications': fetch_reverify_banner_info(request, course_key),
} }
has_content = course.has_children_at_depth(CONTENT_DEPTH)
if not has_content:
# Show empty courseware for a course with no units
return render_to_response('courseware/courseware.html', context)
elif chapter is None:
# passing CONTENT_DEPTH avoids returning 404 for a course with an
# empty first section and a second section with content
return redirect_to_course_position(course_module, CONTENT_DEPTH)
# Only show the chat if it's enabled by the course and in the # Only show the chat if it's enabled by the course and in the
# settings. # settings.
show_chat = course.show_chat and settings.FEATURES['ENABLE_CHAT'] show_chat = course.show_chat and settings.FEATURES['ENABLE_CHAT']
......
...@@ -195,7 +195,11 @@ ${fragment.foot_html()} ...@@ -195,7 +195,11 @@ ${fragment.foot_html()}
<div id="accordion" style="display: none"> <div id="accordion" style="display: none">
<nav aria-label="${_('Course Navigation')}"> <nav aria-label="${_('Course Navigation')}">
% if accordion.strip():
${accordion} ${accordion}
% else:
<div class="chapter">${_("No content has been added to this course")}</div>
% endif
</nav> </nav>
</div> </div>
</div> </div>
......
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