Cached values were leaking across tests, causing difficult to debug errors, particularly when using Config Models. As part of this work, certain tests that had query counts that relied on those values being cached needed to be adjusted up.
689 lines
24 KiB
Python
689 lines
24 KiB
Python
"""
|
|
Run these tests @ Devstack:
|
|
paver test_system -s lms --fasttest --verbose --test_id=lms/djangoapps/course_structure_api
|
|
"""
|
|
# pylint: disable=missing-docstring,invalid-name,maybe-no-member,attribute-defined-outside-init
|
|
from abc import ABCMeta
|
|
from datetime import datetime
|
|
from mock import patch, Mock
|
|
from itertools import product
|
|
|
|
from django.core.urlresolvers import reverse
|
|
|
|
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
|
|
from oauth2_provider.tests.factories import AccessTokenFactory, ClientFactory
|
|
from opaque_keys.edx.locator import CourseLocator
|
|
from xmodule.error_module import ErrorDescriptor
|
|
from xmodule.modulestore import ModuleStoreEnum
|
|
from xmodule.modulestore.django import modulestore
|
|
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
|
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls
|
|
from xmodule.modulestore.xml import CourseLocationManager
|
|
from xmodule.tests import get_test_system
|
|
|
|
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
|
from courseware.tests.factories import GlobalStaffFactory, StaffFactory
|
|
from openedx.core.djangoapps.content.course_structures.models import CourseStructure
|
|
from openedx.core.djangoapps.content.course_structures.tasks import update_course_structure
|
|
|
|
|
|
TEST_SERVER_HOST = 'http://testserver'
|
|
|
|
|
|
class CourseViewTestsMixin(object):
|
|
"""
|
|
Mixin for course view tests.
|
|
"""
|
|
view = None
|
|
|
|
raw_grader = [
|
|
{
|
|
"min_count": 24,
|
|
"weight": 0.2,
|
|
"type": "Homework",
|
|
"drop_count": 0,
|
|
"short_label": "HW"
|
|
},
|
|
{
|
|
"min_count": 4,
|
|
"weight": 0.8,
|
|
"type": "Exam",
|
|
"drop_count": 0,
|
|
"short_label": "Exam"
|
|
}
|
|
]
|
|
|
|
def setUp(self):
|
|
super(CourseViewTestsMixin, self).setUp()
|
|
self.create_user_and_access_token()
|
|
|
|
def create_user(self):
|
|
self.user = GlobalStaffFactory.create()
|
|
|
|
def create_user_and_access_token(self):
|
|
self.create_user()
|
|
self.oauth_client = ClientFactory.create()
|
|
self.access_token = AccessTokenFactory.create(user=self.user, client=self.oauth_client).token
|
|
|
|
@classmethod
|
|
def create_course_data(cls):
|
|
cls.invalid_course_id = 'foo/bar/baz'
|
|
cls.course = CourseFactory.create(display_name='An Introduction to API Testing', raw_grader=cls.raw_grader)
|
|
cls.course_id = unicode(cls.course.id)
|
|
with cls.store.bulk_operations(cls.course.id, emit_signals=False):
|
|
cls.sequential = ItemFactory.create(
|
|
category="sequential",
|
|
parent_location=cls.course.location,
|
|
display_name="Lesson 1",
|
|
format="Homework",
|
|
graded=True
|
|
)
|
|
|
|
factory = MultipleChoiceResponseXMLFactory()
|
|
args = {'choices': [False, True, False]}
|
|
problem_xml = factory.build_xml(**args)
|
|
cls.problem = ItemFactory.create(
|
|
category="problem",
|
|
parent_location=cls.sequential.location,
|
|
display_name="Problem 1",
|
|
format="Homework",
|
|
data=problem_xml,
|
|
)
|
|
|
|
cls.video = ItemFactory.create(
|
|
category="video",
|
|
parent_location=cls.sequential.location,
|
|
display_name="Video 1",
|
|
)
|
|
|
|
cls.html = ItemFactory.create(
|
|
category="html",
|
|
parent_location=cls.sequential.location,
|
|
display_name="HTML 1",
|
|
)
|
|
|
|
cls.empty_course = CourseFactory.create(
|
|
start=datetime(2014, 6, 16, 14, 30),
|
|
end=datetime(2015, 1, 16),
|
|
org="MTD",
|
|
# Use mongo so that we can get a test with a SlashSeparatedCourseKey
|
|
default_store=ModuleStoreEnum.Type.mongo
|
|
)
|
|
|
|
def build_absolute_url(self, path=None):
|
|
""" Build absolute URL pointing to test server.
|
|
:param path: Path to append to the URL
|
|
"""
|
|
url = TEST_SERVER_HOST
|
|
|
|
if path:
|
|
url += path
|
|
|
|
return url
|
|
|
|
def assertValidResponseCourse(self, data, course):
|
|
""" Determines if the given response data (dict) matches the specified course. """
|
|
|
|
course_key = course.id
|
|
self.assertEqual(data['id'], unicode(course_key))
|
|
self.assertEqual(data['name'], course.display_name)
|
|
self.assertEqual(data['course'], course_key.course)
|
|
self.assertEqual(data['org'], course_key.org)
|
|
self.assertEqual(data['run'], course_key.run)
|
|
|
|
uri = self.build_absolute_url(
|
|
reverse('course_structure_api:v0:detail', kwargs={'course_id': unicode(course_key)}))
|
|
self.assertEqual(data['uri'], uri)
|
|
|
|
def http_get(self, uri, **headers):
|
|
"""Submit an HTTP GET request"""
|
|
|
|
default_headers = {
|
|
'HTTP_AUTHORIZATION': 'Bearer ' + self.access_token
|
|
}
|
|
default_headers.update(headers)
|
|
|
|
response = self.client.get(uri, follow=True, **default_headers)
|
|
return response
|
|
|
|
def http_get_for_course(self, course_id=None, **headers):
|
|
"""Submit an HTTP GET request to the view for the given course"""
|
|
|
|
return self.http_get(
|
|
reverse(self.view, kwargs={'course_id': course_id or self.course_id}),
|
|
**headers
|
|
)
|
|
|
|
def test_not_authenticated(self):
|
|
"""
|
|
Verify that access is denied to non-authenticated users.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def test_not_authorized(self):
|
|
"""
|
|
Verify that access is denied to non-authorized users.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
|
|
class CourseDetailTestMixin(object):
|
|
"""
|
|
Mixin for views utilizing only the course_id kwarg.
|
|
"""
|
|
view_supports_debug_mode = True
|
|
|
|
def test_get_invalid_course(self):
|
|
"""
|
|
The view should return a 404 if the course ID is invalid.
|
|
"""
|
|
response = self.http_get_for_course(self.invalid_course_id)
|
|
self.assertEqual(response.status_code, 404)
|
|
|
|
def test_get(self):
|
|
"""
|
|
The view should return a 200 if the course ID is valid.
|
|
"""
|
|
response = self.http_get_for_course()
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
# Return the response so child classes do not have to repeat the request.
|
|
return response
|
|
|
|
def test_not_authenticated(self):
|
|
""" The view should return HTTP status 401 if no user is authenticated. """
|
|
# HTTP 401 should be returned if the user is not authenticated.
|
|
response = self.http_get_for_course(HTTP_AUTHORIZATION=None)
|
|
self.assertEqual(response.status_code, 401)
|
|
|
|
def test_not_authorized(self):
|
|
user = StaffFactory(course_key=self.course.id)
|
|
access_token = AccessTokenFactory.create(user=user, client=self.oauth_client).token
|
|
auth_header = 'Bearer ' + access_token
|
|
|
|
# Access should be granted if the proper access token is supplied.
|
|
response = self.http_get_for_course(HTTP_AUTHORIZATION=auth_header)
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
# Access should be denied if the user is not course staff.
|
|
response = self.http_get_for_course(course_id=unicode(self.empty_course.id), HTTP_AUTHORIZATION=auth_header)
|
|
self.assertEqual(response.status_code, 404)
|
|
|
|
|
|
class CourseListTests(CourseViewTestsMixin, SharedModuleStoreTestCase):
|
|
view = 'course_structure_api:v0:list'
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(CourseListTests, cls).setUpClass()
|
|
cls.create_course_data()
|
|
|
|
def test_get(self):
|
|
"""
|
|
The view should return a list of all courses.
|
|
"""
|
|
response = self.http_get(reverse(self.view))
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.data
|
|
courses = data['results']
|
|
self.assertEqual(len(courses), 2)
|
|
self.assertEqual(data['count'], 2)
|
|
self.assertEqual(data['num_pages'], 1)
|
|
|
|
self.assertValidResponseCourse(courses[0], self.empty_course)
|
|
self.assertValidResponseCourse(courses[1], self.course)
|
|
|
|
def test_get_with_pagination(self):
|
|
"""
|
|
The view should return a paginated list of courses.
|
|
"""
|
|
url = "{}?page_size=1".format(reverse(self.view))
|
|
response = self.http_get(url)
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
courses = response.data['results']
|
|
self.assertEqual(len(courses), 1)
|
|
self.assertValidResponseCourse(courses[0], self.empty_course)
|
|
|
|
def test_get_filtering(self):
|
|
"""
|
|
The view should return a list of details for the specified courses.
|
|
"""
|
|
url = "{}?course_id={}".format(reverse(self.view), self.course_id)
|
|
response = self.http_get(url)
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
courses = response.data['results']
|
|
self.assertEqual(len(courses), 1)
|
|
self.assertValidResponseCourse(courses[0], self.course)
|
|
|
|
def test_not_authenticated(self):
|
|
response = self.http_get(reverse(self.view), HTTP_AUTHORIZATION=None)
|
|
self.assertEqual(response.status_code, 401)
|
|
|
|
def test_not_authorized(self):
|
|
"""
|
|
Unauthorized users should get an empty list.
|
|
"""
|
|
user = StaffFactory(course_key=self.course.id)
|
|
access_token = AccessTokenFactory.create(user=user, client=self.oauth_client).token
|
|
auth_header = 'Bearer ' + access_token
|
|
|
|
# Data should be returned if the user is authorized.
|
|
response = self.http_get(reverse(self.view), HTTP_AUTHORIZATION=auth_header)
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
url = "{}?course_id={}".format(reverse(self.view), self.course_id)
|
|
response = self.http_get(url, HTTP_AUTHORIZATION=auth_header)
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.data['results']
|
|
self.assertEqual(len(data), 1)
|
|
self.assertEqual(data[0]['name'], self.course.display_name)
|
|
|
|
# The view should return an empty list if the user cannot access any courses.
|
|
url = "{}?course_id={}".format(reverse(self.view), unicode(self.empty_course.id))
|
|
response = self.http_get(url, HTTP_AUTHORIZATION=auth_header)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertDictContainsSubset({'count': 0, u'results': []}, response.data)
|
|
|
|
def test_course_error(self):
|
|
"""
|
|
Ensure the view still returns results even if get_courses() returns an ErrorDescriptor. The ErrorDescriptor
|
|
should be filtered out.
|
|
"""
|
|
|
|
error_descriptor = ErrorDescriptor.from_xml(
|
|
'<course></course>',
|
|
get_test_system(),
|
|
CourseLocationManager(CourseLocator(org='org', course='course', run='run')),
|
|
None
|
|
)
|
|
|
|
descriptors = [error_descriptor, self.empty_course, self.course]
|
|
|
|
with patch('xmodule.modulestore.mixed.MixedModuleStore.get_courses', Mock(return_value=descriptors)):
|
|
self.test_get()
|
|
|
|
|
|
class CourseDetailTests(CourseDetailTestMixin, CourseViewTestsMixin, SharedModuleStoreTestCase):
|
|
view = 'course_structure_api:v0:detail'
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(CourseDetailTests, cls).setUpClass()
|
|
cls.create_course_data()
|
|
|
|
def test_get(self):
|
|
response = super(CourseDetailTests, self).test_get()
|
|
self.assertValidResponseCourse(response.data, self.course)
|
|
|
|
|
|
class CourseStructureTests(CourseDetailTestMixin, CourseViewTestsMixin, SharedModuleStoreTestCase):
|
|
view = 'course_structure_api:v0:structure'
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(CourseStructureTests, cls).setUpClass()
|
|
cls.create_course_data()
|
|
|
|
def setUp(self):
|
|
super(CourseStructureTests, self).setUp()
|
|
|
|
# Ensure course structure exists for the course
|
|
update_course_structure(unicode(self.course.id))
|
|
|
|
def test_get(self):
|
|
"""
|
|
If the course structure exists in the database, the view should return the data. Otherwise, the view should
|
|
initiate an asynchronous course structure generation and return a 503.
|
|
"""
|
|
|
|
# Attempt to retrieve data for a course without stored structure
|
|
CourseStructure.objects.all().delete()
|
|
self.assertFalse(CourseStructure.objects.filter(course_id=self.course.id).exists())
|
|
response = self.http_get_for_course()
|
|
self.assertEqual(response.status_code, 503)
|
|
self.assertEqual(response['Retry-After'], '120')
|
|
|
|
# Course structure generation shouldn't take long. Generate the data and try again.
|
|
self.assertTrue(CourseStructure.objects.filter(course_id=self.course.id).exists())
|
|
response = self.http_get_for_course()
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
blocks = {}
|
|
|
|
def add_block(xblock):
|
|
children = xblock.get_children()
|
|
blocks[unicode(xblock.location)] = {
|
|
u'id': unicode(xblock.location),
|
|
u'type': xblock.category,
|
|
u'parent': None,
|
|
u'display_name': xblock.display_name,
|
|
u'format': xblock.format,
|
|
u'graded': xblock.graded,
|
|
u'children': [unicode(child.location) for child in children]
|
|
}
|
|
|
|
for child in children:
|
|
add_block(child)
|
|
|
|
course = self.store.get_course(self.course.id, depth=None)
|
|
add_block(course)
|
|
|
|
expected = {
|
|
u'root': unicode(self.course.location),
|
|
u'blocks': blocks
|
|
}
|
|
|
|
self.maxDiff = None
|
|
self.assertDictEqual(response.data, expected)
|
|
|
|
|
|
class CourseGradingPolicyTests(CourseDetailTestMixin, CourseViewTestsMixin, SharedModuleStoreTestCase):
|
|
view = 'course_structure_api:v0:grading_policy'
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(CourseGradingPolicyTests, cls).setUpClass()
|
|
cls.create_course_data()
|
|
|
|
def test_get(self):
|
|
"""
|
|
The view should return grading policy for a course.
|
|
"""
|
|
response = super(CourseGradingPolicyTests, self).test_get()
|
|
|
|
expected = [
|
|
{
|
|
"count": 24,
|
|
"weight": 0.2,
|
|
"assignment_type": "Homework",
|
|
"dropped": 0
|
|
},
|
|
{
|
|
"count": 4,
|
|
"weight": 0.8,
|
|
"assignment_type": "Exam",
|
|
"dropped": 0
|
|
}
|
|
]
|
|
self.assertListEqual(response.data, expected)
|
|
|
|
|
|
class CourseGradingPolicyMissingFieldsTests(CourseDetailTestMixin, CourseViewTestsMixin, SharedModuleStoreTestCase):
|
|
view = 'course_structure_api:v0:grading_policy'
|
|
|
|
# Update the raw grader to have missing keys
|
|
raw_grader = [
|
|
{
|
|
"min_count": 24,
|
|
"weight": 0.2,
|
|
"type": "Homework",
|
|
"drop_count": 0,
|
|
"short_label": "HW"
|
|
},
|
|
{
|
|
# Deleted "min_count" key
|
|
"weight": 0.8,
|
|
"type": "Exam",
|
|
"drop_count": 0,
|
|
"short_label": "Exam"
|
|
}
|
|
]
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(CourseGradingPolicyMissingFieldsTests, cls).setUpClass()
|
|
cls.create_course_data()
|
|
|
|
def test_get(self):
|
|
"""
|
|
The view should return grading policy for a course.
|
|
"""
|
|
response = super(CourseGradingPolicyMissingFieldsTests, self).test_get()
|
|
|
|
expected = [
|
|
{
|
|
"count": 24,
|
|
"weight": 0.2,
|
|
"assignment_type": "Homework",
|
|
"dropped": 0
|
|
},
|
|
{
|
|
"count": None,
|
|
"weight": 0.8,
|
|
"assignment_type": "Exam",
|
|
"dropped": 0
|
|
}
|
|
]
|
|
self.assertListEqual(response.data, expected)
|
|
|
|
|
|
#####################################################################################
|
|
#
|
|
# The following Mixins/Classes collectively test the CourseBlocksAndNavigation view.
|
|
#
|
|
# The class hierarchy is:
|
|
#
|
|
# -----------------> CourseBlocksOrNavigationTestMixin <--------------
|
|
# | ^ |
|
|
# | | |
|
|
# | CourseNavigationTestMixin | CourseBlocksTestMixin |
|
|
# | ^ ^ | ^ ^ |
|
|
# | | | | | | |
|
|
# | | | | | | |
|
|
# CourseNavigationTests CourseBlocksAndNavigationTests CourseBlocksTests
|
|
#
|
|
#
|
|
# Each Test Mixin is an abstract class that implements tests specific to its
|
|
# corresponding functionality.
|
|
#
|
|
# The concrete Test classes are expected to define the following class fields:
|
|
#
|
|
# block_navigation_view_type - The view's name as it should be passed to the django
|
|
# reverse method.
|
|
# container_fields - A list of fields that are expected to be included in the view's
|
|
# response for all container block types.
|
|
# block_fields - A list of fields that are expected to be included in the view's
|
|
# response for all block types.
|
|
#
|
|
######################################################################################
|
|
|
|
|
|
class CourseBlocksOrNavigationTestMixin(CourseDetailTestMixin, CourseViewTestsMixin):
|
|
"""
|
|
A Mixin class for testing all views related to Course blocks and/or navigation.
|
|
"""
|
|
__metaclass__ = ABCMeta
|
|
|
|
view_supports_debug_mode = False
|
|
|
|
def setUp(self):
|
|
"""
|
|
Override the base `setUp` method to enroll the user in the course, since these views
|
|
require enrollment for non-staff users.
|
|
"""
|
|
super(CourseBlocksOrNavigationTestMixin, self).setUp()
|
|
CourseEnrollmentFactory(user=self.user, course_id=self.course.id)
|
|
|
|
def create_user(self):
|
|
"""
|
|
Override the base `create_user` method to test with non-staff users for these views.
|
|
"""
|
|
self.user = UserFactory.create()
|
|
|
|
@property
|
|
def view(self):
|
|
"""
|
|
Returns the name of the view for testing to use in the django `reverse` call.
|
|
"""
|
|
return 'course_structure_api:v0:' + self.block_navigation_view_type
|
|
|
|
def test_get(self):
|
|
with check_mongo_calls(4):
|
|
response = super(CourseBlocksOrNavigationTestMixin, self).test_get()
|
|
|
|
# verify root element
|
|
self.assertIn('root', response.data)
|
|
root_string = unicode(self.course.location)
|
|
self.assertEquals(response.data['root'], root_string)
|
|
|
|
# verify ~blocks element
|
|
self.assertTrue(self.block_navigation_view_type in response.data)
|
|
blocks = response.data[self.block_navigation_view_type]
|
|
|
|
# verify number of blocks
|
|
self.assertEquals(len(blocks), 5)
|
|
|
|
# verify fields in blocks
|
|
for field, block in product(self.block_fields, blocks.values()):
|
|
self.assertIn(field, block)
|
|
|
|
# verify container fields in container blocks
|
|
for field in self.container_fields:
|
|
self.assertIn(field, blocks[root_string])
|
|
|
|
def test_parse_error(self):
|
|
"""
|
|
Verifies the view returns a 400 when a query parameter is incorrectly formatted.
|
|
"""
|
|
response = self.http_get_for_course(data={'block_json': 'incorrect'})
|
|
self.assertEqual(response.status_code, 400)
|
|
|
|
@SharedModuleStoreTestCase.modifies_courseware
|
|
def test_no_access_to_block(self):
|
|
"""
|
|
Verifies the view returns only the top-level course block, excluding the sequential block
|
|
and its descendants when the user does not have access to the sequential.
|
|
"""
|
|
self.sequential.visible_to_staff_only = True
|
|
modulestore().update_item(self.sequential, self.user.id)
|
|
|
|
response = super(CourseBlocksOrNavigationTestMixin, self).test_get()
|
|
self.assertEquals(len(response.data[self.block_navigation_view_type]), 1)
|
|
|
|
|
|
class CourseBlocksTestMixin(object):
|
|
"""
|
|
A Mixin class for testing all views related to Course blocks.
|
|
"""
|
|
__metaclass__ = ABCMeta
|
|
|
|
view_supports_debug_mode = False
|
|
block_fields = ['id', 'type', 'display_name', 'web_url', 'block_url', 'graded', 'format']
|
|
|
|
def test_block_json(self):
|
|
"""
|
|
Verifies the view's response when the block_json data is requested.
|
|
"""
|
|
response = self.http_get_for_course(
|
|
data={'block_json': '{"video":{"profiles":["mobile_low"]}}'}
|
|
)
|
|
self.assertEquals(response.status_code, 200)
|
|
video_block = response.data[self.block_navigation_view_type][unicode(self.video.location)]
|
|
self.assertIn('block_json', video_block)
|
|
|
|
def test_block_count(self):
|
|
"""
|
|
Verifies the view's response when the block_count data is requested.
|
|
"""
|
|
response = self.http_get_for_course(
|
|
data={'block_count': 'problem'}
|
|
)
|
|
self.assertEquals(response.status_code, 200)
|
|
root_block = response.data[self.block_navigation_view_type][unicode(self.course.location)]
|
|
self.assertIn('block_count', root_block)
|
|
self.assertIn('problem', root_block['block_count'])
|
|
self.assertEquals(root_block['block_count']['problem'], 1)
|
|
|
|
def test_multi_device_support(self):
|
|
"""
|
|
Verifies the view's response when multi_device support is requested.
|
|
"""
|
|
response = self.http_get_for_course(
|
|
data={'fields': 'multi_device'}
|
|
)
|
|
self.assertEquals(response.status_code, 200)
|
|
|
|
for block, expected_multi_device_support in (
|
|
(self.problem, True),
|
|
(self.html, True),
|
|
(self.video, False)
|
|
):
|
|
block_response = response.data[self.block_navigation_view_type][unicode(block.location)]
|
|
self.assertEquals(block_response['multi_device'], expected_multi_device_support)
|
|
|
|
|
|
class CourseNavigationTestMixin(object):
|
|
"""
|
|
A Mixin class for testing all views related to Course navigation.
|
|
"""
|
|
__metaclass__ = ABCMeta
|
|
|
|
def test_depth_zero(self):
|
|
"""
|
|
Tests that all descendants are bundled into the root block when the navigation_depth is set to 0.
|
|
"""
|
|
response = self.http_get_for_course(
|
|
data={'navigation_depth': '0'}
|
|
)
|
|
root_block = response.data[self.block_navigation_view_type][unicode(self.course.location)]
|
|
self.assertIn('descendants', root_block)
|
|
self.assertEquals(len(root_block['descendants']), 4)
|
|
|
|
def test_depth(self):
|
|
"""
|
|
Tests that all container blocks have descendants listed in their data.
|
|
"""
|
|
response = self.http_get_for_course()
|
|
|
|
container_descendants = (
|
|
(self.course.location, 1),
|
|
(self.sequential.location, 3),
|
|
)
|
|
for container_location, expected_num_descendants in container_descendants:
|
|
block = response.data[self.block_navigation_view_type][unicode(container_location)]
|
|
self.assertIn('descendants', block)
|
|
self.assertEquals(len(block['descendants']), expected_num_descendants)
|
|
|
|
|
|
class CourseBlocksTests(CourseBlocksOrNavigationTestMixin, CourseBlocksTestMixin, SharedModuleStoreTestCase):
|
|
"""
|
|
A Test class for testing the Course 'blocks' view.
|
|
"""
|
|
block_navigation_view_type = 'blocks'
|
|
container_fields = ['children']
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(CourseBlocksTests, cls).setUpClass()
|
|
cls.create_course_data()
|
|
|
|
|
|
class CourseNavigationTests(CourseBlocksOrNavigationTestMixin, CourseNavigationTestMixin, SharedModuleStoreTestCase):
|
|
"""
|
|
A Test class for testing the Course 'navigation' view.
|
|
"""
|
|
block_navigation_view_type = 'navigation'
|
|
container_fields = ['descendants']
|
|
block_fields = []
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(CourseNavigationTests, cls).setUpClass()
|
|
cls.create_course_data()
|
|
|
|
|
|
class CourseBlocksAndNavigationTests(CourseBlocksOrNavigationTestMixin, CourseBlocksTestMixin,
|
|
CourseNavigationTestMixin, SharedModuleStoreTestCase):
|
|
"""
|
|
A Test class for testing the Course 'blocks+navigation' view.
|
|
"""
|
|
block_navigation_view_type = 'blocks+navigation'
|
|
container_fields = ['children', 'descendants']
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(CourseBlocksAndNavigationTests, cls).setUpClass()
|
|
cls.create_course_data()
|