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
from django.db import models
from django.core.exceptions import ValidationError
from opaque_keys.edx.keys import CourseKey, UsageKey, BlockTypeKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from south.modelsinspector import add_introspection_rules
......@@ -104,7 +105,7 @@ class OpaqueKeyField(models.CharField):
return None
if isinstance(value, basestring):
return self.KEY_CLASS.from_string(value)
return SlashSeparatedCourseKey.from_deprecated_string(value)
else:
return value
......@@ -148,7 +149,6 @@ class CourseKeyField(OpaqueKeyField):
description = "A CourseKey object, saved to the DB in the form of a string"
KEY_CLASS = CourseKey
class UsageKeyField(OpaqueKeyField):
"""
A django Field that stores a UsageKey object as a string.
......
......@@ -47,5 +47,9 @@ Sessions
* - Goal
- Resource
* -
-
\ No newline at end of file
* - :ref:`Create a Session`
- POST {"username": "name", "password": "password"}
* - :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
.. module:: api_manager
The page contains docstrings for:
The page contains docstrings and example responses for:
*
\ No newline at end of file
* `Create a Session`_
* `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):
class Meta:
""" Serializer/field specification """
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')
......
......@@ -3,9 +3,11 @@
Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/courses/tests.py]
"""
from datetime import datetime
import json
import uuid
from random import randint
from urllib import urlencode
from django.contrib.auth.models import Group
from django.core.cache import cache
......@@ -16,7 +18,6 @@ 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 student.tests.factories import UserFactory, CourseEnrollmentFactory
from xmodule.modulestore import Location
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
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
TEST_API_KEY = str(uuid.uuid4())
USER_COUNT = 4
class SecureClient(Client):
""" Django test client using a "secure" connection. """
def __init__(self, *args, **kwargs):
......@@ -47,8 +49,8 @@ class CoursesApiTests(TestCase):
self.attempts = 3
self.course = CourseFactory.create(
start="2014-06-16T14:30:00Z",
end="2015-01-16T14:30:00Z"
start=datetime(2014, 6, 16, 14, 30),
end=datetime(2015, 1, 16)
)
self.test_data = '<html>{}</html>'.format(str(uuid.uuid4()))
......@@ -56,7 +58,7 @@ class CoursesApiTests(TestCase):
category="chapter",
parent_location=self.course.location,
data=self.test_data,
due="2014-05-16T14:30:00Z",
due=datetime(2014, 5, 16, 14, 30),
display_name="Overview"
)
......@@ -160,7 +162,7 @@ class CoursesApiTests(TestCase):
max_grade=1 if i < j else 0.5,
student=user,
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}),
module_type=module_type
)
......@@ -169,18 +171,18 @@ class CoursesApiTests(TestCase):
StudentModuleFactory.create(
course_id=self.course.id,
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_bogus_course_id = 'foo/bar/baz'
self.test_course_id = unicode(self.course.id)
self.test_bogus_course_id = 'i4x://foo/bar/baz'
self.test_course_name = self.course.display_name
self.test_course_number = self.course.number
self.test_course_org = self.course.org
self.test_chapter_id = self.chapter.id
self.test_course_content_id = self.course_content.id
self.test_chapter_id = unicode(self.chapter.scope_ids.usage_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_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_chapters_uri = self.base_course_content_uri + '?type=chapter'
......@@ -200,9 +202,9 @@ class CoursesApiTests(TestCase):
"""Submit an HTTP POST request"""
headers = {
'X-Edx-Api-Key': str(TEST_API_KEY),
'Content-Type': 'application/json'
}
json_data = json.dumps(data)
response = self.client.post(uri, headers=headers, content_type='application/json', data=json_data)
return response
......@@ -240,7 +242,7 @@ class CoursesApiTests(TestCase):
def test_course_detail_without_date_values(self):
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)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['start'], create_course_with_out_date_values.start)
......@@ -253,8 +255,8 @@ class CoursesApiTests(TestCase):
self.assertGreater(len(response.data), 0)
self.assertEqual(response.data['id'], self.test_course_id)
self.assertEqual(response.data['name'], self.test_course_name)
self.assertEqual(response.data['start'], self.course.start)
self.assertEqual(response.data['end'], self.course.end)
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(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['org'], self.test_course_org)
confirm_uri = self.test_server_prefix + test_uri
......@@ -351,6 +353,11 @@ class CoursesApiTests(TestCase):
matched_child = True
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):
test_uri = '{}/{}/children'.format(self.base_course_content_uri, self.test_bogus_content_id)
response = self.do_get(test_uri)
......@@ -469,12 +476,18 @@ class CoursesApiTests(TestCase):
response = self.do_post(test_uri, data)
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'
data = {'group_id': "98723896"}
response = self.do_post(test_uri, data)
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):
data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
......@@ -504,8 +517,6 @@ class CoursesApiTests(TestCase):
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_courses_groups_detail_delete(self):
data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
......@@ -596,7 +607,7 @@ class CoursesApiTests(TestCase):
def test_courses_overview_get_invalid_content(self):
#try a bogus course_id to test failure case
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(
category="about",
parent_location=test_course.location,
......@@ -646,7 +657,7 @@ class CoursesApiTests(TestCase):
data='',
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)
self.assertEqual(response.status_code, 404)
......@@ -659,14 +670,14 @@ class CoursesApiTests(TestCase):
data=TEST_COURSE_UPDATES_CONTENT_LEGACY,
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)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
self.assertEqual(response.data['content'], TEST_COURSE_UPDATES_CONTENT_LEGACY)
# 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)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
......@@ -749,7 +760,7 @@ class CoursesApiTests(TestCase):
def test_courses_users_list_get_no_students(self):
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)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
......@@ -780,7 +791,7 @@ class CoursesApiTests(TestCase):
def test_courses_users_list_post_nonexisting_user_allow(self):
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['email'] = 'test+pending@tester.com'
post_data['allow_pending'] = True
......@@ -860,6 +871,56 @@ class CoursesApiTests(TestCase):
response = self.do_get(test_uri)
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):
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/users'
test_user_uri = '/api/users'
......@@ -877,6 +938,11 @@ class CoursesApiTests(TestCase):
self.assertGreater(response.data['id'], 0)
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
post_data = {}
post_data['user_id'] = created_user_id
......@@ -888,7 +954,7 @@ class CoursesApiTests(TestCase):
self.assertGreater(len(response.data), 0)
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)
self.assertEqual(response.status_code, 404)
self.assertGreater(len(response.data), 0)
......@@ -928,7 +994,7 @@ class CoursesApiTests(TestCase):
self.assertEqual(response.status_code, 204)
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)
self.assertEqual(response.status_code, 404)
......@@ -943,21 +1009,21 @@ class CoursesApiTests(TestCase):
data = {'name': 'Beta Group', 'type': 'project'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = '{}/{}/groups'.format(self.base_course_content_uri, self.course_project.id)
test_uri = '{}/{}/groups'.format(self.base_course_content_uri, unicode(self.course_project.scope_ids.usage_id))
data = {'group_id': group_id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
confirm_uri = self.test_server_prefix + test_uri + '/' + str(group_id)
self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['course_id'], str(self.test_course_id))
self.assertEqual(response.data['content_id'], str(self.course_project.id))
self.assertEqual(response.data['content_id'], unicode(self.course_project.scope_ids.usage_id))
self.assertEqual(response.data['group_id'], str(group_id))
def test_course_content_groups_list_post_duplicate(self):
data = {'name': 'Beta Group', 'type': 'project'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = '{}/{}/groups'.format(self.base_course_content_uri, self.course_project.id)
test_uri = '{}/{}/groups'.format(self.base_course_content_uri, unicode(self.course_project.scope_ids.usage_id))
data = {'group_id': group_id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
......@@ -971,7 +1037,7 @@ class CoursesApiTests(TestCase):
test_uri = '{}/{}/content/{}/groups'.format(
self.base_courses_uri,
self.test_bogus_course_id,
self.course_project.id
unicode(self.course_project.scope_ids.usage_id)
)
data = {'group_id': group_id}
response = self.do_post(test_uri, data)
......@@ -994,7 +1060,7 @@ class CoursesApiTests(TestCase):
test_uri = '{}/{}/content/{}/groups'.format(
self.base_courses_uri,
self.test_course_id,
self.course_project.id
unicode(self.course_project.scope_ids.usage_id)
)
data = {'group_id': '12398721'}
response = self.do_post(test_uri, data)
......@@ -1004,13 +1070,13 @@ class CoursesApiTests(TestCase):
test_uri = '{}/{}/content/{}/groups'.format(
self.base_courses_uri,
self.test_course_id,
self.course_project.id
unicode(self.course_project.scope_ids.usage_id)
)
response = self.do_post(test_uri, {})
self.assertEqual(response.status_code, 404)
def test_course_content_groups_list_get(self):
test_uri = '{}/{}/groups'.format(self.base_course_content_uri, self.course_project.id)
test_uri = '{}/{}/groups'.format(self.base_course_content_uri, unicode(self.course_project.scope_ids.usage_id))
data = {'name': 'Alpha Group', 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
alpha_group_id = response.data['id']
......@@ -1049,7 +1115,7 @@ class CoursesApiTests(TestCase):
test_uri = '{}/{}/content/{}/groups'.format(
self.base_courses_uri,
self.test_bogus_course_id,
self.course_project.id
unicode(self.course_project.scope_ids.usage_id)
)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
......@@ -1070,7 +1136,7 @@ class CoursesApiTests(TestCase):
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201)
group_id = response.data['id']
test_uri = '{}/{}/groups'.format(self.base_course_content_uri, self.course_project.id)
test_uri = '{}/{}/groups'.format(self.base_course_content_uri, unicode(self.course_project.scope_ids.usage_id))
data = {'group_id': group_id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
......@@ -1080,7 +1146,7 @@ class CoursesApiTests(TestCase):
self.assertEqual(response.data[0]['group_id'], 2)
def test_course_content_groups_detail_get(self):
test_uri = '{}/{}/groups'.format(self.base_course_content_uri, self.course_project.id)
test_uri = '{}/{}/groups'.format(self.base_course_content_uri, unicode(self.course_project.scope_ids.usage_id))
data = {'name': 'Alpha Group', 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
......@@ -1101,7 +1167,7 @@ class CoursesApiTests(TestCase):
data = {'name': 'Alpha Group', 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = '{}/{}/groups/{}'.format(self.base_course_content_uri, self.course_project.id, group_id)
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)
self.assertEqual(response.status_code, 404)
......@@ -1109,7 +1175,7 @@ class CoursesApiTests(TestCase):
test_uri = '{}/{}/content/{}/groups/123456'.format(
self.base_courses_uri,
self.test_bogus_course_id,
self.course_project.id
unicode(self.course_project.scope_ids.usage_id)
)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
......@@ -1127,14 +1193,14 @@ class CoursesApiTests(TestCase):
test_uri = '{}/{}/content/{}/groups/123456'.format(
self.base_courses_uri,
self.test_course_id,
self.course_project.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_users_list_get(self):
test_uri = '{}/{}/groups'.format(self.base_course_content_uri, self.course_project.id)
test_uri_users = '{}/{}/users'.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, unicode(self.course_project.scope_ids.usage_id))
test_course_users_uri = self.base_courses_uri + '/' + self.test_course_id + '/users'
# Create a group and add it to course module
......@@ -1204,6 +1270,15 @@ class CoursesApiTests(TestCase):
self.assertEqual(response.status_code, 200)
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):
data = {
......@@ -1216,15 +1291,17 @@ class CoursesApiTests(TestCase):
response = self.do_post(self.base_users_uri, data)
self.assertEqual(response.status_code, 201)
created_user_id = response.data['id']
completions_uri = '{}/{}/completions/'.format(self.base_courses_uri, self.course.id)
completions_data = {'content_id': self.course_content.id, 'user_id': created_user_id}
completions_uri = '{}/{}/completions/'.format(self.base_courses_uri, unicode(self.course.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)
self.assertEqual(response.status_code, 201)
coursemodulecomp_id = response.data['id']
self.assertGreater(coursemodulecomp_id, 0)
self.assertEqual(response.data['user_id'], created_user_id)
self.assertEqual(response.data['course_id'], self.course.id)
self.assertEqual(response.data['content_id'], self.course_content.id)
self.assertEqual(response.data['course_id'], unicode(self.course.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['modified'])
......@@ -1242,8 +1319,25 @@ class CoursesApiTests(TestCase):
response = self.do_post(completions_uri, completions_data)
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):
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):
data = {
'email': 'test{}@example.com'.format(i),
......@@ -1257,8 +1351,21 @@ class CoursesApiTests(TestCase):
created_user_id = response.data['id']
for i in xrange(1, 26):
content_id = self.course_content.id + str(i)
completions_data = {'content_id': content_id, 'user_id': created_user_id}
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
)
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)
self.assertEqual(response.status_code, 201)
......@@ -1285,19 +1392,39 @@ class CoursesApiTests(TestCase):
self.assertEqual(len(response.data['results']), 0)
#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)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 25)
self.assertEqual(len(response.data['results']), 10)
#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)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 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):
test_uri = '{}/{}/metrics/proficiency/leaders/'.format(self.base_courses_uri, self.test_course_id)
response = self.do_get(test_uri)
......@@ -1311,8 +1438,9 @@ class CoursesApiTests(TestCase):
self.assertEqual(len(response.data['leaders']), 4)
# Filter by content_id
content_filter_uri = '{}/{}/metrics/proficiency/leaders/?content_id={}'\
.format(self.base_courses_uri, self.test_course_id, Location(self.item.location).url())
content_id = {'content_id': self.item.scope_ids.usage_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, 200)
self.assertEqual(len(response.data['leaders']), 3)
......@@ -1328,13 +1456,31 @@ class CoursesApiTests(TestCase):
self.assertEqual(response.data['position'], 2)
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_uri = '{}/{}/metrics/proficiency/leaders/'.format(self.base_courses_uri, self.test_bogus_course_id)
response = self.do_get(test_uri)
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):
completion_uri = '{}/{}/completions/'.format(self.base_courses_uri, self.course.id)
completion_uri = '{}/{}/completions/'.format(self.base_courses_uri, unicode(self.course.id))
users = []
for i in xrange(1, 5):
data = {
......@@ -1349,6 +1495,13 @@ class CoursesApiTests(TestCase):
users.append(response.data['id'])
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:
user_id = users[0]
elif i < 8:
......@@ -1357,7 +1510,7 @@ class CoursesApiTests(TestCase):
user_id = users[2]
else:
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}
response = self.do_post(completion_uri, completions_data)
self.assertEqual(response.status_code, 201)
......@@ -1409,7 +1562,8 @@ class CoursesApiTests(TestCase):
self.assertGreater(len(response.data['grades']), 0)
# 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)
self.assertEqual(response.status_code, 200)
self.assertGreater(response.data['average_grade'], 0)
......@@ -1420,6 +1574,12 @@ class CoursesApiTests(TestCase):
self.assertGreater(response.data['course_points_possible'], 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):
# Retrieve the list of grades for this course
# All the course/item/user scaffolding was handled in Setup
......@@ -1431,9 +1591,17 @@ class CoursesApiTests(TestCase):
projects_uri = '/api/projects/'
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 = {
'course_id': self.test_course_id,
'content_id': '{}_{}'.format(self.test_course_content_id, i)
'content_id': unicode(local_content.scope_ids.usage_id),
'course_id': self.test_course_id
}
response = self.do_post(projects_uri, data)
self.assertEqual(response.status_code, 201)
......@@ -1443,9 +1611,6 @@ class CoursesApiTests(TestCase):
self.assertEqual(len(response.data['results']), 10)
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):
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/users'
test_user_uri = '/api/users'
......@@ -1474,3 +1639,93 @@ class CoursesApiTests(TestCase):
# test with bogus course
response = self.do_get(course_metrics_uri.format(self.test_bogus_course_id))
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
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'^(?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)
......@@ -12,32 +12,35 @@ from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Avg, Sum, Count
from django.http import Http404
from django.utils.translation import ugettext_lazy as _
from django.db.models import Q
from rest_framework import status
from rest_framework.response import Response
from api_manager.models import CourseGroupRelationship, CourseContentGroupRelationship, GroupProfile, \
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.courses import get_course_about_section, get_course_info_section
from courseware.models import StudentModule
from courseware.views import get_static_tab_contents
from student.models import CourseEnrollment, CourseEnrollmentAllowed
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.users.serializers import UserSerializer, UserCountByCitySerializer
from api_manager.utils import generate_base_uri
from projects.models import Project
from projects.serializers import ProjectSerializer
from projects.models import Project, Workgroup
from projects.serializers import ProjectSerializer, BasicWorkgroupSerializer
from .serializers import CourseModuleCompletionSerializer
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__)
def _get_content_children(content, content_type=None):
"""
Parses the provided content object looking for children
......@@ -55,23 +58,22 @@ def _get_content_children(content, content_type=None):
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
This should probably evolve to use DRF serializers
"""
data = {}
if getattr(content, 'id') == course_id:
content_id = content.id
if hasattr(content_descriptor, 'id') and unicode(content_descriptor.id) == unicode(content_key):
content_id = unicode(content_key)
else:
content_id = content.location.url()
data['id'] = content_id
content_id = unicode(content_descriptor.scope_ids.usage_id)
data['id'] = unicode(content_id)
if hasattr(content, 'display_name'):
data['name'] = content.display_name
if hasattr(content_descriptor, 'display_name'):
data['name'] = content_descriptor.display_name
data['category'] = content.location.category
data['category'] = content_descriptor.location.category
protocol = 'http'
if request.is_secure():
......@@ -79,29 +81,28 @@ def _serialize_content(request, course_id, content):
content_uri = '{}://{}/api/courses/{}'.format(
protocol,
request.get_host(),
course_id.encode('utf-8')
unicode(content_key)
)
# Some things we do only if the content object is a course
if (course_id == content_id):
data['number'] = content.location.course
data['org'] = content.location.org
if (unicode(content_key) == content_id):
data['number'] = content_descriptor.location.course
data['org'] = content_descriptor.location.org
# Other things we do only if the content object is not a course
else:
content_uri = '{}/content/{}'.format(content_uri, content_id)
data['uri'] = content_uri
if hasattr(content, 'due'):
data['due'] = content.due
data['start'] = getattr(content, 'start', None)
data['end'] = getattr(content, 'end', None)
if hasattr(content_descriptor, 'due'):
data['due'] = content_descriptor.due
data['start'] = getattr(content_descriptor, 'start', None)
data['end'] = getattr(content_descriptor, 'end', None)
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
This should probably evolve to use DRF serializers
......@@ -111,17 +112,21 @@ def _serialize_content_children(request, course_id, children):
for child in children:
child_data = _serialize_content(
request,
course_id,
course_key,
child
)
data.append(child_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(
request,
course_descriptor.id,
course_key,
descriptor
)
if depth > 0:
......@@ -129,7 +134,7 @@ def _serialize_content_with_children(request, course_descriptor, descriptor, dep
for child in descriptor.get_children():
data['children'].append(_serialize_content_with_children(
request,
course_descriptor,
course_key,
child,
depth - 1
))
......@@ -252,13 +257,13 @@ def _parse_updates_html(html):
posting_data['date'] = posting_date_element.text
content = u''
for el in posting:
for current_element in posting:
# note, we can't delete or skip over the date element in
# the HTML tree because there might be some tailing content
if el != posting_date_element:
content += etree.tostring(el)
if current_element != posting_date_element:
content += etree.tostring(current_element)
else:
content += el.tail if el.tail else u''
content += current_element.tail if current_element.tail else u''
posting_data['content'] = content.strip()
result.append(posting_data)
......@@ -310,23 +315,25 @@ class CourseContentList(SecureAPIView):
"""
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:
content_id = course_id
response_data = []
content_type = request.QUERY_PARAMS.get('type', None)
store = modulestore()
if course_id != content_id:
try:
content = store.get_instance(course_id, Location(content_id))
except InvalidLocationError:
content = None
content_descriptor, content_key, content = get_course_child(request, request.user, course_key, content_id) # pylint: disable=W0612
else:
content = get_course(course_id)
content = course_descriptor
if content:
children = _get_content_children(content, content_type)
response_data = _serialize_content_children(
request,
course_id,
course_key,
children
)
status_code = status.HTTP_200_OK
......@@ -383,50 +390,47 @@ class CourseContentDetail(SecureAPIView):
"""
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 = {}
base_uri = generate_base_uri(request)
content_type = request.QUERY_PARAMS.get('type', None)
response_data['uri'] = base_uri
if course_id != content_id:
element_name = 'children'
try:
content = store.get_instance(course_id, Location(content_id))
except InvalidLocationError:
content = None
content_descriptor, content_key, content = get_course_child(request, request.user, course_key, content_id) # pylint: disable=W0612
else:
element_name = 'content'
content = get_course(course_id)
protocol = 'http'
if request.is_secure():
protocol = protocol + 's'
response_data['uri'] = '{}://{}/api/courses/{}'.format(
protocol,
request.get_host(),
course_id.encode('utf-8')
)
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
unicode(course_key)
)
base_uri_without_qs = generate_base_uri(request, True)
response_data['resources'] = []
resource_uri = '{}/users'.format(base_uri_without_qs)
response_data['resources'].append({'uri': resource_uri})
resource_uri = '{}/groups'.format(base_uri_without_qs)
response_data['resources'].append({'uri': resource_uri})
status_code = status.HTTP_200_OK
else:
status_code = status.HTTP_404_NOT_FOUND
return Response(response_data, status=status_code)
if not content:
return Response(response_data, status=status.HTTP_404_NOT_FOUND)
response_data = _serialize_content(
request,
course_id,
content
)
content_type = request.QUERY_PARAMS.get('type', None)
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)
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):
......@@ -458,6 +462,9 @@ class CoursesList(SecureAPIView):
"""
def get(self, request):
"""
GET /api/courses
"""
response_data = []
store = modulestore()
course_descriptors = store.get_courses()
......@@ -519,21 +526,20 @@ class CoursesDetail(SecureAPIView):
"""
def get(self, request, course_id):
"""
GET /api/courses/{course_id}
"""
depth = request.QUERY_PARAMS.get('depth', 0)
depth_int = int(depth)
# get_course_by_id raises an Http404 if the requested course is invalid
# Rather than catching it, we just let it bubble up
try:
course_descriptor = get_course(course_id, depth=depth_int)
except ValueError:
course_descriptor = None
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 depth_int > 0:
response_data = _serialize_content_with_children(
request,
course_descriptor,
course_key,
course_descriptor, # Primer for recursive function
depth_int
)
......@@ -542,7 +548,7 @@ class CoursesDetail(SecureAPIView):
else:
response_data = _serialize_content(
request,
course_descriptor.id,
course_key,
course_descriptor
)
base_uri = generate_base_uri(request)
......@@ -563,6 +569,7 @@ class CoursesDetail(SecureAPIView):
response_data['resources'].append({'uri': resource_uri})
return Response(response_data, status=status.HTTP_200_OK)
class CoursesGroupsList(SecureAPIView):
"""
**Use Case**
......@@ -608,22 +615,21 @@ class CoursesGroupsList(SecureAPIView):
response_data = {}
group_id = request.DATA['group_id']
base_uri = generate_base_uri(request)
try:
existing_course = get_course(course_id)
except ValueError:
existing_course = None
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)
try:
existing_group = Group.objects.get(id=group_id)
except ObjectDoesNotExist:
existing_group = None
if existing_course and existing_group:
if existing_group:
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:
existing_relationship = None
if existing_relationship is None:
CourseGroupRelationship.objects.create(course_id=course_id, group=existing_group)
response_data['course_id'] = str(existing_course.id)
CourseGroupRelationship.objects.create(course_id=course_key, group=existing_group)
response_data['course_id'] = unicode(course_key)
response_data['group_id'] = str(existing_group.id)
response_data['uri'] = '{}/{}'.format(base_uri, existing_group.id)
response_status = status.HTTP_201_CREATED
......@@ -638,13 +644,11 @@ class CoursesGroupsList(SecureAPIView):
"""
GET /api/courses/{course_id}/groups?type=workgroup
"""
try:
get_course(course_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)
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:
course_groups = course_groups.filter(group__groupprofile__group_type=group_type)
......@@ -656,6 +660,7 @@ class CoursesGroupsList(SecureAPIView):
response_status = status.HTTP_200_OK
return Response(response_data, status=response_status)
class CoursesGroupsDetail(SecureAPIView):
"""
### The CoursesGroupsDetail view allows clients to interact with a specific CourseGroupRelationship entity
......@@ -671,16 +676,15 @@ class CoursesGroupsDetail(SecureAPIView):
"""
GET /api/courses/{course_id}/groups/{group_id}
"""
try:
existing_course = get_course(course_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)
try:
existing_group = Group.objects.get(id=group_id)
except ObjectDoesNotExist:
return Response({}, status=status.HTTP_404_NOT_FOUND)
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:
return Response({}, status=status.HTTP_404_NOT_FOUND)
response_data = {}
......@@ -694,9 +698,12 @@ class CoursesGroupsDetail(SecureAPIView):
"""
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:
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:
pass
response_data = {}
......@@ -730,23 +737,21 @@ class CoursesOverview(SecureAPIView):
"""
def get(self, request, course_id):
"""
GET /api/courses/{course_id}/overview
"""
response_data = OrderedDict()
try:
existing_course = get_course(course_id)
except ValueError:
existing_course = None
if existing_course:
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:
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)
existing_content = get_course_about_section(course_descriptor, 'overview')
if not existing_content:
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):
......@@ -775,14 +780,14 @@ class CoursesUpdates(SecureAPIView):
"""
def get(self, request, course_id):
response_data = OrderedDict()
try:
existing_course = get_course(course_id)
except ValueError:
existing_course = None
if not existing_course:
"""
GET /api/courses/{course_id}/updates
"""
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 = get_course_info_section(request, existing_course, 'updates')
response_data = OrderedDict()
content = get_course_info_section(request, course_descriptor, 'updates')
if not content:
return Response({}, status=status.HTTP_404_NOT_FOUND)
if request.GET.get('parse') and request.GET.get('parse') in ['True', 'true']:
......@@ -819,15 +824,15 @@ class CoursesStaticTabsList(SecureAPIView):
"""
def get(self, request, course_id):
try:
existing_course = get_course(course_id)
except ValueError:
existing_course = None
if not existing_course:
"""
GET /api/courses/{course_id}/static_tabs
"""
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)
response_data = OrderedDict()
tabs = []
for tab in existing_course.tabs:
for tab in course_descriptor.tabs:
if tab.type == 'static_tab':
tab_data = OrderedDict()
tab_data['id'] = tab.url_slug
......@@ -835,7 +840,7 @@ class CoursesStaticTabsList(SecureAPIView):
if request.GET.get('detail') and request.GET.get('detail') in ['True', 'true']:
tab_data['content'] = get_static_tab_contents(
request,
existing_course,
course_descriptor,
tab,
wrap_xmodule_display=False
)
......@@ -868,18 +873,20 @@ class CoursesStaticTabsDetail(SecureAPIView):
"""
def get(self, request, course_id, tab_id):
try:
existing_course = get_course(course_id)
except ValueError:
"""
GET /api/courses/{course_id}/static_tabs/{tab_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_404_NOT_FOUND)
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:
response_data['id'] = tab.url_slug
response_data['name'] = tab.name
response_data['content'] = get_static_tab_contents(
request,
existing_course,
course_descriptor,
tab,
wrap_xmodule_display=False
)
......@@ -914,6 +921,12 @@ class CoursesUsersList(SecureAPIView):
* 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**
To create a new user through POST /api/courses/{course_id}/users, you
......@@ -921,12 +934,11 @@ class CoursesUsersList(SecureAPIView):
"""
def post(self, request, course_id):
response_data = OrderedDict()
try:
existing_course = get_course(course_id)
except ValueError:
existing_course = None
if not existing_course:
"""
POST /api/courses/{course_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)
if 'user_id' in request.DATA:
user_id = request.DATA['user_id']
......@@ -934,7 +946,7 @@ class CoursesUsersList(SecureAPIView):
existing_user = User.objects.get(id=user_id)
except ObjectDoesNotExist:
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)
elif 'email' in request.DATA:
try:
......@@ -946,7 +958,7 @@ class CoursesUsersList(SecureAPIView):
# and the instructor is pre-enrolling them
# Store the pre-enrollment data in the CourseEnrollmentAllowed table
# 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.save()
return Response({}, status.HTTP_201_CREATED)
......@@ -956,29 +968,34 @@ class CoursesUsersList(SecureAPIView):
return Response({}, status=status.HTTP_400_BAD_REQUEST)
def get(self, request, course_id):
"""
GET /api/courses/{course_id}
"""
orgs = request.QUERY_PARAMS.get('organizations')
response_data = OrderedDict()
base_uri = generate_base_uri(request)
response_data['uri'] = base_uri
try:
existing_course = get_course(course_id)
except ValueError:
existing_course = None
if not existing_course:
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)
# 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'] = []
for user in users:
user_data = OrderedDict()
user_data['id'] = user.id
user_data['email'] = user.email
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)
# Then list all enrollments which are pending. These are enrollments for students that have not yet
# created an account
pending_enrollments = CourseEnrollmentAllowed.objects.filter(course_id=course_id)
pending_enrollments = CourseEnrollmentAllowed.objects.filter(course_id=course_key)
if pending_enrollments:
response_data['pending_enrollments'] = []
for cea in pending_enrollments:
......@@ -1021,23 +1038,13 @@ class CoursesUsersDetail(SecureAPIView):
'uri': base_uri,
}
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)
except ObjectDoesNotExist:
user = None
if user and CourseEnrollment.is_enrolled(user, course_id):
field_data_cache = FieldDataCache([course_descriptor], course_id, user)
course_content = module_render.get_module(
user,
request,
course_descriptor.location,
field_data_cache,
course_id)
return Response(response_data, status=status.HTTP_404_NOT_FOUND)
course_descriptor, course_key, course_content = get_course(request, user, course_id)
if not course_descriptor:
return Response(response_data, status=status.HTTP_404_NOT_FOUND)
if CourseEnrollment.is_enrolled(user, course_key):
response_data['position'] = course_content.position
response_status = status.HTTP_200_OK
else:
......@@ -1045,18 +1052,17 @@ class CoursesUsersDetail(SecureAPIView):
return Response(response_data, status=response_status)
def delete(self, request, course_id, user_id):
try:
existing_course = get_course(course_id)
except ValueError:
existing_course = None
if not existing_course:
return Response({}, status=status.HTTP_404_NOT_FOUND)
"""
DELETE /api/courses/{course_id}/users/{user_id}
"""
try:
user = User.objects.get(id=user_id, is_active=True)
except ObjectDoesNotExist:
user = None
if user:
CourseEnrollment.unenroll(user, course_id)
return Response({}, status=status.HTTP_204_NO_CONTENT)
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)
CourseEnrollment.unenroll(user, course_key)
response_data = {}
base_uri = generate_base_uri(request)
response_data['uri'] = base_uri
......@@ -1086,14 +1092,11 @@ class CourseContentGroupsList(SecureAPIView):
"""
POST /api/courses/{course_id}/content/{content_id}/groups
"""
try:
course_descriptor = get_course(course_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)
store = modulestore()
try:
existing_content = store.get_instance(course_id, Location(content_id))
except InvalidLocationError:
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)
group_id = request.DATA.get('group_id')
if group_id is None:
......@@ -1105,21 +1108,21 @@ class CourseContentGroupsList(SecureAPIView):
response_data = {}
base_uri = generate_base_uri(request)
response_data['uri'] = '{}/{}'.format(base_uri, existing_profile.group_id)
response_data['course_id'] = course_descriptor.id
response_data['content_id'] = existing_content.id
response_data['course_id'] = unicode(course_key)
response_data['content_id'] = unicode(existing_content.scope_ids.usage_id)
response_data['group_id'] = str(existing_profile.group_id)
try:
existing_relationship = CourseContentGroupRelationship.objects.get(
course_id=course_id,
content_id=content_id,
CourseContentGroupRelationship.objects.get(
course_id=course_key,
content_id=existing_content.location,
group_profile=existing_profile
)
response_data['message'] = "Relationship already exists."
return Response(response_data, status=status.HTTP_409_CONFLICT)
except ObjectDoesNotExist:
CourseContentGroupRelationship.objects.create(
course_id=course_id,
content_id=content_id,
course_id=course_key,
content_id=existing_content.location,
group_profile=existing_profile
)
return Response(response_data, status=status.HTTP_201_CREATED)
......@@ -1130,18 +1133,15 @@ class CourseContentGroupsList(SecureAPIView):
"""
response_data = []
group_type = request.QUERY_PARAMS.get('type')
try:
course_descriptor = get_course(course_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)
try:
store = modulestore()
existing_content = store.get_instance(course_id, Location(content_id))
except InvalidLocationError:
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)
relationships = CourseContentGroupRelationship.objects.filter(
course_id=course_id,
content_id=content_id,
course_id=course_key,
content_id=existing_content.location,
).select_related("groupprofile")
if group_type:
relationships = relationships.filter(group_profile__group_type=group_type)
......@@ -1165,19 +1165,16 @@ class CourseContentGroupsDetail(SecureAPIView):
"""
GET /api/courses/{course_id}/content/{content_id}/groups/{group_id}
"""
try:
course_descriptor = get_course(course_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)
try:
store = modulestore()
existing_content = store.get_instance(course_id, Location(content_id))
except InvalidLocationError:
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)
try:
relationship = CourseContentGroupRelationship.objects.get(
course_id=course_id,
content_id=content_id,
CourseContentGroupRelationship.objects.get(
course_id=course_key,
content_id=existing_content.location,
group_profile__group_id=group_id
)
except ObjectDoesNotExist:
......@@ -1209,11 +1206,17 @@ class CourseContentUsersList(SecureAPIView):
"""
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')
group_type = self.request.QUERY_PARAMS.get('type', None)
group_id = self.request.QUERY_PARAMS.get('group_id', None)
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:
relationships = relationships.filter(group_profile__group__id=group_id)
......@@ -1223,7 +1226,7 @@ class CourseContentUsersList(SecureAPIView):
lookup_group_ids = relationships.values_list('group_profile', flat=True)
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']:
queryset = enrolled_users
else:
......@@ -1251,6 +1254,7 @@ class CourseModuleCompletionList(SecureListAPIView):
"user_id": "3",
"course_id": "32fgdf",
"content_id": "324dfgd",
"stage": "First",
"created": "2014-06-10T13:14:49.878Z",
"modified": "2014-06-10T13:14:49.914Z"
}
......@@ -1259,13 +1263,14 @@ class CourseModuleCompletionList(SecureListAPIView):
Filters can also be applied
```/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}```
- POST: Creates a Course-Module completion entity
- POST Example:
{
"content_id":"i4x://the/content/location",
"user_id":4
"user_id":4,
"stage": "First"
}
### Use Cases/Notes:
* Use GET operation to retrieve list of course completions by user
......@@ -1279,15 +1284,26 @@ class CourseModuleCompletionList(SecureListAPIView):
"""
user_ids = self.request.QUERY_PARAMS.get('user_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']
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)
if user_ids:
user_ids = map(int, user_ids.split(','))[:upper_bound]
queryset = queryset.filter(user__in=user_ids)
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
......@@ -1297,14 +1313,22 @@ class CourseModuleCompletionList(SecureListAPIView):
"""
content_id = request.DATA.get('content_id', None)
user_id = request.DATA.get('user_id', None)
stage = request.DATA.get('stage', None)
if not content_id:
return Response({'message': _('content_id is missing')}, status.HTTP_400_BAD_REQUEST)
if not user_id:
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,
course_id=course_id,
content_id=content_id)
course_id=course_key,
content_id=existing_content.location,
stage=stage)
serializer = CourseModuleCompletionSerializer(completion)
if created:
return Response(serializer.data, status=status.HTTP_201_CREATED) # pylint: disable=E1101
......@@ -1321,18 +1345,15 @@ class CoursesGradesList(SecureListAPIView):
* 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
"""
try:
existing_course = get_course(course_id)
except ValueError:
existing_course = None
if not existing_course:
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)
queryset = StudentModule.objects.filter(
course_id__exact=course_id,
course_id__exact=course_key,
grade__isnull=False,
max_grade__isnull=False,
max_grade__gt=0
......@@ -1346,14 +1367,17 @@ class CoursesGradesList(SecureListAPIView):
content_id = self.request.QUERY_PARAMS.get('content_id', None)
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_sum = queryset.aggregate(Sum('grade'))
queryset_maxgrade_sum = queryset.aggregate(Sum('max_grade'))
course_queryset = StudentModule.objects.filter(
course_id__exact=course_id,
course_id__exact=course_key,
grade__isnull=False,
max_grade__isnull=False,
max_grade__gt=0
......@@ -1375,7 +1399,7 @@ class CoursesGradesList(SecureListAPIView):
response_data['grades'] = []
for row in queryset:
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)
......@@ -1390,14 +1414,8 @@ class CoursesProjectList(SecureListAPIView):
def get_queryset(self):
course_id = self.kwargs['course_id']
try:
existing_course = get_course(course_id)
except ValueError:
existing_course = None
if not existing_course:
raise Http404
return Project.objects.filter(course_id=course_id)
course_descriptor, course_key, course_content = get_course(self.request, self.request.user, course_id) # pylint: disable=W0612
return Project.objects.filter(course_id=course_key)
class CourseMetrics(SecureAPIView):
......@@ -1413,13 +1431,10 @@ class CourseMetrics(SecureAPIView):
"""
GET /api/courses/{course_id}/metrics/
"""
try:
existing_course = get_course(course_id)
except ValueError:
existing_course = None
if not existing_course:
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)
users_enrolled = CourseEnrollment.num_enrolled_in(course_id)
users_enrolled = CourseEnrollment.num_enrolled_in(course_key)
data = {
'users_enrolled': users_enrolled
}
......@@ -1442,7 +1457,7 @@ class CoursesLeadersList(SecureListAPIView):
* 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/
"""
......@@ -1451,12 +1466,11 @@ class CoursesLeadersList(SecureListAPIView):
count = self.request.QUERY_PARAMS.get('count', 3)
data = {}
course_avg = 0
try:
get_course(course_id)
except ValueError:
raise Http404
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)
queryset = StudentModule.objects.filter(
course_id__exact=course_id,
course_id__exact=course_key,
grade__isnull=False,
max_grade__isnull=False,
max_grade__gt=0,
......@@ -1464,15 +1478,19 @@ class CoursesLeadersList(SecureListAPIView):
)
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:
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'))
user_points = user_points['points'] or 0
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['points'] = user_points['points']
data['points'] = user_points
points = queryset.aggregate(total=Sum('grade'))
users = queryset.filter(student__is_active=True).aggregate(total=Count('student__id', distinct=True))
......@@ -1513,11 +1531,10 @@ class CoursesCompletionsLeadersList(SecureAPIView):
count = self.request.QUERY_PARAMS.get('count', 3)
data = {}
course_avg = 0
try:
get_course(course_id)
except ValueError:
raise Http404
queryset = CourseModuleCompletion.objects.filter(course_id=course_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_404_NOT_FOUND)
queryset = CourseModuleCompletion.objects.filter(course_id=course_key)
if user_id:
user_completions = queryset.filter(user__id=user_id).count()
......@@ -1540,3 +1557,76 @@ class CoursesCompletionsLeadersList(SecureAPIView):
serializer = CourseCompletionsLeadersSerializer(queryset, many=True)
data['leaders'] = serializer.data # pylint: disable=E1101
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 @@
Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/tests/test_group_views.py]
"""
from datetime import datetime
from random import randint
import uuid
import json
from urllib import urlencode
from django.core.cache import cache
from django.test import Client
......@@ -45,26 +47,26 @@ class GroupsApiTests(ModuleStoreTestCase):
self.test_course_data = '<html>{}</html>'.format(str(uuid.uuid4()))
self.course = CourseFactory.create()
self.test_course_id = self.course.id
self.test_course_id = unicode(self.course.id)
self.course_content = ItemFactory.create(
category="videosequence",
parent_location=self.course.location,
data=self.test_course_data,
due="2016-05-16T14:30:00Z",
due=datetime(2016, 5, 16, 14, 30),
display_name="View_Sequence"
)
self.test_organization = Organization.objects.create(
name="Test Organization",
display_name = 'Test Org',
contact_name = 'John Org',
contact_email = 'john@test.org',
contact_phone = '+1 332 232 24234'
display_name='Test Org',
contact_name='John Org',
contact_email='john@test.org',
contact_phone='+1 332 232 24234'
)
self.test_project = Project.objects.create(
course_id=self.course.id,
content_id=self.course_content.id
course_id=unicode(self.course.id),
content_id=unicode(self.course_content.scope_ids.usage_id)
)
self.client = SecureClient()
......@@ -820,7 +822,7 @@ class GroupsApiTests(ModuleStoreTestCase):
data = {'course_id': self.test_course_id}
response = self.do_post(test_uri, data)
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['group_id'], str(group_id))
self.assertEqual(response.data['course_id'], self.test_course_id)
......@@ -847,7 +849,13 @@ class GroupsApiTests(ModuleStoreTestCase):
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201)
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)
self.assertEqual(response.status_code, 404)
......@@ -860,7 +868,7 @@ class GroupsApiTests(ModuleStoreTestCase):
data = {'course_id': self.test_course_id}
response = self.do_post(test_uri, data)
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['group_id'], str(group_id))
self.assertEqual(response.data['course_id'], self.test_course_id)
......@@ -885,6 +893,7 @@ class GroupsApiTests(ModuleStoreTestCase):
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
test_uri = '{}/{}/courses/{}'.format(self.base_groups_uri, group_id, self.test_course_id)
print test_uri
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
confirm_uri = '{}{}/{}/courses/{}'.format(
......@@ -930,7 +939,7 @@ class GroupsApiTests(ModuleStoreTestCase):
data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201)
test_uri = '{}/courses/{}'.format(response.data['uri'], self.course.id)
test_uri = '{}/courses/{}'.format(response.data['uri'], unicode(self.course.id))
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
......@@ -941,14 +950,13 @@ class GroupsApiTests(ModuleStoreTestCase):
group_id = response.data['id']
self.test_organization.groups.add(group_id)
test_uri = response.data['uri'] + '/organizations/'
confirm_uri = test_uri + '/' + str(self.test_organization.id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]['id'], self.test_organization.id)
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/'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
......@@ -981,7 +989,8 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.data['num_pages'], 2)
# 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.data['count'], 11)
self.assertIsNotNone(response.data['results'][0]['name'])
......
......@@ -8,15 +8,15 @@ from api_manager.groups import views as groups_views
urlpatterns = patterns(
'',
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/(?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]+)/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/(?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/(?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)
""" API implementation for group-oriented interactions. """
import uuid
import json
from collections import OrderedDict
from django.contrib.auth.models import Group
from django.core.exceptions import ObjectDoesNotExist
from django.http import Http404
from django.utils import timezone
from rest_framework import status
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.permissions import SecureAPIView, SecureListAPIView
from api_manager.utils import str2bool, generate_base_uri
from api_manager.organizations import serializers
from projects.serializers import BasicWorkgroupSerializer
from xmodule.modulestore import Location, InvalidLocationError
from xmodule.modulestore.django import modulestore
RELATIONSHIP_TYPES = {'hierarchical': 'h', 'graph': 'g'}
......@@ -317,7 +314,7 @@ class GroupsUsersDetail(SecureAPIView):
response_status = status.HTTP_404_NOT_FOUND
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
"""
......@@ -470,7 +467,7 @@ class GroupsGroupsDetail(SecureAPIView):
response_status = status.HTTP_200_OK
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}
"""
......@@ -481,7 +478,6 @@ class GroupsGroupsDetail(SecureAPIView):
try:
to_group_relationship = GroupRelationship.objects.get(group__id=related_group_id)
except ObjectDoesNotExist:
to_group = None
to_group_relationship = None
if from_group_relationship:
if to_group_relationship:
......@@ -524,13 +520,12 @@ class GroupsCoursesList(SecureAPIView):
existing_group = Group.objects.get(id=group_id)
except ObjectDoesNotExist:
return Response({}, status.HTTP_404_NOT_FOUND)
store = modulestore()
course_id = request.DATA['course_id']
base_uri = generate_base_uri(request)
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:
return Response({}, status.HTTP_404_NOT_FOUND)
......@@ -558,11 +553,10 @@ class GroupsCoursesList(SecureAPIView):
existing_group = Group.objects.get(id=group_id)
except ObjectDoesNotExist:
return Response({}, status.HTTP_404_NOT_FOUND)
store = modulestore()
members = CourseGroupRelationship.objects.filter(group=existing_group)
response_data = []
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_id': member.course_id,
'display_name': course.display_name
......@@ -607,7 +601,7 @@ class GroupsCoursesDetail(SecureAPIView):
response_status = status.HTTP_404_NOT_FOUND
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}
"""
......@@ -630,7 +624,7 @@ class GroupsOrganizationsList(SecureAPIView):
* 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/
"""
......@@ -642,7 +636,7 @@ class GroupsOrganizationsList(SecureAPIView):
response_data = []
for org in existing_group.organizations.all():
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)
......
"""
One-time data migration script -- shoulen't need to run it again
"""
import json
from django.contrib.auth.models import Group
......@@ -5,6 +8,8 @@ from django.core.management.base import BaseCommand
from api_manager.models import GroupProfile, Organization
class Command(BaseCommand):
"""
Migrates legacy organization data and user relationships from older Group model approach to newer concrete Organization model
......@@ -40,6 +45,6 @@ class Command(BaseCommand):
migrated_org.users.add(user)
linked_groups = group.grouprelationship.get_linked_group_relationships()
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)
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 uuid
......@@ -9,9 +13,9 @@ from api_manager.models import GroupProfile, GroupRelationship, Organization
class MigrateOrgDataTests(TestCase):
def setUp(self):
setup = True
"""
Test suite for data migration script
"""
def test_migrate_orgdata(self):
"""
......@@ -20,7 +24,6 @@ class MigrateOrgDataTests(TestCase):
# Create some old-style Group organizations to migrate
group_name = str(uuid.uuid4())
group_profile_name = "Group 1 Name"
group_type = "organization"
groupdata = {}
groupdata['name'] = "Group 1 Data Name"
......@@ -29,8 +32,8 @@ class MigrateOrgDataTests(TestCase):
groupdata['contact_email'] = "Group 1 Data Contact Email"
groupdata['contact_phone'] = "Group 1 Data Contact Phone"
group = Group.objects.create(name=group_name)
group_relationship = GroupRelationship.objects.create(group_id=group.id)
profile, _ = GroupProfile.objects.get_or_create(
GroupRelationship.objects.create(group_id=group.id)
GroupProfile.objects.get_or_create(
group_id=group.id,
group_type=group_type,
name=groupdata['name'],
......@@ -43,7 +46,6 @@ class MigrateOrgDataTests(TestCase):
group.grouprelationship.add_linked_group_relationship(linked_group_relationship)
group2_name = str(uuid.uuid4())
group2_profile_name = "Group 2 Name"
group2_type = "organization"
groupdata = {}
groupdata['name'] = "Group 2 Data Name"
......@@ -52,8 +54,8 @@ class MigrateOrgDataTests(TestCase):
groupdata['contact_email'] = "Group 2 Data Contact Email"
groupdata['contact_phone'] = "Group 2 Data Contact Phone"
group2 = Group.objects.create(name=group2_name)
grouprelattionship2 = GroupRelationship.objects.create(group_id=group2.id)
profile2, _ = GroupProfile.objects.get_or_create(
GroupRelationship.objects.create(group_id=group2.id)
GroupProfile.objects.get_or_create(
group_id=group2.id,
group_type=group2_type,
name=groupdata['name'],
......
......@@ -70,7 +70,6 @@ class Migration(SchemaMigration):
self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now),
keep_default=False)
def backwards(self, orm):
# Adding field 'GroupRelationship.record_date_created'
db.add_column('api_manager_grouprelationship', 'record_date_created',
......@@ -122,7 +121,6 @@ class Migration(SchemaMigration):
# Deleting field 'LinkedGroupRelationship.modified'
db.delete_column('api_manager_linkedgrouprelationship', 'modified')
models = {
'api_manager.coursegrouprelationship': {
'Meta': {'object_name': 'CourseGroupRelationship'},
......@@ -184,4 +182,4 @@ class Migration(SchemaMigration):
}
}
complete_apps = ['api_manager']
\ No newline at end of file
complete_apps = ['api_manager']
......@@ -11,12 +11,10 @@ class Migration(SchemaMigration):
# Adding unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group']
db.create_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_id'])
def backwards(self, orm):
# Removing unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group']
db.delete_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_id'])
models = {
'api_manager.coursecontentgrouprelationship': {
'Meta': {'unique_together': "(('course_id', 'content_id', 'group'),)", 'object_name': 'CourseContentGroupRelationship'},
......@@ -88,4 +86,4 @@ class Migration(SchemaMigration):
}
}
complete_apps = ['api_manager']
\ No newline at end of file
complete_apps = ['api_manager']
......@@ -22,7 +22,6 @@ class Migration(SchemaMigration):
# 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'])
def backwards(self, orm):
# 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'])
......@@ -38,7 +37,6 @@ class Migration(SchemaMigration):
# Adding unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group']
db.create_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_id'])
models = {
'api_manager.coursecontentgrouprelationship': {
'Meta': {'unique_together': "(('course_id', 'content_id', 'group_profile'),)", 'object_name': 'CourseContentGroupRelationship'},
......@@ -110,4 +108,4 @@ class Migration(SchemaMigration):
}
}
complete_apps = ['api_manager']
\ No newline at end of file
complete_apps = ['api_manager']
......@@ -14,12 +14,10 @@ class Migration(SchemaMigration):
# Adding unique constraint on 'GroupProfile', fields ['group']
db.create_unique('auth_groupprofile', ['group_id'])
def backwards(self, orm):
# Removing unique constraint on 'GroupProfile', fields ['group']
db.delete_unique('auth_groupprofile', ['group_id'])
# Changing field 'GroupProfile.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):
}
}
complete_apps = ['api_manager']
\ No newline at end of file
complete_apps = ['api_manager']
......@@ -19,12 +19,10 @@ class Migration(SchemaMigration):
))
db.send_create_signal('api_manager', ['CourseModuleCompletion'])
def backwards(self, orm):
# Deleting model 'CourseModuleCompletion'
db.delete_table('api_manager_coursemodulecompletion')
models = {
'api_manager.coursecontentgrouprelationship': {
'Meta': {'unique_together': "(('course_id', 'content_id', 'group_profile'),)", 'object_name': 'CourseContentGroupRelationship'},
......@@ -139,4 +137,4 @@ class Migration(SchemaMigration):
}
}
complete_apps = ['api_manager']
\ No newline at end of file
complete_apps = ['api_manager']
......@@ -28,7 +28,6 @@ class Migration(SchemaMigration):
self.gf('django.db.models.fields.CharField')(max_length=50, null=True, blank=True),
keep_default=False)
def backwards(self, orm):
# Deleting field 'Organization.display_name'
db.delete_column('api_manager_organization', 'display_name')
......@@ -42,7 +41,6 @@ class Migration(SchemaMigration):
# Deleting field 'Organization.contact_phone'
db.delete_column('api_manager_organization', 'contact_phone')
models = {
'api_manager.coursecontentgrouprelationship': {
'Meta': {'unique_together': "(('course_id', 'content_id', 'group_profile'),)", 'object_name': 'CourseContentGroupRelationship'},
......@@ -161,4 +159,4 @@ class Migration(SchemaMigration):
}
}
complete_apps = ['api_manager']
\ No newline at end of file
complete_apps = ['api_manager']
......@@ -7,7 +7,7 @@ from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
def forwards(self, orm):
# Adding M2M table for field groups on 'Organization'
db.create_table('api_manager_organization_groups', (
......@@ -17,13 +17,11 @@ class Migration(SchemaMigration):
))
db.create_unique('api_manager_organization_groups', ['organization_id', 'group_id'])
def backwards(self, orm):
# Removing M2M table for field groups on 'Organization'
db.delete_table('api_manager_organization_groups')
models = {
'api_manager.coursecontentgrouprelationship': {
'Meta': {'unique_together': "(('course_id', 'content_id', 'group_profile'),)", 'object_name': 'CourseContentGroupRelationship'},
......@@ -153,4 +151,4 @@ class Migration(SchemaMigration):
}
}
complete_apps = ['api_manager']
\ No newline at end of file
complete_apps = ['api_manager']
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding 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 @@
from django.contrib.auth.models import Group, User
from django.db import models
from django.utils import timezone
from model_utils.models import TimeStampedModel
from .utils import is_int
......@@ -102,6 +102,9 @@ class GroupProfile(TimeStampedModel):
"""
class Meta:
"""
Meta class for modifying things like table name
"""
db_table = "auth_groupprofile"
group = models.OneToOneField(Group, db_index=True)
......@@ -155,6 +158,7 @@ class CourseModuleCompletion(TimeStampedModel):
user = models.ForeignKey(User, db_index=True, related_name="course_completions")
course_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
......
......@@ -94,7 +94,6 @@ class OrganizationsApiTests(TestCase):
self.assertEqual(response.status_code, 201)
users.append(response.data['id'])
data = {
'name': self.test_organization_name,
'display_name': self.test_organization_display_name,
......
# pylint: disable=C0103
""" ORGANIZATIONS API VIEWS """
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
......@@ -29,7 +31,7 @@ class OrganizationsViewSet(viewsets.ModelViewSet):
if users:
for user in users:
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)
else:
user_id = request.DATA.get('id')
......
......@@ -105,6 +105,9 @@ class CustomPaginationSerializer(pagination.PaginationSerializer):
class SecureAPIView(APIView):
"""
View used for protecting access to specific workflows
"""
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 uuid
import unittest
from mock import patch
from datetime import datetime, timedelta
from freezegun import freeze_time
......@@ -10,7 +14,6 @@ from django.test import TestCase
from django.test.client import Client
from django.test.utils import override_settings
from django.utils.translation import ugettext as _
from django.conf import settings
from django.core.cache import cache
from student.tests.factories import UserFactory
......
# pylint: disable=W0612
"""
Tests for session api with advance security features
"""
......
......@@ -6,10 +6,8 @@ Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/tests/test_session_views.py]
"""
from random import randint
import unittest
import uuid
from django.conf import settings
from django.contrib.auth.models import User
from django.core.cache import cache
from django.test import TestCase, Client
......
......@@ -24,14 +24,35 @@ from student.models import (
)
AUDIT_LOG = logging.getLogger("audit")
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):
"""
POST creates a new system session, supported authentication modes:
1. Open edX username/password
"""
response_data = {}
# Add some rate limiting here by re-using the RateLimitMixin as a helper class
limiter = BadRequestRateLimiter()
......@@ -98,12 +119,28 @@ class SessionsList(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):
"""
GET retrieves an existing system session
"""
response_data = {}
base_uri = generate_base_uri(request)
engine = import_module(settings.SESSION_ENGINE)
......@@ -125,9 +162,6 @@ class SessionsDetail(SecureAPIView):
return Response(response_data, status=status.HTTP_404_NOT_FOUND)
def delete(self, request, session_id):
"""
DELETE flushes an existing system session from the system
"""
response_data = {}
engine = import_module(settings.SESSION_ENGINE)
session = engine.SessionStore(session_id)
......
......@@ -4,10 +4,8 @@
Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/tests/test_views.py]
"""
import unittest
import uuid
from django.conf import settings
from django.core.cache import cache
from django.test import TestCase, Client
from django.test.utils import override_settings
......@@ -77,4 +75,3 @@ class SystemApiTests(TestCase):
self.assertIsNotNone(response.data['description'])
self.assertGreater(len(response.data['description']), 0)
self.assertIsNotNone(response.data['resources'])
......@@ -12,6 +12,9 @@ class SystemDetail(SecureAPIView):
"""Manages system-level information about the Open edX API"""
def get(self, request):
"""
GET /api/system/
"""
base_uri = generate_base_uri(request)
response_data = {}
response_data['name'] = "Open edX System API"
......@@ -25,6 +28,9 @@ class ApiDetail(SecureAPIView):
"""Manages top-level information about the Open edX API"""
def get(self, request):
"""
GET /api/
"""
base_uri = generate_base_uri(request)
response_data = {}
response_data['name'] = "Open edX API"
......
......@@ -3,10 +3,8 @@ Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/tests/test_permissions.py]
"""
from random import randint
import unittest
import uuid
from django.conf import settings
from django.test import TestCase
from django.test.utils import override_settings
......@@ -78,4 +76,3 @@ class PermissionsTests(TestCase):
ip_address = kwargs.get('ip_address', {})
response = self.client.post(uri, headers=headers, data=data, **ip_address)
return response
# pylint: disable=C0103
"""
The URI scheme for resources is as follows:
Resource type: /api/{resource_type}
......
......@@ -153,7 +153,6 @@ class UserPasswordResetTest(TestCase):
)
self._assert_response(response, status=403, message=message)
@override_settings(ADVANCED_SECURITY_CONFIG={'MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS': 1})
def test_is_password_reset_too_frequent(self):
"""
......
# pylint: disable=E1101
# pylint: disable=E1103
"""
Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/tests/test_user_views.py]
"""
from datetime import datetime
from random import randint
import json
import unittest
import uuid
from urllib import urlencode
from mock import patch
from django.utils.translation import ugettext as _
from django.core.cache import cache
from django.test import TestCase, Client
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 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 import Location
TEST_API_KEY = str(uuid.uuid4())
......@@ -51,24 +57,38 @@ class UsersApiTests(TestCase):
self.test_last_name = str(uuid.uuid4())
self.test_city = str(uuid.uuid4())
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.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(
category="videosequence",
parent_location=self.course.location,
data=self.test_course_data,
due="2016-05-16T14:30:00Z",
due=datetime(2016, 5, 16, 14, 30),
display_name="View_Sequence"
)
self.test_project = Project.objects.create(
course_id=self.course.id,
content_id=self.course_content.id
course_id=unicode(self.course.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(
course_id=self.course.id + 'b2',
content_id=self.course_content.id + 'b2'
course_id=unicode(self.course2.id),
content_id=unicode(self.course2_content.scope_ids.usage_id)
)
self.user = UserFactory()
......@@ -245,7 +265,7 @@ class UsersApiTests(TestCase):
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,
'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)
self.assertEqual(response.status_code, 201)
......@@ -428,7 +448,6 @@ class UsersApiTests(TestCase):
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 = test_uri + '/' + str(response.data['id'])
response = self.do_get(test_uri)
test_uri = test_uri + '/groups'
......@@ -561,7 +580,6 @@ class UsersApiTests(TestCase):
self.assertEqual(response.status_code, 404)
def test_user_courses_list_post(self):
course = CourseFactory.create()
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password':
......@@ -569,12 +587,12 @@ class UsersApiTests(TestCase):
response = self.do_post(test_uri, data)
user_id = response.data['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)
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['id'], course.id)
self.assertEqual(response.data['id'], unicode(self.course.id))
self.assertTrue(response.data['is_active'])
def test_user_courses_list_post_undefined_user(self):
......@@ -582,7 +600,7 @@ class UsersApiTests(TestCase):
test_uri = '/api/users'
user_id = '234234'
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)
self.assertEqual(response.status_code, 404)
......@@ -594,12 +612,14 @@ class UsersApiTests(TestCase):
response = self.do_post(test_uri, data)
user_id = response.data['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)
self.assertEqual(response.status_code, 404)
def test_user_courses_list_get(self):
course = CourseFactory.create(display_name="TEST COURSE")
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password':
......@@ -607,16 +627,28 @@ class UsersApiTests(TestCase):
response = self.do_post(test_uri, data)
user_id = response.data['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)
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)
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]['id'], course.id)
self.assertEqual(response.data[0]['id'], unicode(course_with_out_date_values.id))
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):
test_uri = '/api/users/2134234/courses'
......@@ -644,6 +676,37 @@ class UsersApiTests(TestCase):
data=test_data,
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'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password':
......@@ -651,19 +714,47 @@ class UsersApiTests(TestCase):
response = self.do_post(test_uri, data)
user_id = response.data['id']
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)
test_uri = test_uri + '/' + str(course.id)
test_uri = test_uri + '/' + unicode(course.id)
self.assertEqual(response.status_code, 201)
position_data = {
'position': {
'parent_content_id': str(course.id),
'parent_content_id': unicode(course.id),
'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)
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):
course = CourseFactory.create()
......@@ -703,7 +794,7 @@ class UsersApiTests(TestCase):
response = self.do_post(test_uri, data)
user_id = response.data['id']
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)
test_uri = test_uri + '/' + str(course.id)
self.assertEqual(response.status_code, 201)
......@@ -715,7 +806,22 @@ class UsersApiTests(TestCase):
}
}
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):
course = CourseFactory.create()
......@@ -733,25 +839,33 @@ class UsersApiTests(TestCase):
response = self.do_post(test_uri, data)
user_id = response.data['id']
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)
test_uri = test_uri + '/' + str(course.id)
test_uri = test_uri + '/' + unicode(course.id)
self.assertEqual(response.status_code, 201)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
confirm_uri = self.test_server_prefix + test_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)
position_data = {
'position': {
'parent_content_id': str(course.location),
'child_content_id': str(chapter1.location)
'parent_content_id': unicode(course.id),
'child_content_id': unicode(chapter1.scope_ids.usage_id)
}
}
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):
test_uri = '/api/users/2134234/courses/a8df7/asv/d98'
......@@ -779,7 +893,7 @@ class UsersApiTests(TestCase):
response = self.do_post(test_uri, data)
user_id = response.data['id']
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)
self.assertEqual(response.status_code, 201)
test_uri = post_uri + '/' + str(course.id)
......@@ -808,25 +922,13 @@ class UsersApiTests(TestCase):
self.assertEqual(response.status_code, 204)
def test_user_courses_detail_delete_undefined_course(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 = '{}/{}/oasdf987sdf'.format(test_uri, str(user_id))
test_uri = '/api/users/{}/courses/{}'.format(str(self.user.id), self.test_bogus_course_id)
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):
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(
user_id, 'some/unknown/course')
self.user.id, 'slashes:some+unknown+course')
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
......@@ -837,12 +939,12 @@ class UsersApiTests(TestCase):
response = self.do_get(test_uri)
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')
response = self.do_get(test_uri)
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:
# 'pref-lang' = 'en'
user_id = self._create_test_user()
......@@ -852,12 +954,12 @@ class UsersApiTests(TestCase):
self.assertEqual(len(response.data), 1)
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')
response = self.do_post(test_uri, {"foo": "bar"})
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()
test_uri = '/api/users/{}/preferences'.format(user_id)
response = self.do_post(test_uri, {})
......@@ -872,7 +974,7 @@ class UsersApiTests(TestCase):
response = self.do_post(test_uri, {"a_boolean": False})
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()
test_uri = '/api/users/{}/preferences'.format(user_id)
response = self.do_post(test_uri, {"foo": "bar"})
......@@ -883,7 +985,7 @@ class UsersApiTests(TestCase):
self.assertEqual(response.data['pref-lang'], 'en')
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()
test_uri = '/api/users/{}/preferences'.format(user_id)
response = self.do_post(test_uri, {"foo": "bar"})
......@@ -896,13 +998,41 @@ class UsersApiTests(TestCase):
self.assertEqual(response.data['pref-lang'], 'en')
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):
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']
user_id = self.user.id
course = CourseFactory.create()
test_data = '<html>{}</html>'.format(str(uuid.uuid4()))
......@@ -931,8 +1061,35 @@ class UsersApiTests(TestCase):
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(
user_id, course.id)
user_id, unicode(course.id))
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
......@@ -948,7 +1105,7 @@ class UsersApiTests(TestCase):
self.assertEqual(sections[0]['graded'], False)
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]['graded'], False)
......@@ -958,6 +1115,9 @@ class UsersApiTests(TestCase):
self.assertGreater(len(grading_policy['GRADER']), 0)
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):
"""This function compare response with user profile data """
......@@ -1029,7 +1189,8 @@ class UsersApiTests(TestCase):
self.assertEqual(response.data['num_pages'], 2)
# 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.data['count'], 7)
self.assertEqual(len(response.data['results']), 7)
......@@ -1050,44 +1211,53 @@ class UsersApiTests(TestCase):
def test_user_completions_list(self):
user_id = self.user.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):
if i > 7:
for i in xrange(1, 26):
if i > 12:
course_user_id = another_user_id
else:
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)
self.assertEqual(response.status_code, 201)
# 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)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 7)
self.assertEqual(len(response.data['results']), 5)
self.assertEqual(response.data['count'], 12)
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]['course_id'], self.course.id)
self.assertEqual(response.data['results'][0]['course_id'], unicode(self.course.id))
self.assertEqual(response.data['num_pages'], 2)
# 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)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 9)
# Get course module completion by other user and course module id
completion_list_uri = '/api/users/{}/courses/{}/completions/?content_id={}'.format(
another_user_id,
self.course.id,
'{}_{}'.format(self.course_content.id, 10))
self.assertEqual(response.data['count'], 13)
# Get course module completion by other user and course module id (content_id)
content_id = {'content_id': unicode(local_content.scope_ids.usage_id)}
completion_list_uri = '/api/users/{}/courses/{}/completions/?{}'.format(
course_user_id,
unicode(self.course.id),
urlencode(content_id)
)
response = self.do_get(completion_list_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 1)
# 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)
self.assertEqual(response.status_code, 404)
......@@ -1130,3 +1300,13 @@ class UsersApiTests(TestCase):
self.assertEqual(len(response.data['results']), 1)
self.assertEqual(response.data['results'][0]['city'], 'New York City')
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
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'^(?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>[^/]+/[^/]+/[^/]+)/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>[^/]+/[^/]+/[^/]+)$', 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/(?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/(?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]+)/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)
""" API implementation for user-oriented interactions. """
import logging
from requests.exceptions import ConnectionError
from django.contrib.auth.models import Group
from django.core.exceptions import ObjectDoesNotExist
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.conf import settings
from django.http import Http404
......@@ -13,8 +14,8 @@ from django.utils.translation import get_language, ugettext_lazy as _
from rest_framework import status
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.models import GroupProfile, APIUser as User
from api_manager.organizations.serializers import OrganizationSerializer
......@@ -25,7 +26,11 @@ from .serializers import UserSerializer, UserCountByCitySerializer
from courseware import module_render
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 lang_pref import LANGUAGE_KEY
from student.models import CourseEnrollment, PasswordHistory, UserProfile
from openedx.core.djangoapps.user_api.models import UserPreference
......@@ -37,7 +42,6 @@ from util.password_policy_validators import (
from util.bad_request_rate_limiter import BadRequestRateLimiter
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.utils import CommentClientRequestError
......@@ -75,38 +79,23 @@ def _serialize_user(response_data, user):
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
Really no reason to generalize this out of user_courses_detail aside from pylint complaining
"""
field_data_cache = FieldDataCache([course_descriptor], course_id, user)
if course_id == position['parent_content_id']:
parent_content = get_module_for_descriptor(
user,
request,
course_descriptor,
field_data_cache,
course_id
)
parent_content_id = position['parent_content_id']
child_content_id = position['child_content_id']
if unicode(course_key) == parent_content_id:
parent_descriptor, parent_key, parent_content = get_course(request, user, parent_content_id) # pylint: disable=W0612
else:
parent_content = module_render.get_module(
user,
request,
position['parent_content_id'],
field_data_cache,
course_id
)
child_content = module_render.get_module(
user,
request,
position['child_content_id'],
field_data_cache,
course_id
)
parent_descriptor, parent_key, parent_content = get_course_child(request, user, course_key, parent_content_id) # pylint: disable=W0612
child_descriptor, child_key, child_content = get_course_child(request, user, course_key, child_content_id) # pylint: disable=W0612
if not child_descriptor:
return None
save_child_position(parent_content, child_content.location.name)
saved_content = get_current_child(parent_content)
return saved_content.id
return unicode(saved_content.scope_ids.usage_id)
class UsersList(SecureListAPIView):
......@@ -426,14 +415,14 @@ class UsersDetail(SecureAPIView):
err_msg = _(
"You are re-using a password that you have used recently. You must "
"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
if PasswordHistory.is_password_reset_too_soon(existing_user):
num_days = settings.ADVANCED_SECURITY_CONFIG['MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS']
err_msg = _(
"You are resetting passwords too frequently. Due to security policies, "
"{0} day(s) must elapse between password resets"
).format(num_days)
).format(num_days) # pylint: disable=E1101
if err_msg:
# We have an password reset attempt which violates some security policy,
......@@ -522,7 +511,7 @@ class UsersGroupsList(SecureAPIView):
except ObjectDoesNotExist:
return Response({}, status=status.HTTP_404_NOT_FOUND)
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['message'] = "Relationship already exists."
return Response(response_data, status=status.HTTP_409_CONFLICT)
......@@ -584,7 +573,7 @@ class UsersGroupsDetail(SecureAPIView):
response_data['uri'] = generate_base_uri(request)
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}
"""
......@@ -615,19 +604,20 @@ class UsersCoursesList(SecureAPIView):
"""
POST /api/users/{user_id}/courses/
"""
store = modulestore()
response_data = {}
user_id = user_id
course_id = request.DATA['course_id']
try:
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):
return Response({}, status=status.HTTP_404_NOT_FOUND)
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['id'] = course_id
response_data['id'] = unicode(course_key)
response_data['name'] = course_descriptor.display_name
response_data['is_active'] = course_enrollment.is_active
return Response(response_data, status=status.HTTP_201_CREATED)
......@@ -636,7 +626,6 @@ class UsersCoursesList(SecureAPIView):
"""
GET /api/users/{user_id}/courses/
"""
store = modulestore()
base_uri = generate_base_uri(request)
try:
user = User.objects.get(id=user_id)
......@@ -645,19 +634,21 @@ class UsersCoursesList(SecureAPIView):
enrollments = CourseEnrollment.enrollments_for_user(user=user)
response_data = []
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
# database, but the enrollment row in the SQL database still exists
if descriptor:
if course_descriptor:
course_data = {
"id": enrollment.course_id,
"uri": '{}/{}'.format(base_uri, enrollment.course_id),
"id": unicode(course_key),
"uri": '{}/{}'.format(base_uri, unicode(course_key)),
"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)
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)
......@@ -692,52 +683,77 @@ class UsersCoursesDetail(SecureAPIView):
"""
POST /api/users/{user_id}/courses/{course_id}
"""
store = modulestore()
base_uri = generate_base_uri(request)
response_data = {}
response_data['uri'] = base_uri
try:
user = User.objects.get(id=user_id)
course_descriptor = store.get_course(course_id)
except (ObjectDoesNotExist, ValueError):
return Response(response_data, status=status.HTTP_404_NOT_FOUND)
user = User.objects.get(id=user_id, is_active=True)
except ObjectDoesNotExist:
return Response({}, 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['course_id'] = course_id
if request.DATA['position']:
response_data['position'] = _save_content_position(
content_position = _save_content_position(
request,
user,
course_id,
course_descriptor,
course_key,
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)
def get(self, request, user_id, course_id):
"""
GET /api/users/{user_id}/courses/{course_id}
"""
store = modulestore()
response_data = {}
base_uri = generate_base_uri(request)
try:
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):
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)
response_data['user_id'] = user.id
response_data['course_id'] = course_id
response_data['uri'] = base_uri
field_data_cache = FieldDataCache([course_descriptor], course_id, user)
course_content = module_render.get_module(
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
course_key,
user,
course_descriptor,
depth=2)
course_module = module_render.get_module_for_descriptor(
user,
request,
course_descriptor.location,
course_descriptor,
field_data_cache,
course_id)
response_data['position'] = course_content.position
course_key)
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)
def delete(self, request, user_id, course_id):
......@@ -748,7 +764,10 @@ class UsersCoursesDetail(SecureAPIView):
user = User.objects.get(id=user_id, is_active=True)
except ObjectDoesNotExist:
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)
......@@ -767,18 +786,6 @@ class UsersCoursesGradesDetail(SecureAPIView):
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
# additional DB lookup (this kills the Progress page in particular).
try:
......@@ -786,14 +793,43 @@ class UsersCoursesGradesDetail(SecureAPIView):
except ObjectDoesNotExist:
return Response({}, status=status.HTTP_404_NOT_FOUND)
courseware_summary = grades.progress_summary(student, request, course)
grade_summary = grades.grade(student, request, course)
grading_policy = course.grading_policy
# @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
# 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 = {
'courseware_summary': courseware_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)
......@@ -841,16 +877,16 @@ class UsersPreferences(SecureAPIView):
try:
user = User.objects.get(id=user_id)
except ObjectDoesNotExist:
return Response({}, status.HTTP_404_NOT_FOUND)
return Response({}, status=status.HTTP_404_NOT_FOUND)
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
for key in request.DATA.keys():
value = request.DATA[key]
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
for key in request.DATA.keys():
......@@ -873,6 +909,44 @@ class UsersPreferences(SecureAPIView):
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):
"""
......@@ -960,8 +1034,7 @@ class UsersSocialMetrics(SecureListAPIView):
- 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:
user = User.objects.get(id=user_id)
except ObjectDoesNotExist:
......@@ -971,11 +1044,11 @@ class UsersSocialMetrics(SecureListAPIView):
comment_user.course_id = course_id
try:
data = comment_user.social_stats()
data = (comment_user.social_stats())[user_id]
http_status = status.HTTP_200_OK
except CommentClientRequestError, e:
except (CommentClientRequestError, ConnectionError), error:
data = {
"err_msg": str(e)
"err_msg": str(error)
}
http_status = status.HTTP_500_INTERNAL_SERVER_ERROR
......
""" API implementation for Secure api calls. """
import socket
import struct
......@@ -55,3 +54,5 @@ def is_int(value):
return True
except ValueError:
return False
......@@ -70,6 +70,8 @@ from xmodule.lti_module import LTIModule
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.x_module import XModuleDescriptor
from xmodule.mixin import wrap_with_license
from api_manager.models import CourseModuleCompletion
from util.json_request import JsonResponse
from util.sandboxing import can_execute_unsafe_code, get_python_lib_zip
from util import milestones_helpers
......@@ -484,10 +486,20 @@ def get_module_system_for_user(user, field_data_cache, # TODO # pylint: disabl
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):
"""A function that allows XModules to publish events."""
if event_type == 'grade':
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:
track_function(event_type, event)
......
......@@ -245,7 +245,7 @@ def save_child_position(seq_module, child_name):
"""
child_name: url_name of the child
"""
print child_name
for position, c in enumerate(seq_module.get_display_items(), start=1):
if c.location.name == child_name:
# Only save if position changed
......
......@@ -97,7 +97,6 @@ class Migration(SchemaMigration):
))
db.send_create_signal('projects', ['WorkgroupPeerReview'])
def backwards(self, orm):
# Removing unique constraint on 'Project', fields ['course_id', 'content_id']
db.delete_unique('projects_project', ['course_id', 'content_id'])
......@@ -126,7 +125,6 @@ class Migration(SchemaMigration):
# Deleting model 'WorkgroupPeerReview'
db.delete_table('projects_workgrouppeerreview')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
......@@ -226,4 +224,4 @@ class Migration(SchemaMigration):
}
}
complete_apps = ['projects']
\ No newline at end of file
complete_apps = ['projects']
......@@ -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']),
keep_default=False)
def backwards(self, orm):
# Deleting field 'Project.organization'
db.delete_column('projects_project', 'organization_id')
models = {
'api_manager.organization': {
'Meta': {'object_name': 'Organization'},
......@@ -132,4 +130,4 @@ class Migration(SchemaMigration):
}
}
complete_apps = ['projects']
\ No newline at end of file
complete_apps = ['projects']
......@@ -13,12 +13,10 @@ class Migration(SchemaMigration):
self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
keep_default=False)
def backwards(self, orm):
# Deleting field 'WorkgroupSubmission.document_filename'
db.delete_column('projects_workgroupsubmission', 'document_filename')
models = {
'api_manager.organization': {
'Meta': {'object_name': 'Organization'},
......@@ -134,4 +132,4 @@ class Migration(SchemaMigration):
}
}
complete_apps = ['projects']
\ No newline at end of file
complete_apps = ['projects']
# -*- 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
from django.db import models
from model_utils.models import TimeStampedModel
from student.models import AnonymousUserId
class Project(TimeStampedModel):
......@@ -14,8 +13,13 @@ class Project(TimeStampedModel):
"""
course_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"
, on_delete=models.SET_NULL)
organization = models.ForeignKey(
'api_manager.Organization',
blank=True,
null=True,
related_name="projects",
on_delete=models.SET_NULL
)
class Meta:
""" Meta class for defining additional model characteristics """
......@@ -45,6 +49,7 @@ class WorkgroupReview(TimeStampedModel):
reviewer = models.CharField(max_length=255) # AnonymousUserId
question = 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):
......@@ -72,6 +77,7 @@ class WorkgroupSubmissionReview(TimeStampedModel):
reviewer = models.CharField(max_length=255) # AnonymousUserId
question = 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):
......
......@@ -83,7 +83,7 @@ class WorkgroupReviewSerializer(serializers.HyperlinkedModelSerializer):
model = WorkgroupReview
fields = (
'id', 'url', 'created', 'modified', 'question', 'answer',
'workgroup', 'reviewer'
'workgroup', 'reviewer', 'content_id'
)
......@@ -96,7 +96,7 @@ class WorkgroupSubmissionReviewSerializer(serializers.HyperlinkedModelSerializer
model = WorkgroupSubmissionReview
fields = (
'id', 'url', 'created', 'modified', 'question', 'answer',
'submission', 'reviewer'
'submission', 'reviewer', 'content_id'
)
......
......@@ -9,11 +9,13 @@ import uuid
from django.contrib.auth.models import User
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 projects.models import Project, Workgroup
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())
......@@ -29,7 +31,7 @@ class SecureClient(Client):
@override_settings(EDX_API_KEY=TEST_API_KEY)
class PeerReviewsApiTests(TestCase):
class PeerReviewsApiTests(ModuleStoreTestCase):
""" Test suite for Users API views """
......@@ -40,7 +42,17 @@ class PeerReviewsApiTests(TestCase):
self.test_projects_uri = '/api/projects/'
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_course_content_id = "i4x://blah"
self.test_bogus_course_content_id = "14x://foo/bar/baz"
......@@ -59,7 +71,7 @@ class PeerReviewsApiTests(TestCase):
username="reviewer",
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(
course_id=self.test_course_id,
......
......@@ -9,11 +9,13 @@ import uuid
from django.contrib.auth.models import User
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 projects.models import Project, Workgroup, WorkgroupSubmission
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())
......@@ -29,7 +31,7 @@ class SecureClient(Client):
@override_settings(EDX_API_KEY=TEST_API_KEY)
class SubmissionReviewsApiTests(TestCase):
class SubmissionReviewsApiTests(ModuleStoreTestCase):
""" Test suite for Users API views """
......@@ -40,7 +42,17 @@ class SubmissionReviewsApiTests(TestCase):
self.test_projects_uri = '/api/projects/'
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_course_content_id = "i4x://blah"
self.test_bogus_course_content_id = "14x://foo/bar/baz"
......@@ -53,7 +65,7 @@ class SubmissionReviewsApiTests(TestCase):
username="testing",
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(
course_id=self.test_course_id,
......@@ -113,6 +125,7 @@ class SubmissionReviewsApiTests(TestCase):
'reviewer': self.anonymous_user_id,
'question': self.test_question,
'answer': self.test_answer,
'content_id': self.test_course_content_id,
}
response = self.do_post(self.test_submission_reviews_uri, data)
self.assertEqual(response.status_code, 201)
......@@ -128,6 +141,7 @@ class SubmissionReviewsApiTests(TestCase):
self.assertEqual(response.data['submission'], self.test_submission.id)
self.assertEqual(response.data['question'], self.test_question)
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['modified'])
......@@ -137,6 +151,7 @@ class SubmissionReviewsApiTests(TestCase):
'reviewer': self.anonymous_user_id,
'question': self.test_question,
'answer': self.test_answer,
'content_id': self.test_course_content_id,
}
response = self.do_post(self.test_submission_reviews_uri, data)
self.assertEqual(response.status_code, 201)
......@@ -154,6 +169,7 @@ class SubmissionReviewsApiTests(TestCase):
self.assertEqual(response.data['submission'], self.test_submission.id)
self.assertEqual(response.data['question'], self.test_question)
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['modified'])
......
......@@ -14,6 +14,8 @@ from django.test.utils import override_settings
from projects.models import Project, Workgroup, WorkgroupSubmission
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())
......@@ -29,18 +31,29 @@ class SecureClient(Client):
@override_settings(EDX_API_KEY=TEST_API_KEY)
class WorkgroupReviewsApiTests(TestCase):
class WorkgroupReviewsApiTests(ModuleStoreTestCase):
""" Test suite for Users API views """
def setUp(self):
super(WorkgroupReviewsApiTests, self).setUp()
self.test_server_prefix = 'https://testserver'
self.test_users_uri = '/api/users/'
self.test_workgroups_uri = '/api/workgroups/'
self.test_projects_uri = '/api/projects/'
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_course_content_id = "i4x://blah"
self.test_bogus_course_content_id = "14x://foo/bar/baz"
......@@ -53,7 +66,7 @@ class WorkgroupReviewsApiTests(TestCase):
username="testing",
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(
course_id=self.test_course_id,
content_id=self.test_course_content_id,
......@@ -112,6 +125,7 @@ class WorkgroupReviewsApiTests(TestCase):
'reviewer': self.anonymous_user_id,
'question': self.test_question,
'answer': self.test_answer,
'content_id': self.test_course_content_id,
}
response = self.do_post(self.test_workgroup_reviews_uri, data)
self.assertEqual(response.status_code, 201)
......@@ -127,6 +141,7 @@ class WorkgroupReviewsApiTests(TestCase):
self.assertEqual(response.data['workgroup'], self.test_workgroup.id)
self.assertEqual(response.data['question'], self.test_question)
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['modified'])
......@@ -136,6 +151,7 @@ class WorkgroupReviewsApiTests(TestCase):
'reviewer': self.anonymous_user_id,
'question': self.test_question,
'answer': self.test_answer,
'content_id': self.test_course_content_id,
}
response = self.do_post(self.test_workgroup_reviews_uri, data)
self.assertEqual(response.status_code, 201)
......@@ -153,6 +169,7 @@ class WorkgroupReviewsApiTests(TestCase):
self.assertEqual(response.data['workgroup'], self.test_workgroup.id)
self.assertEqual(response.data['question'], self.test_question)
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['modified'])
......
......@@ -4,6 +4,7 @@
Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/projects/tests/test_workgroups.py]
"""
from datetime import datetime
import json
import uuid
......@@ -45,14 +46,14 @@ class WorkgroupsApiTests(ModuleStoreTestCase):
self.test_server_prefix = 'https://testserver'
self.test_workgroups_uri = '/api/workgroups/'
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_bogus_group_id = "2131241123"
self.test_workgroup_name = str(uuid.uuid4())
self.test_course = CourseFactory.create(
start="2014-06-16T14:30:00Z",
end="2015-01-16T14:30:00Z"
start=datetime(2014, 6, 16, 14, 30),
end=datetime(2015, 1, 16, 14, 30)
)
self.test_data = '<html>{}</html>'.format(str(uuid.uuid4()))
......@@ -60,12 +61,12 @@ class WorkgroupsApiTests(ModuleStoreTestCase):
category="group_project",
parent_location=self.test_course.location,
data=self.test_data,
due="2014-05-16T14:30:00Z",
due=datetime(2014, 5, 16, 14, 30),
display_name="Group Project"
)
self.test_course_id = self.test_course.id
self.test_course_content_id = self.test_group_project.id
self.test_course_id = unicode(self.test_course.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 = Group.objects.create(
......@@ -137,7 +138,6 @@ class WorkgroupsApiTests(ModuleStoreTestCase):
response = self.client.delete_with_data(uri, data, headers=headers)
return response
def test_workgroups_list_post(self):
data = {
'name': self.test_workgroup_name,
......@@ -403,6 +403,68 @@ class WorkgroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.status_code, 200)
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):
data = {
'name': self.test_workgroup_name,
......
......@@ -13,9 +13,15 @@ from rest_framework.response import Response
from xblock.fields import Scope
from xblock.runtime import KeyValueStore
from courseware import module_render
from courseware.courses import get_course
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 WorkgroupReview, WorkgroupSubmissionReview, WorkgroupPeerReview
......@@ -24,6 +30,64 @@ from .serializers import ProjectSerializer, WorkgroupSerializer, WorkgroupSubmis
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):
"""
Django Rest Framework ViewSet for the Group model (auth_group).
......@@ -58,7 +122,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
if groups:
for group in groups:
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)
else:
group_id = request.DATA.get('id')
......@@ -84,7 +148,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
if users:
for user in users:
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)
elif request.method == 'POST':
user_id = request.DATA.get('id')
......@@ -118,7 +182,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
if peer_reviews:
for peer_review in peer_reviews:
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)
@link()
......@@ -131,7 +195,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
if workgroup_reviews:
for workgroup_review in workgroup_reviews:
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)
@link()
......@@ -144,7 +208,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
if submissions:
for submission in submissions:
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)
@action()
......@@ -156,16 +220,16 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
course_id = request.DATA.get('course_id')
if course_id is None:
return Response({}, status=status.HTTP_400_BAD_REQUEST)
try:
course_descriptor = get_course(course_id)
except ValueError:
course_descriptor = None
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_400_BAD_REQUEST)
content_id = request.DATA.get('content_id')
if content_id is None:
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')
if grade is None:
......@@ -182,10 +246,10 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
key = KeyValueStore.Key(
scope=Scope.user_state,
user_id=user.id,
block_scope_id=Location(content_id),
block_scope_id=content_key,
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.grade = grade
student_module.max_grade = max_grade
......@@ -211,7 +275,7 @@ class ProjectsViewSet(viewsets.ModelViewSet):
if workgroups:
for workgroup in workgroups:
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)
else:
workgroup_id = request.DATA.get('id')
......
......@@ -421,6 +421,9 @@ FEATURES = {
# The block types to disable need to be specified in "x block disable config" in django admin.
'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
......
......@@ -100,6 +100,9 @@ CC_PROCESSOR = {
FEATURES['API'] = True
########################## USER API ########################
EDX_API_KEY = None
########################### External REST APIs #################################
FEATURES['ENABLE_OAUTH2_PROVIDER'] = True
......
......@@ -161,6 +161,17 @@ class User(models.Model):
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):
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):
def _url_for_user_social_stats(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):
"""
Add a user's tags to the tracking event context.
"""
match = COURSE_REGEX.match(request.build_absolute_uri())
course_id = None
match = COURSE_REGEX.match(request.path)
course_key = None
if match:
course_id = match.group('course_id')
course_key = match.group('course_id')
try:
course_key = CourseKey.from_string(course_id)
course_key = CourseKey.from_string(course_key)
except InvalidKeyError:
course_id = None
course_key = None
context = {}
if course_id:
context['course_id'] = course_id
if course_key:
try:
context['course_id'] = course_key.to_deprecated_string()
except AttributeError:
context['course_id'] = unicode(course_key)
if request.user.is_authenticated():
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