Commit bf5bf27b by Matt Drayer Committed by Jonathan Piacenti

mattdrayer/rebase-20140701: Updated api to support opaque keys

Also contains:

* mattdrayer/rebase-20140722b: cherry-pick 0325694
* mattdrayer/rebase-20140722: cherry-pick e8b1217
* Sessions API Documentation
* mattdrayer/rebase-20140722: cherry-pick ofao48d
* mattdrayer/rebase-20140722: cherry-pick b0c343c
* mattdrayer/rebase-20140722: cherry-pick d56e973
* mattdrayer/status-500-fix: Small API bug
* added content_id to workgroup review submissions
* mattdrayer/api-user-preferences-delete: Added new Detail view, GET/DELETE operations
* mattdrayer/rebase-20140722: cherry-pick 0624385
* handle exception if user never accessed any course module
* mattdrayer/rebase-20140722: cherry-pick caada51
* cdodge/add-progress-publish-api-endpoint: expose a new xblock runtime publish special cased event type 'progress' which adds an entry into the CourseCompletions table
* add default setting for feature flag in common.py
* mattdrayer/rebase-20140722: cherry-pick 7a1e12c
* mattdrayer/rebase-20140722: Functional stabilization
parent df7e1ab9
...@@ -7,6 +7,7 @@ import warnings ...@@ -7,6 +7,7 @@ import warnings
from django.db import models from django.db import models
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from opaque_keys.edx.keys import CourseKey, UsageKey, BlockTypeKey from opaque_keys.edx.keys import CourseKey, UsageKey, BlockTypeKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from south.modelsinspector import add_introspection_rules from south.modelsinspector import add_introspection_rules
...@@ -104,7 +105,7 @@ class OpaqueKeyField(models.CharField): ...@@ -104,7 +105,7 @@ class OpaqueKeyField(models.CharField):
return None return None
if isinstance(value, basestring): if isinstance(value, basestring):
return self.KEY_CLASS.from_string(value) return SlashSeparatedCourseKey.from_deprecated_string(value)
else: else:
return value return value
...@@ -148,7 +149,6 @@ class CourseKeyField(OpaqueKeyField): ...@@ -148,7 +149,6 @@ class CourseKeyField(OpaqueKeyField):
description = "A CourseKey object, saved to the DB in the form of a string" description = "A CourseKey object, saved to the DB in the form of a string"
KEY_CLASS = CourseKey KEY_CLASS = CourseKey
class UsageKeyField(OpaqueKeyField): class UsageKeyField(OpaqueKeyField):
""" """
A django Field that stores a UsageKey object as a string. A django Field that stores a UsageKey object as a string.
......
...@@ -47,5 +47,9 @@ Sessions ...@@ -47,5 +47,9 @@ Sessions
* - Goal * - Goal
- Resource - Resource
* - * - :ref:`Create a Session`
- - POST {"username": "name", "password": "password"}
\ No newline at end of file * - :ref:`Get Session Details`
- GET /api/sessions/{session_id}
* - :ref:`Delete a Session`
- DELETE /api/sessions/{session_id}
\ No newline at end of file
...@@ -4,6 +4,96 @@ Sessions API Module ...@@ -4,6 +4,96 @@ Sessions API Module
.. module:: api_manager .. module:: api_manager
The page contains docstrings for: The page contains docstrings and example responses for:
* * `Create a Session`_
\ No newline at end of file * `Get Session Details`_
* `Delete a Session`_
.. _Create a Session:
**************************
Create a Session
**************************
.. autoclass:: sessions.views.SessionsList
:members:
**Example post**
.. code-block:: json
{
"username": "name",
"password": "password"
}
**Example response**
.. code-block:: json
HTTP 201 CREATED
Vary: Accept
Content-Type: text/html; charset=utf-8
Allow: POST, OPTIONS
{
"token": "938680977d04ed091b67b974b6b6be60",
"expires": 604800,
"user": {
"id": 4,
"email": "staff@example.com",
"username": "staff",
"first_name": "",
"last_name": "",
"created": "2014-04-18T13:44:25Z",
"organizations": []
},
"uri": "http://localhost:8000/api/sessions?username=staff&password=edx/938680977d04ed091b67b974b6b6be60"
}
.. _Get Session Details:
**************************
Get Session Details
**************************
.. autoclass:: sessions.views.SessionsDetail
:members:
**Example GET response**
.. code-block:: json
HTTP 200 OK
Vary: Accept
Content-Type: text/html; charset=utf-8
Allow: GET, DELETE, HEAD, OPTIONS
{
"token": "8c510db85585c64bd33bede01d645a60",
"expires": 1209600,
"user_id": 1,
"uri": "http://localhost:8000/api/sessions//8c510db85585c64bd33bede01d645a60"
}
.. _Delete a Session:
**************************
Delete a Session
**************************
.. autoclass:: sessions.views.SessionsDetail
:members:
**Example DELETE response**
.. code-block:: json
HTTP 204 NO CONTENT
Vary: Accept
Content-Type: text/html; charset=utf-8
Allow: GET, DELETE, HEAD, OPTIONS
{}
\ No newline at end of file
...@@ -11,7 +11,7 @@ class CourseModuleCompletionSerializer(serializers.ModelSerializer): ...@@ -11,7 +11,7 @@ class CourseModuleCompletionSerializer(serializers.ModelSerializer):
class Meta: class Meta:
""" Serializer/field specification """ """ Serializer/field specification """
model = CourseModuleCompletion model = CourseModuleCompletion
fields = ('id', 'user_id', 'course_id', 'content_id', 'created', 'modified') fields = ('id', 'user_id', 'course_id', 'content_id', 'stage', 'created', 'modified')
read_only = ('id', 'created') read_only = ('id', 'created')
......
...@@ -3,9 +3,11 @@ ...@@ -3,9 +3,11 @@
Run these tests @ Devstack: Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/courses/tests.py] rake fasttest_lms[common/djangoapps/api_manager/courses/tests.py]
""" """
from datetime import datetime
import json import json
import uuid import uuid
from random import randint from random import randint
from urllib import urlencode
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.core.cache import cache from django.core.cache import cache
...@@ -16,7 +18,6 @@ from capa.tests.response_xml_factory import StringResponseXMLFactory ...@@ -16,7 +18,6 @@ from capa.tests.response_xml_factory import StringResponseXMLFactory
from courseware.tests.factories import StudentModuleFactory from courseware.tests.factories import StudentModuleFactory
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from student.tests.factories import UserFactory, CourseEnrollmentFactory from student.tests.factories import UserFactory, CourseEnrollmentFactory
from xmodule.modulestore import Location
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from .content import TEST_COURSE_OVERVIEW_CONTENT, TEST_COURSE_UPDATES_CONTENT, TEST_COURSE_UPDATES_CONTENT_LEGACY from .content import TEST_COURSE_OVERVIEW_CONTENT, TEST_COURSE_UPDATES_CONTENT, TEST_COURSE_UPDATES_CONTENT_LEGACY
...@@ -25,6 +26,7 @@ from .content import TEST_STATIC_TAB1_CONTENT, TEST_STATIC_TAB2_CONTENT ...@@ -25,6 +26,7 @@ from .content import TEST_STATIC_TAB1_CONTENT, TEST_STATIC_TAB2_CONTENT
TEST_API_KEY = str(uuid.uuid4()) TEST_API_KEY = str(uuid.uuid4())
USER_COUNT = 4 USER_COUNT = 4
class SecureClient(Client): class SecureClient(Client):
""" Django test client using a "secure" connection. """ """ Django test client using a "secure" connection. """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
...@@ -47,8 +49,8 @@ class CoursesApiTests(TestCase): ...@@ -47,8 +49,8 @@ class CoursesApiTests(TestCase):
self.attempts = 3 self.attempts = 3
self.course = CourseFactory.create( self.course = CourseFactory.create(
start="2014-06-16T14:30:00Z", start=datetime(2014, 6, 16, 14, 30),
end="2015-01-16T14:30:00Z" end=datetime(2015, 1, 16)
) )
self.test_data = '<html>{}</html>'.format(str(uuid.uuid4())) self.test_data = '<html>{}</html>'.format(str(uuid.uuid4()))
...@@ -56,7 +58,7 @@ class CoursesApiTests(TestCase): ...@@ -56,7 +58,7 @@ class CoursesApiTests(TestCase):
category="chapter", category="chapter",
parent_location=self.course.location, parent_location=self.course.location,
data=self.test_data, data=self.test_data,
due="2014-05-16T14:30:00Z", due=datetime(2014, 5, 16, 14, 30),
display_name="Overview" display_name="Overview"
) )
...@@ -160,7 +162,7 @@ class CoursesApiTests(TestCase): ...@@ -160,7 +162,7 @@ class CoursesApiTests(TestCase):
max_grade=1 if i < j else 0.5, max_grade=1 if i < j else 0.5,
student=user, student=user,
course_id=self.course.id, course_id=self.course.id,
module_state_key=Location(self.item.location).url(), module_state_key=self.item.location,
state=json.dumps({'attempts': self.attempts}), state=json.dumps({'attempts': self.attempts}),
module_type=module_type module_type=module_type
) )
...@@ -169,18 +171,18 @@ class CoursesApiTests(TestCase): ...@@ -169,18 +171,18 @@ class CoursesApiTests(TestCase):
StudentModuleFactory.create( StudentModuleFactory.create(
course_id=self.course.id, course_id=self.course.id,
module_type='sequential', module_type='sequential',
module_state_key=Location(self.item.location).url(), module_state_key=self.item.location,
) )
self.test_course_id = self.course.id self.test_course_id = unicode(self.course.id)
self.test_bogus_course_id = 'foo/bar/baz' self.test_bogus_course_id = 'i4x://foo/bar/baz'
self.test_course_name = self.course.display_name self.test_course_name = self.course.display_name
self.test_course_number = self.course.number self.test_course_number = self.course.number
self.test_course_org = self.course.org self.test_course_org = self.course.org
self.test_chapter_id = self.chapter.id self.test_chapter_id = unicode(self.chapter.scope_ids.usage_id)
self.test_course_content_id = self.course_content.id self.test_course_content_id = unicode(self.course_content.scope_ids.usage_id)
self.test_bogus_content_id = "j5y://foo/bar/baz" self.test_bogus_content_id = "j5y://foo/bar/baz"
self.test_content_child_id = self.content_child.id self.test_content_child_id = unicode(self.content_child.scope_ids.usage_id)
self.base_course_content_uri = '/api/courses/' + self.test_course_id + '/content' self.base_course_content_uri = '/api/courses/' + self.test_course_id + '/content'
self.base_chapters_uri = self.base_course_content_uri + '?type=chapter' self.base_chapters_uri = self.base_course_content_uri + '?type=chapter'
...@@ -200,9 +202,9 @@ class CoursesApiTests(TestCase): ...@@ -200,9 +202,9 @@ class CoursesApiTests(TestCase):
"""Submit an HTTP POST request""" """Submit an HTTP POST request"""
headers = { headers = {
'X-Edx-Api-Key': str(TEST_API_KEY), 'X-Edx-Api-Key': str(TEST_API_KEY),
'Content-Type': 'application/json'
} }
json_data = json.dumps(data) json_data = json.dumps(data)
response = self.client.post(uri, headers=headers, content_type='application/json', data=json_data) response = self.client.post(uri, headers=headers, content_type='application/json', data=json_data)
return response return response
...@@ -240,7 +242,7 @@ class CoursesApiTests(TestCase): ...@@ -240,7 +242,7 @@ class CoursesApiTests(TestCase):
def test_course_detail_without_date_values(self): def test_course_detail_without_date_values(self):
create_course_with_out_date_values = CourseFactory.create() # pylint: disable=C0103 create_course_with_out_date_values = CourseFactory.create() # pylint: disable=C0103
test_uri = self.base_courses_uri + '/' + create_course_with_out_date_values.id test_uri = self.base_courses_uri + '/' + unicode(create_course_with_out_date_values.id)
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['start'], create_course_with_out_date_values.start) self.assertEqual(response.data['start'], create_course_with_out_date_values.start)
...@@ -253,8 +255,8 @@ class CoursesApiTests(TestCase): ...@@ -253,8 +255,8 @@ class CoursesApiTests(TestCase):
self.assertGreater(len(response.data), 0) self.assertGreater(len(response.data), 0)
self.assertEqual(response.data['id'], self.test_course_id) self.assertEqual(response.data['id'], self.test_course_id)
self.assertEqual(response.data['name'], self.test_course_name) self.assertEqual(response.data['name'], self.test_course_name)
self.assertEqual(response.data['start'], self.course.start) self.assertEqual(datetime.strftime(response.data['start'], '%Y-%m-%d %H:%M:%S'), datetime.strftime(self.course.start, '%Y-%m-%d %H:%M:%S'))
self.assertEqual(response.data['end'], self.course.end) self.assertEqual(datetime.strftime(response.data['end'], '%Y-%m-%d %H:%M:%S'), datetime.strftime(self.course.end, '%Y-%m-%d %H:%M:%S'))
self.assertEqual(response.data['number'], self.test_course_number) self.assertEqual(response.data['number'], self.test_course_number)
self.assertEqual(response.data['org'], self.test_course_org) self.assertEqual(response.data['org'], self.test_course_org)
confirm_uri = self.test_server_prefix + test_uri confirm_uri = self.test_server_prefix + test_uri
...@@ -351,6 +353,11 @@ class CoursesApiTests(TestCase): ...@@ -351,6 +353,11 @@ class CoursesApiTests(TestCase):
matched_child = True matched_child = True
self.assertTrue(matched_child) self.assertTrue(matched_child)
def test_course_content_list_get_invalid_course(self):
test_uri = '{}/{}/content/{}/children'.format(self.base_courses_uri, self.test_bogus_course_id, unicode(self.course_project.scope_ids.usage_id))
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_course_content_list_get_invalid_content(self): def test_course_content_list_get_invalid_content(self):
test_uri = '{}/{}/children'.format(self.base_course_content_uri, self.test_bogus_content_id) test_uri = '{}/{}/children'.format(self.base_course_content_uri, self.test_bogus_content_id)
response = self.do_get(test_uri) response = self.do_get(test_uri)
...@@ -469,12 +476,18 @@ class CoursesApiTests(TestCase): ...@@ -469,12 +476,18 @@ class CoursesApiTests(TestCase):
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 409) self.assertEqual(response.status_code, 409)
def test_courses_groups_list_post_invalid_resources(self): def test_courses_groups_list_post_invalid_course(self):
test_uri = self.base_courses_uri + '/1239/87/8976/groups' test_uri = self.base_courses_uri + '/1239/87/8976/groups'
data = {'group_id': "98723896"} data = {'group_id': "98723896"}
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_courses_groups_list_post_invalid_group(self):
test_uri = '{}/{}/groups'.format(self.base_courses_uri, self.test_course_id)
data = {'group_id': "98723896"}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 404)
def test_courses_groups_detail_get(self): def test_courses_groups_detail_get(self):
data = {'name': self.test_group_name, 'type': 'test'} data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data) response = self.do_post(self.base_groups_uri, data)
...@@ -504,8 +517,6 @@ class CoursesApiTests(TestCase): ...@@ -504,8 +517,6 @@ class CoursesApiTests(TestCase):
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_courses_groups_detail_delete(self): def test_courses_groups_detail_delete(self):
data = {'name': self.test_group_name, 'type': 'test'} data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data) response = self.do_post(self.base_groups_uri, data)
...@@ -596,7 +607,7 @@ class CoursesApiTests(TestCase): ...@@ -596,7 +607,7 @@ class CoursesApiTests(TestCase):
def test_courses_overview_get_invalid_content(self): def test_courses_overview_get_invalid_content(self):
#try a bogus course_id to test failure case #try a bogus course_id to test failure case
test_course = CourseFactory.create() test_course = CourseFactory.create()
test_uri = '{}/{}/overview'.format(self.base_courses_uri, test_course.id) test_uri = '{}/{}/overview'.format(self.base_courses_uri, unicode(test_course.id))
ItemFactory.create( ItemFactory.create(
category="about", category="about",
parent_location=test_course.location, parent_location=test_course.location,
...@@ -646,7 +657,7 @@ class CoursesApiTests(TestCase): ...@@ -646,7 +657,7 @@ class CoursesApiTests(TestCase):
data='', data='',
display_name="updates" display_name="updates"
) )
test_uri = '{}/{}/updates'.format(self.base_courses_uri, test_course.id) test_uri = '{}/{}/updates'.format(self.base_courses_uri, unicode(test_course.id))
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
...@@ -659,14 +670,14 @@ class CoursesApiTests(TestCase): ...@@ -659,14 +670,14 @@ class CoursesApiTests(TestCase):
data=TEST_COURSE_UPDATES_CONTENT_LEGACY, data=TEST_COURSE_UPDATES_CONTENT_LEGACY,
display_name="updates" display_name="updates"
) )
test_uri = self.base_courses_uri + '/' + test_course.id + '/updates' test_uri = self.base_courses_uri + '/' + unicode(test_course.id) + '/updates'
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0) self.assertGreater(len(response.data), 0)
self.assertEqual(response.data['content'], TEST_COURSE_UPDATES_CONTENT_LEGACY) self.assertEqual(response.data['content'], TEST_COURSE_UPDATES_CONTENT_LEGACY)
# then try parsed # then try parsed
test_uri = self.base_courses_uri + '/' + test_course.id + '/updates?parse=True' test_uri = self.base_courses_uri + '/' + unicode(test_course.id) + '/updates?parse=True'
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0) self.assertGreater(len(response.data), 0)
...@@ -749,7 +760,7 @@ class CoursesApiTests(TestCase): ...@@ -749,7 +760,7 @@ class CoursesApiTests(TestCase):
def test_courses_users_list_get_no_students(self): def test_courses_users_list_get_no_students(self):
course = CourseFactory.create(display_name="TEST COURSE", org='TESTORG') course = CourseFactory.create(display_name="TEST COURSE", org='TESTORG')
test_uri = self.base_courses_uri + '/' + course.id + '/users' test_uri = self.base_courses_uri + '/' + unicode(course.id) + '/users'
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0) self.assertGreater(len(response.data), 0)
...@@ -780,7 +791,7 @@ class CoursesApiTests(TestCase): ...@@ -780,7 +791,7 @@ class CoursesApiTests(TestCase):
def test_courses_users_list_post_nonexisting_user_allow(self): def test_courses_users_list_post_nonexisting_user_allow(self):
course = CourseFactory.create(display_name="TEST COURSE", org='TESTORG2') course = CourseFactory.create(display_name="TEST COURSE", org='TESTORG2')
test_uri = self.base_courses_uri + '/' + course.id + '/users' test_uri = self.base_courses_uri + '/' + unicode(course.id) + '/users'
post_data = {} post_data = {}
post_data['email'] = 'test+pending@tester.com' post_data['email'] = 'test+pending@tester.com'
post_data['allow_pending'] = True post_data['allow_pending'] = True
...@@ -860,6 +871,56 @@ class CoursesApiTests(TestCase): ...@@ -860,6 +871,56 @@ class CoursesApiTests(TestCase):
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_courses_users_list_get_filter_by_orgs(self):
# create 5 users
users = []
for i in xrange(1, 6):
data = {
'email': 'test{}@example.com'.format(i),
'username': 'test_user{}'.format(i),
'password': 'test_pass',
'first_name': 'John{}'.format(i),
'last_name': 'Doe{}'.format(i)
}
response = self.do_post('/api/users', data)
self.assertEqual(response.status_code, 201)
users.append(response.data['id'])
# create 3 organizations each one having one user
org_ids = []
for i in xrange(1, 4):
data = {
'name': '{} {}'.format('Test Organization', i),
'display_name': '{} {}'.format('Test Org Display Name', i),
'users': [users[i]]
}
response = self.do_post('/api/organizations/', data)
self.assertEqual(response.status_code, 201)
self.assertGreater(response.data['id'], 0)
org_ids.append(response.data['id'])
# enroll all users in course
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/users'
for user in users:
data = {'user_id': user}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
# retrieve all users enrolled in the course
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreaterEqual(len(response.data['enrollments']), 5)
# retrieve users by organization
response = self.do_get('{}?organizations={}'.format(test_uri, org_ids[0]))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['enrollments']), 1)
# retrieve all users enrolled in the course
response = self.do_get('{}?organizations={},{},{}'.format(test_uri, org_ids[0], org_ids[1], org_ids[2]))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['enrollments']), 3)
def test_courses_users_detail_get(self): def test_courses_users_detail_get(self):
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/users' test_uri = self.base_courses_uri + '/' + self.test_course_id + '/users'
test_user_uri = '/api/users' test_user_uri = '/api/users'
...@@ -877,6 +938,11 @@ class CoursesApiTests(TestCase): ...@@ -877,6 +938,11 @@ class CoursesApiTests(TestCase):
self.assertGreater(response.data['id'], 0) self.assertGreater(response.data['id'], 0)
created_user_id = response.data['id'] created_user_id = response.data['id']
# Submit the query when unenrolled
confirm_uri = '{}/{}'.format(test_uri, created_user_id)
response = self.do_get(confirm_uri)
self.assertEqual(response.status_code, 404)
# now enroll this user in the course # now enroll this user in the course
post_data = {} post_data = {}
post_data['user_id'] = created_user_id post_data['user_id'] = created_user_id
...@@ -888,7 +954,7 @@ class CoursesApiTests(TestCase): ...@@ -888,7 +954,7 @@ class CoursesApiTests(TestCase):
self.assertGreater(len(response.data), 0) self.assertGreater(len(response.data), 0)
def test_courses_users_detail_get_invalid_course(self): def test_courses_users_detail_get_invalid_course(self):
test_uri = self.base_courses_uri + '/' + self.test_bogus_course_id + '/users/213432' test_uri = '{}/{}/users/{}'.format(self.base_courses_uri, self.test_bogus_course_id, self.users[0].id)
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
self.assertGreater(len(response.data), 0) self.assertGreater(len(response.data), 0)
...@@ -928,7 +994,7 @@ class CoursesApiTests(TestCase): ...@@ -928,7 +994,7 @@ class CoursesApiTests(TestCase):
self.assertEqual(response.status_code, 204) self.assertEqual(response.status_code, 204)
def test_courses_users_detail_delete_invalid_course(self): def test_courses_users_detail_delete_invalid_course(self):
test_uri = self.base_courses_uri + '/' + self.test_bogus_course_id + '/users/213432' test_uri = self.base_courses_uri + '/' + self.test_bogus_course_id + '/users/1'
response = self.do_delete(test_uri) response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
...@@ -943,21 +1009,21 @@ class CoursesApiTests(TestCase): ...@@ -943,21 +1009,21 @@ class CoursesApiTests(TestCase):
data = {'name': 'Beta Group', 'type': 'project'} data = {'name': 'Beta Group', 'type': 'project'}
response = self.do_post(self.base_groups_uri, data) response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id'] group_id = response.data['id']
test_uri = '{}/{}/groups'.format(self.base_course_content_uri, self.course_project.id) test_uri = '{}/{}/groups'.format(self.base_course_content_uri, unicode(self.course_project.scope_ids.usage_id))
data = {'group_id': group_id} data = {'group_id': group_id}
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
confirm_uri = self.test_server_prefix + test_uri + '/' + str(group_id) confirm_uri = self.test_server_prefix + test_uri + '/' + str(group_id)
self.assertEqual(response.data['uri'], confirm_uri) self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['course_id'], str(self.test_course_id)) 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['content_id'], unicode(self.course_project.scope_ids.usage_id))
self.assertEqual(response.data['group_id'], str(group_id)) self.assertEqual(response.data['group_id'], str(group_id))
def test_course_content_groups_list_post_duplicate(self): def test_course_content_groups_list_post_duplicate(self):
data = {'name': 'Beta Group', 'type': 'project'} data = {'name': 'Beta Group', 'type': 'project'}
response = self.do_post(self.base_groups_uri, data) response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id'] group_id = response.data['id']
test_uri = '{}/{}/groups'.format(self.base_course_content_uri, self.course_project.id) test_uri = '{}/{}/groups'.format(self.base_course_content_uri, unicode(self.course_project.scope_ids.usage_id))
data = {'group_id': group_id} data = {'group_id': group_id}
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
...@@ -971,7 +1037,7 @@ class CoursesApiTests(TestCase): ...@@ -971,7 +1037,7 @@ class CoursesApiTests(TestCase):
test_uri = '{}/{}/content/{}/groups'.format( test_uri = '{}/{}/content/{}/groups'.format(
self.base_courses_uri, self.base_courses_uri,
self.test_bogus_course_id, self.test_bogus_course_id,
self.course_project.id unicode(self.course_project.scope_ids.usage_id)
) )
data = {'group_id': group_id} data = {'group_id': group_id}
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
...@@ -994,7 +1060,7 @@ class CoursesApiTests(TestCase): ...@@ -994,7 +1060,7 @@ class CoursesApiTests(TestCase):
test_uri = '{}/{}/content/{}/groups'.format( test_uri = '{}/{}/content/{}/groups'.format(
self.base_courses_uri, self.base_courses_uri,
self.test_course_id, self.test_course_id,
self.course_project.id unicode(self.course_project.scope_ids.usage_id)
) )
data = {'group_id': '12398721'} data = {'group_id': '12398721'}
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
...@@ -1004,13 +1070,13 @@ class CoursesApiTests(TestCase): ...@@ -1004,13 +1070,13 @@ class CoursesApiTests(TestCase):
test_uri = '{}/{}/content/{}/groups'.format( test_uri = '{}/{}/content/{}/groups'.format(
self.base_courses_uri, self.base_courses_uri,
self.test_course_id, self.test_course_id,
self.course_project.id unicode(self.course_project.scope_ids.usage_id)
) )
response = self.do_post(test_uri, {}) response = self.do_post(test_uri, {})
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_course_content_groups_list_get(self): def test_course_content_groups_list_get(self):
test_uri = '{}/{}/groups'.format(self.base_course_content_uri, self.course_project.id) test_uri = '{}/{}/groups'.format(self.base_course_content_uri, unicode(self.course_project.scope_ids.usage_id))
data = {'name': 'Alpha Group', 'type': 'test'} data = {'name': 'Alpha Group', 'type': 'test'}
response = self.do_post(self.base_groups_uri, data) response = self.do_post(self.base_groups_uri, data)
alpha_group_id = response.data['id'] alpha_group_id = response.data['id']
...@@ -1049,7 +1115,7 @@ class CoursesApiTests(TestCase): ...@@ -1049,7 +1115,7 @@ class CoursesApiTests(TestCase):
test_uri = '{}/{}/content/{}/groups'.format( test_uri = '{}/{}/content/{}/groups'.format(
self.base_courses_uri, self.base_courses_uri,
self.test_bogus_course_id, self.test_bogus_course_id,
self.course_project.id unicode(self.course_project.scope_ids.usage_id)
) )
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
...@@ -1070,7 +1136,7 @@ class CoursesApiTests(TestCase): ...@@ -1070,7 +1136,7 @@ class CoursesApiTests(TestCase):
response = self.do_post(self.base_groups_uri, data) response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
group_id = response.data['id'] group_id = response.data['id']
test_uri = '{}/{}/groups'.format(self.base_course_content_uri, self.course_project.id) test_uri = '{}/{}/groups'.format(self.base_course_content_uri, unicode(self.course_project.scope_ids.usage_id))
data = {'group_id': group_id} data = {'group_id': group_id}
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
...@@ -1080,7 +1146,7 @@ class CoursesApiTests(TestCase): ...@@ -1080,7 +1146,7 @@ class CoursesApiTests(TestCase):
self.assertEqual(response.data[0]['group_id'], 2) self.assertEqual(response.data[0]['group_id'], 2)
def test_course_content_groups_detail_get(self): def test_course_content_groups_detail_get(self):
test_uri = '{}/{}/groups'.format(self.base_course_content_uri, self.course_project.id) test_uri = '{}/{}/groups'.format(self.base_course_content_uri, unicode(self.course_project.scope_ids.usage_id))
data = {'name': 'Alpha Group', 'type': 'test'} data = {'name': 'Alpha Group', 'type': 'test'}
response = self.do_post(self.base_groups_uri, data) response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id'] group_id = response.data['id']
...@@ -1101,7 +1167,7 @@ class CoursesApiTests(TestCase): ...@@ -1101,7 +1167,7 @@ class CoursesApiTests(TestCase):
data = {'name': 'Alpha Group', 'type': 'test'} data = {'name': 'Alpha Group', 'type': 'test'}
response = self.do_post(self.base_groups_uri, data) response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id'] group_id = response.data['id']
test_uri = '{}/{}/groups/{}'.format(self.base_course_content_uri, self.course_project.id, group_id) test_uri = '{}/{}/groups/{}'.format(self.base_course_content_uri, unicode(self.course_project.scope_ids.usage_id), group_id)
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
...@@ -1109,7 +1175,7 @@ class CoursesApiTests(TestCase): ...@@ -1109,7 +1175,7 @@ class CoursesApiTests(TestCase):
test_uri = '{}/{}/content/{}/groups/123456'.format( test_uri = '{}/{}/content/{}/groups/123456'.format(
self.base_courses_uri, self.base_courses_uri,
self.test_bogus_course_id, self.test_bogus_course_id,
self.course_project.id unicode(self.course_project.scope_ids.usage_id)
) )
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
...@@ -1127,14 +1193,14 @@ class CoursesApiTests(TestCase): ...@@ -1127,14 +1193,14 @@ class CoursesApiTests(TestCase):
test_uri = '{}/{}/content/{}/groups/123456'.format( test_uri = '{}/{}/content/{}/groups/123456'.format(
self.base_courses_uri, self.base_courses_uri,
self.test_course_id, self.test_course_id,
self.course_project.id unicode(self.course_project.scope_ids.usage_id)
) )
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_course_content_users_list_get(self): def test_course_content_users_list_get(self):
test_uri = '{}/{}/groups'.format(self.base_course_content_uri, self.course_project.id) test_uri = '{}/{}/groups'.format(self.base_course_content_uri, unicode(self.course_project.scope_ids.usage_id))
test_uri_users = '{}/{}/users'.format(self.base_course_content_uri, self.course_project.id) test_uri_users = '{}/{}/users'.format(self.base_course_content_uri, unicode(self.course_project.scope_ids.usage_id))
test_course_users_uri = self.base_courses_uri + '/' + self.test_course_id + '/users' test_course_users_uri = self.base_courses_uri + '/' + self.test_course_id + '/users'
# Create a group and add it to course module # Create a group and add it to course module
...@@ -1204,6 +1270,15 @@ class CoursesApiTests(TestCase): ...@@ -1204,6 +1270,15 @@ class CoursesApiTests(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 1) self.assertEqual(len(response.data), 1)
def test_course_content_users_list_get_invalid_course_and_content(self):
invalid_course_uri = '{}/{}/content/{}/users'.format(self.base_courses_uri, self.test_bogus_course_id, unicode(self.course_project.scope_ids.usage_id))
response = self.do_get(invalid_course_uri)
self.assertEqual(response.status_code, 404)
invalid_content_uri = '{}/{}/content/{}/users'.format(self.base_courses_uri, self.test_course_id, self.test_bogus_content_id)
response = self.do_get(invalid_content_uri)
self.assertEqual(response.status_code, 404)
def test_coursemodulecompletions_post(self): def test_coursemodulecompletions_post(self):
data = { data = {
...@@ -1216,15 +1291,17 @@ class CoursesApiTests(TestCase): ...@@ -1216,15 +1291,17 @@ class CoursesApiTests(TestCase):
response = self.do_post(self.base_users_uri, data) response = self.do_post(self.base_users_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
created_user_id = response.data['id'] created_user_id = response.data['id']
completions_uri = '{}/{}/completions/'.format(self.base_courses_uri, self.course.id) completions_uri = '{}/{}/completions/'.format(self.base_courses_uri, unicode(self.course.id))
completions_data = {'content_id': self.course_content.id, 'user_id': created_user_id} stage = 'First'
completions_data = {'content_id': unicode(self.course_content.scope_ids.usage_id), 'user_id': created_user_id, 'stage': stage}
response = self.do_post(completions_uri, completions_data) response = self.do_post(completions_uri, completions_data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
coursemodulecomp_id = response.data['id'] coursemodulecomp_id = response.data['id']
self.assertGreater(coursemodulecomp_id, 0) self.assertGreater(coursemodulecomp_id, 0)
self.assertEqual(response.data['user_id'], created_user_id) self.assertEqual(response.data['user_id'], created_user_id)
self.assertEqual(response.data['course_id'], self.course.id) self.assertEqual(response.data['course_id'], unicode(self.course.id))
self.assertEqual(response.data['content_id'], self.course_content.id) self.assertEqual(response.data['content_id'], unicode(self.course_content.scope_ids.usage_id))
self.assertEqual(response.data['stage'], stage)
self.assertIsNotNone(response.data['created']) self.assertIsNotNone(response.data['created'])
self.assertIsNotNone(response.data['modified']) self.assertIsNotNone(response.data['modified'])
...@@ -1242,8 +1319,25 @@ class CoursesApiTests(TestCase): ...@@ -1242,8 +1319,25 @@ class CoursesApiTests(TestCase):
response = self.do_post(completions_uri, completions_data) response = self.do_post(completions_uri, completions_data)
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
# test to create course completion with invalid content_id
completions_data['content_id'] = self.test_bogus_content_id
response = self.do_post(completions_uri, completions_data)
self.assertEqual(response.status_code, 400)
def test_course_module_completions_post_invalid_course(self):
completions_uri = '{}/{}/completions/'.format(self.base_courses_uri, self.test_bogus_course_id)
completions_data = {'content_id': unicode(self.course_content.scope_ids.usage_id), 'user_id': self.users[0].id}
response = self.do_post(completions_uri, completions_data)
self.assertEqual(response.status_code, 404)
def test_course_module_completions_post_invalid_content(self):
completions_uri = '{}/{}/completions/'.format(self.base_courses_uri, self.test_course_id)
completions_data = {'content_id': self.test_bogus_content_id, 'user_id': self.users[0].id}
response = self.do_post(completions_uri, completions_data)
self.assertEqual(response.status_code, 400)
def test_coursemodulecompletions_filters(self): def test_coursemodulecompletions_filters(self):
completion_uri = '{}/{}/completions/'.format(self.base_courses_uri, self.course.id) completion_uri = '{}/{}/completions/'.format(self.base_courses_uri, unicode(self.course.id))
for i in xrange(1, 3): for i in xrange(1, 3):
data = { data = {
'email': 'test{}@example.com'.format(i), 'email': 'test{}@example.com'.format(i),
...@@ -1257,8 +1351,21 @@ class CoursesApiTests(TestCase): ...@@ -1257,8 +1351,21 @@ class CoursesApiTests(TestCase):
created_user_id = response.data['id'] created_user_id = response.data['id']
for i in xrange(1, 26): for i in xrange(1, 26):
content_id = self.course_content.id + str(i) local_content_name = 'Video_Sequence{}'.format(i)
completions_data = {'content_id': content_id, 'user_id': created_user_id} local_content = ItemFactory.create(
category="videosequence",
parent_location=self.chapter.location,
data=self.test_data,
display_name=local_content_name
)
content_id = unicode(local_content.scope_ids.usage_id)
if i < 25:
content_id = unicode(self.course_content.scope_ids.usage_id) + str(i)
stage = None
else:
content_id = unicode(self.course_content.scope_ids.usage_id)
stage = 'Last'
completions_data = {'content_id': content_id, 'user_id': created_user_id, 'stage': stage}
response = self.do_post(completion_uri, completions_data) response = self.do_post(completion_uri, completions_data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
...@@ -1285,19 +1392,39 @@ class CoursesApiTests(TestCase): ...@@ -1285,19 +1392,39 @@ class CoursesApiTests(TestCase):
self.assertEqual(len(response.data['results']), 0) self.assertEqual(len(response.data['results']), 0)
#filter course module completion by course_id #filter course module completion by course_id
course_filter_uri = '{}?course_id={}&page_size=10'.format(completion_uri, self.course.id) course_filter_uri = '{}?course_id={}&page_size=10'.format(completion_uri, unicode(self.course.id))
response = self.do_get(course_filter_uri) response = self.do_get(course_filter_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 25) self.assertEqual(response.data['count'], 25)
self.assertEqual(len(response.data['results']), 10) self.assertEqual(len(response.data['results']), 10)
#filter course module completion by content_id #filter course module completion by content_id
content_filter_uri = '{}?content_id={}'.format(completion_uri, self.course_content.id + str(1)) content_id = {'content_id': '{}1'.format(unicode(self.course_content.scope_ids.usage_id))}
content_filter_uri = '{}?{}'.format(completion_uri, urlencode(content_id))
response = self.do_get(content_filter_uri) response = self.do_get(content_filter_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 1) self.assertEqual(response.data['count'], 1)
self.assertEqual(len(response.data['results']), 1) self.assertEqual(len(response.data['results']), 1)
#filter course module completion by invalid content_id
content_id = {'content_id': '{}1'.format(self.test_bogus_content_id)}
content_filter_uri = '{}?{}'.format(completion_uri, urlencode(content_id))
response = self.do_get(content_filter_uri)
self.assertEqual(response.status_code, 404)
#filter course module completion by stage
content_filter_uri = '{}?stage={}'.format(completion_uri, 'Last')
response = self.do_get(content_filter_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 1)
self.assertEqual(len(response.data['results']), 1)
def test_coursemodulecompletions_get_invalid_course(self):
completion_uri = '{}/{}/completions/'.format(self.base_courses_uri, self.test_bogus_course_id)
print completion_uri
response = self.do_get(completion_uri)
self.assertEqual(response.status_code, 404)
def test_courses_leaders_list_get(self): def test_courses_leaders_list_get(self):
test_uri = '{}/{}/metrics/proficiency/leaders/'.format(self.base_courses_uri, self.test_course_id) test_uri = '{}/{}/metrics/proficiency/leaders/'.format(self.base_courses_uri, self.test_course_id)
response = self.do_get(test_uri) response = self.do_get(test_uri)
...@@ -1311,8 +1438,9 @@ class CoursesApiTests(TestCase): ...@@ -1311,8 +1438,9 @@ class CoursesApiTests(TestCase):
self.assertEqual(len(response.data['leaders']), 4) self.assertEqual(len(response.data['leaders']), 4)
# Filter by content_id # Filter by content_id
content_filter_uri = '{}/{}/metrics/proficiency/leaders/?content_id={}'\ content_id = {'content_id': self.item.scope_ids.usage_id}
.format(self.base_courses_uri, self.test_course_id, Location(self.item.location).url()) content_filter_uri = '{}/{}/metrics/proficiency/leaders/?{}'\
.format(self.base_courses_uri, self.test_course_id, urlencode(content_id))
response = self.do_get(content_filter_uri) response = self.do_get(content_filter_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['leaders']), 3) self.assertEqual(len(response.data['leaders']), 3)
...@@ -1328,13 +1456,31 @@ class CoursesApiTests(TestCase): ...@@ -1328,13 +1456,31 @@ class CoursesApiTests(TestCase):
self.assertEqual(response.data['position'], 2) self.assertEqual(response.data['position'], 2)
self.assertEqual(response.data['points'], 4.5) self.assertEqual(response.data['points'], 4.5)
# Filter by user who has never accessed a course module
test_user = UserFactory.create(username="testusernocoursemod")
user_filter_uri = '{}/{}/metrics/proficiency/leaders/?user_id={}'\
.format(self.base_courses_uri, self.test_course_id, test_user.id)
response = self.do_get(user_filter_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['leaders']), 3)
self.assertEqual(response.data['course_avg'], 3.4)
self.assertEqual(response.data['position'], 4)
self.assertEqual(response.data['points'], 0)
# test with bogus course # test with bogus course
test_uri = '{}/{}/metrics/proficiency/leaders/'.format(self.base_courses_uri, self.test_bogus_course_id) test_uri = '{}/{}/metrics/proficiency/leaders/'.format(self.base_courses_uri, self.test_bogus_course_id)
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
# test with bogus content filter
content_id = {'content_id': self.test_bogus_content_id}
content_filter_uri = '{}/{}/metrics/proficiency/leaders/?{}'\
.format(self.base_courses_uri, self.test_course_id, urlencode(content_id))
response = self.do_get(content_filter_uri)
self.assertEqual(response.status_code, 400)
def test_courses_completions_leaders_list_get(self): def test_courses_completions_leaders_list_get(self):
completion_uri = '{}/{}/completions/'.format(self.base_courses_uri, self.course.id) completion_uri = '{}/{}/completions/'.format(self.base_courses_uri, unicode(self.course.id))
users = [] users = []
for i in xrange(1, 5): for i in xrange(1, 5):
data = { data = {
...@@ -1349,6 +1495,13 @@ class CoursesApiTests(TestCase): ...@@ -1349,6 +1495,13 @@ class CoursesApiTests(TestCase):
users.append(response.data['id']) users.append(response.data['id'])
for i in xrange(1, 26): for i in xrange(1, 26):
local_content_name = 'Video_Sequence{}'.format(i)
local_content = ItemFactory.create(
category="videosequence",
parent_location=self.chapter.location,
data=self.test_data,
display_name=local_content_name
)
if i < 3: if i < 3:
user_id = users[0] user_id = users[0]
elif i < 8: elif i < 8:
...@@ -1357,7 +1510,7 @@ class CoursesApiTests(TestCase): ...@@ -1357,7 +1510,7 @@ class CoursesApiTests(TestCase):
user_id = users[2] user_id = users[2]
else: else:
user_id = users[3] user_id = users[3]
content_id = self.course_content.id + str(i) content_id = unicode(local_content.scope_ids.usage_id)
completions_data = {'content_id': content_id, 'user_id': user_id} completions_data = {'content_id': content_id, 'user_id': user_id}
response = self.do_post(completion_uri, completions_data) response = self.do_post(completion_uri, completions_data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
...@@ -1409,7 +1562,8 @@ class CoursesApiTests(TestCase): ...@@ -1409,7 +1562,8 @@ class CoursesApiTests(TestCase):
self.assertGreater(len(response.data['grades']), 0) self.assertGreater(len(response.data['grades']), 0)
# Filter by content_id # Filter by content_id
content_filter_uri = '{}?content_id={}'.format(test_uri, Location(self.item.location).url()) content_id = {'content_id': self.item.scope_ids.usage_id}
content_filter_uri = '{}?{}'.format(test_uri, urlencode(content_id))
response = self.do_get(content_filter_uri) response = self.do_get(content_filter_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertGreater(response.data['average_grade'], 0) self.assertGreater(response.data['average_grade'], 0)
...@@ -1420,6 +1574,12 @@ class CoursesApiTests(TestCase): ...@@ -1420,6 +1574,12 @@ class CoursesApiTests(TestCase):
self.assertGreater(response.data['course_points_possible'], 0) self.assertGreater(response.data['course_points_possible'], 0)
self.assertGreater(len(response.data['grades']), 0) self.assertGreater(len(response.data['grades']), 0)
# Filter by invalid content_id
content_id = {'content_id': self.test_bogus_content_id}
content_filter_uri = '{}?{}'.format(test_uri, urlencode(content_id))
response = self.do_get(content_filter_uri)
self.assertEqual(response.status_code, 400)
def test_courses_grades_list_get_invalid_course(self): def test_courses_grades_list_get_invalid_course(self):
# Retrieve the list of grades for this course # Retrieve the list of grades for this course
# All the course/item/user scaffolding was handled in Setup # All the course/item/user scaffolding was handled in Setup
...@@ -1431,9 +1591,17 @@ class CoursesApiTests(TestCase): ...@@ -1431,9 +1591,17 @@ class CoursesApiTests(TestCase):
projects_uri = '/api/projects/' projects_uri = '/api/projects/'
for i in xrange(0, 25): for i in xrange(0, 25):
local_content_name = 'Video_Sequence{}'.format(i)
local_content = ItemFactory.create(
category="videosequence",
parent_location=self.chapter.location,
data=self.test_data,
display_name=local_content_name
)
# location:MITx+999+Robot_Super_Course+videosequence+Video_Sequence0
data = { data = {
'course_id': self.test_course_id, 'content_id': unicode(local_content.scope_ids.usage_id),
'content_id': '{}_{}'.format(self.test_course_content_id, i) 'course_id': self.test_course_id
} }
response = self.do_post(projects_uri, data) response = self.do_post(projects_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
...@@ -1443,9 +1611,6 @@ class CoursesApiTests(TestCase): ...@@ -1443,9 +1611,6 @@ class CoursesApiTests(TestCase):
self.assertEqual(len(response.data['results']), 10) self.assertEqual(len(response.data['results']), 10)
self.assertEqual(response.data['num_pages'], 3) self.assertEqual(response.data['num_pages'], 3)
response = self.do_get('{}/{}/projects/'.format(self.base_courses_uri, self.test_bogus_course_id))
self.assertEqual(response.status_code, 404)
def test_courses_data_metrics(self): def test_courses_data_metrics(self):
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/users' test_uri = self.base_courses_uri + '/' + self.test_course_id + '/users'
test_user_uri = '/api/users' test_user_uri = '/api/users'
...@@ -1474,3 +1639,93 @@ class CoursesApiTests(TestCase): ...@@ -1474,3 +1639,93 @@ class CoursesApiTests(TestCase):
# test with bogus course # test with bogus course
response = self.do_get(course_metrics_uri.format(self.test_bogus_course_id)) response = self.do_get(course_metrics_uri.format(self.test_bogus_course_id))
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_course_workgroups_list(self):
projects_uri = '/api/projects/'
data = {
'course_id': self.test_course_id,
'content_id': 'self.test_course_content_id'
}
response = self.do_post(projects_uri, data)
self.assertEqual(response.status_code, 201)
project_id = response.data['id']
test_workgroups_uri = '/api/workgroups/'
for i in xrange(1, 12):
data = {
'name': '{} {}'.format('Workgroup', i),
'project': project_id
}
response = self.do_post(test_workgroups_uri, data)
self.assertEqual(response.status_code, 201)
# get workgroups associated to course
test_uri = '/api/courses/{}/workgroups/?page_size=10'.format(self.test_course_id)
response = self.do_get(test_uri)
self.assertEqual(response.data['count'], 11)
self.assertEqual(len(response.data['results']), 10)
self.assertEqual(response.data['num_pages'], 2)
# test with bogus course
test_uri = '/api/courses/{}/workgroups/'.format(self.test_bogus_course_id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_course_users_count_by_city(self):
test_uri = '/api/users'
# create a 25 new users
for i in xrange(1, 26):
if i < 10:
city = 'San Francisco'
elif i < 15:
city = 'Denver'
elif i < 20:
city = 'Dallas'
else:
city = 'New York City'
data = {
'email': 'test{}@example.com'.format(i), 'username': 'test_user{}'.format(i),
'password': 'test.me!',
'first_name': '{} {}'.format('John', i), 'last_name': '{} {}'.format('Doe', i), 'city': city,
'country': 'PK', 'level_of_education': 'b', 'year_of_birth': '2000', 'gender': 'male',
'title': 'Software Engineer', 'avatar_url': 'http://example.com/avatar.png'
}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
created_user_id = response.data['id']
user_uri = response.data['uri']
# now enroll this user in the course
post_data = {'user_id': created_user_id}
courses_test_uri = self.base_courses_uri + '/' + self.test_course_id + '/users'
response = self.do_post(courses_test_uri, post_data)
self.assertEqual(response.status_code, 201)
response = self.do_get(user_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['city'], city)
response = self.do_get('{}{}{}'.format('/api/courses/', self.test_course_id, '/metrics/cities/'))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['results']), 4)
self.assertEqual(response.data['results'][0]['city'], 'San Francisco')
self.assertEqual(response.data['results'][0]['count'], 9)
# filter counts by city
response = self.do_get('{}{}{}'.format('/api/courses/', self.test_course_id,
'/metrics/cities/?city=new york city, San Francisco'))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['results']), 2)
self.assertEqual(response.data['results'][0]['city'], 'San Francisco')
self.assertEqual(response.data['results'][0]['count'], 9)
self.assertEqual(response.data['results'][1]['city'], 'New York City')
self.assertEqual(response.data['results'][1]['count'], 6)
# filter counts by city
response = self.do_get('{}{}{}'.format('/api/courses/', self.test_course_id,
'/metrics/cities/?city=Denver'))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['results']), 1)
self.assertEqual(response.data['results'][0]['city'], 'Denver')
self.assertEqual(response.data['results'][0]['count'], 5)
...@@ -10,28 +10,31 @@ from api_manager.courses import views as courses_views ...@@ -10,28 +10,31 @@ from api_manager.courses import views as courses_views
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/content/(?P<content_id>[a-zA-Z0-9_+\/:]+)/groups/(?P<group_id>[0-9]+)$', courses_views.CourseContentGroupsDetail.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/content/(?P<content_id>[a-zA-Z0-9_+\/:]+)/groups/*$', courses_views.CourseContentGroupsList.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/content/(?P<content_id>[a-zA-Z0-9_+\/:]+)/children/*$', courses_views.CourseContentList.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/content/(?P<content_id>[a-zA-Z0-9_+\/:]+)/users/*$', courses_views.CourseContentUsersList.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/content/(?P<content_id>[a-zA-Z0-9_+\/:]+)$', courses_views.CourseContentDetail.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/content/*$', courses_views.CourseContentList.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/grades/*$', courses_views.CoursesGradesList.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/groups/(?P<group_id>[0-9]+)$', courses_views.CoursesGroupsDetail.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/groups/*$', courses_views.CoursesGroupsList.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/overview/*$', courses_views.CoursesOverview.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/updates/*$', courses_views.CoursesUpdates.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/static_tabs/(?P<tab_id>[a-zA-Z0-9_+\/:]+)$', courses_views.CoursesStaticTabsDetail.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/static_tabs/*$', courses_views.CoursesStaticTabsList.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/users/(?P<user_id>[0-9]+)$', courses_views.CoursesUsersDetail.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/users/*$', courses_views.CoursesUsersList.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/completions/*$', courses_views.CourseModuleCompletionList.as_view(), name='completion-list'),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/projects/*$', courses_views.CoursesProjectList.as_view(), name='courseproject-list'),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/metrics/*$', courses_views.CourseMetrics.as_view(), name='course-metrics'),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/metrics/proficiency/leaders/*$', courses_views.CoursesLeadersList.as_view(), name='course-metrics-proficiency-leaders'),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/metrics/completions/leaders/*$', courses_views.CoursesCompletionsLeadersList.as_view(), name='course-metrics-completions-leaders'),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/workgroups/*$', courses_views.CoursesWorkgroupsList.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/metrics/social/$', courses_views.CoursesSocialMetrics.as_view(), name='courses-social-metrics'),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/metrics/cities/$', courses_views.CoursesCitiesMetrics.as_view(), name='courses-cities-metrics'),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)$', courses_views.CoursesDetail.as_view()),
url(r'/*$^', courses_views.CoursesList.as_view()), url(r'/*$^', courses_views.CoursesList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)$', courses_views.CoursesDetail.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/_:]+)/users/*$', courses_views.CourseContentUsersList.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>[^/]+/[^/]+/[^/]+)/grades/*$', courses_views.CoursesGradesList.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()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/updates/*$', courses_views.CoursesUpdates.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/static_tabs/(?P<tab_id>[a-zA-Z0-9/_:]+)$', courses_views.CoursesStaticTabsDetail.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/static_tabs/*$', courses_views.CoursesStaticTabsList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/users/(?P<user_id>[0-9]+)$', courses_views.CoursesUsersDetail.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/users/*$', courses_views.CoursesUsersList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/completions/*$', courses_views.CourseModuleCompletionList.as_view(), name='completion-list'),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/projects/*$', courses_views.CoursesProjectList.as_view(), name='courseproject-list'),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/metrics/*$', courses_views.CourseMetrics.as_view(), name='course-metrics'),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/metrics/proficiency/leaders/*$', courses_views.CoursesLeadersList.as_view(), name='course-metrics-proficiency-leaders'),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/metrics/completions/leaders/*$', courses_views.CoursesCompletionsLeadersList.as_view(), name='course-metrics-completions-leaders'),
) )
urlpatterns = format_suffix_patterns(urlpatterns) urlpatterns = format_suffix_patterns(urlpatterns)
...@@ -12,32 +12,35 @@ from django.core.exceptions import ObjectDoesNotExist ...@@ -12,32 +12,35 @@ from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Avg, Sum, Count from django.db.models import Avg, Sum, Count
from django.http import Http404 from django.http import Http404
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db.models import Q
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from api_manager.models import CourseGroupRelationship, CourseContentGroupRelationship, GroupProfile, \ from courseware.courses import get_course_about_section, get_course_info_section
CourseModuleCompletion
from api_manager.permissions import SecureAPIView, SecureListAPIView
from api_manager.users.serializers import UserSerializer
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
from courseware.models import StudentModule from courseware.models import StudentModule
from courseware.views import get_static_tab_contents from courseware.views import get_static_tab_contents
from student.models import CourseEnrollment, CourseEnrollmentAllowed from student.models import CourseEnrollment, CourseEnrollmentAllowed
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location, InvalidLocationError
from api_manager.courseware_access import get_course, get_course_child
from api_manager.models import CourseGroupRelationship, CourseContentGroupRelationship, GroupProfile, \
CourseModuleCompletion
from api_manager.permissions import SecureAPIView, SecureListAPIView from api_manager.permissions import SecureAPIView, SecureListAPIView
from api_manager.users.serializers import UserSerializer, UserCountByCitySerializer
from api_manager.utils import generate_base_uri from api_manager.utils import generate_base_uri
from projects.models import Project from projects.models import Project, Workgroup
from projects.serializers import ProjectSerializer from projects.serializers import ProjectSerializer, BasicWorkgroupSerializer
from .serializers import CourseModuleCompletionSerializer from .serializers import CourseModuleCompletionSerializer
from .serializers import GradeSerializer, CourseLeadersSerializer, CourseCompletionsLeadersSerializer from .serializers import GradeSerializer, CourseLeadersSerializer, CourseCompletionsLeadersSerializer
from lms.lib.comment_client.user import get_course_social_stats
from lms.lib.comment_client.utils import CommentClientRequestError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def _get_content_children(content, content_type=None): def _get_content_children(content, content_type=None):
""" """
Parses the provided content object looking for children Parses the provided content object looking for children
...@@ -55,23 +58,22 @@ def _get_content_children(content, content_type=None): ...@@ -55,23 +58,22 @@ def _get_content_children(content, content_type=None):
return children return children
def _serialize_content(request, course_id, content): def _serialize_content(request, content_key, content_descriptor):
""" """
Loads the specified content object into the response dict Loads the specified content object into the response dict
This should probably evolve to use DRF serializers This should probably evolve to use DRF serializers
""" """
data = {} data = {}
if hasattr(content_descriptor, 'id') and unicode(content_descriptor.id) == unicode(content_key):
if getattr(content, 'id') == course_id: content_id = unicode(content_key)
content_id = content.id
else: else:
content_id = content.location.url() content_id = unicode(content_descriptor.scope_ids.usage_id)
data['id'] = content_id data['id'] = unicode(content_id)
if hasattr(content, 'display_name'): if hasattr(content_descriptor, 'display_name'):
data['name'] = content.display_name data['name'] = content_descriptor.display_name
data['category'] = content.location.category data['category'] = content_descriptor.location.category
protocol = 'http' protocol = 'http'
if request.is_secure(): if request.is_secure():
...@@ -79,29 +81,28 @@ def _serialize_content(request, course_id, content): ...@@ -79,29 +81,28 @@ def _serialize_content(request, course_id, content):
content_uri = '{}://{}/api/courses/{}'.format( content_uri = '{}://{}/api/courses/{}'.format(
protocol, protocol,
request.get_host(), request.get_host(),
course_id.encode('utf-8') unicode(content_key)
) )
# Some things we do only if the content object is a course # Some things we do only if the content object is a course
if (course_id == content_id): if (unicode(content_key) == content_id):
data['number'] = content.location.course data['number'] = content_descriptor.location.course
data['org'] = content.location.org data['org'] = content_descriptor.location.org
# Other things we do only if the content object is not a course # Other things we do only if the content object is not a course
else: else:
content_uri = '{}/content/{}'.format(content_uri, content_id) content_uri = '{}/content/{}'.format(content_uri, content_id)
data['uri'] = content_uri data['uri'] = content_uri
if hasattr(content, 'due'): if hasattr(content_descriptor, 'due'):
data['due'] = content.due data['due'] = content_descriptor.due
data['start'] = getattr(content, 'start', None)
data['end'] = getattr(content, 'end', None)
data['start'] = getattr(content_descriptor, 'start', None)
data['end'] = getattr(content_descriptor, 'end', None)
return data return data
def _serialize_content_children(request, course_id, children): def _serialize_content_children(request, course_key, children):
""" """
Loads the specified content child data into the response dict Loads the specified content child data into the response dict
This should probably evolve to use DRF serializers This should probably evolve to use DRF serializers
...@@ -111,17 +112,21 @@ def _serialize_content_children(request, course_id, children): ...@@ -111,17 +112,21 @@ def _serialize_content_children(request, course_id, children):
for child in children: for child in children:
child_data = _serialize_content( child_data = _serialize_content(
request, request,
course_id, course_key,
child child
) )
data.append(child_data) data.append(child_data)
return data return data
def _serialize_content_with_children(request, course_descriptor, descriptor, depth): def _serialize_content_with_children(request, course_key, descriptor, depth): # pylint: disable=C0103
"""
Serializes course content and then dives into the content tree,
serializing each child module until specified depth limit is hit
"""
data = _serialize_content( data = _serialize_content(
request, request,
course_descriptor.id, course_key,
descriptor descriptor
) )
if depth > 0: if depth > 0:
...@@ -129,7 +134,7 @@ def _serialize_content_with_children(request, course_descriptor, descriptor, dep ...@@ -129,7 +134,7 @@ def _serialize_content_with_children(request, course_descriptor, descriptor, dep
for child in descriptor.get_children(): for child in descriptor.get_children():
data['children'].append(_serialize_content_with_children( data['children'].append(_serialize_content_with_children(
request, request,
course_descriptor, course_key,
child, child,
depth - 1 depth - 1
)) ))
...@@ -252,13 +257,13 @@ def _parse_updates_html(html): ...@@ -252,13 +257,13 @@ def _parse_updates_html(html):
posting_data['date'] = posting_date_element.text posting_data['date'] = posting_date_element.text
content = u'' content = u''
for el in posting: for current_element in posting:
# note, we can't delete or skip over the date element in # note, we can't delete or skip over the date element in
# the HTML tree because there might be some tailing content # the HTML tree because there might be some tailing content
if el != posting_date_element: if current_element != posting_date_element:
content += etree.tostring(el) content += etree.tostring(current_element)
else: else:
content += el.tail if el.tail else u'' content += current_element.tail if current_element.tail else u''
posting_data['content'] = content.strip() posting_data['content'] = content.strip()
result.append(posting_data) result.append(posting_data)
...@@ -310,23 +315,25 @@ class CourseContentList(SecureAPIView): ...@@ -310,23 +315,25 @@ class CourseContentList(SecureAPIView):
""" """
def get(self, request, course_id, content_id=None): def get(self, request, course_id, content_id=None):
"""
GET /api/courses/{course_id}/content
"""
course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
if not course_descriptor:
return Response({}, status=status.HTTP_404_NOT_FOUND)
if content_id is None: if content_id is None:
content_id = course_id content_id = course_id
response_data = [] response_data = []
content_type = request.QUERY_PARAMS.get('type', None) content_type = request.QUERY_PARAMS.get('type', None)
store = modulestore()
if course_id != content_id: if course_id != content_id:
try: content_descriptor, content_key, content = get_course_child(request, request.user, course_key, content_id) # pylint: disable=W0612
content = store.get_instance(course_id, Location(content_id))
except InvalidLocationError:
content = None
else: else:
content = get_course(course_id) content = course_descriptor
if content: if content:
children = _get_content_children(content, content_type) children = _get_content_children(content, content_type)
response_data = _serialize_content_children( response_data = _serialize_content_children(
request, request,
course_id, course_key,
children children
) )
status_code = status.HTTP_200_OK status_code = status.HTTP_200_OK
...@@ -383,50 +390,47 @@ class CourseContentDetail(SecureAPIView): ...@@ -383,50 +390,47 @@ class CourseContentDetail(SecureAPIView):
""" """
def get(self, request, course_id, content_id): def get(self, request, course_id, content_id):
store = modulestore() """
GET /api/courses/{course_id}/content/{content_id}
"""
content, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
response_data = {} response_data = {}
base_uri = generate_base_uri(request) base_uri = generate_base_uri(request)
content_type = request.QUERY_PARAMS.get('type', None)
response_data['uri'] = base_uri response_data['uri'] = base_uri
if course_id != content_id: if course_id != content_id:
element_name = 'children' element_name = 'children'
try: content_descriptor, content_key, content = get_course_child(request, request.user, course_key, content_id) # pylint: disable=W0612
content = store.get_instance(course_id, Location(content_id))
except InvalidLocationError:
content = None
else: else:
element_name = 'content' element_name = 'content'
content = get_course(course_id)
protocol = 'http' protocol = 'http'
if request.is_secure(): if request.is_secure():
protocol = protocol + 's' protocol = protocol + 's'
response_data['uri'] = '{}://{}/api/courses/{}'.format( response_data['uri'] = '{}://{}/api/courses/{}'.format(
protocol, protocol,
request.get_host(), request.get_host(),
course_id.encode('utf-8') unicode(course_key)
)
if content:
response_data = _serialize_content(
request,
course_id,
content
)
children = _get_content_children(content, content_type)
response_data[element_name] = _serialize_content_children(
request,
course_id,
children
) )
base_uri_without_qs = generate_base_uri(request, True) if not content:
response_data['resources'] = [] return Response(response_data, status=status.HTTP_404_NOT_FOUND)
resource_uri = '{}/users'.format(base_uri_without_qs) response_data = _serialize_content(
response_data['resources'].append({'uri': resource_uri}) request,
resource_uri = '{}/groups'.format(base_uri_without_qs) course_id,
response_data['resources'].append({'uri': resource_uri}) content
status_code = status.HTTP_200_OK )
else: content_type = request.QUERY_PARAMS.get('type', None)
status_code = status.HTTP_404_NOT_FOUND children = _get_content_children(content, content_type)
return Response(response_data, status=status_code) response_data[element_name] = _serialize_content_children(
request,
course_id,
children
)
base_uri_without_qs = generate_base_uri(request, True)
resource_uri = '{}/groups'.format(base_uri_without_qs)
response_data['resources'] = []
response_data['resources'].append({'uri': resource_uri})
resource_uri = '{}/users'.format(base_uri_without_qs)
response_data['resources'].append({'uri': resource_uri})
return Response(response_data, status=status.HTTP_200_OK)
class CoursesList(SecureAPIView): class CoursesList(SecureAPIView):
...@@ -458,6 +462,9 @@ class CoursesList(SecureAPIView): ...@@ -458,6 +462,9 @@ class CoursesList(SecureAPIView):
""" """
def get(self, request): def get(self, request):
"""
GET /api/courses
"""
response_data = [] response_data = []
store = modulestore() store = modulestore()
course_descriptors = store.get_courses() course_descriptors = store.get_courses()
...@@ -519,21 +526,20 @@ class CoursesDetail(SecureAPIView): ...@@ -519,21 +526,20 @@ class CoursesDetail(SecureAPIView):
""" """
def get(self, request, course_id): def get(self, request, course_id):
"""
GET /api/courses/{course_id}
"""
depth = request.QUERY_PARAMS.get('depth', 0) depth = request.QUERY_PARAMS.get('depth', 0)
depth_int = int(depth) depth_int = int(depth)
# get_course_by_id raises an Http404 if the requested course is invalid # get_course_by_id raises an Http404 if the requested course is invalid
# Rather than catching it, we just let it bubble up # Rather than catching it, we just let it bubble up
try: course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
course_descriptor = get_course(course_id, depth=depth_int)
except ValueError:
course_descriptor = None
if not course_descriptor: if not course_descriptor:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
if depth_int > 0: if depth_int > 0:
response_data = _serialize_content_with_children( response_data = _serialize_content_with_children(
request, request,
course_descriptor, course_key,
course_descriptor, # Primer for recursive function course_descriptor, # Primer for recursive function
depth_int depth_int
) )
...@@ -542,7 +548,7 @@ class CoursesDetail(SecureAPIView): ...@@ -542,7 +548,7 @@ class CoursesDetail(SecureAPIView):
else: else:
response_data = _serialize_content( response_data = _serialize_content(
request, request,
course_descriptor.id, course_key,
course_descriptor course_descriptor
) )
base_uri = generate_base_uri(request) base_uri = generate_base_uri(request)
...@@ -563,6 +569,7 @@ class CoursesDetail(SecureAPIView): ...@@ -563,6 +569,7 @@ class CoursesDetail(SecureAPIView):
response_data['resources'].append({'uri': resource_uri}) response_data['resources'].append({'uri': resource_uri})
return Response(response_data, status=status.HTTP_200_OK) return Response(response_data, status=status.HTTP_200_OK)
class CoursesGroupsList(SecureAPIView): class CoursesGroupsList(SecureAPIView):
""" """
**Use Case** **Use Case**
...@@ -608,22 +615,21 @@ class CoursesGroupsList(SecureAPIView): ...@@ -608,22 +615,21 @@ class CoursesGroupsList(SecureAPIView):
response_data = {} response_data = {}
group_id = request.DATA['group_id'] group_id = request.DATA['group_id']
base_uri = generate_base_uri(request) base_uri = generate_base_uri(request)
try: course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
existing_course = get_course(course_id) if not course_descriptor:
except ValueError: return Response({}, status=status.HTTP_404_NOT_FOUND)
existing_course = None
try: try:
existing_group = Group.objects.get(id=group_id) existing_group = Group.objects.get(id=group_id)
except ObjectDoesNotExist: except ObjectDoesNotExist:
existing_group = None existing_group = None
if existing_course and existing_group: if existing_group:
try: try:
existing_relationship = CourseGroupRelationship.objects.get(course_id=course_id, group=existing_group) existing_relationship = CourseGroupRelationship.objects.get(course_id=course_key, group=existing_group)
except ObjectDoesNotExist: except ObjectDoesNotExist:
existing_relationship = None existing_relationship = None
if existing_relationship is None: if existing_relationship is None:
CourseGroupRelationship.objects.create(course_id=course_id, group=existing_group) CourseGroupRelationship.objects.create(course_id=course_key, group=existing_group)
response_data['course_id'] = str(existing_course.id) response_data['course_id'] = unicode(course_key)
response_data['group_id'] = str(existing_group.id) response_data['group_id'] = str(existing_group.id)
response_data['uri'] = '{}/{}'.format(base_uri, existing_group.id) response_data['uri'] = '{}/{}'.format(base_uri, existing_group.id)
response_status = status.HTTP_201_CREATED response_status = status.HTTP_201_CREATED
...@@ -638,13 +644,11 @@ class CoursesGroupsList(SecureAPIView): ...@@ -638,13 +644,11 @@ class CoursesGroupsList(SecureAPIView):
""" """
GET /api/courses/{course_id}/groups?type=workgroup GET /api/courses/{course_id}/groups?type=workgroup
""" """
try: course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
get_course(course_id) if not course_descriptor:
except ValueError:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
group_type = request.QUERY_PARAMS.get('type', None) group_type = request.QUERY_PARAMS.get('type', None)
course_groups = CourseGroupRelationship.objects.filter(course_id=course_id) course_groups = CourseGroupRelationship.objects.filter(course_id=course_key)
if group_type: if group_type:
course_groups = course_groups.filter(group__groupprofile__group_type=group_type) course_groups = course_groups.filter(group__groupprofile__group_type=group_type)
...@@ -656,6 +660,7 @@ class CoursesGroupsList(SecureAPIView): ...@@ -656,6 +660,7 @@ class CoursesGroupsList(SecureAPIView):
response_status = status.HTTP_200_OK response_status = status.HTTP_200_OK
return Response(response_data, status=response_status) return Response(response_data, status=response_status)
class CoursesGroupsDetail(SecureAPIView): class CoursesGroupsDetail(SecureAPIView):
""" """
### The CoursesGroupsDetail view allows clients to interact with a specific CourseGroupRelationship entity ### The CoursesGroupsDetail view allows clients to interact with a specific CourseGroupRelationship entity
...@@ -671,16 +676,15 @@ class CoursesGroupsDetail(SecureAPIView): ...@@ -671,16 +676,15 @@ class CoursesGroupsDetail(SecureAPIView):
""" """
GET /api/courses/{course_id}/groups/{group_id} GET /api/courses/{course_id}/groups/{group_id}
""" """
try: course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
existing_course = get_course(course_id) if not course_descriptor:
except ValueError:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
try: try:
existing_group = Group.objects.get(id=group_id) existing_group = Group.objects.get(id=group_id)
except ObjectDoesNotExist: except ObjectDoesNotExist:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
try: try:
existing_relationship = CourseGroupRelationship.objects.get(course_id=course_id, group=existing_group) CourseGroupRelationship.objects.get(course_id=course_key, group=existing_group)
except ObjectDoesNotExist: except ObjectDoesNotExist:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
response_data = {} response_data = {}
...@@ -694,9 +698,12 @@ class CoursesGroupsDetail(SecureAPIView): ...@@ -694,9 +698,12 @@ class CoursesGroupsDetail(SecureAPIView):
""" """
DELETE /api/courses/{course_id}/groups/{group_id} DELETE /api/courses/{course_id}/groups/{group_id}
""" """
course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
if not course_descriptor:
return Response({}, status=status.HTTP_204_NO_CONTENT)
try: try:
existing_group = Group.objects.get(id=group_id) existing_group = Group.objects.get(id=group_id)
existing_relationship = CourseGroupRelationship.objects.get(course_id=course_id, group=existing_group).delete() CourseGroupRelationship.objects.get(course_id=course_key, group=existing_group).delete()
except ObjectDoesNotExist: except ObjectDoesNotExist:
pass pass
response_data = {} response_data = {}
...@@ -730,23 +737,21 @@ class CoursesOverview(SecureAPIView): ...@@ -730,23 +737,21 @@ class CoursesOverview(SecureAPIView):
""" """
def get(self, request, course_id): def get(self, request, course_id):
"""
GET /api/courses/{course_id}/overview
"""
response_data = OrderedDict() response_data = OrderedDict()
try: course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
existing_course = get_course(course_id) if not course_descriptor:
except ValueError: return Response({}, status=status.HTTP_404_NOT_FOUND)
existing_course = None existing_content = get_course_about_section(course_descriptor, 'overview')
if existing_course: if not existing_content:
existing_content = get_course_about_section(existing_course, 'overview')
if existing_content:
if request.GET.get('parse') and request.GET.get('parse') in ['True', 'true']:
response_data['sections'] = _parse_overview_html(existing_content)
else:
response_data['overview_html'] = existing_content
return Response(response_data, status=status.HTTP_200_OK)
else:
return Response({}, status=status.HTTP_404_NOT_FOUND)
else:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
if request.GET.get('parse') and request.GET.get('parse') in ['True', 'true']:
response_data['sections'] = _parse_overview_html(existing_content)
else:
response_data['overview_html'] = existing_content
return Response(response_data, status=status.HTTP_200_OK)
class CoursesUpdates(SecureAPIView): class CoursesUpdates(SecureAPIView):
...@@ -775,14 +780,14 @@ class CoursesUpdates(SecureAPIView): ...@@ -775,14 +780,14 @@ class CoursesUpdates(SecureAPIView):
""" """
def get(self, request, course_id): def get(self, request, course_id):
response_data = OrderedDict() """
try: GET /api/courses/{course_id}/updates
existing_course = get_course(course_id) """
except ValueError: course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
existing_course = None if not course_descriptor:
if not existing_course:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
content = get_course_info_section(request, existing_course, 'updates') response_data = OrderedDict()
content = get_course_info_section(request, course_descriptor, 'updates')
if not content: if not content:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
if request.GET.get('parse') and request.GET.get('parse') in ['True', 'true']: if request.GET.get('parse') and request.GET.get('parse') in ['True', 'true']:
...@@ -819,15 +824,15 @@ class CoursesStaticTabsList(SecureAPIView): ...@@ -819,15 +824,15 @@ class CoursesStaticTabsList(SecureAPIView):
""" """
def get(self, request, course_id): def get(self, request, course_id):
try: """
existing_course = get_course(course_id) GET /api/courses/{course_id}/static_tabs
except ValueError: """
existing_course = None course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
if not existing_course: if not course_descriptor:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
response_data = OrderedDict() response_data = OrderedDict()
tabs = [] tabs = []
for tab in existing_course.tabs: for tab in course_descriptor.tabs:
if tab.type == 'static_tab': if tab.type == 'static_tab':
tab_data = OrderedDict() tab_data = OrderedDict()
tab_data['id'] = tab.url_slug tab_data['id'] = tab.url_slug
...@@ -835,7 +840,7 @@ class CoursesStaticTabsList(SecureAPIView): ...@@ -835,7 +840,7 @@ class CoursesStaticTabsList(SecureAPIView):
if request.GET.get('detail') and request.GET.get('detail') in ['True', 'true']: if request.GET.get('detail') and request.GET.get('detail') in ['True', 'true']:
tab_data['content'] = get_static_tab_contents( tab_data['content'] = get_static_tab_contents(
request, request,
existing_course, course_descriptor,
tab, tab,
wrap_xmodule_display=False wrap_xmodule_display=False
) )
...@@ -868,18 +873,20 @@ class CoursesStaticTabsDetail(SecureAPIView): ...@@ -868,18 +873,20 @@ class CoursesStaticTabsDetail(SecureAPIView):
""" """
def get(self, request, course_id, tab_id): def get(self, request, course_id, tab_id):
try: """
existing_course = get_course(course_id) GET /api/courses/{course_id}/static_tabs/{tab_id}
except ValueError: """
course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
if not course_descriptor:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
response_data = OrderedDict() response_data = OrderedDict()
for tab in existing_course.tabs: for tab in course_descriptor.tabs:
if tab.type == 'static_tab' and tab.url_slug == tab_id: if tab.type == 'static_tab' and tab.url_slug == tab_id:
response_data['id'] = tab.url_slug response_data['id'] = tab.url_slug
response_data['name'] = tab.name response_data['name'] = tab.name
response_data['content'] = get_static_tab_contents( response_data['content'] = get_static_tab_contents(
request, request,
existing_course, course_descriptor,
tab, tab,
wrap_xmodule_display=False wrap_xmodule_display=False
) )
...@@ -914,6 +921,12 @@ class CoursesUsersList(SecureAPIView): ...@@ -914,6 +921,12 @@ class CoursesUsersList(SecureAPIView):
* username: The username of the user. * username: The username of the user.
* GET supports filtering of user by organization(s) like this
* To get users enrolled in a course and are also member of organization
/api/courses/{course_id}/users?organizations={organization_id}
* organizations filter can be a single id or multiple ids separated by comma
/api/courses/{course_id}/users?organizations={organization_id1},{organization_id2}
**Post Values** **Post Values**
To create a new user through POST /api/courses/{course_id}/users, you To create a new user through POST /api/courses/{course_id}/users, you
...@@ -921,12 +934,11 @@ class CoursesUsersList(SecureAPIView): ...@@ -921,12 +934,11 @@ class CoursesUsersList(SecureAPIView):
""" """
def post(self, request, course_id): def post(self, request, course_id):
response_data = OrderedDict() """
try: POST /api/courses/{course_id}/users
existing_course = get_course(course_id) """
except ValueError: course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
existing_course = None if not course_descriptor:
if not existing_course:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
if 'user_id' in request.DATA: if 'user_id' in request.DATA:
user_id = request.DATA['user_id'] user_id = request.DATA['user_id']
...@@ -934,7 +946,7 @@ class CoursesUsersList(SecureAPIView): ...@@ -934,7 +946,7 @@ class CoursesUsersList(SecureAPIView):
existing_user = User.objects.get(id=user_id) existing_user = User.objects.get(id=user_id)
except ObjectDoesNotExist: except ObjectDoesNotExist:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
CourseEnrollment.enroll(existing_user, course_id) CourseEnrollment.enroll(existing_user, course_key)
return Response({}, status=status.HTTP_201_CREATED) return Response({}, status=status.HTTP_201_CREATED)
elif 'email' in request.DATA: elif 'email' in request.DATA:
try: try:
...@@ -946,7 +958,7 @@ class CoursesUsersList(SecureAPIView): ...@@ -946,7 +958,7 @@ class CoursesUsersList(SecureAPIView):
# and the instructor is pre-enrolling them # and the instructor is pre-enrolling them
# Store the pre-enrollment data in the CourseEnrollmentAllowed table # Store the pre-enrollment data in the CourseEnrollmentAllowed table
# NOTE: This logic really should live in CourseEnrollment..... # NOTE: This logic really should live in CourseEnrollment.....
cea, _ = CourseEnrollmentAllowed.objects.get_or_create(course_id=course_id, email=email) cea, created = CourseEnrollmentAllowed.objects.get_or_create(course_id=course_key, email=email) # pylint: disable=W0612
cea.auto_enroll = True cea.auto_enroll = True
cea.save() cea.save()
return Response({}, status.HTTP_201_CREATED) return Response({}, status.HTTP_201_CREATED)
...@@ -956,29 +968,34 @@ class CoursesUsersList(SecureAPIView): ...@@ -956,29 +968,34 @@ class CoursesUsersList(SecureAPIView):
return Response({}, status=status.HTTP_400_BAD_REQUEST) return Response({}, status=status.HTTP_400_BAD_REQUEST)
def get(self, request, course_id): def get(self, request, course_id):
"""
GET /api/courses/{course_id}
"""
orgs = request.QUERY_PARAMS.get('organizations')
response_data = OrderedDict() response_data = OrderedDict()
base_uri = generate_base_uri(request) base_uri = generate_base_uri(request)
response_data['uri'] = base_uri response_data['uri'] = base_uri
try: course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
existing_course = get_course(course_id) if not course_descriptor:
except ValueError:
existing_course = None
if not existing_course:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
# Get a list of all enrolled students # Get a list of all enrolled students
users = CourseEnrollment.users_enrolled_in(course_id) users = CourseEnrollment.users_enrolled_in(course_key)
if orgs:
if ',' in orgs:
upper_bound = getattr(settings, 'API_LOOKUP_UPPER_BOUND', 100)
orgs = orgs.split(",")[:upper_bound]
users = users.filter(organizations__in=orgs)
response_data['enrollments'] = [] response_data['enrollments'] = []
for user in users: for user in users:
user_data = OrderedDict() user_data = OrderedDict()
user_data['id'] = user.id user_data['id'] = user.id
user_data['email'] = user.email user_data['email'] = user.email
user_data['username'] = user.username user_data['username'] = user.username
# @TODO: Should we create a URI resourse that points to user?!? But that's in a different URL subpath
response_data['enrollments'].append(user_data) response_data['enrollments'].append(user_data)
# Then list all enrollments which are pending. These are enrollments for students that have not yet # Then list all enrollments which are pending. These are enrollments for students that have not yet
# created an account # created an account
pending_enrollments = CourseEnrollmentAllowed.objects.filter(course_id=course_id) pending_enrollments = CourseEnrollmentAllowed.objects.filter(course_id=course_key)
if pending_enrollments: if pending_enrollments:
response_data['pending_enrollments'] = [] response_data['pending_enrollments'] = []
for cea in pending_enrollments: for cea in pending_enrollments:
...@@ -1021,23 +1038,13 @@ class CoursesUsersDetail(SecureAPIView): ...@@ -1021,23 +1038,13 @@ class CoursesUsersDetail(SecureAPIView):
'uri': base_uri, 'uri': base_uri,
} }
try: try:
course_descriptor = get_course(course_id)
except ValueError:
course_descriptor = None
if not course_descriptor:
return Response(response_data, status=status.HTTP_404_NOT_FOUND)
try:
user = User.objects.get(id=user_id, is_active=True) user = User.objects.get(id=user_id, is_active=True)
except ObjectDoesNotExist: except ObjectDoesNotExist:
user = None return Response(response_data, status=status.HTTP_404_NOT_FOUND)
if user and CourseEnrollment.is_enrolled(user, course_id): course_descriptor, course_key, course_content = get_course(request, user, course_id)
field_data_cache = FieldDataCache([course_descriptor], course_id, user) if not course_descriptor:
course_content = module_render.get_module( return Response(response_data, status=status.HTTP_404_NOT_FOUND)
user, if CourseEnrollment.is_enrolled(user, course_key):
request,
course_descriptor.location,
field_data_cache,
course_id)
response_data['position'] = course_content.position response_data['position'] = course_content.position
response_status = status.HTTP_200_OK response_status = status.HTTP_200_OK
else: else:
...@@ -1045,18 +1052,17 @@ class CoursesUsersDetail(SecureAPIView): ...@@ -1045,18 +1052,17 @@ class CoursesUsersDetail(SecureAPIView):
return Response(response_data, status=response_status) return Response(response_data, status=response_status)
def delete(self, request, course_id, user_id): def delete(self, request, course_id, user_id):
try: """
existing_course = get_course(course_id) DELETE /api/courses/{course_id}/users/{user_id}
except ValueError: """
existing_course = None
if not existing_course:
return Response({}, status=status.HTTP_404_NOT_FOUND)
try: try:
user = User.objects.get(id=user_id, is_active=True) user = User.objects.get(id=user_id, is_active=True)
except ObjectDoesNotExist: except ObjectDoesNotExist:
user = None return Response({}, status=status.HTTP_204_NO_CONTENT)
if user: course_descriptor, course_key, course_content = get_course(request, user, course_id) # pylint: disable=W0612
CourseEnrollment.unenroll(user, course_id) if not course_descriptor:
return Response({}, status=status.HTTP_404_NOT_FOUND)
CourseEnrollment.unenroll(user, course_key)
response_data = {} response_data = {}
base_uri = generate_base_uri(request) base_uri = generate_base_uri(request)
response_data['uri'] = base_uri response_data['uri'] = base_uri
...@@ -1086,14 +1092,11 @@ class CourseContentGroupsList(SecureAPIView): ...@@ -1086,14 +1092,11 @@ class CourseContentGroupsList(SecureAPIView):
""" """
POST /api/courses/{course_id}/content/{content_id}/groups POST /api/courses/{course_id}/content/{content_id}/groups
""" """
try: course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
course_descriptor = get_course(course_id) if not course_descriptor:
except ValueError:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
store = modulestore() content_descriptor, content_key, existing_content = get_course_child(request, request.user, course_key, content_id) # pylint: disable=W0612
try: if not existing_content:
existing_content = store.get_instance(course_id, Location(content_id))
except InvalidLocationError:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
group_id = request.DATA.get('group_id') group_id = request.DATA.get('group_id')
if group_id is None: if group_id is None:
...@@ -1105,21 +1108,21 @@ class CourseContentGroupsList(SecureAPIView): ...@@ -1105,21 +1108,21 @@ class CourseContentGroupsList(SecureAPIView):
response_data = {} response_data = {}
base_uri = generate_base_uri(request) base_uri = generate_base_uri(request)
response_data['uri'] = '{}/{}'.format(base_uri, existing_profile.group_id) response_data['uri'] = '{}/{}'.format(base_uri, existing_profile.group_id)
response_data['course_id'] = course_descriptor.id response_data['course_id'] = unicode(course_key)
response_data['content_id'] = existing_content.id response_data['content_id'] = unicode(existing_content.scope_ids.usage_id)
response_data['group_id'] = str(existing_profile.group_id) response_data['group_id'] = str(existing_profile.group_id)
try: try:
existing_relationship = CourseContentGroupRelationship.objects.get( CourseContentGroupRelationship.objects.get(
course_id=course_id, course_id=course_key,
content_id=content_id, content_id=existing_content.location,
group_profile=existing_profile group_profile=existing_profile
) )
response_data['message'] = "Relationship already exists." response_data['message'] = "Relationship already exists."
return Response(response_data, status=status.HTTP_409_CONFLICT) return Response(response_data, status=status.HTTP_409_CONFLICT)
except ObjectDoesNotExist: except ObjectDoesNotExist:
CourseContentGroupRelationship.objects.create( CourseContentGroupRelationship.objects.create(
course_id=course_id, course_id=course_key,
content_id=content_id, content_id=existing_content.location,
group_profile=existing_profile group_profile=existing_profile
) )
return Response(response_data, status=status.HTTP_201_CREATED) return Response(response_data, status=status.HTTP_201_CREATED)
...@@ -1130,18 +1133,15 @@ class CourseContentGroupsList(SecureAPIView): ...@@ -1130,18 +1133,15 @@ class CourseContentGroupsList(SecureAPIView):
""" """
response_data = [] response_data = []
group_type = request.QUERY_PARAMS.get('type') group_type = request.QUERY_PARAMS.get('type')
try: course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
course_descriptor = get_course(course_id) if not course_descriptor:
except ValueError:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
try: content_descriptor, content_key, existing_content = get_course_child(request, request.user, course_key, content_id) # pylint: disable=W0612
store = modulestore() if not existing_content:
existing_content = store.get_instance(course_id, Location(content_id))
except InvalidLocationError:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
relationships = CourseContentGroupRelationship.objects.filter( relationships = CourseContentGroupRelationship.objects.filter(
course_id=course_id, course_id=course_key,
content_id=content_id, content_id=existing_content.location,
).select_related("groupprofile") ).select_related("groupprofile")
if group_type: if group_type:
relationships = relationships.filter(group_profile__group_type=group_type) relationships = relationships.filter(group_profile__group_type=group_type)
...@@ -1165,19 +1165,16 @@ class CourseContentGroupsDetail(SecureAPIView): ...@@ -1165,19 +1165,16 @@ class CourseContentGroupsDetail(SecureAPIView):
""" """
GET /api/courses/{course_id}/content/{content_id}/groups/{group_id} GET /api/courses/{course_id}/content/{content_id}/groups/{group_id}
""" """
try: course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
course_descriptor = get_course(course_id) if not course_descriptor:
except ValueError:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
try: content_descriptor, content_key, existing_content = get_course_child(request, request.user, course_key, content_id) # pylint: disable=W0612
store = modulestore() if not existing_content:
existing_content = store.get_instance(course_id, Location(content_id))
except InvalidLocationError:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
try: try:
relationship = CourseContentGroupRelationship.objects.get( CourseContentGroupRelationship.objects.get(
course_id=course_id, course_id=course_key,
content_id=content_id, content_id=existing_content.location,
group_profile__group_id=group_id group_profile__group_id=group_id
) )
except ObjectDoesNotExist: except ObjectDoesNotExist:
...@@ -1209,11 +1206,17 @@ class CourseContentUsersList(SecureAPIView): ...@@ -1209,11 +1206,17 @@ class CourseContentUsersList(SecureAPIView):
""" """
GET /api/courses/{course_id}/content/{content_id}/users GET /api/courses/{course_id}/content/{content_id}/users
""" """
course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
if not course_descriptor:
return Response({}, status=status.HTTP_404_NOT_FOUND)
content_descriptor, content_key, existing_content = get_course_child(request, request.user, course_key, content_id) # pylint: disable=W0612
if not existing_content:
return Response({}, status=status.HTTP_404_NOT_FOUND)
enrolled = self.request.QUERY_PARAMS.get('enrolled', 'True') enrolled = self.request.QUERY_PARAMS.get('enrolled', 'True')
group_type = self.request.QUERY_PARAMS.get('type', None) group_type = self.request.QUERY_PARAMS.get('type', None)
group_id = self.request.QUERY_PARAMS.get('group_id', None) group_id = self.request.QUERY_PARAMS.get('group_id', None)
relationships = CourseContentGroupRelationship.objects.filter( relationships = CourseContentGroupRelationship.objects.filter(
course_id=course_id, content_id=content_id).select_related("groupprofile") course_id=course_key, content_id=existing_content.location).select_related("groupprofile")
if group_id: if group_id:
relationships = relationships.filter(group_profile__group__id=group_id) relationships = relationships.filter(group_profile__group__id=group_id)
...@@ -1223,7 +1226,7 @@ class CourseContentUsersList(SecureAPIView): ...@@ -1223,7 +1226,7 @@ class CourseContentUsersList(SecureAPIView):
lookup_group_ids = relationships.values_list('group_profile', flat=True) lookup_group_ids = relationships.values_list('group_profile', flat=True)
users = User.objects.filter(groups__id__in=lookup_group_ids) users = User.objects.filter(groups__id__in=lookup_group_ids)
enrolled_users = CourseEnrollment.users_enrolled_in(course_id).filter(groups__id__in=lookup_group_ids) enrolled_users = CourseEnrollment.users_enrolled_in(course_key).filter(groups__id__in=lookup_group_ids)
if enrolled in ['True', 'true']: if enrolled in ['True', 'true']:
queryset = enrolled_users queryset = enrolled_users
else: else:
...@@ -1251,6 +1254,7 @@ class CourseModuleCompletionList(SecureListAPIView): ...@@ -1251,6 +1254,7 @@ class CourseModuleCompletionList(SecureListAPIView):
"user_id": "3", "user_id": "3",
"course_id": "32fgdf", "course_id": "32fgdf",
"content_id": "324dfgd", "content_id": "324dfgd",
"stage": "First",
"created": "2014-06-10T13:14:49.878Z", "created": "2014-06-10T13:14:49.878Z",
"modified": "2014-06-10T13:14:49.914Z" "modified": "2014-06-10T13:14:49.914Z"
} }
...@@ -1259,13 +1263,14 @@ class CourseModuleCompletionList(SecureListAPIView): ...@@ -1259,13 +1263,14 @@ class CourseModuleCompletionList(SecureListAPIView):
Filters can also be applied Filters can also be applied
```/api/courses/{course_id}/completions/?user_id={user_id}``` ```/api/courses/{course_id}/completions/?user_id={user_id}```
```/api/courses/{course_id}/completions/?content_id={content_id}``` ```/api/courses/{course_id}/completions/?content_id={content_id}&stage={stage}```
```/api/courses/{course_id}/completions/?user_id={user_id}&content_id={content_id}``` ```/api/courses/{course_id}/completions/?user_id={user_id}&content_id={content_id}```
- POST: Creates a Course-Module completion entity - POST: Creates a Course-Module completion entity
- POST Example: - POST Example:
{ {
"content_id":"i4x://the/content/location", "content_id":"i4x://the/content/location",
"user_id":4 "user_id":4,
"stage": "First"
} }
### Use Cases/Notes: ### Use Cases/Notes:
* Use GET operation to retrieve list of course completions by user * Use GET operation to retrieve list of course completions by user
...@@ -1279,15 +1284,26 @@ class CourseModuleCompletionList(SecureListAPIView): ...@@ -1279,15 +1284,26 @@ class CourseModuleCompletionList(SecureListAPIView):
""" """
user_ids = self.request.QUERY_PARAMS.get('user_id', None) user_ids = self.request.QUERY_PARAMS.get('user_id', None)
content_id = self.request.QUERY_PARAMS.get('content_id', None) content_id = self.request.QUERY_PARAMS.get('content_id', None)
stage = self.request.QUERY_PARAMS.get('stage', None)
course_id = self.kwargs['course_id'] course_id = self.kwargs['course_id']
queryset = CourseModuleCompletion.objects.filter(course_id=course_id) course_descriptor, course_key, course_content = get_course(self.request, self.request.user, course_id) # pylint: disable=W0612
print course_descriptor
if not course_descriptor:
raise Http404
queryset = CourseModuleCompletion.objects.filter(course_id=course_key)
upper_bound = getattr(settings, 'API_LOOKUP_UPPER_BOUND', 100) upper_bound = getattr(settings, 'API_LOOKUP_UPPER_BOUND', 100)
if user_ids: if user_ids:
user_ids = map(int, user_ids.split(','))[:upper_bound] user_ids = map(int, user_ids.split(','))[:upper_bound]
queryset = queryset.filter(user__in=user_ids) queryset = queryset.filter(user__in=user_ids)
if content_id: if content_id:
queryset = queryset.filter(content_id=content_id) content_descriptor, content_key, existing_content = get_course_child(self.request, self.request.user, course_key, content_id) # pylint: disable=W0612
if not existing_content:
raise Http404
queryset = queryset.filter(content_id=existing_content.location)
if stage:
queryset = queryset.filter(stage=stage)
return queryset return queryset
...@@ -1297,14 +1313,22 @@ class CourseModuleCompletionList(SecureListAPIView): ...@@ -1297,14 +1313,22 @@ class CourseModuleCompletionList(SecureListAPIView):
""" """
content_id = request.DATA.get('content_id', None) content_id = request.DATA.get('content_id', None)
user_id = request.DATA.get('user_id', None) user_id = request.DATA.get('user_id', None)
stage = request.DATA.get('stage', None)
if not content_id: if not content_id:
return Response({'message': _('content_id is missing')}, status.HTTP_400_BAD_REQUEST) return Response({'message': _('content_id is missing')}, status.HTTP_400_BAD_REQUEST)
if not user_id: if not user_id:
return Response({'message': _('user_id is missing')}, status.HTTP_400_BAD_REQUEST) return Response({'message': _('user_id is missing')}, status.HTTP_400_BAD_REQUEST)
course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
if not course_descriptor:
return Response({}, status=status.HTTP_404_NOT_FOUND)
content_descriptor, content_key, existing_content = get_course_child(request, request.user, course_key, content_id) # pylint: disable=W0612
if not existing_content:
return Response({'message': _('content_id is invalid')}, status.HTTP_400_BAD_REQUEST)
completion, created = CourseModuleCompletion.objects.get_or_create(user_id=user_id, completion, created = CourseModuleCompletion.objects.get_or_create(user_id=user_id,
course_id=course_id, course_id=course_key,
content_id=content_id) content_id=existing_content.location,
stage=stage)
serializer = CourseModuleCompletionSerializer(completion) serializer = CourseModuleCompletionSerializer(completion)
if created: if created:
return Response(serializer.data, status=status.HTTP_201_CREATED) # pylint: disable=E1101 return Response(serializer.data, status=status.HTTP_201_CREATED) # pylint: disable=E1101
...@@ -1321,18 +1345,15 @@ class CoursesGradesList(SecureListAPIView): ...@@ -1321,18 +1345,15 @@ class CoursesGradesList(SecureListAPIView):
* Example: Display a graph of all of the grades awarded for a given course * Example: Display a graph of all of the grades awarded for a given course
""" """
def get(self, request, course_id): def get(self, request, course_id): # pylint: disable=W0221
""" """
GET /api/courses/{course_id}/grades?user_ids=1,2&content_ids=i4x://1/2/3,i4x://a/b/c GET /api/courses/{course_id}/grades?user_ids=1,2&content_ids=i4x://1/2/3,i4x://a/b/c
""" """
try: course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
existing_course = get_course(course_id) if not course_descriptor:
except ValueError:
existing_course = None
if not existing_course:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
queryset = StudentModule.objects.filter( queryset = StudentModule.objects.filter(
course_id__exact=course_id, course_id__exact=course_key,
grade__isnull=False, grade__isnull=False,
max_grade__isnull=False, max_grade__isnull=False,
max_grade__gt=0 max_grade__gt=0
...@@ -1346,14 +1367,17 @@ class CoursesGradesList(SecureListAPIView): ...@@ -1346,14 +1367,17 @@ class CoursesGradesList(SecureListAPIView):
content_id = self.request.QUERY_PARAMS.get('content_id', None) content_id = self.request.QUERY_PARAMS.get('content_id', None)
if content_id: if content_id:
queryset = queryset.filter(module_state_key=content_id) content_descriptor, content_key, existing_content = get_course_child(request, request.user, course_key, content_id) # pylint: disable=W0612
if not existing_content:
return Response({}, status=status.HTTP_400_BAD_REQUEST)
queryset = queryset.filter(module_state_key=existing_content.location)
queryset_grade_avg = queryset.aggregate(Avg('grade')) queryset_grade_avg = queryset.aggregate(Avg('grade'))
queryset_grade_sum = queryset.aggregate(Sum('grade')) queryset_grade_sum = queryset.aggregate(Sum('grade'))
queryset_maxgrade_sum = queryset.aggregate(Sum('max_grade')) queryset_maxgrade_sum = queryset.aggregate(Sum('max_grade'))
course_queryset = StudentModule.objects.filter( course_queryset = StudentModule.objects.filter(
course_id__exact=course_id, course_id__exact=course_key,
grade__isnull=False, grade__isnull=False,
max_grade__isnull=False, max_grade__isnull=False,
max_grade__gt=0 max_grade__gt=0
...@@ -1375,7 +1399,7 @@ class CoursesGradesList(SecureListAPIView): ...@@ -1375,7 +1399,7 @@ class CoursesGradesList(SecureListAPIView):
response_data['grades'] = [] response_data['grades'] = []
for row in queryset: for row in queryset:
serializer = GradeSerializer(row) serializer = GradeSerializer(row)
response_data['grades'].append(serializer.data) response_data['grades'].append(serializer.data) # pylint: disable=E1101
return Response(response_data, status=status.HTTP_200_OK) return Response(response_data, status=status.HTTP_200_OK)
...@@ -1390,14 +1414,8 @@ class CoursesProjectList(SecureListAPIView): ...@@ -1390,14 +1414,8 @@ class CoursesProjectList(SecureListAPIView):
def get_queryset(self): def get_queryset(self):
course_id = self.kwargs['course_id'] course_id = self.kwargs['course_id']
try: course_descriptor, course_key, course_content = get_course(self.request, self.request.user, course_id) # pylint: disable=W0612
existing_course = get_course(course_id) return Project.objects.filter(course_id=course_key)
except ValueError:
existing_course = None
if not existing_course:
raise Http404
return Project.objects.filter(course_id=course_id)
class CourseMetrics(SecureAPIView): class CourseMetrics(SecureAPIView):
...@@ -1413,13 +1431,10 @@ class CourseMetrics(SecureAPIView): ...@@ -1413,13 +1431,10 @@ class CourseMetrics(SecureAPIView):
""" """
GET /api/courses/{course_id}/metrics/ GET /api/courses/{course_id}/metrics/
""" """
try: course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
existing_course = get_course(course_id) if not course_descriptor:
except ValueError:
existing_course = None
if not existing_course:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
users_enrolled = CourseEnrollment.num_enrolled_in(course_id) users_enrolled = CourseEnrollment.num_enrolled_in(course_key)
data = { data = {
'users_enrolled': users_enrolled 'users_enrolled': users_enrolled
} }
...@@ -1442,7 +1457,7 @@ class CoursesLeadersList(SecureListAPIView): ...@@ -1442,7 +1457,7 @@ class CoursesLeadersList(SecureListAPIView):
* Example: Display position of a users in a course in terms of proficiency points and course avg * Example: Display position of a users in a course in terms of proficiency points and course avg
""" """
def get(self, request, course_id): # pylint: disable=W0613 def get(self, request, course_id): # pylint: disable=W0613,W0221
""" """
GET /api/courses/{course_id}/metrics/proficiency/leaders/ GET /api/courses/{course_id}/metrics/proficiency/leaders/
""" """
...@@ -1451,12 +1466,11 @@ class CoursesLeadersList(SecureListAPIView): ...@@ -1451,12 +1466,11 @@ class CoursesLeadersList(SecureListAPIView):
count = self.request.QUERY_PARAMS.get('count', 3) count = self.request.QUERY_PARAMS.get('count', 3)
data = {} data = {}
course_avg = 0 course_avg = 0
try: course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
get_course(course_id) if not course_descriptor:
except ValueError: return Response({}, status=status.HTTP_404_NOT_FOUND)
raise Http404
queryset = StudentModule.objects.filter( queryset = StudentModule.objects.filter(
course_id__exact=course_id, course_id__exact=course_key,
grade__isnull=False, grade__isnull=False,
max_grade__isnull=False, max_grade__isnull=False,
max_grade__gt=0, max_grade__gt=0,
...@@ -1464,15 +1478,19 @@ class CoursesLeadersList(SecureListAPIView): ...@@ -1464,15 +1478,19 @@ class CoursesLeadersList(SecureListAPIView):
) )
if content_id: if content_id:
queryset = queryset.filter(module_state_key=content_id) content_descriptor, content_key, existing_content = get_course_child(request, request.user, course_key, content_id) # pylint: disable=W0612
if not existing_content:
return Response({}, status=status.HTTP_400_BAD_REQUEST)
queryset = queryset.filter(module_state_key=existing_content.location)
if user_id: if user_id:
user_points = StudentModule.objects.filter(course_id__exact=course_id, user_points = StudentModule.objects.filter(course_id__exact=course_key,
student__id=user_id).aggregate(points=Sum('grade')) student__id=user_id).aggregate(points=Sum('grade'))
user_points = user_points['points'] or 0
users_above = queryset.values('student__id').annotate(points=Sum('grade')).\ users_above = queryset.values('student__id').annotate(points=Sum('grade')).\
filter(points__gt=user_points['points']).count() filter(points__gt=user_points).count()
data['position'] = users_above + 1 data['position'] = users_above + 1
data['points'] = user_points['points'] data['points'] = user_points
points = queryset.aggregate(total=Sum('grade')) points = queryset.aggregate(total=Sum('grade'))
users = queryset.filter(student__is_active=True).aggregate(total=Count('student__id', distinct=True)) users = queryset.filter(student__is_active=True).aggregate(total=Count('student__id', distinct=True))
...@@ -1513,11 +1531,10 @@ class CoursesCompletionsLeadersList(SecureAPIView): ...@@ -1513,11 +1531,10 @@ class CoursesCompletionsLeadersList(SecureAPIView):
count = self.request.QUERY_PARAMS.get('count', 3) count = self.request.QUERY_PARAMS.get('count', 3)
data = {} data = {}
course_avg = 0 course_avg = 0
try: course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
get_course(course_id) if not course_descriptor:
except ValueError: return Response({}, status=status.HTTP_404_NOT_FOUND)
raise Http404 queryset = CourseModuleCompletion.objects.filter(course_id=course_key)
queryset = CourseModuleCompletion.objects.filter(course_id=course_id)
if user_id: if user_id:
user_completions = queryset.filter(user__id=user_id).count() user_completions = queryset.filter(user__id=user_id).count()
...@@ -1540,3 +1557,76 @@ class CoursesCompletionsLeadersList(SecureAPIView): ...@@ -1540,3 +1557,76 @@ class CoursesCompletionsLeadersList(SecureAPIView):
serializer = CourseCompletionsLeadersSerializer(queryset, many=True) serializer = CourseCompletionsLeadersSerializer(queryset, many=True)
data['leaders'] = serializer.data # pylint: disable=E1101 data['leaders'] = serializer.data # pylint: disable=E1101
return Response(data, status=status.HTTP_200_OK) return Response(data, status=status.HTTP_200_OK)
class CoursesWorkgroupsList(SecureListAPIView):
"""
### The CoursesWorkgroupsList view allows clients to retrieve a list of workgroups
associated to a course
- URI: ```/api/courses/{course_id}/workgroups/```
- GET: Provides paginated list of workgroups associated to a course
"""
serializer_class = BasicWorkgroupSerializer
def get_queryset(self):
course_id = self.kwargs['course_id']
course_descriptor, course_key, course_content = get_course(self.request, self.request.user, course_id) # pylint: disable=W0612
if not course_descriptor:
raise Http404
queryset = Workgroup.objects.filter(project__course_id=course_id)
return queryset
class CoursesSocialMetrics(SecureListAPIView):
"""
### The CoursesSocialMetrics view allows clients to query about the activity of all users in the
forums
- URI: ```/api/users/{course_id}/metrics/social/```
- GET: Returns a list of social metrics for users in the specified course
"""
def get(self, request, course_id): # pylint: disable=W0613
try:
data = get_course_social_stats(course_id)
http_status = status.HTTP_200_OK
except CommentClientRequestError, e:
data = {
"err_msg": str(e)
}
http_status = status.HTTP_500_INTERNAL_SERVER_ERROR
return Response(data, http_status)
class CoursesCitiesMetrics(SecureListAPIView):
"""
### The CoursesCitiesMetrics view allows clients to retrieve ordered list of user
count by city in a particular course
- URI: ```/api/courses/{course_id}/metrics/cities/```
- GET: Provides paginated list of user count by cities
list can be filtered by city
GET ```/api/courses/{course_id}/metrics/cities/?city={city1},{city2}```
"""
serializer_class = UserCountByCitySerializer
def get_queryset(self):
course_id = self.kwargs['course_id']
city = self.request.QUERY_PARAMS.get('city', None)
upper_bound = getattr(settings, 'API_LOOKUP_UPPER_BOUND', 100)
course_descriptor, course_key, course_content = get_course(self.request, self.request.user, course_id) # pylint: disable=W0612
if not course_descriptor:
raise Http404
queryset = CourseEnrollment.users_enrolled_in(course_key)
if city:
city = city.split(',')[:upper_bound]
q_list = [Q(profile__city__iexact=item.strip()) for item in city]
q_list = reduce(lambda a, b: a | b, q_list)
queryset = queryset.filter(q_list)
queryset = queryset.values('profile__city').annotate(count=Count('profile__city'))\
.filter(count__gt=0).order_by('-count')
return queryset
""" Centralized access to LMS courseware app """
from courseware import courses, module_render
from courseware.model_data import FieldDataCache
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
from xmodule.modulestore import InvalidLocationError
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
def get_course(request, user, course_id, depth=0):
"""
Utility method to obtain course components
"""
course_descriptor = None
course_key = None
course_content = None
try:
course_key = CourseKey.from_string(course_id)
except InvalidKeyError:
try:
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
except InvalidKeyError:
pass
if course_key:
try:
course_descriptor = courses.get_course(course_key, depth)
except ValueError:
pass
if course_descriptor:
field_data_cache = FieldDataCache([course_descriptor], course_key, user)
course_content = module_render.get_module(
user,
request,
course_descriptor.location,
field_data_cache,
course_key)
return course_descriptor, course_key, course_content
def get_course_child(request, user, course_key, content_id):
"""
Return a course xmodule/xblock to the caller
"""
content_descriptor = None
content_key = None
content = None
try:
content_key = UsageKey.from_string(content_id)
except InvalidKeyError:
try:
content_key = Location.from_deprecated_string(content_id)
except (InvalidLocationError, InvalidKeyError):
pass
if content_key:
try:
content_descriptor = modulestore().get_item(content_key)
except ItemNotFoundError:
pass
if content_descriptor:
field_data_cache = FieldDataCache([content_descriptor], course_key, user)
content = module_render.get_module(
user,
request,
content_key,
field_data_cache,
course_key)
return content_descriptor, content_key, content
def get_course_total_score(course_summary):
"""
Traverse course summary to calculate max possible score for a course
"""
score = 0
for chapter in course_summary: # accumulate score of each chapter
for section in chapter['sections']:
if section['section_total']:
score += section['section_total'][1]
return score
...@@ -4,9 +4,11 @@ ...@@ -4,9 +4,11 @@
Run these tests @ Devstack: Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/tests/test_group_views.py] rake fasttest_lms[common/djangoapps/api_manager/tests/test_group_views.py]
""" """
from datetime import datetime
from random import randint from random import randint
import uuid import uuid
import json import json
from urllib import urlencode
from django.core.cache import cache from django.core.cache import cache
from django.test import Client from django.test import Client
...@@ -45,26 +47,26 @@ class GroupsApiTests(ModuleStoreTestCase): ...@@ -45,26 +47,26 @@ class GroupsApiTests(ModuleStoreTestCase):
self.test_course_data = '<html>{}</html>'.format(str(uuid.uuid4())) self.test_course_data = '<html>{}</html>'.format(str(uuid.uuid4()))
self.course = CourseFactory.create() self.course = CourseFactory.create()
self.test_course_id = self.course.id self.test_course_id = unicode(self.course.id)
self.course_content = ItemFactory.create( self.course_content = ItemFactory.create(
category="videosequence", category="videosequence",
parent_location=self.course.location, parent_location=self.course.location,
data=self.test_course_data, data=self.test_course_data,
due="2016-05-16T14:30:00Z", due=datetime(2016, 5, 16, 14, 30),
display_name="View_Sequence" display_name="View_Sequence"
) )
self.test_organization = Organization.objects.create( self.test_organization = Organization.objects.create(
name="Test Organization", name="Test Organization",
display_name = 'Test Org', display_name='Test Org',
contact_name = 'John Org', contact_name='John Org',
contact_email = 'john@test.org', contact_email='john@test.org',
contact_phone = '+1 332 232 24234' contact_phone='+1 332 232 24234'
) )
self.test_project = Project.objects.create( self.test_project = Project.objects.create(
course_id=self.course.id, course_id=unicode(self.course.id),
content_id=self.course_content.id content_id=unicode(self.course_content.scope_ids.usage_id)
) )
self.client = SecureClient() self.client = SecureClient()
...@@ -820,7 +822,7 @@ class GroupsApiTests(ModuleStoreTestCase): ...@@ -820,7 +822,7 @@ class GroupsApiTests(ModuleStoreTestCase):
data = {'course_id': self.test_course_id} data = {'course_id': self.test_course_id}
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
confirm_uri = test_uri + '/' + self.course.id confirm_uri = test_uri + '/' + unicode(self.course.id)
self.assertEqual(response.data['uri'], confirm_uri) self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['group_id'], str(group_id)) self.assertEqual(response.data['group_id'], str(group_id))
self.assertEqual(response.data['course_id'], self.test_course_id) self.assertEqual(response.data['course_id'], self.test_course_id)
...@@ -847,7 +849,13 @@ class GroupsApiTests(ModuleStoreTestCase): ...@@ -847,7 +849,13 @@ class GroupsApiTests(ModuleStoreTestCase):
response = self.do_post(self.base_groups_uri, data) response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
test_uri = response.data['uri'] + '/courses' test_uri = response.data['uri'] + '/courses'
data = {'course_id': "987/23/896"} data = {'course_id': "slashes:invalid+course+id"}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 404)
data = {'course_id': "invalid/course/id"}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 404)
data = {'course_id': "really-invalid-course-id"}
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
...@@ -860,7 +868,7 @@ class GroupsApiTests(ModuleStoreTestCase): ...@@ -860,7 +868,7 @@ class GroupsApiTests(ModuleStoreTestCase):
data = {'course_id': self.test_course_id} data = {'course_id': self.test_course_id}
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
confirm_uri = test_uri + '/' + self.course.id confirm_uri = test_uri + '/' + unicode(self.course.id)
self.assertEqual(response.data['uri'], confirm_uri) self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['group_id'], str(group_id)) self.assertEqual(response.data['group_id'], str(group_id))
self.assertEqual(response.data['course_id'], self.test_course_id) self.assertEqual(response.data['course_id'], self.test_course_id)
...@@ -885,6 +893,7 @@ class GroupsApiTests(ModuleStoreTestCase): ...@@ -885,6 +893,7 @@ class GroupsApiTests(ModuleStoreTestCase):
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
test_uri = '{}/{}/courses/{}'.format(self.base_groups_uri, group_id, self.test_course_id) test_uri = '{}/{}/courses/{}'.format(self.base_groups_uri, group_id, self.test_course_id)
print test_uri
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
confirm_uri = '{}{}/{}/courses/{}'.format( confirm_uri = '{}{}/{}/courses/{}'.format(
...@@ -930,7 +939,7 @@ class GroupsApiTests(ModuleStoreTestCase): ...@@ -930,7 +939,7 @@ class GroupsApiTests(ModuleStoreTestCase):
data = {'name': self.test_group_name, 'type': 'test'} data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data) response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
test_uri = '{}/courses/{}'.format(response.data['uri'], self.course.id) test_uri = '{}/courses/{}'.format(response.data['uri'], unicode(self.course.id))
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
...@@ -941,14 +950,13 @@ class GroupsApiTests(ModuleStoreTestCase): ...@@ -941,14 +950,13 @@ class GroupsApiTests(ModuleStoreTestCase):
group_id = response.data['id'] group_id = response.data['id']
self.test_organization.groups.add(group_id) self.test_organization.groups.add(group_id)
test_uri = response.data['uri'] + '/organizations/' test_uri = response.data['uri'] + '/organizations/'
confirm_uri = test_uri + '/' + str(self.test_organization.id)
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 1) self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]['id'], self.test_organization.id) self.assertEqual(response.data[0]['id'], self.test_organization.id)
self.assertEqual(response.data[0]['name'], self.test_organization.name) self.assertEqual(response.data[0]['name'], self.test_organization.name)
def test_group_courses_list_get_invalid_group(self): def test_group_organizations_list_get_invalid_group(self):
test_uri = self.base_groups_uri + '/1231241/organizations/' test_uri = self.base_groups_uri + '/1231241/organizations/'
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
...@@ -981,7 +989,8 @@ class GroupsApiTests(ModuleStoreTestCase): ...@@ -981,7 +989,8 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.data['num_pages'], 2) self.assertEqual(response.data['num_pages'], 2)
# test with course_id filter # test with course_id filter
response = self.do_get('/api/groups/{}/workgroups/?course_id={}'.format(group_id, self.course.id)) course_id = {'course_id': unicode(self.course.id)}
response = self.do_get('/api/groups/{}/workgroups/?{}'.format(group_id, urlencode(course_id)))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 11) self.assertEqual(response.data['count'], 11)
self.assertIsNotNone(response.data['results'][0]['name']) self.assertIsNotNone(response.data['results'][0]['name'])
......
...@@ -8,15 +8,15 @@ from api_manager.groups import views as groups_views ...@@ -8,15 +8,15 @@ from api_manager.groups import views as groups_views
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url(r'/*$^', groups_views.GroupsList.as_view()), url(r'/*$^', groups_views.GroupsList.as_view()),
url(r'^(?P<group_id>[0-9]+)$', groups_views.GroupsDetail.as_view()), url(r'^(?P<group_id>[0-9]+)/courses/(?P<course_id>[a-zA-Z0-9_+\/:]+)$', groups_views.GroupsCoursesDetail.as_view()),
url(r'^(?P<group_id>[0-9]+)/courses/*$', groups_views.GroupsCoursesList.as_view()), url(r'^(?P<group_id>[0-9]+)/courses/*$', groups_views.GroupsCoursesList.as_view()),
url(r'^(?P<group_id>[0-9]+)/courses/(?P<course_id>[a-zA-Z0-9/_:]+)$', groups_views.GroupsCoursesDetail.as_view()),
url(r'^(?P<group_id>[0-9]+)/organizations/*$', groups_views.GroupsOrganizationsList.as_view()), url(r'^(?P<group_id>[0-9]+)/organizations/*$', groups_views.GroupsOrganizationsList.as_view()),
url(r'^(?P<group_id>[0-9]+)/workgroups/*$', groups_views.GroupsWorkgroupsList.as_view()), url(r'^(?P<group_id>[0-9]+)/workgroups/*$', groups_views.GroupsWorkgroupsList.as_view()),
url(r'^(?P<group_id>[0-9]+)/users/*$', groups_views.GroupsUsersList.as_view()), url(r'^(?P<group_id>[0-9]+)/users/*$', groups_views.GroupsUsersList.as_view()),
url(r'^(?P<group_id>[0-9]+)/users/(?P<user_id>[0-9]+)$', groups_views.GroupsUsersDetail.as_view()), url(r'^(?P<group_id>[0-9]+)/users/(?P<user_id>[0-9]+)$', groups_views.GroupsUsersDetail.as_view()),
url(r'^(?P<group_id>[0-9]+)/groups/*$', groups_views.GroupsGroupsList.as_view()), url(r'^(?P<group_id>[0-9]+)/groups/*$', groups_views.GroupsGroupsList.as_view()),
url(r'^(?P<group_id>[0-9]+)/groups/(?P<related_group_id>[0-9]+)$', groups_views.GroupsGroupsDetail.as_view()), url(r'^(?P<group_id>[0-9]+)/groups/(?P<related_group_id>[0-9]+)$', groups_views.GroupsGroupsDetail.as_view()),
url(r'^(?P<group_id>[0-9]+)$', groups_views.GroupsDetail.as_view()),
) )
urlpatterns = format_suffix_patterns(urlpatterns) urlpatterns = format_suffix_patterns(urlpatterns)
""" API implementation for group-oriented interactions. """ """ API implementation for group-oriented interactions. """
import uuid import uuid
import json import json
from collections import OrderedDict
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.http import Http404 from django.http import Http404
from django.utils import timezone
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from api_manager.courseware_access import get_course
from api_manager.models import GroupRelationship, CourseGroupRelationship, GroupProfile, APIUser as User from api_manager.models import GroupRelationship, CourseGroupRelationship, GroupProfile, APIUser as User
from api_manager.permissions import SecureAPIView, SecureListAPIView from api_manager.permissions import SecureAPIView, SecureListAPIView
from api_manager.utils import str2bool, generate_base_uri from api_manager.utils import str2bool, generate_base_uri
from api_manager.organizations import serializers from api_manager.organizations import serializers
from projects.serializers import BasicWorkgroupSerializer from projects.serializers import BasicWorkgroupSerializer
from xmodule.modulestore import Location, InvalidLocationError
from xmodule.modulestore.django import modulestore
RELATIONSHIP_TYPES = {'hierarchical': 'h', 'graph': 'g'} RELATIONSHIP_TYPES = {'hierarchical': 'h', 'graph': 'g'}
...@@ -317,7 +314,7 @@ class GroupsUsersDetail(SecureAPIView): ...@@ -317,7 +314,7 @@ class GroupsUsersDetail(SecureAPIView):
response_status = status.HTTP_404_NOT_FOUND response_status = status.HTTP_404_NOT_FOUND
return Response(response_data, status=response_status) return Response(response_data, status=response_status)
def delete(self, request, group_id, user_id): def delete(self, request, group_id, user_id): # pylint: disable=W0612,W0613
""" """
DELETE removes/inactivates/etc. an existing group-user relationship DELETE removes/inactivates/etc. an existing group-user relationship
""" """
...@@ -470,7 +467,7 @@ class GroupsGroupsDetail(SecureAPIView): ...@@ -470,7 +467,7 @@ class GroupsGroupsDetail(SecureAPIView):
response_status = status.HTTP_200_OK response_status = status.HTTP_200_OK
return Response(response_data, response_status) return Response(response_data, response_status)
def delete(self, request, group_id, related_group_id): def delete(self, request, group_id, related_group_id): # pylint: disable=W0613
""" """
DELETE /api/groups/{group_id}/groups/{related_group_id} DELETE /api/groups/{group_id}/groups/{related_group_id}
""" """
...@@ -481,7 +478,6 @@ class GroupsGroupsDetail(SecureAPIView): ...@@ -481,7 +478,6 @@ class GroupsGroupsDetail(SecureAPIView):
try: try:
to_group_relationship = GroupRelationship.objects.get(group__id=related_group_id) to_group_relationship = GroupRelationship.objects.get(group__id=related_group_id)
except ObjectDoesNotExist: except ObjectDoesNotExist:
to_group = None
to_group_relationship = None to_group_relationship = None
if from_group_relationship: if from_group_relationship:
if to_group_relationship: if to_group_relationship:
...@@ -524,13 +520,12 @@ class GroupsCoursesList(SecureAPIView): ...@@ -524,13 +520,12 @@ class GroupsCoursesList(SecureAPIView):
existing_group = Group.objects.get(id=group_id) existing_group = Group.objects.get(id=group_id)
except ObjectDoesNotExist: except ObjectDoesNotExist:
return Response({}, status.HTTP_404_NOT_FOUND) return Response({}, status.HTTP_404_NOT_FOUND)
store = modulestore()
course_id = request.DATA['course_id'] course_id = request.DATA['course_id']
base_uri = generate_base_uri(request) base_uri = generate_base_uri(request)
response_data['uri'] = '{}/{}'.format(base_uri, course_id) response_data['uri'] = '{}/{}'.format(base_uri, course_id)
existing_course = store.get_course(course_id) existing_course, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
if not existing_course: if not existing_course:
return Response({}, status.HTTP_404_NOT_FOUND) return Response({}, status.HTTP_404_NOT_FOUND)
...@@ -558,11 +553,10 @@ class GroupsCoursesList(SecureAPIView): ...@@ -558,11 +553,10 @@ class GroupsCoursesList(SecureAPIView):
existing_group = Group.objects.get(id=group_id) existing_group = Group.objects.get(id=group_id)
except ObjectDoesNotExist: except ObjectDoesNotExist:
return Response({}, status.HTTP_404_NOT_FOUND) return Response({}, status.HTTP_404_NOT_FOUND)
store = modulestore()
members = CourseGroupRelationship.objects.filter(group=existing_group) members = CourseGroupRelationship.objects.filter(group=existing_group)
response_data = [] response_data = []
for member in members: for member in members:
course = store.get_course(member.course_id) course, course_key, course_content = get_course(request, request.user, member.course_id) # pylint: disable=W0612
course_data = { course_data = {
'course_id': member.course_id, 'course_id': member.course_id,
'display_name': course.display_name 'display_name': course.display_name
...@@ -607,7 +601,7 @@ class GroupsCoursesDetail(SecureAPIView): ...@@ -607,7 +601,7 @@ class GroupsCoursesDetail(SecureAPIView):
response_status = status.HTTP_404_NOT_FOUND response_status = status.HTTP_404_NOT_FOUND
return Response(response_data, status=response_status) return Response(response_data, status=response_status)
def delete(self, request, group_id, course_id): def delete(self, request, group_id, course_id): # pylint: disable=W0613
""" """
DELETE /api/groups/{group_id}/courses/{course_id} DELETE /api/groups/{group_id}/courses/{course_id}
""" """
...@@ -630,7 +624,7 @@ class GroupsOrganizationsList(SecureAPIView): ...@@ -630,7 +624,7 @@ class GroupsOrganizationsList(SecureAPIView):
* View all of the Organizations related to a particular Program (currently modeled as a Group entity) * View all of the Organizations related to a particular Program (currently modeled as a Group entity)
""" """
def get(self, request, group_id): def get(self, request, group_id): # pylint: disable=W0613
""" """
GET /api/groups/{group_id}/organizations/ GET /api/groups/{group_id}/organizations/
""" """
...@@ -642,7 +636,7 @@ class GroupsOrganizationsList(SecureAPIView): ...@@ -642,7 +636,7 @@ class GroupsOrganizationsList(SecureAPIView):
response_data = [] response_data = []
for org in existing_group.organizations.all(): for org in existing_group.organizations.all():
serializer = serializers.OrganizationSerializer(org) serializer = serializers.OrganizationSerializer(org)
response_data.append(serializer.data) response_data.append(serializer.data) # pylint: disable=E1101
return Response(response_data, status=status.HTTP_200_OK) return Response(response_data, status=status.HTTP_200_OK)
......
"""
One-time data migration script -- shoulen't need to run it again
"""
import json import json
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
...@@ -5,6 +8,8 @@ from django.core.management.base import BaseCommand ...@@ -5,6 +8,8 @@ from django.core.management.base import BaseCommand
from api_manager.models import GroupProfile, Organization from api_manager.models import GroupProfile, Organization
class Command(BaseCommand): class Command(BaseCommand):
""" """
Migrates legacy organization data and user relationships from older Group model approach to newer concrete Organization model Migrates legacy organization data and user relationships from older Group model approach to newer concrete Organization model
...@@ -40,6 +45,6 @@ class Command(BaseCommand): ...@@ -40,6 +45,6 @@ class Command(BaseCommand):
migrated_org.users.add(user) migrated_org.users.add(user)
linked_groups = group.grouprelationship.get_linked_group_relationships() linked_groups = group.grouprelationship.get_linked_group_relationships()
for linked_group in linked_groups: for linked_group in linked_groups:
if linked_group.to_group_relationship_id is not org.id: # Don't need to carry the symmetrical component if linked_group.to_group_relationship_id is not org.id: # Don't need to carry the symmetrical component
actual_group = Group.objects.get(id=linked_group.to_group_relationship_id) actual_group = Group.objects.get(id=linked_group.to_group_relationship_id)
migrated_org.groups.add(actual_group) migrated_org.groups.add(actual_group)
"""
Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/management/commands/tests/test_migrate_orgdata.py]
"""
import json import json
import uuid import uuid
...@@ -9,9 +13,9 @@ from api_manager.models import GroupProfile, GroupRelationship, Organization ...@@ -9,9 +13,9 @@ from api_manager.models import GroupProfile, GroupRelationship, Organization
class MigrateOrgDataTests(TestCase): class MigrateOrgDataTests(TestCase):
"""
def setUp(self): Test suite for data migration script
setup = True """
def test_migrate_orgdata(self): def test_migrate_orgdata(self):
""" """
...@@ -20,7 +24,6 @@ class MigrateOrgDataTests(TestCase): ...@@ -20,7 +24,6 @@ class MigrateOrgDataTests(TestCase):
# Create some old-style Group organizations to migrate # Create some old-style Group organizations to migrate
group_name = str(uuid.uuid4()) group_name = str(uuid.uuid4())
group_profile_name = "Group 1 Name"
group_type = "organization" group_type = "organization"
groupdata = {} groupdata = {}
groupdata['name'] = "Group 1 Data Name" groupdata['name'] = "Group 1 Data Name"
...@@ -29,8 +32,8 @@ class MigrateOrgDataTests(TestCase): ...@@ -29,8 +32,8 @@ class MigrateOrgDataTests(TestCase):
groupdata['contact_email'] = "Group 1 Data Contact Email" groupdata['contact_email'] = "Group 1 Data Contact Email"
groupdata['contact_phone'] = "Group 1 Data Contact Phone" groupdata['contact_phone'] = "Group 1 Data Contact Phone"
group = Group.objects.create(name=group_name) group = Group.objects.create(name=group_name)
group_relationship = GroupRelationship.objects.create(group_id=group.id) GroupRelationship.objects.create(group_id=group.id)
profile, _ = GroupProfile.objects.get_or_create( GroupProfile.objects.get_or_create(
group_id=group.id, group_id=group.id,
group_type=group_type, group_type=group_type,
name=groupdata['name'], name=groupdata['name'],
...@@ -43,7 +46,6 @@ class MigrateOrgDataTests(TestCase): ...@@ -43,7 +46,6 @@ class MigrateOrgDataTests(TestCase):
group.grouprelationship.add_linked_group_relationship(linked_group_relationship) group.grouprelationship.add_linked_group_relationship(linked_group_relationship)
group2_name = str(uuid.uuid4()) group2_name = str(uuid.uuid4())
group2_profile_name = "Group 2 Name"
group2_type = "organization" group2_type = "organization"
groupdata = {} groupdata = {}
groupdata['name'] = "Group 2 Data Name" groupdata['name'] = "Group 2 Data Name"
...@@ -52,8 +54,8 @@ class MigrateOrgDataTests(TestCase): ...@@ -52,8 +54,8 @@ class MigrateOrgDataTests(TestCase):
groupdata['contact_email'] = "Group 2 Data Contact Email" groupdata['contact_email'] = "Group 2 Data Contact Email"
groupdata['contact_phone'] = "Group 2 Data Contact Phone" groupdata['contact_phone'] = "Group 2 Data Contact Phone"
group2 = Group.objects.create(name=group2_name) group2 = Group.objects.create(name=group2_name)
grouprelattionship2 = GroupRelationship.objects.create(group_id=group2.id) GroupRelationship.objects.create(group_id=group2.id)
profile2, _ = GroupProfile.objects.get_or_create( GroupProfile.objects.get_or_create(
group_id=group2.id, group_id=group2.id,
group_type=group2_type, group_type=group2_type,
name=groupdata['name'], name=groupdata['name'],
......
...@@ -70,7 +70,6 @@ class Migration(SchemaMigration): ...@@ -70,7 +70,6 @@ class Migration(SchemaMigration):
self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now), self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now),
keep_default=False) keep_default=False)
def backwards(self, orm): def backwards(self, orm):
# Adding field 'GroupRelationship.record_date_created' # Adding field 'GroupRelationship.record_date_created'
db.add_column('api_manager_grouprelationship', 'record_date_created', db.add_column('api_manager_grouprelationship', 'record_date_created',
...@@ -122,7 +121,6 @@ class Migration(SchemaMigration): ...@@ -122,7 +121,6 @@ class Migration(SchemaMigration):
# Deleting field 'LinkedGroupRelationship.modified' # Deleting field 'LinkedGroupRelationship.modified'
db.delete_column('api_manager_linkedgrouprelationship', 'modified') db.delete_column('api_manager_linkedgrouprelationship', 'modified')
models = { models = {
'api_manager.coursegrouprelationship': { 'api_manager.coursegrouprelationship': {
'Meta': {'object_name': 'CourseGroupRelationship'}, 'Meta': {'object_name': 'CourseGroupRelationship'},
...@@ -184,4 +182,4 @@ class Migration(SchemaMigration): ...@@ -184,4 +182,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['api_manager'] complete_apps = ['api_manager']
\ No newline at end of file
...@@ -11,12 +11,10 @@ class Migration(SchemaMigration): ...@@ -11,12 +11,10 @@ class Migration(SchemaMigration):
# Adding unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group'] # Adding unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group']
db.create_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_id']) db.create_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_id'])
def backwards(self, orm): def backwards(self, orm):
# Removing unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group'] # Removing unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group']
db.delete_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_id']) db.delete_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_id'])
models = { models = {
'api_manager.coursecontentgrouprelationship': { 'api_manager.coursecontentgrouprelationship': {
'Meta': {'unique_together': "(('course_id', 'content_id', 'group'),)", 'object_name': 'CourseContentGroupRelationship'}, 'Meta': {'unique_together': "(('course_id', 'content_id', 'group'),)", 'object_name': 'CourseContentGroupRelationship'},
...@@ -88,4 +86,4 @@ class Migration(SchemaMigration): ...@@ -88,4 +86,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['api_manager'] complete_apps = ['api_manager']
\ No newline at end of file
...@@ -22,7 +22,6 @@ class Migration(SchemaMigration): ...@@ -22,7 +22,6 @@ class Migration(SchemaMigration):
# Adding unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group_profile'] # Adding unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group_profile']
db.create_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_profile_id']) db.create_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_profile_id'])
def backwards(self, orm): def backwards(self, orm):
# Removing unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group_profile'] # Removing unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group_profile']
db.delete_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_profile_id']) db.delete_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_profile_id'])
...@@ -38,7 +37,6 @@ class Migration(SchemaMigration): ...@@ -38,7 +37,6 @@ class Migration(SchemaMigration):
# Adding unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group'] # Adding unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group']
db.create_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_id']) db.create_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_id'])
models = { models = {
'api_manager.coursecontentgrouprelationship': { 'api_manager.coursecontentgrouprelationship': {
'Meta': {'unique_together': "(('course_id', 'content_id', 'group_profile'),)", 'object_name': 'CourseContentGroupRelationship'}, 'Meta': {'unique_together': "(('course_id', 'content_id', 'group_profile'),)", 'object_name': 'CourseContentGroupRelationship'},
...@@ -110,4 +108,4 @@ class Migration(SchemaMigration): ...@@ -110,4 +108,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['api_manager'] complete_apps = ['api_manager']
\ No newline at end of file
...@@ -14,12 +14,10 @@ class Migration(SchemaMigration): ...@@ -14,12 +14,10 @@ class Migration(SchemaMigration):
# Adding unique constraint on 'GroupProfile', fields ['group'] # Adding unique constraint on 'GroupProfile', fields ['group']
db.create_unique('auth_groupprofile', ['group_id']) db.create_unique('auth_groupprofile', ['group_id'])
def backwards(self, orm): def backwards(self, orm):
# Removing unique constraint on 'GroupProfile', fields ['group'] # Removing unique constraint on 'GroupProfile', fields ['group']
db.delete_unique('auth_groupprofile', ['group_id']) db.delete_unique('auth_groupprofile', ['group_id'])
# Changing field 'GroupProfile.group' # Changing field 'GroupProfile.group'
db.alter_column('auth_groupprofile', 'group_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.Group'])) db.alter_column('auth_groupprofile', 'group_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.Group']))
...@@ -128,4 +126,4 @@ class Migration(SchemaMigration): ...@@ -128,4 +126,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['api_manager'] complete_apps = ['api_manager']
\ No newline at end of file
...@@ -19,12 +19,10 @@ class Migration(SchemaMigration): ...@@ -19,12 +19,10 @@ class Migration(SchemaMigration):
)) ))
db.send_create_signal('api_manager', ['CourseModuleCompletion']) db.send_create_signal('api_manager', ['CourseModuleCompletion'])
def backwards(self, orm): def backwards(self, orm):
# Deleting model 'CourseModuleCompletion' # Deleting model 'CourseModuleCompletion'
db.delete_table('api_manager_coursemodulecompletion') db.delete_table('api_manager_coursemodulecompletion')
models = { models = {
'api_manager.coursecontentgrouprelationship': { 'api_manager.coursecontentgrouprelationship': {
'Meta': {'unique_together': "(('course_id', 'content_id', 'group_profile'),)", 'object_name': 'CourseContentGroupRelationship'}, 'Meta': {'unique_together': "(('course_id', 'content_id', 'group_profile'),)", 'object_name': 'CourseContentGroupRelationship'},
...@@ -139,4 +137,4 @@ class Migration(SchemaMigration): ...@@ -139,4 +137,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['api_manager'] complete_apps = ['api_manager']
\ No newline at end of file
...@@ -28,7 +28,6 @@ class Migration(SchemaMigration): ...@@ -28,7 +28,6 @@ class Migration(SchemaMigration):
self.gf('django.db.models.fields.CharField')(max_length=50, null=True, blank=True), self.gf('django.db.models.fields.CharField')(max_length=50, null=True, blank=True),
keep_default=False) keep_default=False)
def backwards(self, orm): def backwards(self, orm):
# Deleting field 'Organization.display_name' # Deleting field 'Organization.display_name'
db.delete_column('api_manager_organization', 'display_name') db.delete_column('api_manager_organization', 'display_name')
...@@ -42,7 +41,6 @@ class Migration(SchemaMigration): ...@@ -42,7 +41,6 @@ class Migration(SchemaMigration):
# Deleting field 'Organization.contact_phone' # Deleting field 'Organization.contact_phone'
db.delete_column('api_manager_organization', 'contact_phone') db.delete_column('api_manager_organization', 'contact_phone')
models = { models = {
'api_manager.coursecontentgrouprelationship': { 'api_manager.coursecontentgrouprelationship': {
'Meta': {'unique_together': "(('course_id', 'content_id', 'group_profile'),)", 'object_name': 'CourseContentGroupRelationship'}, 'Meta': {'unique_together': "(('course_id', 'content_id', 'group_profile'),)", 'object_name': 'CourseContentGroupRelationship'},
...@@ -161,4 +159,4 @@ class Migration(SchemaMigration): ...@@ -161,4 +159,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['api_manager'] complete_apps = ['api_manager']
\ No newline at end of file
...@@ -7,7 +7,7 @@ from django.db import models ...@@ -7,7 +7,7 @@ from django.db import models
class Migration(SchemaMigration): class Migration(SchemaMigration):
def forwards(self, orm): def forwards(self, orm):
# Adding M2M table for field groups on 'Organization' # Adding M2M table for field groups on 'Organization'
db.create_table('api_manager_organization_groups', ( db.create_table('api_manager_organization_groups', (
...@@ -17,13 +17,11 @@ class Migration(SchemaMigration): ...@@ -17,13 +17,11 @@ class Migration(SchemaMigration):
)) ))
db.create_unique('api_manager_organization_groups', ['organization_id', 'group_id']) db.create_unique('api_manager_organization_groups', ['organization_id', 'group_id'])
def backwards(self, orm): def backwards(self, orm):
# Removing M2M table for field groups on 'Organization' # Removing M2M table for field groups on 'Organization'
db.delete_table('api_manager_organization_groups') db.delete_table('api_manager_organization_groups')
models = { models = {
'api_manager.coursecontentgrouprelationship': { 'api_manager.coursecontentgrouprelationship': {
'Meta': {'unique_together': "(('course_id', 'content_id', 'group_profile'),)", 'object_name': 'CourseContentGroupRelationship'}, 'Meta': {'unique_together': "(('course_id', 'content_id', 'group_profile'),)", 'object_name': 'CourseContentGroupRelationship'},
...@@ -153,4 +151,4 @@ class Migration(SchemaMigration): ...@@ -153,4 +151,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['api_manager'] complete_apps = ['api_manager']
\ No newline at end of file
# -*- 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 field 'CourseModuleCompletion.stage'
db.add_column('api_manager_coursemodulecompletion', 'stage',
self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
keep_default=False)
def backwards(self, orm):
# Deleting field 'CourseModuleCompletion.stage'
db.delete_column('api_manager_coursemodulecompletion', 'stage')
models = {
'api_manager.coursecontentgrouprelationship': {
'Meta': {'unique_together': "(('course_id', 'content_id', 'group_profile'),)", '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_profile': ('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.coursemodulecompletion': {
'Meta': {'object_name': 'CourseModuleCompletion'},
'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'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'stage': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'course_completions'", 'to': "orm['auth.User']"})
},
'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.OneToOneField', [], {'to': "orm['auth.Group']", 'unique': 'True'}),
'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.organization': {
'Meta': {'object_name': 'Organization'},
'contact_email': ('django.db.models.fields.EmailField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'contact_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'contact_phone': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'display_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'organizations'", 'symmetrical': 'False', 'to': "orm['auth.Group']"}),
'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'}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'organizations'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
'workgroups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'organizations'", 'symmetrical': 'False', 'to': "orm['projects.Workgroup']"})
},
'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'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'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'})
},
'projects.project': {
'Meta': {'unique_together': "(('course_id', 'content_id'),)", 'object_name': 'Project'},
'content_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'organization': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'projects'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['api_manager.Organization']"})
},
'projects.workgroup': {
'Meta': {'object_name': 'Workgroup'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'workgroups'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.Group']"}),
'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'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'workgroups'", 'to': "orm['projects.Project']"}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'workgroups'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"})
}
}
complete_apps = ['api_manager']
\ No newline at end of file
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.db import models from django.db import models
from django.utils import timezone
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
from .utils import is_int from .utils import is_int
...@@ -102,6 +102,9 @@ class GroupProfile(TimeStampedModel): ...@@ -102,6 +102,9 @@ class GroupProfile(TimeStampedModel):
""" """
class Meta: class Meta:
"""
Meta class for modifying things like table name
"""
db_table = "auth_groupprofile" db_table = "auth_groupprofile"
group = models.OneToOneField(Group, db_index=True) group = models.OneToOneField(Group, db_index=True)
...@@ -155,6 +158,7 @@ class CourseModuleCompletion(TimeStampedModel): ...@@ -155,6 +158,7 @@ class CourseModuleCompletion(TimeStampedModel):
user = models.ForeignKey(User, db_index=True, related_name="course_completions") user = models.ForeignKey(User, db_index=True, related_name="course_completions")
course_id = models.CharField(max_length=255, db_index=True) course_id = models.CharField(max_length=255, db_index=True)
content_id = models.CharField(max_length=255, db_index=True) content_id = models.CharField(max_length=255, db_index=True)
stage = models.CharField(max_length=255, null=True, blank=True)
class APIUserQuerySet(models.query.QuerySet): # pylint: disable=R0924 class APIUserQuerySet(models.query.QuerySet): # pylint: disable=R0924
......
...@@ -94,7 +94,6 @@ class OrganizationsApiTests(TestCase): ...@@ -94,7 +94,6 @@ class OrganizationsApiTests(TestCase):
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
users.append(response.data['id']) users.append(response.data['id'])
data = { data = {
'name': self.test_organization_name, 'name': self.test_organization_name,
'display_name': self.test_organization_display_name, 'display_name': self.test_organization_display_name,
......
# pylint: disable=C0103
""" ORGANIZATIONS API VIEWS """ """ ORGANIZATIONS API VIEWS """
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
...@@ -29,7 +31,7 @@ class OrganizationsViewSet(viewsets.ModelViewSet): ...@@ -29,7 +31,7 @@ class OrganizationsViewSet(viewsets.ModelViewSet):
if users: if users:
for user in users: for user in users:
serializer = UserSerializer(user) serializer = UserSerializer(user)
response_data.append(serializer.data) response_data.append(serializer.data) # pylint: disable=E1101
return Response(response_data, status=status.HTTP_200_OK) return Response(response_data, status=status.HTTP_200_OK)
else: else:
user_id = request.DATA.get('id') user_id = request.DATA.get('id')
......
...@@ -105,6 +105,9 @@ class CustomPaginationSerializer(pagination.PaginationSerializer): ...@@ -105,6 +105,9 @@ class CustomPaginationSerializer(pagination.PaginationSerializer):
class SecureAPIView(APIView): class SecureAPIView(APIView):
"""
View used for protecting access to specific workflows
"""
permission_classes = (ApiKeyHeaderPermission, ) permission_classes = (ApiKeyHeaderPermission, )
......
# pylint: disable=W0612
"""
Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/sessions/test_login_ratelimit.py]
"""
import json import json
import uuid import uuid
import unittest
from mock import patch from mock import patch
from datetime import datetime, timedelta from datetime import datetime, timedelta
from freezegun import freeze_time from freezegun import freeze_time
...@@ -10,7 +14,6 @@ from django.test import TestCase ...@@ -10,7 +14,6 @@ from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.test.utils import override_settings from django.test.utils import override_settings
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
......
# pylint: disable=W0612
""" """
Tests for session api with advance security features Tests for session api with advance security features
""" """
......
...@@ -6,10 +6,8 @@ Run these tests @ Devstack: ...@@ -6,10 +6,8 @@ Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/tests/test_session_views.py] rake fasttest_lms[common/djangoapps/api_manager/tests/test_session_views.py]
""" """
from random import randint from random import randint
import unittest
import uuid import uuid
from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.cache import cache from django.core.cache import cache
from django.test import TestCase, Client from django.test import TestCase, Client
......
...@@ -24,14 +24,35 @@ from student.models import ( ...@@ -24,14 +24,35 @@ from student.models import (
) )
AUDIT_LOG = logging.getLogger("audit") AUDIT_LOG = logging.getLogger("audit")
class SessionsList(SecureAPIView): class SessionsList(SecureAPIView):
""" Inherit with SecureAPIView """ """
**Use Case**
SessionsList creates a new session with the edX LMS.
**Example Request**
POST {"username": "staff", "password": "edx"}
**Response Values**
* token: A unique token value for the session created.
* expires: The number of seconds until the new session expires.
* user: The following data about the user for whom the session is
created.
* id: The unique user identifier.
* email: The user's email address.
* username: The user's edX username.
* first_name: The user's first name, if defined.
* last_name: The user's last name, if defined.
* creaed: The time and date the user account was created.
* organizations: An array of organizations the user is associated
with.
* uri: The URI to use to get details about the new session.
"""
def post(self, request): def post(self, request):
"""
POST creates a new system session, supported authentication modes:
1. Open edX username/password
"""
response_data = {} response_data = {}
# Add some rate limiting here by re-using the RateLimitMixin as a helper class # Add some rate limiting here by re-using the RateLimitMixin as a helper class
limiter = BadRequestRateLimiter() limiter = BadRequestRateLimiter()
...@@ -98,12 +119,28 @@ class SessionsList(SecureAPIView): ...@@ -98,12 +119,28 @@ class SessionsList(SecureAPIView):
class SessionsDetail(SecureAPIView): class SessionsDetail(SecureAPIView):
""" Inherit with SecureAPIView """ """
**Use Case**
SessionsDetail gets a details about a specific API session, as well as
enables you to delete an API session.
**Example Requests**
GET /api/session/{session_id}
DELETE /api/session/{session_id}/delete
**GET Response Values**
* token: A unique token value for the session.
* expires: The number of seconds until the session expires.
* user_id: The unique user identifier.
* uri: The URI to use to get details about the session.
"""
def get(self, request, session_id): def get(self, request, session_id):
"""
GET retrieves an existing system session
"""
response_data = {} response_data = {}
base_uri = generate_base_uri(request) base_uri = generate_base_uri(request)
engine = import_module(settings.SESSION_ENGINE) engine = import_module(settings.SESSION_ENGINE)
...@@ -125,9 +162,6 @@ class SessionsDetail(SecureAPIView): ...@@ -125,9 +162,6 @@ class SessionsDetail(SecureAPIView):
return Response(response_data, status=status.HTTP_404_NOT_FOUND) return Response(response_data, status=status.HTTP_404_NOT_FOUND)
def delete(self, request, session_id): def delete(self, request, session_id):
"""
DELETE flushes an existing system session from the system
"""
response_data = {} response_data = {}
engine = import_module(settings.SESSION_ENGINE) engine = import_module(settings.SESSION_ENGINE)
session = engine.SessionStore(session_id) session = engine.SessionStore(session_id)
......
...@@ -4,10 +4,8 @@ ...@@ -4,10 +4,8 @@
Run these tests @ Devstack: Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/tests/test_views.py] rake fasttest_lms[common/djangoapps/api_manager/tests/test_views.py]
""" """
import unittest
import uuid import uuid
from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.test import TestCase, Client from django.test import TestCase, Client
from django.test.utils import override_settings from django.test.utils import override_settings
...@@ -77,4 +75,3 @@ class SystemApiTests(TestCase): ...@@ -77,4 +75,3 @@ class SystemApiTests(TestCase):
self.assertIsNotNone(response.data['description']) self.assertIsNotNone(response.data['description'])
self.assertGreater(len(response.data['description']), 0) self.assertGreater(len(response.data['description']), 0)
self.assertIsNotNone(response.data['resources']) self.assertIsNotNone(response.data['resources'])
...@@ -12,6 +12,9 @@ class SystemDetail(SecureAPIView): ...@@ -12,6 +12,9 @@ class SystemDetail(SecureAPIView):
"""Manages system-level information about the Open edX API""" """Manages system-level information about the Open edX API"""
def get(self, request): def get(self, request):
"""
GET /api/system/
"""
base_uri = generate_base_uri(request) base_uri = generate_base_uri(request)
response_data = {} response_data = {}
response_data['name'] = "Open edX System API" response_data['name'] = "Open edX System API"
...@@ -25,6 +28,9 @@ class ApiDetail(SecureAPIView): ...@@ -25,6 +28,9 @@ class ApiDetail(SecureAPIView):
"""Manages top-level information about the Open edX API""" """Manages top-level information about the Open edX API"""
def get(self, request): def get(self, request):
"""
GET /api/
"""
base_uri = generate_base_uri(request) base_uri = generate_base_uri(request)
response_data = {} response_data = {}
response_data['name'] = "Open edX API" response_data['name'] = "Open edX API"
......
...@@ -3,10 +3,8 @@ Run these tests @ Devstack: ...@@ -3,10 +3,8 @@ Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/tests/test_permissions.py] rake fasttest_lms[common/djangoapps/api_manager/tests/test_permissions.py]
""" """
from random import randint from random import randint
import unittest
import uuid import uuid
from django.conf import settings
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
...@@ -78,4 +76,3 @@ class PermissionsTests(TestCase): ...@@ -78,4 +76,3 @@ class PermissionsTests(TestCase):
ip_address = kwargs.get('ip_address', {}) ip_address = kwargs.get('ip_address', {})
response = self.client.post(uri, headers=headers, data=data, **ip_address) response = self.client.post(uri, headers=headers, data=data, **ip_address)
return response return response
# pylint: disable=C0103
""" """
The URI scheme for resources is as follows: The URI scheme for resources is as follows:
Resource type: /api/{resource_type} Resource type: /api/{resource_type}
......
...@@ -153,7 +153,6 @@ class UserPasswordResetTest(TestCase): ...@@ -153,7 +153,6 @@ class UserPasswordResetTest(TestCase):
) )
self._assert_response(response, status=403, message=message) self._assert_response(response, status=403, message=message)
@override_settings(ADVANCED_SECURITY_CONFIG={'MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS': 1}) @override_settings(ADVANCED_SECURITY_CONFIG={'MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS': 1})
def test_is_password_reset_too_frequent(self): def test_is_password_reset_too_frequent(self):
""" """
......
# pylint: disable=E1101
# pylint: disable=E1103 # pylint: disable=E1103
""" """
Run these tests @ Devstack: Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/tests/test_user_views.py] rake fasttest_lms[common/djangoapps/api_manager/tests/test_user_views.py]
""" """
from datetime import datetime
from random import randint from random import randint
import json import json
import unittest
import uuid import uuid
from urllib import urlencode
from mock import patch from mock import patch
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.core.cache import cache from django.core.cache import cache
from django.test import TestCase, Client from django.test import TestCase, Client
from django.test.utils import override_settings from django.test.utils import override_settings
from student.tests.factories import UserFactory
from student.models import anonymous_id_for_user
from projects.models import Project
from capa.tests.response_xml_factory import StringResponseXMLFactory
from courseware.tests.factories import StudentModuleFactory
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from projects.models import Project
from student.tests.factories import UserFactory
from student.models import anonymous_id_for_user
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore import Location
TEST_API_KEY = str(uuid.uuid4()) TEST_API_KEY = str(uuid.uuid4())
...@@ -51,24 +57,38 @@ class UsersApiTests(TestCase): ...@@ -51,24 +57,38 @@ class UsersApiTests(TestCase):
self.test_last_name = str(uuid.uuid4()) self.test_last_name = str(uuid.uuid4())
self.test_city = str(uuid.uuid4()) self.test_city = str(uuid.uuid4())
self.org_base_uri = '/api/organizations/' self.org_base_uri = '/api/organizations/'
self.test_bogus_course_id = 'foo/bar/baz'
self.test_bogus_content_id = 'i4x://foo/bar/baz/Chapter1'
self.test_course_data = '<html>{}</html>'.format(str(uuid.uuid4())) self.test_course_data = '<html>{}</html>'.format(str(uuid.uuid4()))
self.course = CourseFactory.create() self.course = CourseFactory.create(
display_name="TEST COURSE",
start=datetime(2014, 6, 16, 14, 30),
end=datetime(2015, 1, 16, 14, 30)
)
self.course_content = ItemFactory.create( self.course_content = ItemFactory.create(
category="videosequence", category="videosequence",
parent_location=self.course.location, parent_location=self.course.location,
data=self.test_course_data, data=self.test_course_data,
due="2016-05-16T14:30:00Z", due=datetime(2016, 5, 16, 14, 30),
display_name="View_Sequence" display_name="View_Sequence"
) )
self.test_project = Project.objects.create( self.test_project = Project.objects.create(
course_id=self.course.id, course_id=unicode(self.course.id),
content_id=self.course_content.id content_id=unicode(self.course_content.scope_ids.usage_id)
) )
self.course2 = CourseFactory.create(display_name="TEST COURSE2", org='TESTORG2')
self.course2_content = ItemFactory.create(
category="videosequence",
parent_location=self.course2.location,
data=self.test_course_data,
due=datetime(2016, 5, 16, 14, 30),
display_name="View_Sequence2"
)
self.second_test_project = Project.objects.create( self.second_test_project = Project.objects.create(
course_id=self.course.id + 'b2', course_id=unicode(self.course2.id),
content_id=self.course_content.id + 'b2' content_id=unicode(self.course2_content.scope_ids.usage_id)
) )
self.user = UserFactory() self.user = UserFactory()
...@@ -245,7 +265,7 @@ class UsersApiTests(TestCase): ...@@ -245,7 +265,7 @@ class UsersApiTests(TestCase):
test_uri = '/api/users' test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99)) local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, data = {'email': self.test_email,
'username': local_username, 'password':self.test_password, 'username': local_username, 'password': self.test_password,
'first_name': self.test_first_name, 'last_name': self.test_last_name} 'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
...@@ -428,7 +448,6 @@ class UsersApiTests(TestCase): ...@@ -428,7 +448,6 @@ class UsersApiTests(TestCase):
data = {'email': self.test_email, 'username': local_username, 'password': data = {'email': self.test_email, 'username': local_username, 'password':
self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name} self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
user_id = response.data['id']
test_uri = test_uri + '/' + str(response.data['id']) test_uri = test_uri + '/' + str(response.data['id'])
response = self.do_get(test_uri) response = self.do_get(test_uri)
test_uri = test_uri + '/groups' test_uri = test_uri + '/groups'
...@@ -561,7 +580,6 @@ class UsersApiTests(TestCase): ...@@ -561,7 +580,6 @@ class UsersApiTests(TestCase):
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_user_courses_list_post(self): def test_user_courses_list_post(self):
course = CourseFactory.create()
test_uri = '/api/users' test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99)) local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': data = {'email': self.test_email, 'username': local_username, 'password':
...@@ -569,12 +587,12 @@ class UsersApiTests(TestCase): ...@@ -569,12 +587,12 @@ class UsersApiTests(TestCase):
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
user_id = response.data['id'] user_id = response.data['id']
test_uri = '{}/{}/courses'.format(test_uri, str(user_id)) test_uri = '{}/{}/courses'.format(test_uri, str(user_id))
data = {'course_id': course.id} data = {'course_id': unicode(self.course.id)}
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
confirm_uri = self.test_server_prefix + test_uri + '/' + course.id confirm_uri = self.test_server_prefix + test_uri + '/' + unicode(self.course.id)
self.assertEqual(response.data['uri'], confirm_uri) self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['id'], course.id) self.assertEqual(response.data['id'], unicode(self.course.id))
self.assertTrue(response.data['is_active']) self.assertTrue(response.data['is_active'])
def test_user_courses_list_post_undefined_user(self): def test_user_courses_list_post_undefined_user(self):
...@@ -582,7 +600,7 @@ class UsersApiTests(TestCase): ...@@ -582,7 +600,7 @@ class UsersApiTests(TestCase):
test_uri = '/api/users' test_uri = '/api/users'
user_id = '234234' user_id = '234234'
test_uri = '{}/{}/courses'.format(test_uri, str(user_id)) test_uri = '{}/{}/courses'.format(test_uri, str(user_id))
data = {'course_id': course.id} data = {'course_id': unicode(course.id)}
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
...@@ -594,12 +612,14 @@ class UsersApiTests(TestCase): ...@@ -594,12 +612,14 @@ class UsersApiTests(TestCase):
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
user_id = response.data['id'] user_id = response.data['id']
test_uri = '{}/{}/courses'.format(test_uri, str(user_id)) test_uri = '{}/{}/courses'.format(test_uri, str(user_id))
data = {'course_id': '234asdfapsdf'} data = {'course_id': 'slashes:234asdfapsdf+2sdfs+sdf'}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 404)
data = {'course_id': 'really-invalid-course-id-oh-boy-watch-out'}
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_user_courses_list_get(self): def test_user_courses_list_get(self):
course = CourseFactory.create(display_name="TEST COURSE")
test_uri = '/api/users' test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99)) local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': data = {'email': self.test_email, 'username': local_username, 'password':
...@@ -607,16 +627,28 @@ class UsersApiTests(TestCase): ...@@ -607,16 +627,28 @@ class UsersApiTests(TestCase):
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
user_id = response.data['id'] user_id = response.data['id']
test_uri = '{}/{}/courses'.format(test_uri, str(user_id)) test_uri = '{}/{}/courses'.format(test_uri, str(user_id))
data = {'course_id': course.id}
data = {'course_id': unicode(self.course.id)}
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
confirm_uri = self.test_server_prefix + test_uri + '/' + unicode(self.course.id)
course_with_out_date_values = CourseFactory.create()
data = {'course_id': unicode(course_with_out_date_values.id)}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
confirm_uri = self.test_server_prefix + test_uri + '/' + course.id confirm_uri = self.test_server_prefix + test_uri + '/' + unicode(course_with_out_date_values.id)
self.assertEqual(response.data[0]['uri'], confirm_uri) self.assertEqual(response.data[0]['uri'], confirm_uri)
self.assertEqual(response.data[0]['id'], course.id) self.assertEqual(response.data[0]['id'], unicode(course_with_out_date_values.id))
self.assertTrue(response.data[0]['is_active']) self.assertTrue(response.data[0]['is_active'])
self.assertEqual(response.data[0]['name'], course.display_name) self.assertEqual(response.data[0]['name'], course_with_out_date_values.display_name)
self.assertEqual(response.data[0]['start'], course_with_out_date_values.start)
self.assertEqual(response.data[0]['end'], course_with_out_date_values.end)
self.assertEqual(datetime.strftime(response.data[1]['start'], '%Y-%m-%d %H:%M:%S'), datetime.strftime(self.course.start, '%Y-%m-%d %H:%M:%S'))
self.assertEqual(datetime.strftime(response.data[1]['end'], '%Y-%m-%d %H:%M:%S'), datetime.strftime(self.course.end, '%Y-%m-%d %H:%M:%S'))
def test_user_courses_list_get_undefined_user(self): def test_user_courses_list_get_undefined_user(self):
test_uri = '/api/users/2134234/courses' test_uri = '/api/users/2134234/courses'
...@@ -644,6 +676,37 @@ class UsersApiTests(TestCase): ...@@ -644,6 +676,37 @@ class UsersApiTests(TestCase):
data=test_data, data=test_data,
display_name="Chapter 3" display_name="Chapter 3"
) )
sequential1 = ItemFactory.create(
category="sequential",
parent_location=chapter3.location,
data=test_data,
display_name="Sequential 1"
)
sequential2 = ItemFactory.create(
category="sequential",
parent_location=chapter3.location,
data=test_data,
display_name="Sequential 2"
)
vertical1 = ItemFactory.create(
category="vertical",
parent_location=sequential2.location,
data=test_data,
display_name="Vertical 1"
)
vertical2 = ItemFactory.create(
category="vertical",
parent_location=sequential2.location,
data=test_data,
display_name="Vertical 2"
)
vertical3 = ItemFactory.create(
category="vertical",
parent_location=sequential2.location,
data=test_data,
display_name="Vertical 3"
)
test_uri = '/api/users' test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99)) local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': data = {'email': self.test_email, 'username': local_username, 'password':
...@@ -651,19 +714,47 @@ class UsersApiTests(TestCase): ...@@ -651,19 +714,47 @@ class UsersApiTests(TestCase):
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
user_id = response.data['id'] user_id = response.data['id']
test_uri = test_uri + '/' + str(user_id) + '/courses' test_uri = test_uri + '/' + str(user_id) + '/courses'
data = {'course_id': course.id} data = {'course_id': unicode(course.id)}
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
test_uri = test_uri + '/' + str(course.id) test_uri = test_uri + '/' + unicode(course.id)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
position_data = { position_data = {
'position': { 'position': {
'parent_content_id': str(course.id), 'parent_content_id': unicode(course.id),
'child_content_id': str(chapter3.location) 'child_content_id': str(chapter3.location)
}
}
response = self.do_post(test_uri, data=position_data)
self.assertEqual(response.data['position'], unicode(chapter3.scope_ids.usage_id))
position_data = {
'position': {
'parent_content_id': unicode(chapter3.scope_ids.usage_id),
'child_content_id': str(sequential2.location)
} }
} }
response = self.do_post(test_uri, data=position_data) response = self.do_post(test_uri, data=position_data)
self.assertEqual(response.data['position'], chapter3.id) self.assertEqual(response.data['position'], unicode(sequential2.scope_ids.usage_id))
position_data = {
'position': {
'parent_content_id': unicode(sequential2.scope_ids.usage_id),
'child_content_id': str(vertical3.location)
}
}
response = self.do_post(test_uri, data=position_data)
self.assertEqual(response.data['position'], unicode(vertical3.scope_ids.usage_id))
response = self.do_get(response.data['uri'])
self.assertEqual(response.data['position_tree']['chapter']['id'], unicode(chapter3.scope_ids.usage_id))
self.assertEqual(response.data['position_tree']['sequential']['id'], unicode(sequential2.scope_ids.usage_id))
self.assertEqual(response.data['position_tree']['vertical']['id'], unicode(vertical3.scope_ids.usage_id))
def test_user_courses_detail_post_invalid_course(self):
test_uri = '/api/users/{}/courses/{}'.format(self.user.id, self.test_bogus_course_id)
response = self.do_post(test_uri, data={})
self.assertEqual(response.status_code, 404)
def test_user_courses_detail_post_position_invalid_user(self): def test_user_courses_detail_post_position_invalid_user(self):
course = CourseFactory.create() course = CourseFactory.create()
...@@ -703,7 +794,7 @@ class UsersApiTests(TestCase): ...@@ -703,7 +794,7 @@ class UsersApiTests(TestCase):
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
user_id = response.data['id'] user_id = response.data['id']
test_uri = test_uri + '/' + str(user_id) + '/courses' test_uri = test_uri + '/' + str(user_id) + '/courses'
data = {'course_id': course.id} data = {'course_id': unicode(course.id)}
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
test_uri = test_uri + '/' + str(course.id) test_uri = test_uri + '/' + str(course.id)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
...@@ -715,7 +806,22 @@ class UsersApiTests(TestCase): ...@@ -715,7 +806,22 @@ class UsersApiTests(TestCase):
} }
} }
response = self.do_post(test_uri, data=position_data) response = self.do_post(test_uri, data=position_data)
self.assertEqual(response.data['position'], chapter1.id) self.assertEqual(response.data['position'], unicode(chapter1.scope_ids.usage_id))
def test_user_courses_detail_post_position_invalid_course(self):
test_uri = '/api/users/{}/courses'.format(self.user.id)
data = {'course_id': unicode(self.course.id)}
response = self.do_post(test_uri, data)
test_uri = test_uri + '/' + unicode(self.course.id)
self.assertEqual(response.status_code, 201)
position_data = {
'position': {
'parent_content_id': self.test_bogus_course_id,
'child_content_id': self.test_bogus_content_id
}
}
response = self.do_post(test_uri, data=position_data)
self.assertEqual(response.status_code, 400)
def test_user_courses_detail_get(self): def test_user_courses_detail_get(self):
course = CourseFactory.create() course = CourseFactory.create()
...@@ -733,25 +839,33 @@ class UsersApiTests(TestCase): ...@@ -733,25 +839,33 @@ class UsersApiTests(TestCase):
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
user_id = response.data['id'] user_id = response.data['id']
test_uri = test_uri + '/' + str(user_id) + '/courses' test_uri = test_uri + '/' + str(user_id) + '/courses'
data = {'course_id': course.id} data = {'course_id': unicode(course.id)}
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
test_uri = test_uri + '/' + str(course.id) test_uri = test_uri + '/' + unicode(course.id)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
confirm_uri = self.test_server_prefix + test_uri confirm_uri = self.test_server_prefix + test_uri
self.assertEqual(response.data['uri'], confirm_uri) self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['course_id'], course.id) self.assertEqual(response.data['course_id'], unicode(course.id))
self.assertEqual(response.data['user_id'], user_id) self.assertEqual(response.data['user_id'], user_id)
position_data = { position_data = {
'position': { 'position': {
'parent_content_id': str(course.location), 'parent_content_id': unicode(course.id),
'child_content_id': str(chapter1.location) 'child_content_id': unicode(chapter1.scope_ids.usage_id)
} }
} }
response = self.do_post(confirm_uri, data=position_data) response = self.do_post(confirm_uri, data=position_data)
self.assertEqual(response.data['position'], chapter1.id) self.assertEqual(response.data['position'], unicode(chapter1.scope_ids.usage_id))
response = self.do_get(confirm_uri)
self.assertGreater(response.data['position'], 0) # Position in the GET response is an integer!
self.assertEqual(response.data['position_tree']['chapter']['id'], unicode(chapter1.scope_ids.usage_id))
def test_user_courses_detail_get_invalid_course(self):
test_uri = '/api/users/{}/courses/{}'.format(self.user.id, self.test_bogus_course_id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_user_courses_detail_get_undefined_user(self): def test_user_courses_detail_get_undefined_user(self):
test_uri = '/api/users/2134234/courses/a8df7/asv/d98' test_uri = '/api/users/2134234/courses/a8df7/asv/d98'
...@@ -779,7 +893,7 @@ class UsersApiTests(TestCase): ...@@ -779,7 +893,7 @@ class UsersApiTests(TestCase):
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
user_id = response.data['id'] user_id = response.data['id']
post_uri = test_uri + '/' + str(user_id) + '/courses' post_uri = test_uri + '/' + str(user_id) + '/courses'
data = {'course_id': course.id} data = {'course_id': unicode(course.id)}
response = self.do_post(post_uri, data) response = self.do_post(post_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
test_uri = post_uri + '/' + str(course.id) test_uri = post_uri + '/' + str(course.id)
...@@ -808,25 +922,13 @@ class UsersApiTests(TestCase): ...@@ -808,25 +922,13 @@ class UsersApiTests(TestCase):
self.assertEqual(response.status_code, 204) self.assertEqual(response.status_code, 204)
def test_user_courses_detail_delete_undefined_course(self): def test_user_courses_detail_delete_undefined_course(self):
test_uri = '/api/users' test_uri = '/api/users/{}/courses/{}'.format(str(self.user.id), self.test_bogus_course_id)
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password':
self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data)
user_id = response.data['id']
test_uri = '{}/{}/oasdf987sdf'.format(test_uri, str(user_id))
response = self.do_delete(test_uri) response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 204)
def test_user_course_grades_course_not_found(self): def test_user_course_grades_course_not_found(self):
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password':
self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data)
user_id = response.data['id']
test_uri = '/api/users/{}/courses/{}/grades'.format( test_uri = '/api/users/{}/courses/{}/grades'.format(
user_id, 'some/unknown/course') self.user.id, 'slashes:some+unknown+course')
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
...@@ -837,12 +939,12 @@ class UsersApiTests(TestCase): ...@@ -837,12 +939,12 @@ class UsersApiTests(TestCase):
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_get_user_preferences_user_not_found(self): def test_user_preferences_user_list_get_not_found(self):
test_uri = '/api/users/{}/preferences'.format('999999') test_uri = '/api/users/{}/preferences'.format('999999')
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_get_user_preferences_default(self): def test_user_preferences_list_get_default(self):
# By default newly created users will have one initial preference settings: # By default newly created users will have one initial preference settings:
# 'pref-lang' = 'en' # 'pref-lang' = 'en'
user_id = self._create_test_user() user_id = self._create_test_user()
...@@ -852,12 +954,12 @@ class UsersApiTests(TestCase): ...@@ -852,12 +954,12 @@ class UsersApiTests(TestCase):
self.assertEqual(len(response.data), 1) self.assertEqual(len(response.data), 1)
self.assertEqual(response.data['pref-lang'], 'en') self.assertEqual(response.data['pref-lang'], 'en')
def test_post_user_preferences_user_not_found(self): def test_user_preferences_list_post_user_not_found(self):
test_uri = '/api/users/{}/preferences'.format('999999') test_uri = '/api/users/{}/preferences'.format('999999')
response = self.do_post(test_uri, {"foo": "bar"}) response = self.do_post(test_uri, {"foo": "bar"})
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_post_user_preferences_bad_request(self): def test_user_preferences_list_post_bad_request(self):
user_id = self._create_test_user() user_id = self._create_test_user()
test_uri = '/api/users/{}/preferences'.format(user_id) test_uri = '/api/users/{}/preferences'.format(user_id)
response = self.do_post(test_uri, {}) response = self.do_post(test_uri, {})
...@@ -872,7 +974,7 @@ class UsersApiTests(TestCase): ...@@ -872,7 +974,7 @@ class UsersApiTests(TestCase):
response = self.do_post(test_uri, {"a_boolean": False}) response = self.do_post(test_uri, {"a_boolean": False})
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
def test_post_user_preferences(self): def test_user_preferences_list_post(self):
user_id = self._create_test_user() user_id = self._create_test_user()
test_uri = '/api/users/{}/preferences'.format(user_id) test_uri = '/api/users/{}/preferences'.format(user_id)
response = self.do_post(test_uri, {"foo": "bar"}) response = self.do_post(test_uri, {"foo": "bar"})
...@@ -883,7 +985,7 @@ class UsersApiTests(TestCase): ...@@ -883,7 +985,7 @@ class UsersApiTests(TestCase):
self.assertEqual(response.data['pref-lang'], 'en') self.assertEqual(response.data['pref-lang'], 'en')
self.assertEqual(response.data['foo'], 'bar') self.assertEqual(response.data['foo'], 'bar')
def test_update_user_preferences(self): def test_user_preferences_list_update(self):
user_id = self._create_test_user() user_id = self._create_test_user()
test_uri = '/api/users/{}/preferences'.format(user_id) test_uri = '/api/users/{}/preferences'.format(user_id)
response = self.do_post(test_uri, {"foo": "bar"}) response = self.do_post(test_uri, {"foo": "bar"})
...@@ -896,13 +998,41 @@ class UsersApiTests(TestCase): ...@@ -896,13 +998,41 @@ class UsersApiTests(TestCase):
self.assertEqual(response.data['pref-lang'], 'en') self.assertEqual(response.data['pref-lang'], 'en')
self.assertEqual(response.data['foo'], 'updated') self.assertEqual(response.data['foo'], 'updated')
def test_user_preferences_detail_get(self):
user_id = self._create_test_user()
test_uri = '/api/users/{}/preferences'.format(user_id)
response = self.do_post(test_uri, {"foo": "bar"})
self.assertEqual(response.status_code, 201)
test_uri = '{}/{}'.format(test_uri, 'foo')
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['foo'], 'bar')
def test_user_preferences_detail_get_invalid_user(self):
test_uri = '/api/users/12345/preferences/foo'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_user_preferences_detail_delete(self):
user_id = self._create_test_user()
test_uri = '/api/users/{}/preferences'.format(user_id)
response = self.do_post(test_uri, {"foo": "bar"})
self.assertEqual(response.status_code, 201)
test_uri = '{}/{}'.format(test_uri, 'foo')
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_user_preferences_detail_delete_invalid_user(self):
test_uri = '/api/users/12345/preferences/foo'
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 404)
def test_course_grades(self): def test_course_grades(self):
test_uri = '/api/users' user_id = self.user.id
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password':
self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data)
user_id = response.data['id']
course = CourseFactory.create() course = CourseFactory.create()
test_data = '<html>{}</html>'.format(str(uuid.uuid4())) test_data = '<html>{}</html>'.format(str(uuid.uuid4()))
...@@ -931,8 +1061,35 @@ class UsersApiTests(TestCase): ...@@ -931,8 +1061,35 @@ class UsersApiTests(TestCase):
display_name="Sequence 2", display_name="Sequence 2",
) )
ItemFactory.create(
parent_location=chapter2.location,
category='problem',
data=StringResponseXMLFactory().build_xml(answer='foo'),
metadata={'rerandomize': 'always'},
display_name="test problem 1",
max_grade=45
)
problem = ItemFactory.create(
parent_location=chapter2.location,
category='problem',
data=StringResponseXMLFactory().build_xml(answer='bar'),
metadata={'rerandomize': 'always'},
display_name="test problem 2"
)
StudentModuleFactory.create(
grade=1,
max_grade=1,
student=self.user,
course_id=course.id,
module_state_key=problem.location,
state=json.dumps({'attempts': 3}),
module_type='mentoring'
)
test_uri = '/api/users/{}/courses/{}/grades'.format( test_uri = '/api/users/{}/courses/{}/grades'.format(
user_id, course.id) user_id, unicode(course.id))
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -948,7 +1105,7 @@ class UsersApiTests(TestCase): ...@@ -948,7 +1105,7 @@ class UsersApiTests(TestCase):
self.assertEqual(sections[0]['graded'], False) self.assertEqual(sections[0]['graded'], False)
sections = courseware_summary[1]['sections'] sections = courseware_summary[1]['sections']
self.assertEqual(len(sections), 1) self.assertEqual(len(sections), 3)
self.assertEqual(sections[0]['display_name'], 'Sequence 2') self.assertEqual(sections[0]['display_name'], 'Sequence 2')
self.assertEqual(sections[0]['graded'], False) self.assertEqual(sections[0]['graded'], False)
...@@ -958,6 +1115,9 @@ class UsersApiTests(TestCase): ...@@ -958,6 +1115,9 @@ class UsersApiTests(TestCase):
self.assertGreater(len(grading_policy['GRADER']), 0) self.assertGreater(len(grading_policy['GRADER']), 0)
self.assertIsNotNone(grading_policy['GRADE_CUTOFFS']) self.assertIsNotNone(grading_policy['GRADE_CUTOFFS'])
self.assertEqual(response.data['current_grade'], 50)
self.assertEqual(response.data['pro_forma_grade'], 100)
def is_user_profile_created_updated(self, response, data): def is_user_profile_created_updated(self, response, data):
"""This function compare response with user profile data """ """This function compare response with user profile data """
...@@ -1029,7 +1189,8 @@ class UsersApiTests(TestCase): ...@@ -1029,7 +1189,8 @@ class UsersApiTests(TestCase):
self.assertEqual(response.data['num_pages'], 2) self.assertEqual(response.data['num_pages'], 2)
# test with course_id filter and integer user id # test with course_id filter and integer user id
response = self.do_get('/api/users/{}/workgroups/?course_id={}'.format(user_id, self.course.id)) course_id = {'course_id': unicode(self.course.id)}
response = self.do_get('/api/users/{}/workgroups/?{}'.format(user_id, urlencode(course_id)))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 7) self.assertEqual(response.data['count'], 7)
self.assertEqual(len(response.data['results']), 7) self.assertEqual(len(response.data['results']), 7)
...@@ -1050,44 +1211,53 @@ class UsersApiTests(TestCase): ...@@ -1050,44 +1211,53 @@ class UsersApiTests(TestCase):
def test_user_completions_list(self): def test_user_completions_list(self):
user_id = self.user.id user_id = self.user.id
another_user_id = UserFactory().id another_user_id = UserFactory().id
completion_uri = '/api/courses/{}/completions/'.format(self.course.id) completion_uri = '/api/courses/{}/completions/'.format(unicode(self.course.id))
for i in xrange(1, 17): for i in xrange(1, 26):
if i > 7: if i > 12:
course_user_id = another_user_id course_user_id = another_user_id
else: else:
course_user_id = user_id course_user_id = user_id
completions_data = {'content_id': '{}_{}'.format(self.course_content.id, i), 'user_id': course_user_id} local_content_name = 'Video_Sequence{}'.format(i)
local_content = ItemFactory.create(
category="videosequence",
parent_location=self.course_content.location,
data=str(uuid.uuid4()),
display_name=local_content_name
)
completions_data = {'content_id': unicode(local_content.scope_ids.usage_id), 'user_id': course_user_id}
response = self.do_post(completion_uri, completions_data) response = self.do_post(completion_uri, completions_data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
# Get course module completion by user # Get course module completion by user
completion_list_uri = '/api/users/{}/courses/{}/completions/?page_size=5'.format(user_id, self.course.id) completion_list_uri = '/api/users/{}/courses/{}/completions/?page_size=6'.format(user_id, unicode(self.course.id))
response = self.do_get(completion_list_uri) response = self.do_get(completion_list_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 7) self.assertEqual(response.data['count'], 12)
self.assertEqual(len(response.data['results']), 5) self.assertEqual(len(response.data['results']), 6) # 12 matches, but only 6 per page
self.assertEqual(response.data['results'][0]['user_id'], user_id) self.assertEqual(response.data['results'][0]['user_id'], user_id)
self.assertEqual(response.data['results'][0]['course_id'], self.course.id) self.assertEqual(response.data['results'][0]['course_id'], unicode(self.course.id))
self.assertEqual(response.data['num_pages'], 2) self.assertEqual(response.data['num_pages'], 2)
# Get course module completion by other user # Get course module completion by other user
completion_list_uri = '/api/users/{}/courses/{}/completions/'.format(another_user_id, self.course.id) completion_list_uri = '/api/users/{}/courses/{}/completions/'.format(another_user_id, unicode(self.course.id))
response = self.do_get(completion_list_uri) response = self.do_get(completion_list_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 9) self.assertEqual(response.data['count'], 13)
# Get course module completion by other user and course module id # Get course module completion by other user and course module id (content_id)
completion_list_uri = '/api/users/{}/courses/{}/completions/?content_id={}'.format( content_id = {'content_id': unicode(local_content.scope_ids.usage_id)}
another_user_id, completion_list_uri = '/api/users/{}/courses/{}/completions/?{}'.format(
self.course.id, course_user_id,
'{}_{}'.format(self.course_content.id, 10)) unicode(self.course.id),
urlencode(content_id)
)
response = self.do_get(completion_list_uri) response = self.do_get(completion_list_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 1) self.assertEqual(response.data['count'], 1)
# Get course module completion by bogus user # Get course module completion by bogus user
completion_list_uri = '/api/users/{}/courses/{}/completions/'.format('34323422', self.course.id) completion_list_uri = '/api/users/{}/courses/{}/completions/'.format('34323422', unicode(self.course.id))
response = self.do_get(completion_list_uri) response = self.do_get(completion_list_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
...@@ -1130,3 +1300,13 @@ class UsersApiTests(TestCase): ...@@ -1130,3 +1300,13 @@ class UsersApiTests(TestCase):
self.assertEqual(len(response.data['results']), 1) self.assertEqual(len(response.data['results']), 1)
self.assertEqual(response.data['results'][0]['city'], 'New York City') self.assertEqual(response.data['results'][0]['city'], 'New York City')
self.assertEqual(response.data['results'][0]['count'], 6) self.assertEqual(response.data['results'][0]['count'], 6)
def test_users_social_metrics_get_service_unavailable(self):
test_uri = '/api/users/{}/courses/{}/metrics/social/'.format(self.user.id, self.course.id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 500)
def test_users_social_metrics_get_invalid_user(self):
test_uri = '/api/users/{}/courses/{}/metrics/social/'.format(12345, self.course.id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
...@@ -7,19 +7,24 @@ from api_manager.users import views as users_views ...@@ -7,19 +7,24 @@ from api_manager.users import views as users_views
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url(r'/*$^', users_views.UsersList.as_view(), name='apimgr-users-list'),
url(r'^metrics/cities/$', users_views.UsersMetricsCitiesList.as_view(), name='apimgr-users-metrics-cities-list'), url(r'^metrics/cities/$', users_views.UsersMetricsCitiesList.as_view(), name='apimgr-users-metrics-cities-list'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)$', users_views.UsersDetail.as_view(), name='apimgr-users-detail'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/*$', users_views.UsersCoursesList.as_view(), name='users-courses-list'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)$', users_views.UsersCoursesDetail.as_view(), name='users-courses-detail'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/grades$', users_views.UsersCoursesGradesDetail.as_view(), name='users-courses-grades-detail'), url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/grades$', users_views.UsersCoursesGradesDetail.as_view(), name='users-courses-grades-detail'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/metrics/social/$', users_views.UsersSocialMetrics.as_view(), name='users-social-metrics'), url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/metrics/social/$', users_views.UsersSocialMetrics.as_view(), name='users-social-metrics'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/completions/$', users_views.UsersCoursesCompletionsList.as_view(), name='users-courses-completions-list'), url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/completions/$', users_views.UsersCoursesCompletionsList.as_view(), name='users-courses-completions-list'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)$', users_views.UsersCoursesDetail.as_view(), name='users-courses-detail'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/(?P<course_id>[a-zA-Z0-9_+\/:]+)/grades$', users_views.UsersCoursesGradesDetail.as_view(), name='users-courses-grades-detail'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/(?P<course_id>[a-zA-Z0-9_+\/:]+)/metrics/social/$', users_views.UsersSocialMetrics.as_view(), name='users-social-metrics'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/(?P<course_id>[a-zA-Z0-9_+\/:]+)/completions/$', users_views.UsersCoursesCompletionsList.as_view(), name='users-courses-completions-list'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/(?P<course_id>[a-zA-Z0-9_+\/:]+)$', users_views.UsersCoursesDetail.as_view(), name='users-courses-detail'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/*$', users_views.UsersCoursesList.as_view(), name='users-courses-list'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/groups/*$', users_views.UsersGroupsList.as_view(), name='users-groups-list'), url(r'^(?P<user_id>[a-zA-Z0-9]+)/groups/*$', users_views.UsersGroupsList.as_view(), name='users-groups-list'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/groups/(?P<group_id>[0-9]+)$', users_views.UsersGroupsDetail.as_view(), name='users-groups-detail'), url(r'^(?P<user_id>[a-zA-Z0-9]+)/groups/(?P<group_id>[0-9]+)$', users_views.UsersGroupsDetail.as_view(), name='users-groups-detail'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/preferences$', users_views.UsersPreferences.as_view(), name='users-preferences-list'), url(r'^(?P<user_id>[a-zA-Z0-9]+)/preferences$', users_views.UsersPreferences.as_view(), name='users-preferences-list'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/preferences/(?P<preference_id>[a-zA-Z0-9_]+)$', users_views.UsersPreferencesDetail.as_view(), name='users-preferences-detail'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/organizations/$', users_views.UsersOrganizationsList.as_view(), name='users-organizations-list'), url(r'^(?P<user_id>[a-zA-Z0-9]+)/organizations/$', users_views.UsersOrganizationsList.as_view(), name='users-organizations-list'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/workgroups/$', users_views.UsersWorkgroupsList.as_view(), name='users-workgroups-list'), url(r'^(?P<user_id>[a-zA-Z0-9]+)/workgroups/$', users_views.UsersWorkgroupsList.as_view(), name='users-workgroups-list'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)$', users_views.UsersDetail.as_view(), name='apimgr-users-detail'),
url(r'/*$^', users_views.UsersList.as_view(), name='apimgr-users-list'),
) )
urlpatterns = format_suffix_patterns(urlpatterns) urlpatterns = format_suffix_patterns(urlpatterns)
""" API implementation for user-oriented interactions. """ """ API implementation for user-oriented interactions. """
import logging import logging
from requests.exceptions import ConnectionError
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db import IntegrityError from django.db import IntegrityError
from django.db.models import Count from django.db.models import Count, Q, Sum
from django.core.validators import validate_email, validate_slug, ValidationError from django.core.validators import validate_email, validate_slug, ValidationError
from django.conf import settings from django.conf import settings
from django.http import Http404 from django.http import Http404
...@@ -13,8 +14,8 @@ from django.utils.translation import get_language, ugettext_lazy as _ ...@@ -13,8 +14,8 @@ from django.utils.translation import get_language, ugettext_lazy as _
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from django.db.models import Q
from api_manager.courseware_access import get_course, get_course_child, get_course_total_score
from api_manager.permissions import SecureAPIView, SecureListAPIView from api_manager.permissions import SecureAPIView, SecureListAPIView
from api_manager.models import GroupProfile, APIUser as User from api_manager.models import GroupProfile, APIUser as User
from api_manager.organizations.serializers import OrganizationSerializer from api_manager.organizations.serializers import OrganizationSerializer
...@@ -25,7 +26,11 @@ from .serializers import UserSerializer, UserCountByCitySerializer ...@@ -25,7 +26,11 @@ from .serializers import UserSerializer, UserCountByCitySerializer
from courseware import module_render from courseware import module_render
from courseware.model_data import FieldDataCache from courseware.model_data import FieldDataCache
from courseware.models import StudentModule
from courseware.views import get_module_for_descriptor, save_child_position, get_current_child from courseware.views import get_module_for_descriptor, save_child_position, get_current_child
from lang_pref import LANGUAGE_KEY from lang_pref import LANGUAGE_KEY
from student.models import CourseEnrollment, PasswordHistory, UserProfile from student.models import CourseEnrollment, PasswordHistory, UserProfile
from openedx.core.djangoapps.user_api.models import UserPreference from openedx.core.djangoapps.user_api.models import UserPreference
...@@ -37,7 +42,6 @@ from util.password_policy_validators import ( ...@@ -37,7 +42,6 @@ from util.password_policy_validators import (
from util.bad_request_rate_limiter import BadRequestRateLimiter from util.bad_request_rate_limiter import BadRequestRateLimiter
from courseware import grades from courseware import grades
from courseware.courses import get_course
from lms.lib.comment_client.user import User as CommentUser from lms.lib.comment_client.user import User as CommentUser
from lms.lib.comment_client.utils import CommentClientRequestError from lms.lib.comment_client.utils import CommentClientRequestError
...@@ -75,38 +79,23 @@ def _serialize_user(response_data, user): ...@@ -75,38 +79,23 @@ def _serialize_user(response_data, user):
return response_data return response_data
def _save_content_position(request, user, course_id, course_descriptor, position): def _save_content_position(request, user, course_key, position):
""" """
Records the indicated position for the specified course Records the indicated position for the specified course
Really no reason to generalize this out of user_courses_detail aside from pylint complaining Really no reason to generalize this out of user_courses_detail aside from pylint complaining
""" """
field_data_cache = FieldDataCache([course_descriptor], course_id, user) parent_content_id = position['parent_content_id']
if course_id == position['parent_content_id']: child_content_id = position['child_content_id']
parent_content = get_module_for_descriptor( if unicode(course_key) == parent_content_id:
user, parent_descriptor, parent_key, parent_content = get_course(request, user, parent_content_id) # pylint: disable=W0612
request,
course_descriptor,
field_data_cache,
course_id
)
else: else:
parent_content = module_render.get_module( parent_descriptor, parent_key, parent_content = get_course_child(request, user, course_key, parent_content_id) # pylint: disable=W0612
user, child_descriptor, child_key, child_content = get_course_child(request, user, course_key, child_content_id) # pylint: disable=W0612
request, if not child_descriptor:
position['parent_content_id'], return None
field_data_cache,
course_id
)
child_content = module_render.get_module(
user,
request,
position['child_content_id'],
field_data_cache,
course_id
)
save_child_position(parent_content, child_content.location.name) save_child_position(parent_content, child_content.location.name)
saved_content = get_current_child(parent_content) saved_content = get_current_child(parent_content)
return saved_content.id return unicode(saved_content.scope_ids.usage_id)
class UsersList(SecureListAPIView): class UsersList(SecureListAPIView):
...@@ -426,14 +415,14 @@ class UsersDetail(SecureAPIView): ...@@ -426,14 +415,14 @@ class UsersDetail(SecureAPIView):
err_msg = _( err_msg = _(
"You are re-using a password that you have used recently. You must " "You are re-using a password that you have used recently. You must "
"have {0} distinct password(s) before reusing a previous password." "have {0} distinct password(s) before reusing a previous password."
).format(num_distinct) ).format(num_distinct) # pylint: disable=E1101
# also, check to see if passwords are getting reset too frequent # also, check to see if passwords are getting reset too frequent
if PasswordHistory.is_password_reset_too_soon(existing_user): if PasswordHistory.is_password_reset_too_soon(existing_user):
num_days = settings.ADVANCED_SECURITY_CONFIG['MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS'] num_days = settings.ADVANCED_SECURITY_CONFIG['MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS']
err_msg = _( err_msg = _(
"You are resetting passwords too frequently. Due to security policies, " "You are resetting passwords too frequently. Due to security policies, "
"{0} day(s) must elapse between password resets" "{0} day(s) must elapse between password resets"
).format(num_days) ).format(num_days) # pylint: disable=E1101
if err_msg: if err_msg:
# We have an password reset attempt which violates some security policy, # We have an password reset attempt which violates some security policy,
...@@ -522,7 +511,7 @@ class UsersGroupsList(SecureAPIView): ...@@ -522,7 +511,7 @@ class UsersGroupsList(SecureAPIView):
except ObjectDoesNotExist: except ObjectDoesNotExist:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
try: try:
existing_relationship = existing_user.groups.get(id=existing_group.id) existing_user.groups.get(id=existing_group.id)
response_data['uri'] = '{}/{}'.format(base_uri, existing_group.id) response_data['uri'] = '{}/{}'.format(base_uri, existing_group.id)
response_data['message'] = "Relationship already exists." response_data['message'] = "Relationship already exists."
return Response(response_data, status=status.HTTP_409_CONFLICT) return Response(response_data, status=status.HTTP_409_CONFLICT)
...@@ -584,7 +573,7 @@ class UsersGroupsDetail(SecureAPIView): ...@@ -584,7 +573,7 @@ class UsersGroupsDetail(SecureAPIView):
response_data['uri'] = generate_base_uri(request) response_data['uri'] = generate_base_uri(request)
return Response(response_data, status=status.HTTP_200_OK) return Response(response_data, status=status.HTTP_200_OK)
def delete(self, request, user_id, group_id): def delete(self, request, user_id, group_id): # pylint: disable=W0612,W0613
""" """
DELETE /api/users/{user_id}/groups/{group_id} DELETE /api/users/{user_id}/groups/{group_id}
""" """
...@@ -615,19 +604,20 @@ class UsersCoursesList(SecureAPIView): ...@@ -615,19 +604,20 @@ class UsersCoursesList(SecureAPIView):
""" """
POST /api/users/{user_id}/courses/ POST /api/users/{user_id}/courses/
""" """
store = modulestore()
response_data = {} response_data = {}
user_id = user_id user_id = user_id
course_id = request.DATA['course_id'] course_id = request.DATA['course_id']
try: try:
user = User.objects.get(id=user_id) user = User.objects.get(id=user_id)
course_descriptor = store.get_course(course_id) course_descriptor, course_key, course_content = get_course(request, user, course_id) # pylint: disable=W0612
if not course_descriptor:
return Response({}, status=status.HTTP_404_NOT_FOUND)
except (ObjectDoesNotExist, ValueError): except (ObjectDoesNotExist, ValueError):
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
base_uri = generate_base_uri(request) base_uri = generate_base_uri(request)
course_enrollment = CourseEnrollment.enroll(user, course_id) course_enrollment = CourseEnrollment.enroll(user, course_key)
response_data['uri'] = '{}/{}'.format(base_uri, course_id) response_data['uri'] = '{}/{}'.format(base_uri, course_id)
response_data['id'] = course_id response_data['id'] = unicode(course_key)
response_data['name'] = course_descriptor.display_name response_data['name'] = course_descriptor.display_name
response_data['is_active'] = course_enrollment.is_active response_data['is_active'] = course_enrollment.is_active
return Response(response_data, status=status.HTTP_201_CREATED) return Response(response_data, status=status.HTTP_201_CREATED)
...@@ -636,7 +626,6 @@ class UsersCoursesList(SecureAPIView): ...@@ -636,7 +626,6 @@ class UsersCoursesList(SecureAPIView):
""" """
GET /api/users/{user_id}/courses/ GET /api/users/{user_id}/courses/
""" """
store = modulestore()
base_uri = generate_base_uri(request) base_uri = generate_base_uri(request)
try: try:
user = User.objects.get(id=user_id) user = User.objects.get(id=user_id)
...@@ -645,19 +634,21 @@ class UsersCoursesList(SecureAPIView): ...@@ -645,19 +634,21 @@ class UsersCoursesList(SecureAPIView):
enrollments = CourseEnrollment.enrollments_for_user(user=user) enrollments = CourseEnrollment.enrollments_for_user(user=user)
response_data = [] response_data = []
for enrollment in enrollments: for enrollment in enrollments:
descriptor = store.get_course(enrollment.course_id) course_descriptor, course_key, course_content = get_course(request, user, unicode(enrollment.course_id)) # pylint: disable=W0612
# NOTE: It is possible that a course has been hard deleted from the courseware # NOTE: It is possible that a course has been hard deleted from the courseware
# database, but the enrollment row in the SQL database still exists # database, but the enrollment row in the SQL database still exists
if descriptor: if course_descriptor:
course_data = { course_data = {
"id": enrollment.course_id, "id": unicode(course_key),
"uri": '{}/{}'.format(base_uri, enrollment.course_id), "uri": '{}/{}'.format(base_uri, unicode(course_key)),
"is_active": enrollment.is_active, "is_active": enrollment.is_active,
"name": descriptor.display_name "name": course_descriptor.display_name,
"start": getattr(course_descriptor, 'start', None),
"end": getattr(course_descriptor, 'end', None)
} }
response_data.append(course_data) response_data.append(course_data)
else: else:
log.warning("User {0} enrolled in course_id {1}, but course could not be found.".format(user_id, enrollment.course_id)) log.warning("User {0} enrolled in course_id {1}, but course could not be found.".format(user_id, unicode(enrollment.course_id)))
return Response(response_data, status=status.HTTP_200_OK) return Response(response_data, status=status.HTTP_200_OK)
...@@ -692,52 +683,77 @@ class UsersCoursesDetail(SecureAPIView): ...@@ -692,52 +683,77 @@ class UsersCoursesDetail(SecureAPIView):
""" """
POST /api/users/{user_id}/courses/{course_id} POST /api/users/{user_id}/courses/{course_id}
""" """
store = modulestore()
base_uri = generate_base_uri(request) base_uri = generate_base_uri(request)
response_data = {} response_data = {}
response_data['uri'] = base_uri response_data['uri'] = base_uri
try: try:
user = User.objects.get(id=user_id) user = User.objects.get(id=user_id, is_active=True)
course_descriptor = store.get_course(course_id) except ObjectDoesNotExist:
except (ObjectDoesNotExist, ValueError): return Response({}, status=status.HTTP_404_NOT_FOUND)
return Response(response_data, status=status.HTTP_404_NOT_FOUND) course_descriptor, course_key, course_content = get_course(request, user, course_id) # pylint: disable=W0612
if not course_descriptor:
return Response({}, status=status.HTTP_404_NOT_FOUND)
response_data['user_id'] = user.id response_data['user_id'] = user.id
response_data['course_id'] = course_id response_data['course_id'] = course_id
if request.DATA['position']: if request.DATA['position']:
response_data['position'] = _save_content_position( content_position = _save_content_position(
request, request,
user, user,
course_id, course_key,
course_descriptor,
request.DATA['position'] request.DATA['position']
) )
if not content_position:
return Response(response_data, status=status.HTTP_400_BAD_REQUEST)
response_data['position'] = content_position
return Response(response_data, status=status.HTTP_200_OK) return Response(response_data, status=status.HTTP_200_OK)
def get(self, request, user_id, course_id): def get(self, request, user_id, course_id):
""" """
GET /api/users/{user_id}/courses/{course_id} GET /api/users/{user_id}/courses/{course_id}
""" """
store = modulestore()
response_data = {} response_data = {}
base_uri = generate_base_uri(request) base_uri = generate_base_uri(request)
try: try:
user = User.objects.get(id=user_id, is_active=True) user = User.objects.get(id=user_id, is_active=True)
course_descriptor = store.get_course(course_id) course_descriptor, course_key, course_content = get_course(request, user, course_id, depth=2)
except (ObjectDoesNotExist, ValueError): except (ObjectDoesNotExist, ValueError):
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
if not CourseEnrollment.is_enrolled(user, course_id): if not CourseEnrollment.is_enrolled(user, course_key):
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
response_data['user_id'] = user.id response_data['user_id'] = user.id
response_data['course_id'] = course_id response_data['course_id'] = course_id
response_data['uri'] = base_uri response_data['uri'] = base_uri
field_data_cache = FieldDataCache([course_descriptor], course_id, user) field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
course_content = module_render.get_module( course_key,
user,
course_descriptor,
depth=2)
course_module = module_render.get_module_for_descriptor(
user, user,
request, request,
course_descriptor.location, course_descriptor,
field_data_cache, field_data_cache,
course_id) course_key)
response_data['position'] = course_content.position response_data['position'] = course_module.position
response_data['position_tree'] = {}
parent_module = course_module
while parent_module is not None:
current_child_descriptor = get_current_child(parent_module)
if current_child_descriptor:
response_data['position_tree'][current_child_descriptor.category] = {}
response_data['position_tree'][current_child_descriptor.category]['id'] = unicode(current_child_descriptor.scope_ids.usage_id)
parent_module = module_render.get_module(
user,
request,
current_child_descriptor.scope_ids.usage_id,
field_data_cache,
course_key
)
else:
parent_module = None
return Response(response_data, status=status.HTTP_200_OK) return Response(response_data, status=status.HTTP_200_OK)
def delete(self, request, user_id, course_id): def delete(self, request, user_id, course_id):
...@@ -748,7 +764,10 @@ class UsersCoursesDetail(SecureAPIView): ...@@ -748,7 +764,10 @@ class UsersCoursesDetail(SecureAPIView):
user = User.objects.get(id=user_id, is_active=True) user = User.objects.get(id=user_id, is_active=True)
except ObjectDoesNotExist: except ObjectDoesNotExist:
return Response({}, status=status.HTTP_204_NO_CONTENT) return Response({}, status=status.HTTP_204_NO_CONTENT)
CourseEnrollment.unenroll(user, course_id) course_descriptor, course_key, course_content = get_course(request, user, course_id) # pylint: disable=W0612
if not course_descriptor:
return Response({}, status=status.HTTP_204_NO_CONTENT)
CourseEnrollment.unenroll(user, course_key)
return Response({}, status=status.HTTP_204_NO_CONTENT) return Response({}, status=status.HTTP_204_NO_CONTENT)
...@@ -767,18 +786,6 @@ class UsersCoursesGradesDetail(SecureAPIView): ...@@ -767,18 +786,6 @@ class UsersCoursesGradesDetail(SecureAPIView):
GET /api/users/{user_id}/courses/{course_id}/grades GET /api/users/{user_id}/courses/{course_id}/grades
""" """
# @TODO: Add authorization check here once we get caller identity
# Only student can get his/her own information *or* course staff
# can get everyone's grades
try:
# get the full course tree with depth=None which reduces the number of
# round trips to the database
course = get_course(course_id, depth=None)
except ValueError:
return Response({}, status=status.HTTP_404_NOT_FOUND)
# The pre-fetching of groups is done to make auth checks not require an # The pre-fetching of groups is done to make auth checks not require an
# additional DB lookup (this kills the Progress page in particular). # additional DB lookup (this kills the Progress page in particular).
try: try:
...@@ -786,14 +793,43 @@ class UsersCoursesGradesDetail(SecureAPIView): ...@@ -786,14 +793,43 @@ class UsersCoursesGradesDetail(SecureAPIView):
except ObjectDoesNotExist: except ObjectDoesNotExist:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
courseware_summary = grades.progress_summary(student, request, course) # @TODO: Add authorization check here once we get caller identity
grade_summary = grades.grade(student, request, course) # Only student can get his/her own information *or* course staff
grading_policy = course.grading_policy # can get everyone's grades
# get the full course tree with depth=None which reduces the number of
# round trips to the database
course_descriptor, course_key, course_content = get_course(request, student, course_id, depth=None) # pylint: disable=W0612
if not course_descriptor:
return Response({}, status=status.HTTP_404_NOT_FOUND)
courseware_summary = grades.progress_summary(student, request, course_descriptor) # pylint: disable=W0612
grade_summary = grades.grade(student, request, course_descriptor)
grading_policy = course_descriptor.grading_policy
queryset = StudentModule.objects.filter(
course_id__exact=course_key,
max_grade__isnull=False,
max_grade__gt=0
)
total_score = get_course_total_score(courseware_summary)
user_queryset = queryset.filter(grade__isnull=False, student=student)
comp_modules = user_queryset.aggregate(Sum('grade'))
score_of_comp_module = comp_modules['grade__sum'] or 0
max_possible_score = user_queryset.aggregate(Sum('max_grade'))
current_grade = 0
pro_forma_grade = 0
if total_score:
current_grade = score_of_comp_module / float(total_score) * 100
if max_possible_score['max_grade__sum']:
pro_forma_grade = score_of_comp_module / float(max_possible_score['max_grade__sum']) * 100
response_data = { response_data = {
'courseware_summary': courseware_summary, 'courseware_summary': courseware_summary,
'grade_summary': grade_summary, 'grade_summary': grade_summary,
'grading_policy': grading_policy 'grading_policy': grading_policy,
'current_grade': current_grade,
'pro_forma_grade': pro_forma_grade
} }
return Response(response_data) return Response(response_data)
...@@ -841,16 +877,16 @@ class UsersPreferences(SecureAPIView): ...@@ -841,16 +877,16 @@ class UsersPreferences(SecureAPIView):
try: try:
user = User.objects.get(id=user_id) user = User.objects.get(id=user_id)
except ObjectDoesNotExist: except ObjectDoesNotExist:
return Response({}, status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
if not len(request.DATA): if not len(request.DATA):
return Response({}, status.HTTP_400_BAD_REQUEST) return Response({}, status=status.HTTP_400_BAD_REQUEST)
# do a quick inspection to make sure we're only getting strings as values # do a quick inspection to make sure we're only getting strings as values
for key in request.DATA.keys(): for key in request.DATA.keys():
value = request.DATA[key] value = request.DATA[key]
if not isinstance(value, basestring): if not isinstance(value, basestring):
return Response({}, status.HTTP_400_BAD_REQUEST) return Response({}, status=status.HTTP_400_BAD_REQUEST)
status_code = status.HTTP_200_OK status_code = status.HTTP_200_OK
for key in request.DATA.keys(): for key in request.DATA.keys():
...@@ -873,6 +909,44 @@ class UsersPreferences(SecureAPIView): ...@@ -873,6 +909,44 @@ class UsersPreferences(SecureAPIView):
return Response({}, status_code) return Response({}, status_code)
class UsersPreferencesDetail(SecureAPIView):
"""
### The UsersPreferencesDetail view allows clients to interact with the User's preferences
- URI: ```/api/users/{user_id}/preferences/{preference_id}```
- DELETE: Removes the specified preference from the user's record
### Use Cases/Notes:
* Use DELETE to remove the last-visited course for a user (for example)
"""
def get(self, request, user_id, preference_id): # pylint: disable=W0613
"""
GET returns the specified preference for the specified user
"""
try:
user = User.objects.get(id=user_id)
except ObjectDoesNotExist:
return Response({}, status.HTTP_404_NOT_FOUND)
response_data = {}
try:
response_data[preference_id] = user.preferences.get(key=preference_id).value
except ObjectDoesNotExist:
return Response({}, status=status.HTTP_404_NOT_FOUND)
return Response(response_data)
def delete(self, request, user_id, preference_id): # pylint: disable=W0613
"""
DELETE /api/users/{user_id}/preferences/{preference_id}
"""
try:
user = User.objects.get(id=user_id)
except ObjectDoesNotExist:
return Response({}, status.HTTP_404_NOT_FOUND)
for preference in user.preferences.all():
if preference.key == preference_id:
UserPreference.objects.get(user_id=user_id, key=preference.key).delete()
break
return Response({}, status=status.HTTP_204_NO_CONTENT)
class UsersOrganizationsList(SecureListAPIView): class UsersOrganizationsList(SecureListAPIView):
""" """
...@@ -960,8 +1034,7 @@ class UsersSocialMetrics(SecureListAPIView): ...@@ -960,8 +1034,7 @@ class UsersSocialMetrics(SecureListAPIView):
- GET: Returns a list of social metrics for that user in the specified course - GET: Returns a list of social metrics for that user in the specified course
""" """
def get(self, request, user_id, course_id): # pylint: disable=W0613 def get(self, request, user_id, course_id): # pylint: disable=W0613,W0221
try: try:
user = User.objects.get(id=user_id) user = User.objects.get(id=user_id)
except ObjectDoesNotExist: except ObjectDoesNotExist:
...@@ -971,11 +1044,11 @@ class UsersSocialMetrics(SecureListAPIView): ...@@ -971,11 +1044,11 @@ class UsersSocialMetrics(SecureListAPIView):
comment_user.course_id = course_id comment_user.course_id = course_id
try: try:
data = comment_user.social_stats() data = (comment_user.social_stats())[user_id]
http_status = status.HTTP_200_OK http_status = status.HTTP_200_OK
except CommentClientRequestError, e: except (CommentClientRequestError, ConnectionError), error:
data = { data = {
"err_msg": str(e) "err_msg": str(error)
} }
http_status = status.HTTP_500_INTERNAL_SERVER_ERROR http_status = status.HTTP_500_INTERNAL_SERVER_ERROR
......
""" API implementation for Secure api calls. """ """ API implementation for Secure api calls. """
import socket import socket
import struct import struct
...@@ -55,3 +54,5 @@ def is_int(value): ...@@ -55,3 +54,5 @@ def is_int(value):
return True return True
except ValueError: except ValueError:
return False return False
...@@ -70,6 +70,8 @@ from xmodule.lti_module import LTIModule ...@@ -70,6 +70,8 @@ from xmodule.lti_module import LTIModule
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from xmodule.mixin import wrap_with_license from xmodule.mixin import wrap_with_license
from api_manager.models import CourseModuleCompletion
from util.json_request import JsonResponse from util.json_request import JsonResponse
from util.sandboxing import can_execute_unsafe_code, get_python_lib_zip from util.sandboxing import can_execute_unsafe_code, get_python_lib_zip
from util import milestones_helpers from util import milestones_helpers
...@@ -484,10 +486,20 @@ def get_module_system_for_user(user, field_data_cache, # TODO # pylint: disabl ...@@ -484,10 +486,20 @@ def get_module_system_for_user(user, field_data_cache, # TODO # pylint: disabl
usage_id=unicode(descriptor.location) usage_id=unicode(descriptor.location)
) )
CourseModuleCompletion.objects.get_or_create(
user_id=user_id,
course_id=course_id,
content_id=unicode(descriptor.location)
)
def publish(block, event_type, event): def publish(block, event_type, event):
"""A function that allows XModules to publish events.""" """A function that allows XModules to publish events."""
if event_type == 'grade': if event_type == 'grade':
handle_grade_event(block, event_type, event) handle_grade_event(block, event_type, event)
elif event_type == 'progress':
# expose another special case event type which gets sent
# into the CourseCompletions models
handle_progress_event(block, event_type, event)
else: else:
track_function(event_type, event) track_function(event_type, event)
......
...@@ -245,7 +245,7 @@ def save_child_position(seq_module, child_name): ...@@ -245,7 +245,7 @@ def save_child_position(seq_module, child_name):
""" """
child_name: url_name of the child child_name: url_name of the child
""" """
print child_name
for position, c in enumerate(seq_module.get_display_items(), start=1): for position, c in enumerate(seq_module.get_display_items(), start=1):
if c.location.name == child_name: if c.location.name == child_name:
# Only save if position changed # Only save if position changed
......
...@@ -97,7 +97,6 @@ class Migration(SchemaMigration): ...@@ -97,7 +97,6 @@ class Migration(SchemaMigration):
)) ))
db.send_create_signal('projects', ['WorkgroupPeerReview']) db.send_create_signal('projects', ['WorkgroupPeerReview'])
def backwards(self, orm): def backwards(self, orm):
# Removing unique constraint on 'Project', fields ['course_id', 'content_id'] # Removing unique constraint on 'Project', fields ['course_id', 'content_id']
db.delete_unique('projects_project', ['course_id', 'content_id']) db.delete_unique('projects_project', ['course_id', 'content_id'])
...@@ -126,7 +125,6 @@ class Migration(SchemaMigration): ...@@ -126,7 +125,6 @@ class Migration(SchemaMigration):
# Deleting model 'WorkgroupPeerReview' # Deleting model 'WorkgroupPeerReview'
db.delete_table('projects_workgrouppeerreview') db.delete_table('projects_workgrouppeerreview')
models = { models = {
'auth.group': { 'auth.group': {
'Meta': {'object_name': 'Group'}, 'Meta': {'object_name': 'Group'},
...@@ -226,4 +224,4 @@ class Migration(SchemaMigration): ...@@ -226,4 +224,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['projects'] complete_apps = ['projects']
\ No newline at end of file
...@@ -13,12 +13,10 @@ class Migration(SchemaMigration): ...@@ -13,12 +13,10 @@ class Migration(SchemaMigration):
self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='projects', null=True, on_delete=models.SET_NULL, to=orm['api_manager.Organization']), self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='projects', null=True, on_delete=models.SET_NULL, to=orm['api_manager.Organization']),
keep_default=False) keep_default=False)
def backwards(self, orm): def backwards(self, orm):
# Deleting field 'Project.organization' # Deleting field 'Project.organization'
db.delete_column('projects_project', 'organization_id') db.delete_column('projects_project', 'organization_id')
models = { models = {
'api_manager.organization': { 'api_manager.organization': {
'Meta': {'object_name': 'Organization'}, 'Meta': {'object_name': 'Organization'},
...@@ -132,4 +130,4 @@ class Migration(SchemaMigration): ...@@ -132,4 +130,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['projects'] complete_apps = ['projects']
\ No newline at end of file
...@@ -13,12 +13,10 @@ class Migration(SchemaMigration): ...@@ -13,12 +13,10 @@ class Migration(SchemaMigration):
self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True), self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
keep_default=False) keep_default=False)
def backwards(self, orm): def backwards(self, orm):
# Deleting field 'WorkgroupSubmission.document_filename' # Deleting field 'WorkgroupSubmission.document_filename'
db.delete_column('projects_workgroupsubmission', 'document_filename') db.delete_column('projects_workgroupsubmission', 'document_filename')
models = { models = {
'api_manager.organization': { 'api_manager.organization': {
'Meta': {'object_name': 'Organization'}, 'Meta': {'object_name': 'Organization'},
...@@ -134,4 +132,4 @@ class Migration(SchemaMigration): ...@@ -134,4 +132,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['projects'] complete_apps = ['projects']
\ No newline at end of file
# -*- 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 field 'WorkgroupSubmissionReview.content_id'
db.add_column('projects_workgroupsubmissionreview', 'content_id',
self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
keep_default=False)
# Adding field 'WorkgroupReview.content_id'
db.add_column('projects_workgroupreview', 'content_id',
self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
keep_default=False)
def backwards(self, orm):
# Deleting field 'WorkgroupSubmissionReview.content_id'
db.delete_column('projects_workgroupsubmissionreview', 'content_id')
# Deleting field 'WorkgroupReview.content_id'
db.delete_column('projects_workgroupreview', 'content_id')
models = {
'api_manager.organization': {
'Meta': {'object_name': 'Organization'},
'contact_email': ('django.db.models.fields.EmailField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'contact_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'contact_phone': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'display_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'organizations'", 'symmetrical': 'False', 'to': "orm['auth.Group']"}),
'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'}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'organizations'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
'workgroups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'organizations'", 'symmetrical': 'False', 'to': "orm['projects.Workgroup']"})
},
'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'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'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'})
},
'projects.project': {
'Meta': {'unique_together': "(('course_id', 'content_id'),)", 'object_name': 'Project'},
'content_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'organization': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'projects'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['api_manager.Organization']"})
},
'projects.workgroup': {
'Meta': {'object_name': 'Workgroup'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'workgroups'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.Group']"}),
'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'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'workgroups'", 'to': "orm['projects.Project']"}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'workgroups'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"})
},
'projects.workgrouppeerreview': {
'Meta': {'object_name': 'WorkgroupPeerReview'},
'answer': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'question': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'reviewer': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'workgroup_peer_reviewees'", 'to': "orm['auth.User']"}),
'workgroup': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'peer_reviews'", 'to': "orm['projects.Workgroup']"})
},
'projects.workgroupreview': {
'Meta': {'object_name': 'WorkgroupReview'},
'answer': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'content_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'question': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'reviewer': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'workgroup': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'workgroup_reviews'", 'to': "orm['projects.Workgroup']"})
},
'projects.workgroupsubmission': {
'Meta': {'object_name': 'WorkgroupSubmission'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'document_filename': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'document_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'document_mime_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'document_url': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'submissions'", 'to': "orm['auth.User']"}),
'workgroup': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'submissions'", 'to': "orm['projects.Workgroup']"})
},
'projects.workgroupsubmissionreview': {
'Meta': {'object_name': 'WorkgroupSubmissionReview'},
'answer': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'content_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'question': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'reviewer': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'submission': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reviews'", 'to': "orm['projects.WorkgroupSubmission']"})
}
}
complete_apps = ['projects']
\ No newline at end of file
...@@ -4,7 +4,6 @@ from django.contrib.auth.models import Group, User ...@@ -4,7 +4,6 @@ from django.contrib.auth.models import Group, User
from django.db import models from django.db import models
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
from student.models import AnonymousUserId
class Project(TimeStampedModel): class Project(TimeStampedModel):
...@@ -14,8 +13,13 @@ class Project(TimeStampedModel): ...@@ -14,8 +13,13 @@ class Project(TimeStampedModel):
""" """
course_id = models.CharField(max_length=255) course_id = models.CharField(max_length=255)
content_id = models.CharField(max_length=255) content_id = models.CharField(max_length=255)
organization = models.ForeignKey('api_manager.Organization', blank=True, null=True, related_name="projects" organization = models.ForeignKey(
, on_delete=models.SET_NULL) 'api_manager.Organization',
blank=True,
null=True,
related_name="projects",
on_delete=models.SET_NULL
)
class Meta: class Meta:
""" Meta class for defining additional model characteristics """ """ Meta class for defining additional model characteristics """
...@@ -45,6 +49,7 @@ class WorkgroupReview(TimeStampedModel): ...@@ -45,6 +49,7 @@ class WorkgroupReview(TimeStampedModel):
reviewer = models.CharField(max_length=255) # AnonymousUserId reviewer = models.CharField(max_length=255) # AnonymousUserId
question = models.CharField(max_length=255) question = models.CharField(max_length=255)
answer = models.CharField(max_length=255) answer = models.CharField(max_length=255)
content_id = models.CharField(max_length=255, null=True, blank=True)
class WorkgroupSubmission(TimeStampedModel): class WorkgroupSubmission(TimeStampedModel):
...@@ -72,6 +77,7 @@ class WorkgroupSubmissionReview(TimeStampedModel): ...@@ -72,6 +77,7 @@ class WorkgroupSubmissionReview(TimeStampedModel):
reviewer = models.CharField(max_length=255) # AnonymousUserId reviewer = models.CharField(max_length=255) # AnonymousUserId
question = models.CharField(max_length=255) question = models.CharField(max_length=255)
answer = models.CharField(max_length=255) answer = models.CharField(max_length=255)
content_id = models.CharField(max_length=255, null=True, blank=True)
class WorkgroupPeerReview(TimeStampedModel): class WorkgroupPeerReview(TimeStampedModel):
......
...@@ -83,7 +83,7 @@ class WorkgroupReviewSerializer(serializers.HyperlinkedModelSerializer): ...@@ -83,7 +83,7 @@ class WorkgroupReviewSerializer(serializers.HyperlinkedModelSerializer):
model = WorkgroupReview model = WorkgroupReview
fields = ( fields = (
'id', 'url', 'created', 'modified', 'question', 'answer', 'id', 'url', 'created', 'modified', 'question', 'answer',
'workgroup', 'reviewer' 'workgroup', 'reviewer', 'content_id'
) )
...@@ -96,7 +96,7 @@ class WorkgroupSubmissionReviewSerializer(serializers.HyperlinkedModelSerializer ...@@ -96,7 +96,7 @@ class WorkgroupSubmissionReviewSerializer(serializers.HyperlinkedModelSerializer
model = WorkgroupSubmissionReview model = WorkgroupSubmissionReview
fields = ( fields = (
'id', 'url', 'created', 'modified', 'question', 'answer', 'id', 'url', 'created', 'modified', 'question', 'answer',
'submission', 'reviewer' 'submission', 'reviewer', 'content_id'
) )
......
...@@ -9,11 +9,13 @@ import uuid ...@@ -9,11 +9,13 @@ import uuid
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.cache import cache from django.core.cache import cache
from django.test import TestCase, Client from django.test import Client
from django.test.utils import override_settings from django.test.utils import override_settings
from projects.models import Project, Workgroup from projects.models import Project, Workgroup
from student.models import anonymous_id_for_user from student.models import anonymous_id_for_user
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
TEST_API_KEY = str(uuid.uuid4()) TEST_API_KEY = str(uuid.uuid4())
...@@ -29,7 +31,7 @@ class SecureClient(Client): ...@@ -29,7 +31,7 @@ class SecureClient(Client):
@override_settings(EDX_API_KEY=TEST_API_KEY) @override_settings(EDX_API_KEY=TEST_API_KEY)
class PeerReviewsApiTests(TestCase): class PeerReviewsApiTests(ModuleStoreTestCase):
""" Test suite for Users API views """ """ Test suite for Users API views """
...@@ -40,7 +42,17 @@ class PeerReviewsApiTests(TestCase): ...@@ -40,7 +42,17 @@ class PeerReviewsApiTests(TestCase):
self.test_projects_uri = '/api/projects/' self.test_projects_uri = '/api/projects/'
self.test_peer_reviews_uri = '/api/peer_reviews/' self.test_peer_reviews_uri = '/api/peer_reviews/'
self.test_course_id = 'edx/demo/course' self.course = CourseFactory.create()
self.test_data = '<html>{}</html>'.format(str(uuid.uuid4()))
self.chapter = ItemFactory.create(
category="chapter",
parent_location=self.course.location,
data=self.test_data,
display_name="Overview"
)
self.test_course_id = unicode(self.course.id)
self.test_bogus_course_id = 'foo/bar/baz' self.test_bogus_course_id = 'foo/bar/baz'
self.test_course_content_id = "i4x://blah" self.test_course_content_id = "i4x://blah"
self.test_bogus_course_content_id = "14x://foo/bar/baz" self.test_bogus_course_content_id = "14x://foo/bar/baz"
...@@ -59,7 +71,7 @@ class PeerReviewsApiTests(TestCase): ...@@ -59,7 +71,7 @@ class PeerReviewsApiTests(TestCase):
username="reviewer", username="reviewer",
is_active=True is_active=True
) )
self.anonymous_user_id = anonymous_id_for_user(self.test_reviewer_user, self.test_course_id) self.anonymous_user_id = anonymous_id_for_user(self.test_reviewer_user, self.course.id)
self.test_project = Project.objects.create( self.test_project = Project.objects.create(
course_id=self.test_course_id, course_id=self.test_course_id,
......
...@@ -9,11 +9,13 @@ import uuid ...@@ -9,11 +9,13 @@ import uuid
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.cache import cache from django.core.cache import cache
from django.test import TestCase, Client from django.test import Client
from django.test.utils import override_settings from django.test.utils import override_settings
from projects.models import Project, Workgroup, WorkgroupSubmission from projects.models import Project, Workgroup, WorkgroupSubmission
from student.models import anonymous_id_for_user from student.models import anonymous_id_for_user
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
TEST_API_KEY = str(uuid.uuid4()) TEST_API_KEY = str(uuid.uuid4())
...@@ -29,7 +31,7 @@ class SecureClient(Client): ...@@ -29,7 +31,7 @@ class SecureClient(Client):
@override_settings(EDX_API_KEY=TEST_API_KEY) @override_settings(EDX_API_KEY=TEST_API_KEY)
class SubmissionReviewsApiTests(TestCase): class SubmissionReviewsApiTests(ModuleStoreTestCase):
""" Test suite for Users API views """ """ Test suite for Users API views """
...@@ -40,7 +42,17 @@ class SubmissionReviewsApiTests(TestCase): ...@@ -40,7 +42,17 @@ class SubmissionReviewsApiTests(TestCase):
self.test_projects_uri = '/api/projects/' self.test_projects_uri = '/api/projects/'
self.test_submission_reviews_uri = '/api/submission_reviews/' self.test_submission_reviews_uri = '/api/submission_reviews/'
self.test_course_id = 'edx/demo/course' self.course = CourseFactory.create()
self.test_data = '<html>{}</html>'.format(str(uuid.uuid4()))
self.chapter = ItemFactory.create(
category="chapter",
parent_location=self.course.location,
data=self.test_data,
display_name="Overview"
)
self.test_course_id = unicode(self.course.id)
self.test_bogus_course_id = 'foo/bar/baz' self.test_bogus_course_id = 'foo/bar/baz'
self.test_course_content_id = "i4x://blah" self.test_course_content_id = "i4x://blah"
self.test_bogus_course_content_id = "14x://foo/bar/baz" self.test_bogus_course_content_id = "14x://foo/bar/baz"
...@@ -53,7 +65,7 @@ class SubmissionReviewsApiTests(TestCase): ...@@ -53,7 +65,7 @@ class SubmissionReviewsApiTests(TestCase):
username="testing", username="testing",
is_active=True is_active=True
) )
self.anonymous_user_id = anonymous_id_for_user(self.test_user, self.test_course_id) self.anonymous_user_id = anonymous_id_for_user(self.test_user, self.course.id)
self.test_project = Project.objects.create( self.test_project = Project.objects.create(
course_id=self.test_course_id, course_id=self.test_course_id,
...@@ -113,6 +125,7 @@ class SubmissionReviewsApiTests(TestCase): ...@@ -113,6 +125,7 @@ class SubmissionReviewsApiTests(TestCase):
'reviewer': self.anonymous_user_id, 'reviewer': self.anonymous_user_id,
'question': self.test_question, 'question': self.test_question,
'answer': self.test_answer, 'answer': self.test_answer,
'content_id': self.test_course_content_id,
} }
response = self.do_post(self.test_submission_reviews_uri, data) response = self.do_post(self.test_submission_reviews_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
...@@ -128,6 +141,7 @@ class SubmissionReviewsApiTests(TestCase): ...@@ -128,6 +141,7 @@ class SubmissionReviewsApiTests(TestCase):
self.assertEqual(response.data['submission'], self.test_submission.id) self.assertEqual(response.data['submission'], self.test_submission.id)
self.assertEqual(response.data['question'], self.test_question) self.assertEqual(response.data['question'], self.test_question)
self.assertEqual(response.data['answer'], self.test_answer) self.assertEqual(response.data['answer'], self.test_answer)
self.assertEqual(response.data['content_id'], self.test_course_content_id)
self.assertIsNotNone(response.data['created']) self.assertIsNotNone(response.data['created'])
self.assertIsNotNone(response.data['modified']) self.assertIsNotNone(response.data['modified'])
...@@ -137,6 +151,7 @@ class SubmissionReviewsApiTests(TestCase): ...@@ -137,6 +151,7 @@ class SubmissionReviewsApiTests(TestCase):
'reviewer': self.anonymous_user_id, 'reviewer': self.anonymous_user_id,
'question': self.test_question, 'question': self.test_question,
'answer': self.test_answer, 'answer': self.test_answer,
'content_id': self.test_course_content_id,
} }
response = self.do_post(self.test_submission_reviews_uri, data) response = self.do_post(self.test_submission_reviews_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
...@@ -154,6 +169,7 @@ class SubmissionReviewsApiTests(TestCase): ...@@ -154,6 +169,7 @@ class SubmissionReviewsApiTests(TestCase):
self.assertEqual(response.data['submission'], self.test_submission.id) self.assertEqual(response.data['submission'], self.test_submission.id)
self.assertEqual(response.data['question'], self.test_question) self.assertEqual(response.data['question'], self.test_question)
self.assertEqual(response.data['answer'], self.test_answer) self.assertEqual(response.data['answer'], self.test_answer)
self.assertEqual(response.data['content_id'], self.test_course_content_id)
self.assertIsNotNone(response.data['created']) self.assertIsNotNone(response.data['created'])
self.assertIsNotNone(response.data['modified']) self.assertIsNotNone(response.data['modified'])
......
...@@ -14,6 +14,8 @@ from django.test.utils import override_settings ...@@ -14,6 +14,8 @@ from django.test.utils import override_settings
from projects.models import Project, Workgroup, WorkgroupSubmission from projects.models import Project, Workgroup, WorkgroupSubmission
from student.models import anonymous_id_for_user from student.models import anonymous_id_for_user
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
TEST_API_KEY = str(uuid.uuid4()) TEST_API_KEY = str(uuid.uuid4())
...@@ -29,18 +31,29 @@ class SecureClient(Client): ...@@ -29,18 +31,29 @@ class SecureClient(Client):
@override_settings(EDX_API_KEY=TEST_API_KEY) @override_settings(EDX_API_KEY=TEST_API_KEY)
class WorkgroupReviewsApiTests(TestCase): class WorkgroupReviewsApiTests(ModuleStoreTestCase):
""" Test suite for Users API views """ """ Test suite for Users API views """
def setUp(self): def setUp(self):
super(WorkgroupReviewsApiTests, self).setUp()
self.test_server_prefix = 'https://testserver' self.test_server_prefix = 'https://testserver'
self.test_users_uri = '/api/users/' self.test_users_uri = '/api/users/'
self.test_workgroups_uri = '/api/workgroups/' self.test_workgroups_uri = '/api/workgroups/'
self.test_projects_uri = '/api/projects/' self.test_projects_uri = '/api/projects/'
self.test_workgroup_reviews_uri = '/api/workgroup_reviews/' self.test_workgroup_reviews_uri = '/api/workgroup_reviews/'
self.test_course_id = 'edx/demo/course' self.course = CourseFactory.create()
self.test_data = '<html>{}</html>'.format(str(uuid.uuid4()))
self.chapter = ItemFactory.create(
category="chapter",
parent_location=self.course.location,
data=self.test_data,
display_name="Overview"
)
self.test_course_id = unicode(self.course.id)
self.test_bogus_course_id = 'foo/bar/baz' self.test_bogus_course_id = 'foo/bar/baz'
self.test_course_content_id = "i4x://blah" self.test_course_content_id = "i4x://blah"
self.test_bogus_course_content_id = "14x://foo/bar/baz" self.test_bogus_course_content_id = "14x://foo/bar/baz"
...@@ -53,7 +66,7 @@ class WorkgroupReviewsApiTests(TestCase): ...@@ -53,7 +66,7 @@ class WorkgroupReviewsApiTests(TestCase):
username="testing", username="testing",
is_active=True is_active=True
) )
self.anonymous_user_id = anonymous_id_for_user(self.test_user, self.test_course_id) self.anonymous_user_id = anonymous_id_for_user(self.test_user, self.course.id)
self.test_project = Project.objects.create( self.test_project = Project.objects.create(
course_id=self.test_course_id, course_id=self.test_course_id,
content_id=self.test_course_content_id, content_id=self.test_course_content_id,
...@@ -112,6 +125,7 @@ class WorkgroupReviewsApiTests(TestCase): ...@@ -112,6 +125,7 @@ class WorkgroupReviewsApiTests(TestCase):
'reviewer': self.anonymous_user_id, 'reviewer': self.anonymous_user_id,
'question': self.test_question, 'question': self.test_question,
'answer': self.test_answer, 'answer': self.test_answer,
'content_id': self.test_course_content_id,
} }
response = self.do_post(self.test_workgroup_reviews_uri, data) response = self.do_post(self.test_workgroup_reviews_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
...@@ -127,6 +141,7 @@ class WorkgroupReviewsApiTests(TestCase): ...@@ -127,6 +141,7 @@ class WorkgroupReviewsApiTests(TestCase):
self.assertEqual(response.data['workgroup'], self.test_workgroup.id) self.assertEqual(response.data['workgroup'], self.test_workgroup.id)
self.assertEqual(response.data['question'], self.test_question) self.assertEqual(response.data['question'], self.test_question)
self.assertEqual(response.data['answer'], self.test_answer) self.assertEqual(response.data['answer'], self.test_answer)
self.assertEqual(response.data['content_id'], self.test_course_content_id)
self.assertIsNotNone(response.data['created']) self.assertIsNotNone(response.data['created'])
self.assertIsNotNone(response.data['modified']) self.assertIsNotNone(response.data['modified'])
...@@ -136,6 +151,7 @@ class WorkgroupReviewsApiTests(TestCase): ...@@ -136,6 +151,7 @@ class WorkgroupReviewsApiTests(TestCase):
'reviewer': self.anonymous_user_id, 'reviewer': self.anonymous_user_id,
'question': self.test_question, 'question': self.test_question,
'answer': self.test_answer, 'answer': self.test_answer,
'content_id': self.test_course_content_id,
} }
response = self.do_post(self.test_workgroup_reviews_uri, data) response = self.do_post(self.test_workgroup_reviews_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
...@@ -153,6 +169,7 @@ class WorkgroupReviewsApiTests(TestCase): ...@@ -153,6 +169,7 @@ class WorkgroupReviewsApiTests(TestCase):
self.assertEqual(response.data['workgroup'], self.test_workgroup.id) self.assertEqual(response.data['workgroup'], self.test_workgroup.id)
self.assertEqual(response.data['question'], self.test_question) self.assertEqual(response.data['question'], self.test_question)
self.assertEqual(response.data['answer'], self.test_answer) self.assertEqual(response.data['answer'], self.test_answer)
self.assertEqual(response.data['content_id'], self.test_course_content_id)
self.assertIsNotNone(response.data['created']) self.assertIsNotNone(response.data['created'])
self.assertIsNotNone(response.data['modified']) self.assertIsNotNone(response.data['modified'])
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
Run these tests @ Devstack: Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/projects/tests/test_workgroups.py] rake fasttest_lms[common/djangoapps/projects/tests/test_workgroups.py]
""" """
from datetime import datetime
import json import json
import uuid import uuid
...@@ -45,14 +46,14 @@ class WorkgroupsApiTests(ModuleStoreTestCase): ...@@ -45,14 +46,14 @@ class WorkgroupsApiTests(ModuleStoreTestCase):
self.test_server_prefix = 'https://testserver' self.test_server_prefix = 'https://testserver'
self.test_workgroups_uri = '/api/workgroups/' self.test_workgroups_uri = '/api/workgroups/'
self.test_bogus_course_id = 'foo/bar/baz' self.test_bogus_course_id = 'foo/bar/baz'
self.test_bogus_course_content_id = "14x://foo/bar/baz" self.test_bogus_course_content_id = "i4x://foo/bar/baz"
self.test_group_id = '1' self.test_group_id = '1'
self.test_bogus_group_id = "2131241123" self.test_bogus_group_id = "2131241123"
self.test_workgroup_name = str(uuid.uuid4()) self.test_workgroup_name = str(uuid.uuid4())
self.test_course = CourseFactory.create( self.test_course = CourseFactory.create(
start="2014-06-16T14:30:00Z", start=datetime(2014, 6, 16, 14, 30),
end="2015-01-16T14:30:00Z" end=datetime(2015, 1, 16, 14, 30)
) )
self.test_data = '<html>{}</html>'.format(str(uuid.uuid4())) self.test_data = '<html>{}</html>'.format(str(uuid.uuid4()))
...@@ -60,12 +61,12 @@ class WorkgroupsApiTests(ModuleStoreTestCase): ...@@ -60,12 +61,12 @@ class WorkgroupsApiTests(ModuleStoreTestCase):
category="group_project", category="group_project",
parent_location=self.test_course.location, parent_location=self.test_course.location,
data=self.test_data, data=self.test_data,
due="2014-05-16T14:30:00Z", due=datetime(2014, 5, 16, 14, 30),
display_name="Group Project" display_name="Group Project"
) )
self.test_course_id = self.test_course.id self.test_course_id = unicode(self.test_course.id)
self.test_course_content_id = self.test_group_project.id self.test_course_content_id = unicode(self.test_group_project.scope_ids.usage_id)
self.test_group_name = str(uuid.uuid4()) self.test_group_name = str(uuid.uuid4())
self.test_group = Group.objects.create( self.test_group = Group.objects.create(
...@@ -137,7 +138,6 @@ class WorkgroupsApiTests(ModuleStoreTestCase): ...@@ -137,7 +138,6 @@ class WorkgroupsApiTests(ModuleStoreTestCase):
response = self.client.delete_with_data(uri, data, headers=headers) response = self.client.delete_with_data(uri, data, headers=headers)
return response return response
def test_workgroups_list_post(self): def test_workgroups_list_post(self):
data = { data = {
'name': self.test_workgroup_name, 'name': self.test_workgroup_name,
...@@ -403,6 +403,68 @@ class WorkgroupsApiTests(ModuleStoreTestCase): ...@@ -403,6 +403,68 @@ class WorkgroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data['grades']), 0) self.assertGreater(len(response.data['grades']), 0)
def test_workgroups_grades_post_invalid_course(self):
data = {
'name': self.test_workgroup_name,
'project': self.test_project.id
}
response = self.do_post(self.test_workgroups_uri, data)
self.assertEqual(response.status_code, 201)
workgroup_id = response.data['id']
users_uri = '{}{}/users/'.format(self.test_workgroups_uri, workgroup_id)
data = {"id": self.test_user.id}
response = self.do_post(users_uri, data)
self.assertEqual(response.status_code, 201)
data = {"id": self.test_user2.id}
response = self.do_post(users_uri, data)
self.assertEqual(response.status_code, 201)
grade_data = {
'course_id': self.test_bogus_course_id,
'content_id': self.test_course_content_id,
'grade': 0.85,
'max_grade': 0.75,
}
grades_uri = '{}{}/grades/'.format(self.test_workgroups_uri, workgroup_id)
response = self.do_post(grades_uri, grade_data)
self.assertEqual(response.status_code, 400)
grade_data = {
'course_id': "really-invalid-course-id",
'content_id': self.test_course_content_id,
'grade': 0.85,
'max_grade': 0.75,
}
grades_uri = '{}{}/grades/'.format(self.test_workgroups_uri, workgroup_id)
response = self.do_post(grades_uri, grade_data)
self.assertEqual(response.status_code, 400)
def test_workgroups_grades_post_invalid_course_content(self):
data = {
'name': self.test_workgroup_name,
'project': self.test_project.id
}
response = self.do_post(self.test_workgroups_uri, data)
self.assertEqual(response.status_code, 201)
workgroup_id = response.data['id']
users_uri = '{}{}/users/'.format(self.test_workgroups_uri, workgroup_id)
data = {"id": self.test_user.id}
response = self.do_post(users_uri, data)
self.assertEqual(response.status_code, 201)
data = {"id": self.test_user2.id}
response = self.do_post(users_uri, data)
self.assertEqual(response.status_code, 201)
grade_data = {
'course_id': self.test_course_id,
'content_id': self.test_bogus_course_content_id,
'grade': 0.85,
'max_grade': 0.75,
}
grades_uri = '{}{}/grades/'.format(self.test_workgroups_uri, workgroup_id)
response = self.do_post(grades_uri, grade_data)
self.assertEqual(response.status_code, 400)
def test_workgroups_grades_post_invalid_requests(self): def test_workgroups_grades_post_invalid_requests(self):
data = { data = {
'name': self.test_workgroup_name, 'name': self.test_workgroup_name,
......
...@@ -13,9 +13,15 @@ from rest_framework.response import Response ...@@ -13,9 +13,15 @@ from rest_framework.response import Response
from xblock.fields import Scope from xblock.fields import Scope
from xblock.runtime import KeyValueStore from xblock.runtime import KeyValueStore
from courseware import module_render
from courseware.courses import get_course from courseware.courses import get_course
from courseware.model_data import FieldDataCache from courseware.model_data import FieldDataCache
from xmodule.modulestore import Location from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
from xmodule.modulestore import Location, InvalidLocationError
from xmodule.modulestore.django import modulestore
from .models import Project, Workgroup, WorkgroupSubmission from .models import Project, Workgroup, WorkgroupSubmission
from .models import WorkgroupReview, WorkgroupSubmissionReview, WorkgroupPeerReview from .models import WorkgroupReview, WorkgroupSubmissionReview, WorkgroupPeerReview
...@@ -24,6 +30,64 @@ from .serializers import ProjectSerializer, WorkgroupSerializer, WorkgroupSubmis ...@@ -24,6 +30,64 @@ from .serializers import ProjectSerializer, WorkgroupSerializer, WorkgroupSubmis
from .serializers import WorkgroupReviewSerializer, WorkgroupSubmissionReviewSerializer, WorkgroupPeerReviewSerializer from .serializers import WorkgroupReviewSerializer, WorkgroupSubmissionReviewSerializer, WorkgroupPeerReviewSerializer
def _get_course(request, user, course_id, depth=0):
"""
Utility method to obtain course components
"""
course_descriptor = None
course_key = None
course_content = None
try:
course_key = CourseKey.from_string(course_id)
except InvalidKeyError:
try:
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
except InvalidKeyError:
pass
if course_key:
try:
course_descriptor = get_course(course_key, depth=depth)
except ValueError:
pass
if course_descriptor:
field_data_cache = FieldDataCache([course_descriptor], course_key, user)
course_content = module_render.get_module(
user,
request,
course_descriptor.location,
field_data_cache,
course_key)
return course_descriptor, course_key, course_content
def _get_course_child(request, user, course_key, content_id):
"""
Return a course xmodule/xblock to the caller
"""
content_descriptor = None
content_key = None
content = None
try:
content_key = UsageKey.from_string(content_id)
except InvalidKeyError:
try:
content_key = Location.from_deprecated_string(content_id)
except (InvalidKeyError, InvalidLocationError):
pass
if content_key:
store = modulestore()
content_descriptor = store.get_item(content_key)
if content_descriptor:
field_data_cache = FieldDataCache([content_descriptor], course_key, user)
content = module_render.get_module(
user,
request,
content_key,
field_data_cache,
course_key)
return content_descriptor, content_key, content
class GroupViewSet(viewsets.ModelViewSet): class GroupViewSet(viewsets.ModelViewSet):
""" """
Django Rest Framework ViewSet for the Group model (auth_group). Django Rest Framework ViewSet for the Group model (auth_group).
...@@ -58,7 +122,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet): ...@@ -58,7 +122,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
if groups: if groups:
for group in groups: for group in groups:
serializer = GroupSerializer(group) serializer = GroupSerializer(group)
response_data.append(serializer.data) response_data.append(serializer.data) # pylint: disable=E1101
return Response(response_data, status=status.HTTP_200_OK) return Response(response_data, status=status.HTTP_200_OK)
else: else:
group_id = request.DATA.get('id') group_id = request.DATA.get('id')
...@@ -84,7 +148,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet): ...@@ -84,7 +148,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
if users: if users:
for user in users: for user in users:
serializer = UserSerializer(user) serializer = UserSerializer(user)
response_data.append(serializer.data) response_data.append(serializer.data) # pylint: disable=E1101
return Response(response_data, status=status.HTTP_200_OK) return Response(response_data, status=status.HTTP_200_OK)
elif request.method == 'POST': elif request.method == 'POST':
user_id = request.DATA.get('id') user_id = request.DATA.get('id')
...@@ -118,7 +182,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet): ...@@ -118,7 +182,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
if peer_reviews: if peer_reviews:
for peer_review in peer_reviews: for peer_review in peer_reviews:
serializer = WorkgroupPeerReviewSerializer(peer_review) serializer = WorkgroupPeerReviewSerializer(peer_review)
response_data.append(serializer.data) response_data.append(serializer.data) # pylint: disable=E1101
return Response(response_data, status=status.HTTP_200_OK) return Response(response_data, status=status.HTTP_200_OK)
@link() @link()
...@@ -131,7 +195,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet): ...@@ -131,7 +195,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
if workgroup_reviews: if workgroup_reviews:
for workgroup_review in workgroup_reviews: for workgroup_review in workgroup_reviews:
serializer = WorkgroupReviewSerializer(workgroup_review) serializer = WorkgroupReviewSerializer(workgroup_review)
response_data.append(serializer.data) response_data.append(serializer.data) # pylint: disable=E1101
return Response(response_data, status=status.HTTP_200_OK) return Response(response_data, status=status.HTTP_200_OK)
@link() @link()
...@@ -144,7 +208,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet): ...@@ -144,7 +208,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
if submissions: if submissions:
for submission in submissions: for submission in submissions:
serializer = WorkgroupSubmissionSerializer(submission) serializer = WorkgroupSubmissionSerializer(submission)
response_data.append(serializer.data) response_data.append(serializer.data) # pylint: disable=E1101
return Response(response_data, status=status.HTTP_200_OK) return Response(response_data, status=status.HTTP_200_OK)
@action() @action()
...@@ -156,16 +220,16 @@ class WorkgroupsViewSet(viewsets.ModelViewSet): ...@@ -156,16 +220,16 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
course_id = request.DATA.get('course_id') course_id = request.DATA.get('course_id')
if course_id is None: if course_id is None:
return Response({}, status=status.HTTP_400_BAD_REQUEST) return Response({}, status=status.HTTP_400_BAD_REQUEST)
try: course_descriptor, course_key, course_content = _get_course(request, request.user, course_id) # pylint: disable=W0612
course_descriptor = get_course(course_id)
except ValueError:
course_descriptor = None
if not course_descriptor: if not course_descriptor:
return Response({}, status=status.HTTP_400_BAD_REQUEST) return Response({}, status=status.HTTP_400_BAD_REQUEST)
content_id = request.DATA.get('content_id') content_id = request.DATA.get('content_id')
if content_id is None: if content_id is None:
return Response({}, status=status.HTTP_400_BAD_REQUEST) return Response({}, status=status.HTTP_400_BAD_REQUEST)
content_descriptor, content_key, content = _get_course_child(request, request.user, course_key, content_id) # pylint: disable=W0612
if content_descriptor is None:
return Response({}, status=status.HTTP_400_BAD_REQUEST)
grade = request.DATA.get('grade') grade = request.DATA.get('grade')
if grade is None: if grade is None:
...@@ -182,10 +246,10 @@ class WorkgroupsViewSet(viewsets.ModelViewSet): ...@@ -182,10 +246,10 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
key = KeyValueStore.Key( key = KeyValueStore.Key(
scope=Scope.user_state, scope=Scope.user_state,
user_id=user.id, user_id=user.id,
block_scope_id=Location(content_id), block_scope_id=content_key,
field_name='grade' field_name='grade'
) )
field_data_cache = FieldDataCache([course_descriptor], course_id, user) field_data_cache = FieldDataCache([course_descriptor], course_key, user)
student_module = field_data_cache.find_or_create(key) student_module = field_data_cache.find_or_create(key)
student_module.grade = grade student_module.grade = grade
student_module.max_grade = max_grade student_module.max_grade = max_grade
...@@ -211,7 +275,7 @@ class ProjectsViewSet(viewsets.ModelViewSet): ...@@ -211,7 +275,7 @@ class ProjectsViewSet(viewsets.ModelViewSet):
if workgroups: if workgroups:
for workgroup in workgroups: for workgroup in workgroups:
serializer = WorkgroupSerializer(workgroup) serializer = WorkgroupSerializer(workgroup)
response_data.append(serializer.data) response_data.append(serializer.data) # pylint: disable=E1101
return Response(response_data, status=status.HTTP_200_OK) return Response(response_data, status=status.HTTP_200_OK)
else: else:
workgroup_id = request.DATA.get('id') workgroup_id = request.DATA.get('id')
......
...@@ -421,6 +421,9 @@ FEATURES = { ...@@ -421,6 +421,9 @@ FEATURES = {
# The block types to disable need to be specified in "x block disable config" in django admin. # The block types to disable need to be specified in "x block disable config" in django admin.
'ENABLE_DISABLING_XBLOCK_TYPES': True, 'ENABLE_DISABLING_XBLOCK_TYPES': True,
# Whether an xBlock publishing a 'grade' event should be considered a 'progress' event as well
'MARK_PROGRESS_ON_GRADING_EVENT': False
} }
# Ignore static asset files on import which match this pattern # Ignore static asset files on import which match this pattern
......
...@@ -100,6 +100,9 @@ CC_PROCESSOR = { ...@@ -100,6 +100,9 @@ CC_PROCESSOR = {
FEATURES['API'] = True FEATURES['API'] = True
########################## USER API ########################
EDX_API_KEY = None
########################### External REST APIs ################################# ########################### External REST APIs #################################
FEATURES['ENABLE_OAUTH2_PROVIDER'] = True FEATURES['ENABLE_OAUTH2_PROVIDER'] = True
......
...@@ -161,6 +161,17 @@ class User(models.Model): ...@@ -161,6 +161,17 @@ class User(models.Model):
self._update_from_response(response) self._update_from_response(response)
def get_course_social_stats(course_id):
url = _url_for_course_social_stats()
params = {'course_id': course_id}
response = perform_request(
'get',
url,
params
)
return response
def _url_for_vote_comment(comment_id): def _url_for_vote_comment(comment_id):
return "{prefix}/comments/{comment_id}/votes".format(prefix=settings.PREFIX, comment_id=comment_id) return "{prefix}/comments/{comment_id}/votes".format(prefix=settings.PREFIX, comment_id=comment_id)
...@@ -186,3 +197,6 @@ def _url_for_user_stats(user_id,course_id): ...@@ -186,3 +197,6 @@ def _url_for_user_stats(user_id,course_id):
def _url_for_user_social_stats(user_id): def _url_for_user_social_stats(user_id):
return "{prefix}/users/{user_id}/social_stats".format(prefix=settings.PREFIX, user_id=user_id) return "{prefix}/users/{user_id}/social_stats".format(prefix=settings.PREFIX, user_id=user_id)
def _url_for_course_social_stats():
return "{prefix}/users/*/social_stats".format(prefix=settings.PREFIX)
...@@ -20,20 +20,22 @@ class UserTagsEventContextMiddleware(object): ...@@ -20,20 +20,22 @@ class UserTagsEventContextMiddleware(object):
""" """
Add a user's tags to the tracking event context. Add a user's tags to the tracking event context.
""" """
match = COURSE_REGEX.match(request.build_absolute_uri()) match = COURSE_REGEX.match(request.path)
course_id = None course_key = None
if match: if match:
course_id = match.group('course_id') course_key = match.group('course_id')
try: try:
course_key = CourseKey.from_string(course_id) course_key = CourseKey.from_string(course_key)
except InvalidKeyError: except InvalidKeyError:
course_id = None
course_key = None course_key = None
context = {} context = {}
if course_id: if course_key:
context['course_id'] = course_id try:
context['course_id'] = course_key.to_deprecated_string()
except AttributeError:
context['course_id'] = unicode(course_key)
if request.user.is_authenticated(): if request.user.is_authenticated():
context['course_user_tags'] = dict( context['course_user_tags'] = dict(
......
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