Commit 03517b26 by Matt Drayer Committed by Jonathan Piacenti

mattdrayer/api-modulesgroups: API - Added grouping capability for courseware content

parent b4f9b84d
......@@ -50,16 +50,30 @@ class CoursesApiTests(TestCase):
display_name="Overview"
)
self.module = ItemFactory.create(
self.course_project = ItemFactory.create(
category="chapter",
parent_location=self.course.location,
data=self.test_data,
display_name="Group Project"
)
self.course_project2 = ItemFactory.create(
category="chapter",
parent_location=self.course.location,
data=self.test_data,
display_name="Group Project2"
)
self.course_content = ItemFactory.create(
category="videosequence",
parent_location=self.chapter.location,
data=self.test_data,
display_name="Video_Sequence"
)
self.submodule = ItemFactory.create(
self.content_child = ItemFactory.create(
category="video",
parent_location=self.module.location,
parent_location=self.course_content.location,
data=self.test_data,
display_name="Video_Resources"
)
......@@ -98,10 +112,11 @@ class CoursesApiTests(TestCase):
self.test_course_number = self.course.number
self.test_course_org = self.course.org
self.test_chapter_id = self.chapter.id
self.test_module_id = self.module.id
self.test_submodule_id = self.submodule.id
self.base_modules_uri = '/api/courses/' + self.test_course_id + '/modules'
self.base_chapters_uri = self.base_modules_uri + '?type=chapter'
self.test_course_content_id = self.course_content.id
self.test_bogus_content_id = "j5y://foo/bar/baz"
self.test_content_child_id = self.content_child.id
self.base_course_content_uri = '/api/courses/' + self.test_course_id + '/content'
self.base_chapters_uri = self.base_course_content_uri + '?type=chapter'
self.client = SecureClient()
cache.clear()
......@@ -169,7 +184,7 @@ class CoursesApiTests(TestCase):
confirm_uri = self.test_server_prefix + test_uri
self.assertEqual(response.data['uri'], confirm_uri)
def test_courses_detail_get_with_submodules(self):
def test_courses_detail_get_with_child_content(self):
test_uri = self.base_courses_uri + '/' + self.test_course_id + '?depth=100'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
......@@ -180,7 +195,7 @@ class CoursesApiTests(TestCase):
self.assertEqual(response.data['org'], self.test_course_org)
confirm_uri = self.test_server_prefix + test_uri
self.assertEqual(response.data['uri'], confirm_uri)
self.assertGreater(len(response.data['modules']), 0)
self.assertGreater(len(response.data['content']), 0)
def test_courses_detail_get_notfound(self):
test_uri = self.base_courses_uri + '/' + self.test_bogus_course_id
......@@ -195,17 +210,17 @@ class CoursesApiTests(TestCase):
self.assertGreater(len(response.data), 0)
self.assertEqual(response.data['category'], 'course')
self.assertEqual(response.data['name'], self.course.display_name)
self.assertEqual(len(response.data['modules']), 1)
self.assertEqual(len(response.data['content']), 3)
chapter = response.data['modules'][0]
chapter = response.data['content'][0]
self.assertEqual(chapter['category'], 'chapter')
self.assertEqual(chapter['name'], 'Overview')
self.assertEqual(len(chapter['modules']), 1)
self.assertEqual(len(chapter['children']), 1)
sequence = chapter['modules'][0]
sequence = chapter['children'][0]
self.assertEqual(sequence['category'], 'videosequence')
self.assertEqual(sequence['name'], 'Video_Sequence')
self.assertNotIn('modules', sequence)
self.assertNotIn('children', sequence)
def test_courses_tree_get_root(self):
# query the course tree to quickly get naviation information
......@@ -215,7 +230,7 @@ class CoursesApiTests(TestCase):
self.assertGreater(len(response.data), 0)
self.assertEqual(response.data['category'], 'course')
self.assertEqual(response.data['name'], self.course.display_name)
self.assertNotIn('modules', response.data)
self.assertNotIn('content', response.data)
def test_chapter_list_get(self):
test_uri = self.base_chapters_uri
......@@ -227,81 +242,86 @@ class CoursesApiTests(TestCase):
if matched_chapter is False and chapter['id'] == self.test_chapter_id:
self.assertIsNotNone(chapter['uri'])
self.assertGreater(len(chapter['uri']), 0)
confirm_uri = self.test_server_prefix + self.base_modules_uri + '/' + chapter['id']
confirm_uri = self.test_server_prefix + self.base_course_content_uri + '/' + chapter['id']
self.assertEqual(chapter['uri'], confirm_uri)
matched_chapter = True
self.assertTrue(matched_chapter)
def test_chapter_detail_get(self):
test_uri = self.base_modules_uri + '/' + self.test_chapter_id
test_uri = self.base_course_content_uri + '/' + self.test_chapter_id
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data['id']), 0)
self.assertEqual(response.data['id'], self.test_chapter_id)
confirm_uri = self.test_server_prefix + test_uri
self.assertEqual(response.data['uri'], confirm_uri)
self.assertGreater(len(response.data['modules']), 0)
self.assertGreater(len(response.data['children']), 0)
def test_modules_list_get(self):
test_uri = self.base_modules_uri + '/' + self.test_module_id
def test_course_content_list_get(self):
test_uri = '{}/{}/children'.format(self.base_course_content_uri, self.test_course_content_id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
matched_submodule = False
for submodule in response.data['modules']:
if matched_submodule is False and submodule['id'] == self.test_submodule_id:
self.assertIsNotNone(submodule['uri'])
self.assertGreater(len(submodule['uri']), 0)
confirm_uri = self.test_server_prefix + self.base_modules_uri + '/' + submodule['id']
self.assertEqual(submodule['uri'], confirm_uri)
matched_submodule = True
self.assertTrue(matched_submodule)
matched_child = False
for child in response.data:
if matched_child is False and child['id'] == self.test_content_child_id:
self.assertIsNotNone(child['uri'])
self.assertGreater(len(child['uri']), 0)
confirm_uri = self.test_server_prefix + self.base_course_content_uri + '/' + child['id']
self.assertEqual(child['uri'], confirm_uri)
matched_child = True
self.assertTrue(matched_child)
def test_course_content_list_get_invalid_content(self):
test_uri = '{}/{}/children'.format(self.base_course_content_uri, self.test_bogus_content_id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_modules_detail_get(self):
test_uri = self.base_modules_uri + '/' + self.test_module_id
def test_course_content_detail_get(self):
test_uri = self.base_course_content_uri + '/' + self.test_course_content_id
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
self.assertEqual(response.data['id'], self.test_module_id)
self.assertEqual(response.data['id'], self.test_course_content_id)
confirm_uri = self.test_server_prefix + test_uri
self.assertEqual(response.data['uri'], confirm_uri)
self.assertGreater(len(response.data['modules']), 0)
self.assertGreater(len(response.data['children']), 0)
def test_modules_detail_get_course(self):
test_uri = self.base_modules_uri + '/' + self.test_course_id
def test_course_content_detail_get_course(self):
test_uri = self.base_course_content_uri + '/' + self.test_course_id
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
self.assertEqual(response.data['id'], self.test_course_id)
confirm_uri = self.test_server_prefix + self.base_courses_uri + '/' + self.test_course_id
self.assertEqual(response.data['uri'], confirm_uri)
self.assertGreater(len(response.data['modules']), 0)
self.assertGreater(len(response.data['content']), 0)
def test_modules_detail_get_notfound(self):
test_uri = self.base_modules_uri + '/' + '2p38fp2hjfp9283'
def test_course_content_detail_get_notfound(self):
test_uri = self.base_course_content_uri + '/' + self.test_bogus_content_id
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_modules_list_get_filtered_submodules_for_module(self):
test_uri = self.base_modules_uri + '/' + self.test_module_id + '/submodules?type=video'
def test_course_content_list_get_filtered_children_for_child(self):
test_uri = self.base_course_content_uri + '/' + self.test_course_content_id + '/children?type=video'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
matched_submodule = False
for submodule in response.data:
if matched_submodule is False and submodule['id'] == self.test_submodule_id:
confirm_uri = self.test_server_prefix + self.base_modules_uri + '/' + submodule['id']
self.assertEqual(submodule['uri'], confirm_uri)
matched_submodule = True
self.assertTrue(matched_submodule)
matched_child = False
for child in response.data:
if matched_child is False and child['id'] == self.test_content_child_id:
confirm_uri = '{}{}/{}'.format(self.test_server_prefix, self.base_course_content_uri, child['id'])
self.assertEqual(child['uri'], confirm_uri)
matched_child = True
self.assertTrue(matched_child)
def test_modules_list_get_notfound(self):
test_uri = self.base_modules_uri + '/2p38fp2hjfp9283/submodules?type=video'
def test_course_content_list_get_notfound(self):
test_uri = '{}{}/children?type=video'.format(self.base_course_content_uri, self.test_bogus_content_id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_courses_groups_list_post(self):
data = {'name': self.test_group_name}
data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
......@@ -320,7 +340,7 @@ class CoursesApiTests(TestCase):
course_fail_uri = '{}/{}/groups'.format(self.base_courses_uri, '/ed/Open_DemoX/edx_demo_course')
for i in xrange(2):
data_dict = {
'name': 'Alpha Group {}'.format(i), 'group_type': 'Programming',
'name': 'Alpha Group {}'.format(i), 'type': 'Programming',
}
response = self.do_post(self.base_groups_uri, data_dict)
group_id = response.data['id']
......@@ -329,7 +349,7 @@ class CoursesApiTests(TestCase):
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
data_dict['group_type'] = 'Calculus'
data_dict['type'] = 'Calculus'
response = self.do_post(self.base_groups_uri, data_dict)
group_id = response.data['id']
data = {'group_id': group_id}
......@@ -360,7 +380,7 @@ class CoursesApiTests(TestCase):
self.assertEqual(response.status_code, 404)
def test_courses_groups_list_post_duplicate(self):
data = {'name': self.test_group_name}
data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = '{}/{}/groups'.format(self.base_courses_uri, self.test_course_id)
......@@ -377,7 +397,7 @@ class CoursesApiTests(TestCase):
self.assertEqual(response.status_code, 404)
def test_courses_groups_detail_get(self):
data = {'name': self.test_group_name}
data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = '{}/{}/groups'.format(self.base_courses_uri, self.test_course_id)
......@@ -402,7 +422,7 @@ class CoursesApiTests(TestCase):
self.assertEqual(response.data['group_id'], group_id)
def test_courses_groups_detail_delete(self):
data = {'name': self.test_group_name}
data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
test_uri = '{}/{}/groups'.format(self.base_courses_uri, self.test_course_id)
data = {'group_id': response.data['id']}
......@@ -426,7 +446,7 @@ class CoursesApiTests(TestCase):
self.assertEqual(response.status_code, 204)
def test_courses_groups_detail_get_undefined(self):
data = {'name': self.test_group_name}
data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = '{}/{}/groups/{}'.format(self.base_courses_uri, self.test_course_id, group_id)
......@@ -792,3 +812,187 @@ class CoursesApiTests(TestCase):
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/users/213432'
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204)
def test_course_content_groups_list_post(self):
data = {'name': 'Alpha Group', 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
data = {'name': 'Beta Group', 'type': 'project'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = '{}/{}/groups'.format(self.base_course_content_uri, self.course_project.id)
data = {'group_id': group_id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
confirm_uri = self.test_server_prefix + test_uri + '/' + str(group_id)
self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['course_id'], str(self.test_course_id))
self.assertEqual(response.data['content_id'], str(self.course_project.id))
self.assertEqual(response.data['group_id'], str(group_id))
def test_course_content_groups_list_post_duplicate(self):
data = {'name': 'Beta Group', 'type': 'project'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = '{}/{}/groups'.format(self.base_course_content_uri, self.course_project.id)
data = {'group_id': group_id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 409)
def test_course_content_groups_list_post_invalid_course(self):
data = {'name': 'Beta Group', 'type': 'project'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = '{}/{}/content/{}/groups'.format(
self.base_courses_uri,
self.test_bogus_course_id,
self.course_project.id
)
data = {'group_id': group_id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 404)
def test_course_content_groups_list_post_invalid_content(self):
data = {'name': 'Beta Group', 'type': 'project'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = '{}/{}/content/{}/groups'.format(
self.base_courses_uri,
self.test_course_id,
self.test_bogus_content_id
)
data = {'group_id': group_id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 404)
def test_course_content_groups_list_post_invalid_group(self):
test_uri = '{}/{}/content/{}/groups'.format(
self.base_courses_uri,
self.test_course_id,
self.course_project.id
)
data = {'group_id': '12398721'}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 404)
def test_course_content_groups_list_post_missing_group(self):
test_uri = '{}/{}/content/{}/groups'.format(
self.base_courses_uri,
self.test_course_id,
self.course_project.id
)
response = self.do_post(test_uri, {})
self.assertEqual(response.status_code, 404)
def test_course_content_groups_list_get(self):
test_uri = '{}/{}/groups'.format(self.base_course_content_uri, self.course_project.id)
data = {'name': 'Alpha Group', 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
data = {'group_id': group_id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
data = {'name': 'Beta Group', 'type': 'project'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
data = {'group_id': group_id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 2)
test_uri = test_uri + '?type=project'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 1)
def test_course_content_groups_list_get_invalid_course(self):
test_uri = '{}/{}/content/{}/groups'.format(
self.base_courses_uri,
self.test_bogus_course_id,
self.course_project.id
)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_course_content_groups_list_get_invalid_content(self):
test_uri = '{}/{}/content/{}/groups'.format(
self.base_courses_uri,
self.test_course_id,
self.test_bogus_content_id
)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_course_content_groups_list_get_filter_by_type(self):
data = {'name': 'Alpha Group', 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
data = {'name': 'Beta Group', 'type': 'project'}
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201)
group_id = response.data['id']
test_uri = '{}/{}/groups'.format(self.base_course_content_uri, self.course_project.id)
data = {'group_id': group_id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]['group_id'], 2)
def test_course_content_groups_detail_get(self):
test_uri = '{}/{}/groups'.format(self.base_course_content_uri, self.course_project.id)
data = {'name': 'Alpha Group', 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
data = {'group_id': group_id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
data = {'name': 'Beta Group', 'type': 'project'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
data = {'group_id': group_id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
response = self.do_get(response.data['uri'])
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['group_id'], str(group_id))
def test_course_content_groups_detail_get_invalid_relationship(self):
data = {'name': 'Alpha Group', 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = '{}/{}/groups/{}'.format(self.base_course_content_uri, self.course_project.id, group_id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_course_content_groups_detail_get_invalid_course(self):
test_uri = '{}/{}/content/{}/groups/123456'.format(
self.base_courses_uri,
self.test_bogus_course_id,
self.course_project.id
)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_course_content_groups_detail_get_invalid_content(self):
test_uri = '{}/{}/content/{}/groups/123456'.format(
self.base_courses_uri,
self.test_course_id,
self.test_bogus_content_id
)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_course_content_groups_detail_get_invalid_group(self):
test_uri = '{}/{}/content/{}/groups/123456'.format(
self.base_courses_uri,
self.test_course_id,
self.course_project.id
)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
......@@ -12,9 +12,11 @@ urlpatterns = patterns(
'',
url(r'/*$^', courses_views.CoursesList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)$', courses_views.CoursesDetail.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/modules/(?P<module_id>[a-zA-Z0-9/_:]+)/submodules/*$', courses_views.ModulesList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/modules/(?P<module_id>[a-zA-Z0-9/_:]+)$', courses_views.ModulesDetail.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/modules/*$', courses_views.ModulesList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/content/(?P<content_id>[a-zA-Z0-9/_:]+)/groups/(?P<group_id>[0-9]+)$', courses_views.CourseContentGroupsDetail.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/content/(?P<content_id>[a-zA-Z0-9/_:]+)/groups/*$', courses_views.CourseContentGroupsList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/content/(?P<content_id>[a-zA-Z0-9/_:]+)/children/*$', courses_views.CourseContentList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/content/(?P<content_id>[a-zA-Z0-9/_:]+)$', courses_views.CourseContentDetail.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/content/*$', courses_views.CourseContentList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/groups/(?P<group_id>[0-9]+)$', courses_views.CoursesGroupsDetail.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/groups/*$', courses_views.CoursesGroupsList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/overview$', courses_views.CoursesOverview.as_view()),
......
......@@ -14,7 +14,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from api_manager.permissions import ApiKeyHeaderPermission
from api_manager.models import CourseGroupRelationship, GroupProfile
from api_manager.models import CourseGroupRelationship, CourseContentGroupRelationship, GroupProfile
from courseware import module_render
from courseware.courses import get_course, get_course_about_section, get_course_info_section
from courseware.model_data import FieldDataCache
......@@ -42,90 +42,101 @@ def _generate_base_uri(request):
return resource_uri
def _get_module_submodules(module, submodule_type=None):
def _get_content_children(content, content_type=None):
"""
Parses the provided module looking for child modules
Matches on submodule type (category) when specified
Parses the provided content object looking for children
Matches on child type (category) when specified
"""
submodules = []
if hasattr(module, 'children'):
child_modules = module.get_children()
for child_module in child_modules:
if submodule_type:
if getattr(child_module, 'category') == submodule_type:
submodules.append(child_module)
children = []
if hasattr(content, 'children'):
child_content = content.get_children()
for child in child_content:
if content_type:
if getattr(child, 'category') == content_type:
children.append(child)
else:
submodules.append(child_module)
return submodules
children.append(child)
return children
def _serialize_module(request, course_id, module):
def _serialize_content(request, course_id, content):
"""
Loads the specified module data into the response dict
Loads the specified content object into the response dict
This should probably evolve to use DRF serializers
"""
data = {}
if getattr(module, 'id') == course_id:
module_id = module.id
if getattr(content, 'id') == course_id:
content_id = content.id
else:
module_id = module.location.url()
data['id'] = module_id
content_id = content.location.url()
data['id'] = content_id
if hasattr(module, 'display_name'):
data['name'] = module.display_name
if hasattr(content, 'display_name'):
data['name'] = content.display_name
data['category'] = module.location.category
data['category'] = content.location.category
protocol = 'http'
if request.is_secure():
protocol = protocol + 's'
module_uri = '{}://{}/api/courses/{}'.format(
content_uri = '{}://{}/api/courses/{}'.format(
protocol,
request.get_host(),
course_id.encode('utf-8')
)
# Some things we do only if the module is a course
if (course_id == module_id):
data['number'] = module.location.course
data['org'] = module.location.org
# Some things we do only if the content object is a course
if (course_id == content_id):
data['number'] = content.location.course
data['org'] = content.location.org
# Other things we do only if the module is not a course
# Other things we do only if the content object is not a course
else:
module_uri = '{}/modules/{}'.format(module_uri, module_id)
data['uri'] = module_uri
content_uri = '{}/content/{}'.format(content_uri, content_id)
data['uri'] = content_uri
return data
def _serialize_module_submodules(request, course_id, submodules):
def _serialize_content_children(request, course_id, children):
"""
Loads the specified module submodule data into the response dict
Loads the specified content child data into the response dict
This should probably evolve to use DRF serializers
"""
data = []
if submodules:
for submodule in submodules:
submodule_data = _serialize_module(
if children:
for child in children:
child_data = _serialize_content(
request,
course_id,
submodule
child
)
data.append(submodule_data)
data.append(child_data)
return data
def _serialize_module_with_children(request, course_descriptor, descriptor, depth):
data = _serialize_module(
def _serialize_content_groups(request, course_id, content_id, groups):
"""
Loads the specified content group data into the response dict
This should probably evolve to use DRF serializers
"""
return [
{'course_id': course_id, 'content_id': content_id, 'group_id': group.group_id}
for group in groups
]
def _serialize_content_with_children(request, course_descriptor, descriptor, depth):
data = _serialize_content(
request,
course_descriptor.id,
descriptor
)
if depth > 0:
data['modules'] = []
data['children'] = []
for child in descriptor.get_children():
data['modules'].append(_serialize_module_with_children(
data['children'].append(_serialize_content_with_children(
request,
course_descriptor,
child,
......@@ -237,32 +248,32 @@ def _parse_updates_html(html):
return result
class ModulesList(APIView):
class CourseContentList(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def get(self, request, course_id, module_id=None):
def get(self, request, course_id, content_id=None):
"""
GET retrieves the list of submodules for a given module
We don't know where in the module hierarchy we are -- could even be the top
GET retrieves the list of children for a given content object
We don't know where in the content hierarchy we are -- could even be the top
"""
if module_id is None:
module_id = course_id
if content_id is None:
content_id = course_id
response_data = []
submodule_type = request.QUERY_PARAMS.get('type', None)
content_type = request.QUERY_PARAMS.get('type', None)
store = modulestore()
if course_id != module_id:
if course_id != content_id:
try:
module = store.get_instance(course_id, Location(module_id))
content = store.get_instance(course_id, Location(content_id))
except InvalidLocationError:
module = None
content = None
else:
module = get_course(course_id)
if module:
submodules = _get_module_submodules(module, submodule_type)
response_data = _serialize_module_submodules(
content = get_course(course_id)
if content:
children = _get_content_children(content, content_type)
response_data = _serialize_content_children(
request,
course_id,
submodules
children
)
status_code = status.HTTP_200_OK
else:
......@@ -270,34 +281,36 @@ class ModulesList(APIView):
return Response(response_data, status=status_code)
class ModulesDetail(APIView):
class CourseContentDetail(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def get(self, request, course_id, module_id):
def get(self, request, course_id, content_id):
"""
GET retrieves an existing module from the system
GET retrieves an existing content object from the system
"""
store = modulestore()
response_data = {}
submodule_type = request.QUERY_PARAMS.get('type', None)
if course_id != module_id:
content_type = request.QUERY_PARAMS.get('type', None)
if course_id != content_id:
element_name = 'children'
try:
module = store.get_instance(course_id, Location(module_id))
content = store.get_instance(course_id, Location(content_id))
except InvalidLocationError:
module = None
content = None
else:
module = get_course(course_id)
if module:
response_data = _serialize_module(
element_name = 'content'
content = get_course(course_id)
if content:
response_data = _serialize_content(
request,
course_id,
module
content
)
submodules = _get_module_submodules(module, submodule_type)
response_data['modules'] = _serialize_module_submodules(
children = _get_content_children(content, content_type)
response_data[element_name] = _serialize_content_children(
request,
course_id,
submodules
children
)
status_code = status.HTTP_200_OK
else:
......@@ -316,7 +329,7 @@ class CoursesList(APIView):
store = modulestore()
course_descriptors = store.get_courses()
for course_descriptor in course_descriptors:
course_data = _serialize_module(
course_data = _serialize_content(
request,
course_descriptor.id,
course_descriptor
......@@ -330,8 +343,8 @@ class CoursesDetail(APIView):
def get(self, request, course_id):
"""
GET retrieves an existing course from the system and returns summary information about the submodules
to the specified depth
GET retrieves an existing course from the system and returns
summary information about its content children to the specified depth
"""
depth = request.QUERY_PARAMS.get('depth', 0)
depth_int = int(depth)
......@@ -343,14 +356,16 @@ class CoursesDetail(APIView):
course_descriptor = None
if course_descriptor:
if depth_int > 0:
response_data = _serialize_module_with_children(
response_data = _serialize_content_with_children(
request,
course_descriptor,
course_descriptor, # Primer for recursive function
depth_int
)
response_data['content'] = response_data['children']
response_data.pop('children')
else:
response_data = _serialize_module(
response_data = _serialize_content(
request,
course_descriptor.id,
course_descriptor
......@@ -472,7 +487,7 @@ class CoursesOverview(APIView):
def get(self, request, course_id):
"""
GET retrieves the course overview module, which - in MongoDB - is stored with the following
GET retrieves the course overview content, which - in MongoDB - is stored with the following
naming convention {"_id.org":"i4x", "_id.course":<course_num>, "_id.category":"about", "_id.name":"overview"}
"""
response_data = OrderedDict()
......@@ -499,7 +514,7 @@ class CoursesUpdates(APIView):
def get(self, request, course_id):
"""
GET retrieves the course overview module, which - in MongoDB - is stored with the following
GET retrieves the course overview content, which - in MongoDB - is stored with the following
naming convention {"_id.org":"i4x", "_id.course":<course_num>, "_id.category":"course_info", "_id.name":"updates"}
"""
response_data = OrderedDict()
......@@ -685,13 +700,13 @@ class CoursesUsersDetail(APIView):
user = None
if user and CourseEnrollment.is_enrolled(user, course_id):
field_data_cache = FieldDataCache([course_descriptor], course_id, user)
course_module = module_render.get_module(
course_content = module_render.get_module(
user,
request,
course_descriptor.location,
field_data_cache,
course_id)
response_data['position'] = course_module.position
response_data['position'] = course_content.position
response_status = status.HTTP_200_OK
else:
response_status = status.HTTP_404_NOT_FOUND
......@@ -717,3 +732,111 @@ class CoursesUsersDetail(APIView):
base_uri = _generate_base_uri(request)
response_data['uri'] = base_uri
return Response(response_data, status=status.HTTP_204_NO_CONTENT)
class CourseContentGroupsList(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def post(self, request, course_id, content_id):
try:
course_descriptor = get_course(course_id)
except ValueError:
return Response({}, status=status.HTTP_404_NOT_FOUND)
store = modulestore()
try:
existing_content = store.get_instance(course_id, Location(content_id))
except InvalidLocationError:
return Response({}, status=status.HTTP_404_NOT_FOUND)
group_id = request.DATA.get('group_id')
if group_id is None:
return Response({}, status=status.HTTP_404_NOT_FOUND)
try:
existing_group = GroupProfile.objects.get(group_id=group_id)
except ObjectDoesNotExist:
return Response({}, status=status.HTTP_404_NOT_FOUND)
try:
existing_relationship = CourseContentGroupRelationship.objects.get(
course_id=course_id,
content_id=content_id,
group=existing_group
)
except ObjectDoesNotExist:
existing_relationship = None
response_data = {}
base_uri = _generate_base_uri(request)
response_data['uri'] = '{}/{}'.format(base_uri, existing_group.group_id)
if existing_relationship:
response_data['message'] = "Relationship already exists."
return Response(response_data, status=status.HTTP_409_CONFLICT)
CourseContentGroupRelationship.objects.create(
course_id=course_id,
content_id=content_id,
group=existing_group
)
response_data['course_id'] = course_descriptor.id
response_data['content_id'] = existing_content.id
response_data['group_id'] = str(existing_group.group_id)
return Response(response_data, status=status.HTTP_201_CREATED)
def get(self, request, course_id, content_id):
"""
GET retrieves the list of groups for a given content object.
The 'type' query parameter is available for filtering by group_type
"""
response_data = []
group_type = request.QUERY_PARAMS.get('type')
try:
course_descriptor = get_course(course_id)
except ValueError:
return Response({}, status=status.HTTP_404_NOT_FOUND)
try:
store = modulestore()
existing_content = store.get_instance(course_id, Location(content_id))
except InvalidLocationError:
return Response({}, status=status.HTTP_404_NOT_FOUND)
groups = CourseContentGroupRelationship.objects.filter(
course_id=course_id,
content_id=content_id
)
if group_type:
groups = groups.filter(group__group_type=group_type)
response_data = _serialize_content_groups(
request,
course_id,
content_id,
groups
)
return Response(response_data, status=status.HTTP_200_OK)
class CourseContentGroupsDetail(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def get(self, request, course_id, content_id, group_id):
"""
GET retrieves an existing content-group relationship from the system
"""
try:
course_descriptor = get_course(course_id)
except ValueError:
return Response({}, status=status.HTTP_404_NOT_FOUND)
try:
store = modulestore()
existing_content = store.get_instance(course_id, Location(content_id))
except InvalidLocationError:
return Response({}, status=status.HTTP_404_NOT_FOUND)
try:
groups = CourseContentGroupRelationship.objects.get(
course_id=course_id,
content_id=content_id,
group=group_id
)
except ObjectDoesNotExist:
return Response({}, status=status.HTTP_404_NOT_FOUND)
response_data = {
'course_id': course_id,
'content_id': content_id,
'group_id': group_id,
}
return Response(response_data, status=status.HTTP_200_OK)
......@@ -74,7 +74,7 @@ class GroupsApiTests(ModuleStoreTestCase):
return response
def test_group_list_post(self):
data = {'name': self.test_group_name}
data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201)
self.assertGreater(response.data['id'], 0)
......@@ -83,12 +83,12 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertGreater(len(response.data['name']), 0)
def test_group_list_get_with_profile(self):
group_type = 'series'
profile_data = {'display_name': 'My first series'}
data = {
'name': self.test_group_name,
'group_type': 'series',
'data': {
'display_name': 'My first series'
}
'type': group_type,
'data': profile_data
}
response = self.do_post(self.base_groups_uri, data)
self.assertGreater(response.data['id'], 0)
......@@ -97,45 +97,41 @@ class GroupsApiTests(ModuleStoreTestCase):
# query for list of groups, but don't put the type filter
test_uri = self.base_groups_uri
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.status_code, 400)
# try again with filter
test_uri = self.base_groups_uri + '?type=series'
test_uri = '{}?type={}'.format(self.base_groups_uri, group_type)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]['group_id'], group_id)
self.assertEqual(response.data[0]['group_type'], 'series')
self.assertEqual(response.data[0]['id'], group_id)
self.assertEqual(response.data[0]['type'], 'series')
self.assertEqual(response.data[0]['name'], self.test_group_name)
self.assertEqual(response.data[0]['data']['display_name'], 'My first series')
response_profile_data = response.data[0]['data']
self.assertEqual(response_profile_data['display_name'], 'My first series')
# query the group detail
test_uri = self.base_groups_uri + '/' + str(group_id)
test_uri = '{}/{}'.format(self.base_groups_uri, str(group_id))
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['id'], group_id)
confirm_uri = self.test_server_prefix + test_uri
self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['name'], self.test_group_name)
self.assertEqual(response.data['group_type'], 'series')
self.assertEqual(response.data['data']['display_name'], 'My first series')
self.assertEqual(response.data['type'], 'series')
response_profile_data = response.data['data']
self.assertEqual(response_profile_data['display_name'], 'My first series')
# update the profile
# first with missing data
response = self.do_post(test_uri, {})
self.assertEqual(response.status_code, 400)
profile_data = {'display_name': 'My updated series'}
data = {
'name': self.test_group_name,
'group_type': 'seriesX',
'data': {
'display_name': 'My updated series'
}
'type': 'seriesX',
'data': profile_data
}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
self.assertEqual(response.status_code, 200)
# requery the filter
test_uri = self.base_groups_uri + '?type=series'
......@@ -147,35 +143,42 @@ class GroupsApiTests(ModuleStoreTestCase):
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]['group_id'], group_id)
self.assertEqual(response.data[0]['group_type'], 'seriesX')
self.assertEqual(response.data[0]['id'], group_id)
self.assertEqual(response.data[0]['type'], 'seriesX')
self.assertEqual(response.data[0]['name'], self.test_group_name)
self.assertEqual(response.data[0]['data']['display_name'], 'My updated series')
response_profile_data = response.data[0]['data']
self.assertEqual(response_profile_data['display_name'], 'My updated series')
def test_group_list_post_invalid_name(self):
data = {'name': '', 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 400)
def test_group_list_post_missing_type(self):
data = {'name': ''}
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 400)
def test_group_list_get_uses_base_group_name(self):
data = {'name': self.test_group_name}
data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201)
group_id = response.data['id']
profile = GroupProfile.objects.get(group_id=group_id)
profile.name = ''
profile.save()
response = self.do_get(self.base_groups_uri)
test_uri = '{}?type=test'.format(self.base_groups_uri)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data[0]['name'], '{:04d}: {}'.format(group_id, self.test_group_name))
profile.name = None
profile.save()
response = self.do_get(self.base_groups_uri)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data[0]['name'], '{:04d}: {}'.format(group_id, self.test_group_name))
def test_group_detail_get(self):
data = {'name': self.test_group_name}
data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201)
self.assertGreater(response.data['id'], 0)
......@@ -189,7 +192,7 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.data['name'], self.test_group_name)
def test_group_detail_get_uses_base_group_name(self):
data = {'name': self.test_group_name}
data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201)
self.assertGreater(response.data['id'], 0)
......@@ -206,7 +209,7 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.data['name'], '{:04d}: {}'.format(group_id, self.test_group_name))
def test_group_detail_get_with_missing_profile(self):
data = {'name': self.test_group_name}
data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201)
self.assertGreater(response.data['id'], 0)
......@@ -226,7 +229,7 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.status_code, 404)
def test_group_detail_post(self):
data = {'name': self.test_group_name}
data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = response.data['uri']
......@@ -234,13 +237,13 @@ class GroupsApiTests(ModuleStoreTestCase):
group_type = 'seriesX'
data = {
'name': self.test_group_name,
'group_type': group_type,
'type': group_type,
'data': {
'display_name': 'My updated series'
}
}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['id'], group_id)
self.assertEqual(response.data['name'], self.test_group_name)
self.assertEqual(response.data['uri'], test_uri)
......@@ -250,7 +253,7 @@ class GroupsApiTests(ModuleStoreTestCase):
group_type = 'seriesX'
data = {
'name': self.test_group_name,
'group_type': group_type,
'type': group_type,
'data': {
'display_name': 'My updated series'
}
......@@ -269,7 +272,7 @@ class GroupsApiTests(ModuleStoreTestCase):
}
response = self.do_post(self.base_users_uri, data)
user_id = response.data['id']
data = {'name': 'Alpha Group'}
data = {'name': 'Alpha Group', 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = self.base_groups_uri + '/' + str(group_id)
......@@ -288,7 +291,7 @@ class GroupsApiTests(ModuleStoreTestCase):
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password}
response = self.do_post(self.base_users_uri, data)
user_id = response.data['id']
data = {'name': 'Alpha Group'}
data = {'name': 'Alpha Group', 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
test_uri = self.base_groups_uri + '/' + str(response.data['id'])
response = self.do_get(test_uri)
......@@ -307,7 +310,7 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.status_code, 404)
def test_group_users_list_post_invalid_user(self):
data = {'name': 'Alpha Group'}
data = {'name': 'Alpha Group', 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
test_uri = '{}/{}/users'.format(self.base_groups_uri, str(response.data['id']))
data = {'user_id': "98723896"}
......@@ -325,7 +328,7 @@ class GroupsApiTests(ModuleStoreTestCase):
}
response = self.do_post(self.base_users_uri, data)
user_id = response.data['id']
data = {'name': 'Alpha Group'}
data = {'name': 'Alpha Group', 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = self.base_groups_uri + '/' + str(group_id)
......@@ -354,7 +357,7 @@ class GroupsApiTests(ModuleStoreTestCase):
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password}
response = self.do_post(self.base_users_uri, data)
user_id = response.data['id']
data = {'name': 'Alpha Group'}
data = {'name': 'Alpha Group', 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = self.base_groups_uri + '/' + str(response.data['id'])
......@@ -376,7 +379,7 @@ class GroupsApiTests(ModuleStoreTestCase):
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password}
response = self.do_post(self.base_users_uri, data)
user_id = response.data['id']
data = {'name': 'Alpha Group'}
data = {'name': 'Alpha Group', 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
test_uri = self.base_groups_uri + '/' + str(response.data['id'])
response = self.do_get(test_uri)
......@@ -397,7 +400,7 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.status_code, 204)
def test_group_users_detail_delete_invalid_user(self):
data = {'name': self.test_group_name}
data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
test_uri = self.base_groups_uri + '/' + str(response.data['id'])
test_uri = test_uri + '/users/123124'
......@@ -409,7 +412,7 @@ class GroupsApiTests(ModuleStoreTestCase):
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password}
response = self.do_post(self.base_users_uri, data)
user_id = response.data['id']
data = {'name': 'Alpha Group'}
data = {'name': 'Alpha Group', 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = self.base_groups_uri + '/' + str(group_id) + '/users/' + str(user_id)
......@@ -417,13 +420,13 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.status_code, 404)
def test_group_groups_list_post_hierarchical(self):
data = {'name': 'Alpha Group'}
data = {'name': 'Alpha Group', 'type': 'test'}
alpha_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(alpha_response.status_code, 201)
data = {'name': 'Beta Group'}
data = {'name': 'Beta Group', 'type': 'test'}
beta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(beta_response.status_code, 201)
data = {'name': 'Delta Group'}
data = {'name': 'Delta Group', 'type': 'test'}
delta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(delta_response.status_code, 201)
test_uri = alpha_response.data['uri'] + '/groups'
......@@ -439,13 +442,13 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.data['relationship_type'], relationship_type)
def test_group_groups_list_post_linked(self):
data = {'name': 'Alpha Group'}
data = {'name': 'Alpha Group', 'type': 'test'}
alpha_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(alpha_response.status_code, 201)
data = {'name': 'Beta Group'}
data = {'name': 'Beta Group', 'type': 'test'}
beta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(beta_response.status_code, 201)
data = {'name': 'Delta Group'}
data = {'name': 'Delta Group', 'type': 'test'}
delta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(delta_response.status_code, 201)
test_uri = alpha_response.data['uri'] + '/groups'
......@@ -461,13 +464,13 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.data['relationship_type'], relationship_type)
def test_group_groups_list_post_linked_duplicate(self):
data = {'name': 'Alpha Group'}
data = {'name': 'Alpha Group', 'type': 'test'}
alpha_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(alpha_response.status_code, 201)
data = {'name': 'Beta Group'}
data = {'name': 'Beta Group', 'type': 'test'}
beta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(beta_response.status_code, 201)
data = {'name': 'Delta Group'}
data = {'name': 'Delta Group', 'type': 'test'}
delta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(delta_response.status_code, 201)
test_uri = alpha_response.data['uri'] + '/groups'
......@@ -488,13 +491,13 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.status_code, 404)
def test_group_groups_list_post_invalid_relationship_type(self):
data = {'name': 'Alpha Group'}
data = {'name': 'Alpha Group', 'type': 'test'}
alpha_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(alpha_response.status_code, 201)
data = {'name': 'Beta Group'}
data = {'name': 'Beta Group', 'type': 'test'}
beta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(beta_response.status_code, 201)
data = {'name': 'Delta Group'}
data = {'name': 'Delta Group', 'type': 'test'}
delta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(delta_response.status_code, 201)
test_uri = alpha_response.data['uri'] + '/groups'
......@@ -505,13 +508,13 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.status_code, 406)
def test_group_groups_list_get(self):
data = {'name': 'Bravo Group'}
data = {'name': 'Bravo Group', 'type': 'test'}
bravo_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(bravo_response.status_code, 201)
bravo_group_id = bravo_response.data['id']
bravo_groups_uri = bravo_response.data['uri'] + '/groups'
data = {'name': 'Charlie Group'}
data = {'name': 'Charlie Group', 'type': 'test'}
charlie_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(charlie_response.status_code, 201)
charlie_group_id = charlie_response.data['id']
......@@ -520,7 +523,7 @@ class GroupsApiTests(ModuleStoreTestCase):
response = self.do_post(bravo_groups_uri, data)
self.assertEqual(response.status_code, 201)
data = {'name': 'Foxtrot Group'}
data = {'name': 'Foxtrot Group', 'type': 'test'}
foxtrot_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(foxtrot_response.status_code, 201)
foxtrot_group_id = foxtrot_response.data['id']
......@@ -529,7 +532,7 @@ class GroupsApiTests(ModuleStoreTestCase):
response = self.do_post(bravo_groups_uri, data)
self.assertEqual(response.status_code, 201)
data = {'name': 'Tango Group'}
data = {'name': 'Tango Group', 'type': 'test'}
tango_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(tango_response.status_code, 201)
tango_group_id = tango_response.data['id']
......@@ -554,13 +557,13 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(relationship_count, len(group_idlist))
def test_group_groups_list_get_with_profile_type(self):
data = {'name': 'Bravo Group'}
data = {'name': 'Bravo Group', 'type': 'test'}
bravo_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(bravo_response.status_code, 201)
bravo_group_id = bravo_response.data['id']
bravo_groups_uri = bravo_response.data['uri'] + '/groups?type=test_group'
data = {'name': 'Charlie Group', 'group_type': 'test_group'}
data = {'name': 'Charlie Group', 'type': 'test_group'}
charlie_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(charlie_response.status_code, 201)
charlie_group_id = charlie_response.data['id']
......@@ -569,7 +572,7 @@ class GroupsApiTests(ModuleStoreTestCase):
response = self.do_post(bravo_groups_uri, data)
self.assertEqual(response.status_code, 201)
data = {'name': 'Foxtrot Group', 'group_type': 'test_group'}
data = {'name': 'Foxtrot Group', 'type': 'test_group'}
foxtrot_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(foxtrot_response.status_code, 201)
foxtrot_group_id = foxtrot_response.data['id']
......@@ -578,7 +581,7 @@ class GroupsApiTests(ModuleStoreTestCase):
response = self.do_post(bravo_groups_uri, data)
self.assertEqual(response.status_code, 201)
data = {'name': 'Tango Group'}
data = {'name': 'Tango Group', 'type': 'test'}
tango_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(tango_response.status_code, 201)
tango_uri = tango_response.data['uri']
......@@ -607,14 +610,14 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.status_code, 404)
def test_group_groups_detail_get_hierarchical(self):
data = {'name': 'Alpha Group'}
data = {'name': 'Alpha Group', 'type': 'test'}
alpha_response = self.do_post(self.base_groups_uri, data)
alpha_group_id = alpha_response.data['id']
self.assertEqual(alpha_response.status_code, 201)
data = {'name': 'Beta Group'}
data = {'name': 'Beta Group', 'type': 'test'}
beta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(beta_response.status_code, 201)
data = {'name': 'Delta Group'}
data = {'name': 'Delta Group', 'type': 'test'}
delta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(delta_response.status_code, 201)
test_uri = alpha_response.data['uri'] + '/groups'
......@@ -634,14 +637,14 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.data['relationship_type'], relationship_type)
def test_group_groups_detail_get_linked(self):
data = {'name': 'Alpha Group'}
data = {'name': 'Alpha Group', 'type': 'test'}
alpha_response = self.do_post(self.base_groups_uri, data)
alpha_group_id = alpha_response.data['id']
self.assertEqual(alpha_response.status_code, 201)
data = {'name': 'Beta Group'}
data = {'name': 'Beta Group', 'type': 'test'}
beta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(beta_response.status_code, 201)
data = {'name': 'Delta Group'}
data = {'name': 'Delta Group', 'type': 'test'}
delta_response = self.do_post(self.base_groups_uri, data)
delta_group_id = delta_response.data['id']
self.assertEqual(delta_response.status_code, 201)
......@@ -664,7 +667,7 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.data['relationship_type'], relationship_type)
def test_group_groups_detail_get_notfound(self):
data = {'name': 'Alpha Group'}
data = {'name': 'Alpha Group', 'type': 'test'}
alpha_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(alpha_response.status_code, 201)
test_uri = alpha_response.data['uri'] + '/groups/gaois89sdf98'
......@@ -672,16 +675,16 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.status_code, 404)
def test_group_groups_detail_delete_hierarchical(self):
data = {'name': 'Alpha Group'}
data = {'name': 'Alpha Group', 'type': 'test'}
alpha_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(alpha_response.status_code, 201)
data = {'name': 'Beta Group'}
data = {'name': 'Beta Group', 'type': 'test'}
beta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(beta_response.status_code, 201)
data = {'name': 'Delta Group'}
data = {'name': 'Delta Group', 'type': 'test'}
delta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(delta_response.status_code, 201)
data = {'name': 'Gamma Group'}
data = {'name': 'Gamma Group', 'type': 'test'}
gamma_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(gamma_response.status_code, 201)
test_uri = alpha_response.data['uri'] + '/groups'
......@@ -703,16 +706,16 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.status_code, 404)
def test_group_groups_detail_delete_linked(self):
data = {'name': 'Alpha Group'}
data = {'name': 'Alpha Group', 'type': 'test'}
alpha_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(alpha_response.status_code, 201)
data = {'name': 'Beta Group'}
data = {'name': 'Beta Group', 'type': 'test'}
beta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(beta_response.status_code, 201)
data = {'name': 'Delta Group'}
data = {'name': 'Delta Group', 'type': 'test'}
delta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(delta_response.status_code, 201)
data = {'name': 'Gamma Group'}
data = {'name': 'Gamma Group', 'type': 'test'}
gamma_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(gamma_response.status_code, 201)
test_uri = alpha_response.data['uri'] + '/groups'
......@@ -737,7 +740,7 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.status_code, 404)
def test_group_courses_list_post(self):
data = {'name': self.test_group_name}
data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201)
group_id = response.data['id']
......@@ -751,7 +754,7 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.data['course_id'], self.test_course_id)
def test_group_courses_list_post_duplicate(self):
data = {'name': self.test_group_name}
data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201)
test_uri = response.data['uri'] + '/courses'
......@@ -768,7 +771,7 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.status_code, 404)
def test_group_courses_list_post_invalid_course(self):
data = {'name': self.test_group_name}
data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201)
test_uri = response.data['uri'] + '/courses'
......@@ -777,7 +780,7 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.status_code, 404)
def test_group_courses_list_get(self):
data = {'name': self.test_group_name}
data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201)
group_id = response.data['id']
......@@ -791,9 +794,9 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.data['course_id'], self.test_course_id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['courses']), 1)
self.assertEqual(response.data['courses'][0]['course_id'], self.test_course_id)
self.assertEqual(response.data['courses'][0]['display_name'], self.course.display_name)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]['course_id'], self.test_course_id)
self.assertEqual(response.data[0]['display_name'], self.course.display_name)
def test_group_courses_list_get_invalid_group(self):
test_uri = self.base_groups_uri + '/1231241/courses'
......@@ -801,7 +804,7 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.status_code, 404)
def test_group_courses_detail_get(self):
data = {'name': self.test_group_name}
data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201)
group_id = response.data['id']
......@@ -823,7 +826,7 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.data['course_id'], self.test_course_id)
def test_group_courses_detail_delete(self):
data = {'name': self.test_group_name}
data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201)
test_uri = response.data['uri'] + '/courses'
......@@ -844,7 +847,7 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.status_code, 204)
def test_group_courses_detail_delete_invalid_course(self):
data = {'name': self.test_group_name}
data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201)
test_uri = response.data['uri'] + '/courses/123124'
......@@ -852,7 +855,7 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.status_code, 204)
def test_group_courses_detail_get_undefined(self):
data = {'name': self.test_group_name}
data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201)
test_uri = '{}/courses/{}'.format(response.data['uri'], self.course.id)
......
......@@ -20,113 +20,182 @@ from xmodule.modulestore import Location, InvalidLocationError
RELATIONSHIP_TYPES = {'hierarchical': 'h', 'graph': 'g'}
def _generate_base_uri(request):
def _generate_base_uri(request, include_query_string=True):
"""
Constructs the protocol:host:path component of the resource uri
"""
protocol = 'http'
if request.is_secure():
protocol = protocol + 's'
if include_query_string:
path_to_use = request.get_full_path()
else:
path_to_use = request.path_info
resource_uri = '{}://{}{}'.format(
protocol,
request.get_host(),
request.get_full_path()
path_to_use
)
return resource_uri
class GroupsList(APIView):
"""
### The GroupsList view allows clients to retrieve/append a list of Group entities
- URI: ```/api/groups/```
- GET: Returns a JSON representation (array) of the set of Group entities
* type: __required__, Set filtering parameter
- POST: Provides the ability to append to the Group entity set
* name: The name of the group being added
* type: __required__, Client-specified Group entity type, used for set filtering
* data: Free-form, JSON-formatted metadata attached to this Group entity
- POST Example:
{
"name" : "Alpha Series",
"type" : "series",
"data" : {
"display_name" : "Demo Program",
"start_date" : "2014-01-01",
"end_date" : "2014-12-31"
}
}
### Use Cases/Notes:
* GET requests for _all_ groups are not currently allowed via the API
* If no 'type' parameter is specified during GET, the server will return a 400 Bad Request
* 'type' is a free-form field used to tag/filter group entities.
* Some sample of types include:
** workgroup: a group of users working on a project together
** series: a group of related courses
** company: a group of groups (such as departments)
* 'data' is a free-form field containing type-specific metadata in JSON format, which bypasses the need for extensive database modeling
* Some sample 'data' elements include:
** series: display_name, start_date, end_date
** organization: display_name, contact_name, phone, email
* Ultimately, both 'type' and 'data' are determined by the client/caller. Open edX has no type or data specifications at the present time.
"""
permissions_classes = (ApiKeyHeaderPermission,)
def post(self, request):
"""
POST creates a new group in the system
POST /api/groups
"""
group_type = request.DATA.get('type', None)
if group_type is None:
return Response({}, status=status.HTTP_400_BAD_REQUEST)
response_data = {}
base_uri = _generate_base_uri(request)
# Group name must be unique, but we need to support dupes
group = Group.objects.create(name=str(uuid.uuid4()))
if request.DATA.get('name'):
original_group_name = request.DATA.get('name')
else:
original_group_name = request.DATA.get('name', None)
if original_group_name is None or len(original_group_name) == 0:
return Response(response_data, status=status.HTTP_400_BAD_REQUEST)
group.name = '{:04d}: {}'.format(group.id, original_group_name)
group.record_active = True
group.record_date_created = timezone.now()
group.record_date_modified = timezone.now()
group.save()
# Create a corresponding relationship management record
GroupRelationship.objects.create(group_id=group.id, parent_group=None)
# Create a corresponding profile record (for extra meta info)
group_type = request.DATA.get('group_type', None)
data = json.dumps(request.DATA.get('data')) if request.DATA.get('data') else {}
profile, _ = GroupProfile.objects.get_or_create(group_id=group.id, group_type=group_type, name=original_group_name, data=data)
data = request.DATA.get('data', {})
profile, _ = GroupProfile.objects.get_or_create(
group_id=group.id,
group_type=group_type,
name=original_group_name,
data=json.dumps(data)
)
response_data = {
'id': group.id,
'name': original_group_name,
'type': group_type,
'name': profile.name,
}
base_uri = _generate_base_uri(request)
response_data['uri'] = '{}/{}'.format(base_uri, group.id)
response_data['uri'] = '{}/{}'.format(_generate_base_uri(request, False), group.id)
response_status = status.HTTP_201_CREATED
return Response(response_data, status=response_status)
def get(self, request):
"""
GET retrieves a list of groups in the system filtered by type
GET /api/groups
"""
response_data = []
if 'type' in request.GET:
profiles = GroupProfile.objects.filter(group_type=request.GET['type'])
else:
profiles = GroupProfile.objects.all()
group_type = request.QUERY_PARAMS.get('type', None)
if group_type is None:
return Response({}, status=status.HTTP_400_BAD_REQUEST)
profiles = GroupProfile.objects.filter(group_type=request.GET['type']).select_related('group')
for profile in profiles:
item_data = {}
item_data['group_id'] = profile.group_id
item_data['id'] = profile.group_id
if profile.name and len(profile.name):
group_name = profile.name
else:
group = Group.objects.get(id=profile.group_id)
group_name = group.name
group_name = profile.group.name
item_data['name'] = group_name
if profile.group_type:
item_data['group_type'] = profile.group_type
item_data['type'] = profile.group_type
if profile.data:
item_data['data'] = json.loads(profile.data)
item_data['uri'] = '{}/{}'.format(_generate_base_uri(request, False), profile.group_id)
response_data.append(item_data)
return Response(response_data, status=status.HTTP_200_OK)
class GroupsDetail(APIView):
"""
### The GroupsDetail view allows clients to interact with a specific Group entity
- URI: ```/api/groups/{group_id}```
- GET: Returns a JSON representation of the specified Group entity
- POST: Provides the ability to modify specific fields for this Group entity
* type: Client-specified Group entity type
* data: Free-form, JSON-formatted metadata attached to this Group entity
- POST Example:
{
"type" : "series",
"data" : {
"display_name" : "Demo Program",
"start_date" : "2014-01-01",
"end_date" : "2014-12-31"
}
}
### Use Cases/Notes:
* Use the GroupsDetail view to obtain the current state for a specific Group
* For POSTs, you may include only those parameters you wish to modify, for example:
** Modifying the start_date for a 'series' without changing the 'type' field
* A GET response will additionally include a list of URIs to available sub-resources:
** Related Users (/api/groups/{group_id}/users)
** Related Courses (/api/groups/{group_id}/courses)
** Related Groups(/api/groups/{group_id}/groups)
"""
permission_classes = (ApiKeyHeaderPermission,)
def post(self, request, group_id):
"""
POST /api/groups/{group_id}
"""
response_data = {}
base_uri = _generate_base_uri(request)
print base_uri
try:
existing_group = Group.objects.get(id=group_id)
except ObjectDoesNotExist:
return Response({}, status.HTTP_404_NOT_FOUND)
group_type = request.DATA.get('group_type')
data = json.dumps(request.DATA.get('data')) if request.DATA.get('data') else None
if not group_type and not data:
return Response({}, status.HTTP_400_BAD_REQUEST)
profile, _ = GroupProfile.objects.get_or_create(group_id=group_id)
group_type = request.DATA.get('type', None)
if group_type:
profile.group_type = group_type
profile.data = data
data = request.DATA.get('data', None)
if data:
profile.data = json.dumps(data)
profile.save()
response_data['id'] = existing_group.id
response_data['name'] = profile.name
response_data['type'] = profile.group_type
response_data['uri'] = _generate_base_uri(request)
return Response(response_data, status=status.HTTP_201_CREATED)
return Response(response_data, status=status.HTTP_200_OK)
def get(self, request, group_id):
"""
GET retrieves an existing group from the system
GET /api/groups/{group_id}
"""
response_data = {}
base_uri = _generate_base_uri(request)
......@@ -141,6 +210,8 @@ class GroupsDetail(APIView):
response_data['resources'].append({'uri': resource_uri})
resource_uri = '{}/groups'.format(base_uri)
response_data['resources'].append({'uri': resource_uri})
resource_uri = '{}/courses'.format(base_uri)
response_data['resources'].append({'uri': resource_uri})
try:
group_profile = GroupProfile.objects.get(group_id=group_id)
except ObjectDoesNotExist:
......@@ -151,7 +222,7 @@ class GroupsDetail(APIView):
else:
response_data['name'] = existing_group.name
if group_profile.group_type:
response_data['group_type'] = group_profile.group_type
response_data['type'] = group_profile.group_type
data = group_profile.data
if data:
response_data['data'] = json.loads(data)
......@@ -161,11 +232,27 @@ class GroupsDetail(APIView):
class GroupsUsersList(APIView):
"""
### The GroupsUserList view allows clients to interact with the set of User entities related to the specified Group
- URI: ```/api/groups/{group_id}/users/```
- GET: Returns a JSON representation (array) of the set of related User entities
- POST: Append a User entity to the set of related User entities for the specified group
* user_id: __required__, The identifier for the User being added
- POST Example:
{
"user_id" : 123
}
### Use Cases/Notes:
* Use the GroupsUsersList view to manage User membership for a specific Group
* For example, as a newly-added member of a 'workgroup' group, a User could be presented with a list of their peers
* Once a User Group exists, you can additionally link to Courses and other Groups (see GroupsCoursesList, GroupsGroupsList)
"""
permission_classes = (ApiKeyHeaderPermission,)
def post(self, request, group_id):
"""
POST creates a new group-user relationship in the system
POST /api/groups/{group_id}/users/
"""
base_uri = _generate_base_uri(request)
try:
......@@ -178,7 +265,7 @@ class GroupsUsersList(APIView):
except ObjectDoesNotExist:
return Response({}, status.HTTP_404_NOT_FOUND)
try:
existing_relationship = Group.objects.get(user=existing_user)
existing_relationship = Group.objects.filter(id=existing_group.id).get(user=existing_user)
except ObjectDoesNotExist:
existing_relationship = None
response_data = {}
......@@ -196,7 +283,7 @@ class GroupsUsersList(APIView):
def get(self, request, group_id):
"""
GET retrieves the list of users related to the specified group
GET /api/groups/{group_id}/users/
"""
try:
existing_group = Group.objects.get(id=group_id)
......@@ -218,11 +305,20 @@ class GroupsUsersList(APIView):
class GroupsUsersDetail(APIView):
"""
### The GroupsUsersDetail view allows clients to interact with a specific Group-User relationship
- URI: ```/api/groups/{group_id}/users/{user_id}```
- GET: Returns a JSON representation of the specified Group-User relationship
- DELETE: Removes an existing Group-User relationship
### Use Cases/Notes:
* Use the GroupsUsersDetail to validate that a User is a member of a specific Group
* Cancelling a User's membership in a Group is as simple as calling DELETE on the URI
"""
permission_classes = (ApiKeyHeaderPermission,)
def get(self, request, group_id, user_id):
"""
GET retrieves an existing group-user relationship from the system
GET /api/groups/{group_id}/users/{user_id}
"""
response_data = {}
base_uri = _generate_base_uri(request)
......@@ -255,11 +351,41 @@ class GroupsUsersDetail(APIView):
class GroupsGroupsList(APIView):
"""
### The GroupsGroupsList view allows clients to interact with the set of Groups related to the specified Group
- URI: ```/api/groups/{group_id}/groups/```
- GET: Returns a JSON representation (array) of the set of related Group entities
- POST: Provides the ability to append to the related Group entity set
* group_id: __required__, The name of the Group being added
* relationship_type: __required__, Relationship paradigm, select from the following values:
** 'g', _graph_, create a graph(aka, linked) relationship with the specified Group
** 'h', _hierarchical_, create a parent-child relationship with the specified Group
- POST Example:
{
"group_id" : 1234,
"relationship_type" : "g"
}
### Use Cases/Notes:
* Use a graph-type relationship when you simply want to indicate a link between two groups:
** Linking a course series with a particular company
** Linking a user workgroup with a particular course series
* Use a hierarchical-type relationship when you want to enforce a parent-child link between two groups:
** Linking a company (parent) to a department (child)
** Linking a user workgroup (parent) to a breakout user workgroup (child)
* Note that posting a new hierarchical relationship for a child group having a parent will overwrite the current relationship:
** POST /groups/123/groups { "group_id": 246}
** GET /groups/123/groups/246 -> 200 OK
** POST /groups/987/groups {"group_id": 246}
** GET /groups/123/groups/246 -> 404 NOT FOUND
** GET /groups/987/groups/246 -> 200 OK
* Once a Group Group exists, you can additionally link to Users and Courses (see GroupsUsersList, GroupsCoursesList)
"""
permission_classes = (ApiKeyHeaderPermission,)
def post(self, request, group_id):
"""
POST creates a new group-group relationship in the system
POST /api/groups/{group_id}/groups/{related_group_id}
"""
response_data = {}
to_group_id = request.DATA['group_id']
......@@ -291,7 +417,7 @@ class GroupsGroupsList(APIView):
def get(self, request, group_id):
"""
GET retrieves the existing group-group relationships for the specified group
GET /api/groups/{group_id}/groups/{related_group_id}
"""
try:
from_group_relationship = GroupRelationship.objects.get(group__id=group_id)
......@@ -329,11 +455,22 @@ class GroupsGroupsList(APIView):
class GroupsGroupsDetail(APIView):
"""
### The GroupsGroupsDetail view allows clients to interact with a specific Group-Group relationship
- URI: ```/api/groups/{group_id}/groups/{related_group_id}```
- GET: Returns a JSON representation of the specified Group-Group relationship
- DELETE: Removes an existing Group-Group relationship
### Use Cases/Notes:
* Use the GroupsGroupsDetail operation to confirm that a relationship exists between two Groups
** Is the current workgroup linked to the specified company?
** Is the current course series linked to the specified workgroup?
* To remove an existing Group-Group relationship, simply call DELETE on the URI
"""
permission_classes = (ApiKeyHeaderPermission,)
def get(self, request, group_id, related_group_id):
"""
GET retrieves an existing group-group relationship from the system
GET /api/groups/{group_id}/groups/{related_group_id}
"""
response_data = {}
base_uri = _generate_base_uri(request)
......@@ -357,7 +494,7 @@ class GroupsGroupsDetail(APIView):
def delete(self, request, group_id, related_group_id):
"""
DELETE removes/inactivates/etc. an existing group-group relationship
DELETE /api/groups/{group_id}/groups/{related_group_id}
"""
try:
from_group_relationship = GroupRelationship.objects.get(group__id=group_id)
......@@ -384,11 +521,26 @@ class GroupsGroupsDetail(APIView):
class GroupsCoursesList(APIView):
"""
### The GroupsCoursesList view allows clients to interact with the set of Courses related to the specified Group
- URI: ```/api/groups/{group_id}/courses/```
- GET: Returns a JSON representation (array) of the set of related Course entities
- POST: Provides the ability to append to the related Course entity set
* course_id: __required__, The name of the Course being added
- POST Example:
{
"course_id" : "edx/demo/course",
}
### Use Cases/Notes:
* Create a Group of Courses to model cases such as an academic program or topical series
* Once a Course Group exists, you can additionally link to Users and other Groups (see GroupsUsersList, GroupsGroupsList)
"""
permission_classes = (ApiKeyHeaderPermission,)
def post(self, request, group_id):
"""
POST creates a new group-course relationship in the system
POST /api/groups/{group_id}/courses/{course_id}
"""
response_data = {}
try:
......@@ -422,7 +574,7 @@ class GroupsCoursesList(APIView):
def get(self, request, group_id):
"""
GET returns all courses that has a relationship to the group
GET /api/groups/{group_id}/courses/{course_id}
"""
response_data = {}
try:
......@@ -431,24 +583,36 @@ class GroupsCoursesList(APIView):
return Response({}, status.HTTP_404_NOT_FOUND)
store = modulestore()
members = CourseGroupRelationship.objects.filter(group=existing_group)
response_data['courses'] = []
response_data = []
for member in members:
course = store.get_course(member.course_id)
course_data = {
'course_id': member.course_id,
'display_name': course.display_name
}
response_data['courses'].append(course_data)
response_data.append(course_data)
response_status = status.HTTP_200_OK
return Response(response_data, status=response_status)
class GroupsCoursesDetail(APIView):
"""
### The GroupsCoursesDetail view allows clients to interact with a specific Group-Course relationship
- URI: ```/api/groups/{group_id}/courses/{course_id}```
- GET: Returns a JSON representation of the specified Group-Course relationship
- DELETE: Removes an existing Group-Course relationship
### Use Cases/Notes:
* Use the GroupsCoursesDetail to validate that a Course is linked to a specific Group
* Is Course part of the specified series?
* Is Course linked to the specified workgroup?
* Removing a Course from a Group is as simple as calling DELETE on the URI
* Remove a course from the specified academic program
"""
permission_classes = (ApiKeyHeaderPermission,)
def get(self, request, group_id, course_id):
"""
GET retrieves an existing group-course relationship from the system
GET /api/groups/{group_id}/courses/{course_id}
"""
response_data = {}
base_uri = _generate_base_uri(request)
......@@ -469,7 +633,7 @@ class GroupsCoursesDetail(APIView):
def delete(self, request, group_id, course_id):
"""
DELETE removes/inactivates/etc. an existing group-course relationship
DELETE /api/groups/{group_id}/courses/{course_id}
"""
try:
existing_group = Group.objects.get(id=group_id)
......
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'CourseContentGroupRelationship'
db.create_table('api_manager_coursecontentgrouprelationship', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('created', self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now)),
('modified', self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now)),
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('content_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('group', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['api_manager.GroupProfile'])),
('record_active', self.gf('django.db.models.fields.BooleanField')(default=True)),
))
db.send_create_signal('api_manager', ['CourseContentGroupRelationship'])
db.create_index('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id'], unique=False, db_tablespace='')
def backwards(self, orm):
# Deleting model 'CourseContentGroupRelationship'
db.delete_table('api_manager_coursecontentgrouprelationship')
models = {
'api_manager.coursegrouprelationship': {
'Meta': {'object_name': 'CourseGroupRelationship'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'record_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
'api_manager.groupprofile': {
'Meta': {'object_name': 'GroupProfile', 'db_table': "'auth_groupprofile'"},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'data': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}),
'group_type': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'record_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
'api_manager.grouprelationship': {
'Meta': {'object_name': 'GroupRelationship'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'group': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.Group']", 'unique': 'True', 'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'parent_group': ('django.db.models.fields.related.ForeignKey', [], {'default': '0', 'related_name': "'child_groups'", 'null': 'True', 'blank': 'True', 'to': "orm['api_manager.GroupRelationship']"}),
'record_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
'api_manager.linkedgrouprelationship': {
'Meta': {'object_name': 'LinkedGroupRelationship'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'from_group_relationship': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'from_group_relationships'", 'to': "orm['api_manager.GroupRelationship']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'record_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'to_group_relationship': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'to_group_relationships'", 'to': "orm['api_manager.GroupRelationship']"})
},
'api_manager.coursecontentgrouprelationship': {
'Meta': {'object_name': 'CourseContentGroupRelationship'},
'content_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['api_manager.GroupProfile']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'record_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}
}
complete_apps = ['api_manager']
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group']
db.create_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_id'])
def backwards(self, orm):
# Removing unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group']
db.delete_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_id'])
models = {
'api_manager.coursecontentgrouprelationship': {
'Meta': {'unique_together': "(('course_id', 'content_id', 'group'),)", 'object_name': 'CourseContentGroupRelationship'},
'content_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['api_manager.GroupProfile']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'record_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
'api_manager.coursegrouprelationship': {
'Meta': {'object_name': 'CourseGroupRelationship'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'record_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
'api_manager.groupprofile': {
'Meta': {'object_name': 'GroupProfile', 'db_table': "'auth_groupprofile'"},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'data': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}),
'group_type': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'record_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
'api_manager.grouprelationship': {
'Meta': {'object_name': 'GroupRelationship'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'group': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.Group']", 'unique': 'True', 'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'parent_group': ('django.db.models.fields.related.ForeignKey', [], {'default': '0', 'related_name': "'child_groups'", 'null': 'True', 'blank': 'True', 'to': "orm['api_manager.GroupRelationship']"}),
'record_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
'api_manager.linkedgrouprelationship': {
'Meta': {'object_name': 'LinkedGroupRelationship'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'from_group_relationship': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'from_group_relationships'", 'to': "orm['api_manager.GroupRelationship']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'record_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'to_group_relationship': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'to_group_relationships'", 'to': "orm['api_manager.GroupRelationship']"})
},
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}
}
complete_apps = ['api_manager']
\ No newline at end of file
......@@ -106,3 +106,20 @@ class GroupProfile(TimeStampedModel):
name = models.CharField(max_length=255, null=True, blank=True)
data = models.TextField(blank=True) # JSON dictionary for generic key/value pairs
record_active = models.BooleanField(default=True)
class CourseContentGroupRelationship(TimeStampedModel):
"""
The CourseContentGroupRelationship model contains information describing the
link between a particular courseware element (chapter, unit, video, etc.)
and a group. A typical use case for this table is to support the concept
of a student workgroup for a given course, where the project is actually
a Chapter courseware element.
"""
course_id = models.CharField(max_length=255, db_index=True)
content_id = models.CharField(max_length=255, db_index=True)
group = models.ForeignKey(GroupProfile, db_index=True)
record_active = models.BooleanField(default=True)
class Meta:
unique_together = ("course_id", "content_id", "group")
......@@ -239,7 +239,7 @@ class UsersApiTests(TestCase):
def test_user_groups_list_post(self):
test_uri = '/api/groups'
data = {'name': 'Alpha Group'}
data = {'name': 'Alpha Group', 'type': 'test'}
response = self.do_post(test_uri, data)
group_id = response.data['id']
test_uri = '/api/users'
......@@ -262,7 +262,7 @@ class UsersApiTests(TestCase):
def test_user_groups_list_post_duplicate(self):
test_uri = '/api/groups'
data = {'name': 'Alpha Group'}
data = {'name': 'Alpha Group', 'type': 'test'}
response = self.do_post(test_uri, data)
group_id = response.data['id']
test_uri = '/api/users'
......@@ -281,7 +281,7 @@ class UsersApiTests(TestCase):
def test_user_groups_list_post_invalid_user(self):
test_uri = '/api/groups'
data = {'name': 'Alpha Group'}
data = {'name': 'Alpha Group', 'type': 'test'}
response = self.do_post(test_uri, data)
group_id = response.data['id']
test_uri = '/api/users/897698769/groups'
......@@ -292,7 +292,7 @@ class UsersApiTests(TestCase):
def test_user_groups_list_get(self):
test_uri = '/api/groups'
group_name = 'Alpha Group'
data = {'name': group_name}
data = {'name': group_name, 'type': 'test'}
response = self.do_post(test_uri, data)
group_id = response.data['id']
test_uri = '/api/users'
......@@ -327,7 +327,7 @@ class UsersApiTests(TestCase):
group_url = '/api/groups'
group_name = 'Alpha Group'
data = {'name': group_name, 'group_type': 'Engineer'}
data = {'name': group_name, 'type': 'Engineer'}
response = self.do_post(group_url, data)
group_id = response.data['id']
user_groups_uri = '{}/groups'.format(test_uri)
......@@ -336,7 +336,7 @@ class UsersApiTests(TestCase):
self.assertEqual(response.status_code, 201)
group_name = 'Beta Group'
data = {'name': group_name, 'group_type': 'Architect'}
data = {'name': group_name, 'type': 'Architect'}
response = self.do_post(group_url, data)
group_id = response.data['id']
data = {'group_id': group_id}
......@@ -367,7 +367,7 @@ class UsersApiTests(TestCase):
def test_user_groups_detail_get(self):
test_uri = '/api/groups'
data = {'name': 'Alpha Group'}
data = {'name': 'Alpha Group', 'type': 'test'}
response = self.do_post(test_uri, data)
group_id = response.data['id']
test_uri = '/api/users'
......@@ -390,7 +390,7 @@ class UsersApiTests(TestCase):
def test_user_groups_detail_delete(self):
test_uri = '/api/groups'
data = {'name': 'Alpha Group'}
data = {'name': 'Alpha Group', 'type': 'test'}
response = self.do_post(test_uri, data)
group_id = response.data['id']
test_uri = '/api/users'
......@@ -419,7 +419,7 @@ class UsersApiTests(TestCase):
def test_user_groups_detail_get_undefined(self):
test_uri = '/api/groups'
data = {'name': 'Alpha Group'}
data = {'name': 'Alpha Group', 'type': 'test'}
response = self.do_post(test_uri, data)
group_id = response.data['id']
test_uri = '/api/users'
......@@ -529,8 +529,8 @@ class UsersApiTests(TestCase):
self.assertEqual(response.status_code, 201)
position_data = {
'position': {
'parent_module_id': str(course.id),
'child_module_id': str(chapter3.location)
'parent_content_id': str(course.id),
'child_content_id': str(chapter3.location)
}
}
......@@ -551,15 +551,15 @@ class UsersApiTests(TestCase):
test_uri = '/api/users/{}/courses/{}'.format(str(user_id), course_id)
position_data = {
'position': {
'parent_module_id': course_id,
'child_module_id': str(chapter1.location)
'parent_content_id': course_id,
'child_content_id': str(chapter1.location)
}
}
response = self.do_post(test_uri, data=position_data)
self.assertEqual(response.status_code, 404)
def test_user_courses_detail_post_position_course_as_module(self):
def test_user_courses_detail_post_position_course_as_content(self):
course = CourseFactory.create()
test_data = '<html>{}</html>'.format(str(uuid.uuid4()))
chapter1 = ItemFactory.create(
......@@ -581,8 +581,8 @@ class UsersApiTests(TestCase):
self.assertEqual(response.status_code, 201)
position_data = {
'position': {
'parent_module_id': str(course.location),
'child_module_id': str(chapter1.location)
'parent_content_id': str(course.location),
'child_content_id': str(chapter1.location)
}
}
......@@ -617,8 +617,8 @@ class UsersApiTests(TestCase):
self.assertEqual(response.data['user_id'], user_id)
position_data = {
'position': {
'parent_module_id': str(course.location),
'child_module_id': str(chapter1.location)
'parent_content_id': str(course.location),
'child_content_id': str(chapter1.location)
}
}
......
......@@ -31,6 +31,7 @@ from util.bad_request_rate_limiter import BadRequestRateLimiter
log = logging.getLogger(__name__)
AUDIT_LOG = logging.getLogger("audit")
def _generate_base_uri(request):
"""
Constructs the protocol:host:path component of the resource uri
......@@ -57,6 +58,7 @@ def _serialize_user_profile(response_data, user_profile):
return response_data
def _serialize_user(response_data, user):
"""
Loads the object data into the response dict
......@@ -70,14 +72,15 @@ def _serialize_user(response_data, user):
response_data['is_active'] = user.is_active
return response_data
def _save_module_position(request, user, course_id, course_descriptor, position):
def _save_content_position(request, user, course_id, course_descriptor, position):
"""
Records the indicated position for the specified course
Really no reason to generalize this out of user_courses_detail aside from pylint complaining
"""
field_data_cache = FieldDataCache([course_descriptor], course_id, user)
if course_id == position['parent_module_id']:
parent_module = get_module_for_descriptor(
if course_id == position['parent_content_id']:
parent_content = get_module_for_descriptor(
user,
request,
course_descriptor,
......@@ -85,23 +88,23 @@ def _save_module_position(request, user, course_id, course_descriptor, position)
course_id
)
else:
parent_module = module_render.get_module(
parent_content = module_render.get_module(
user,
request,
position['parent_module_id'],
position['parent_content_id'],
field_data_cache,
course_id
)
child_module = module_render.get_module(
child_content = module_render.get_module(
user,
request,
position['child_module_id'],
position['child_content_id'],
field_data_cache,
course_id
)
save_child_position(parent_module, child_module.location.name)
saved_module = get_current_child(parent_module)
return saved_module.id
save_child_position(parent_content, child_content.location.name)
saved_content = get_current_child(parent_content)
return saved_content.id
class UsersList(APIView):
......@@ -539,7 +542,7 @@ class UsersCoursesDetail(APIView):
response_data['course_id'] = course_id
response_status = status.HTTP_201_CREATED
if request.DATA['position']:
response_data['position'] = _save_module_position(
response_data['position'] = _save_content_position(
request,
user,
course_id,
......@@ -568,13 +571,13 @@ class UsersCoursesDetail(APIView):
response_data['course_id'] = course_id
response_data['uri'] = base_uri
field_data_cache = FieldDataCache([course_descriptor], course_id, user)
course_module = module_render.get_module(
course_content = module_render.get_module(
user,
request,
course_descriptor.location,
field_data_cache,
course_id)
response_data['position'] = course_module.position
response_data['position'] = course_content.position
response_status = status.HTTP_200_OK
else:
response_status = status.HTTP_404_NOT_FOUND
......
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