Commit bf5bf27b by Matt Drayer Committed by Jonathan Piacenti

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

Also contains:

* mattdrayer/rebase-20140722b: cherry-pick 0325694
* mattdrayer/rebase-20140722: cherry-pick e8b1217
* Sessions API Documentation
* mattdrayer/rebase-20140722: cherry-pick ofao48d
* mattdrayer/rebase-20140722: cherry-pick b0c343c
* mattdrayer/rebase-20140722: cherry-pick d56e973
* mattdrayer/status-500-fix: Small API bug
* added content_id to workgroup review submissions
* mattdrayer/api-user-preferences-delete: Added new Detail view, GET/DELETE operations
* mattdrayer/rebase-20140722: cherry-pick 0624385
* handle exception if user never accessed any course module
* mattdrayer/rebase-20140722: cherry-pick caada51
* cdodge/add-progress-publish-api-endpoint: expose a new xblock runtime publish special cased event type 'progress' which adds an entry into the CourseCompletions table
* add default setting for feature flag in common.py
* mattdrayer/rebase-20140722: cherry-pick 7a1e12c
* mattdrayer/rebase-20140722: Functional stabilization
parent df7e1ab9
...@@ -7,6 +7,7 @@ import warnings ...@@ -7,6 +7,7 @@ import warnings
from django.db import models from django.db import models
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from opaque_keys.edx.keys import CourseKey, UsageKey, BlockTypeKey from opaque_keys.edx.keys import CourseKey, UsageKey, BlockTypeKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from south.modelsinspector import add_introspection_rules from south.modelsinspector import add_introspection_rules
...@@ -104,7 +105,7 @@ class OpaqueKeyField(models.CharField): ...@@ -104,7 +105,7 @@ class OpaqueKeyField(models.CharField):
return None return None
if isinstance(value, basestring): if isinstance(value, basestring):
return self.KEY_CLASS.from_string(value) return SlashSeparatedCourseKey.from_deprecated_string(value)
else: else:
return value return value
...@@ -148,7 +149,6 @@ class CourseKeyField(OpaqueKeyField): ...@@ -148,7 +149,6 @@ class CourseKeyField(OpaqueKeyField):
description = "A CourseKey object, saved to the DB in the form of a string" description = "A CourseKey object, saved to the DB in the form of a string"
KEY_CLASS = CourseKey KEY_CLASS = CourseKey
class UsageKeyField(OpaqueKeyField): class UsageKeyField(OpaqueKeyField):
""" """
A django Field that stores a UsageKey object as a string. A django Field that stores a UsageKey object as a string.
......
...@@ -47,5 +47,9 @@ Sessions ...@@ -47,5 +47,9 @@ Sessions
* - Goal * - Goal
- Resource - Resource
* - * - :ref:`Create a Session`
- - POST {"username": "name", "password": "password"}
\ No newline at end of file * - :ref:`Get Session Details`
- GET /api/sessions/{session_id}
* - :ref:`Delete a Session`
- DELETE /api/sessions/{session_id}
\ No newline at end of file
...@@ -4,6 +4,96 @@ Sessions API Module ...@@ -4,6 +4,96 @@ Sessions API Module
.. module:: api_manager .. module:: api_manager
The page contains docstrings for: The page contains docstrings and example responses for:
* * `Create a Session`_
\ No newline at end of file * `Get Session Details`_
* `Delete a Session`_
.. _Create a Session:
**************************
Create a Session
**************************
.. autoclass:: sessions.views.SessionsList
:members:
**Example post**
.. code-block:: json
{
"username": "name",
"password": "password"
}
**Example response**
.. code-block:: json
HTTP 201 CREATED
Vary: Accept
Content-Type: text/html; charset=utf-8
Allow: POST, OPTIONS
{
"token": "938680977d04ed091b67b974b6b6be60",
"expires": 604800,
"user": {
"id": 4,
"email": "staff@example.com",
"username": "staff",
"first_name": "",
"last_name": "",
"created": "2014-04-18T13:44:25Z",
"organizations": []
},
"uri": "http://localhost:8000/api/sessions?username=staff&password=edx/938680977d04ed091b67b974b6b6be60"
}
.. _Get Session Details:
**************************
Get Session Details
**************************
.. autoclass:: sessions.views.SessionsDetail
:members:
**Example GET response**
.. code-block:: json
HTTP 200 OK
Vary: Accept
Content-Type: text/html; charset=utf-8
Allow: GET, DELETE, HEAD, OPTIONS
{
"token": "8c510db85585c64bd33bede01d645a60",
"expires": 1209600,
"user_id": 1,
"uri": "http://localhost:8000/api/sessions//8c510db85585c64bd33bede01d645a60"
}
.. _Delete a Session:
**************************
Delete a Session
**************************
.. autoclass:: sessions.views.SessionsDetail
:members:
**Example DELETE response**
.. code-block:: json
HTTP 204 NO CONTENT
Vary: Accept
Content-Type: text/html; charset=utf-8
Allow: GET, DELETE, HEAD, OPTIONS
{}
\ No newline at end of file
...@@ -11,7 +11,7 @@ class CourseModuleCompletionSerializer(serializers.ModelSerializer): ...@@ -11,7 +11,7 @@ class CourseModuleCompletionSerializer(serializers.ModelSerializer):
class Meta: class Meta:
""" Serializer/field specification """ """ Serializer/field specification """
model = CourseModuleCompletion model = CourseModuleCompletion
fields = ('id', 'user_id', 'course_id', 'content_id', 'created', 'modified') fields = ('id', 'user_id', 'course_id', 'content_id', 'stage', 'created', 'modified')
read_only = ('id', 'created') read_only = ('id', 'created')
......
...@@ -10,28 +10,31 @@ from api_manager.courses import views as courses_views ...@@ -10,28 +10,31 @@ from api_manager.courses import views as courses_views
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/content/(?P<content_id>[a-zA-Z0-9_+\/:]+)/groups/(?P<group_id>[0-9]+)$', courses_views.CourseContentGroupsDetail.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/content/(?P<content_id>[a-zA-Z0-9_+\/:]+)/groups/*$', courses_views.CourseContentGroupsList.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/content/(?P<content_id>[a-zA-Z0-9_+\/:]+)/children/*$', courses_views.CourseContentList.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/content/(?P<content_id>[a-zA-Z0-9_+\/:]+)/users/*$', courses_views.CourseContentUsersList.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/content/(?P<content_id>[a-zA-Z0-9_+\/:]+)$', courses_views.CourseContentDetail.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/content/*$', courses_views.CourseContentList.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/grades/*$', courses_views.CoursesGradesList.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/groups/(?P<group_id>[0-9]+)$', courses_views.CoursesGroupsDetail.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/groups/*$', courses_views.CoursesGroupsList.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/overview/*$', courses_views.CoursesOverview.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/updates/*$', courses_views.CoursesUpdates.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/static_tabs/(?P<tab_id>[a-zA-Z0-9_+\/:]+)$', courses_views.CoursesStaticTabsDetail.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/static_tabs/*$', courses_views.CoursesStaticTabsList.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/users/(?P<user_id>[0-9]+)$', courses_views.CoursesUsersDetail.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/users/*$', courses_views.CoursesUsersList.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/completions/*$', courses_views.CourseModuleCompletionList.as_view(), name='completion-list'),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/projects/*$', courses_views.CoursesProjectList.as_view(), name='courseproject-list'),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/metrics/*$', courses_views.CourseMetrics.as_view(), name='course-metrics'),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/metrics/proficiency/leaders/*$', courses_views.CoursesLeadersList.as_view(), name='course-metrics-proficiency-leaders'),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/metrics/completions/leaders/*$', courses_views.CoursesCompletionsLeadersList.as_view(), name='course-metrics-completions-leaders'),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/workgroups/*$', courses_views.CoursesWorkgroupsList.as_view()),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/metrics/social/$', courses_views.CoursesSocialMetrics.as_view(), name='courses-social-metrics'),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)/metrics/cities/$', courses_views.CoursesCitiesMetrics.as_view(), name='courses-cities-metrics'),
url(r'^(?P<course_id>[a-zA-Z0-9_+\/:]+)$', courses_views.CoursesDetail.as_view()),
url(r'/*$^', courses_views.CoursesList.as_view()), url(r'/*$^', courses_views.CoursesList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)$', courses_views.CoursesDetail.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/content/(?P<content_id>[a-zA-Z0-9/_:]+)/groups/(?P<group_id>[0-9]+)$', courses_views.CourseContentGroupsDetail.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/content/(?P<content_id>[a-zA-Z0-9/_:]+)/groups/*$', courses_views.CourseContentGroupsList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/content/(?P<content_id>[a-zA-Z0-9/_:]+)/children/*$', courses_views.CourseContentList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/content/(?P<content_id>[a-zA-Z0-9/_:]+)/users/*$', courses_views.CourseContentUsersList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/content/(?P<content_id>[a-zA-Z0-9/_:]+)$', courses_views.CourseContentDetail.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/content/*$', courses_views.CourseContentList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/grades/*$', courses_views.CoursesGradesList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/groups/(?P<group_id>[0-9]+)$', courses_views.CoursesGroupsDetail.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/groups/*$', courses_views.CoursesGroupsList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/overview/*$', courses_views.CoursesOverview.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/updates/*$', courses_views.CoursesUpdates.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/static_tabs/(?P<tab_id>[a-zA-Z0-9/_:]+)$', courses_views.CoursesStaticTabsDetail.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/static_tabs/*$', courses_views.CoursesStaticTabsList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/users/(?P<user_id>[0-9]+)$', courses_views.CoursesUsersDetail.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/users/*$', courses_views.CoursesUsersList.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/completions/*$', courses_views.CourseModuleCompletionList.as_view(), name='completion-list'),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/projects/*$', courses_views.CoursesProjectList.as_view(), name='courseproject-list'),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/metrics/*$', courses_views.CourseMetrics.as_view(), name='course-metrics'),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/metrics/proficiency/leaders/*$', courses_views.CoursesLeadersList.as_view(), name='course-metrics-proficiency-leaders'),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/metrics/completions/leaders/*$', courses_views.CoursesCompletionsLeadersList.as_view(), name='course-metrics-completions-leaders'),
) )
urlpatterns = format_suffix_patterns(urlpatterns) urlpatterns = format_suffix_patterns(urlpatterns)
""" Centralized access to LMS courseware app """
from courseware import courses, module_render
from courseware.model_data import FieldDataCache
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
from xmodule.modulestore import InvalidLocationError
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
def get_course(request, user, course_id, depth=0):
"""
Utility method to obtain course components
"""
course_descriptor = None
course_key = None
course_content = None
try:
course_key = CourseKey.from_string(course_id)
except InvalidKeyError:
try:
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
except InvalidKeyError:
pass
if course_key:
try:
course_descriptor = courses.get_course(course_key, depth)
except ValueError:
pass
if course_descriptor:
field_data_cache = FieldDataCache([course_descriptor], course_key, user)
course_content = module_render.get_module(
user,
request,
course_descriptor.location,
field_data_cache,
course_key)
return course_descriptor, course_key, course_content
def get_course_child(request, user, course_key, content_id):
"""
Return a course xmodule/xblock to the caller
"""
content_descriptor = None
content_key = None
content = None
try:
content_key = UsageKey.from_string(content_id)
except InvalidKeyError:
try:
content_key = Location.from_deprecated_string(content_id)
except (InvalidLocationError, InvalidKeyError):
pass
if content_key:
try:
content_descriptor = modulestore().get_item(content_key)
except ItemNotFoundError:
pass
if content_descriptor:
field_data_cache = FieldDataCache([content_descriptor], course_key, user)
content = module_render.get_module(
user,
request,
content_key,
field_data_cache,
course_key)
return content_descriptor, content_key, content
def get_course_total_score(course_summary):
"""
Traverse course summary to calculate max possible score for a course
"""
score = 0
for chapter in course_summary: # accumulate score of each chapter
for section in chapter['sections']:
if section['section_total']:
score += section['section_total'][1]
return score
...@@ -4,9 +4,11 @@ ...@@ -4,9 +4,11 @@
Run these tests @ Devstack: Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/tests/test_group_views.py] rake fasttest_lms[common/djangoapps/api_manager/tests/test_group_views.py]
""" """
from datetime import datetime
from random import randint from random import randint
import uuid import uuid
import json import json
from urllib import urlencode
from django.core.cache import cache from django.core.cache import cache
from django.test import Client from django.test import Client
...@@ -45,26 +47,26 @@ class GroupsApiTests(ModuleStoreTestCase): ...@@ -45,26 +47,26 @@ class GroupsApiTests(ModuleStoreTestCase):
self.test_course_data = '<html>{}</html>'.format(str(uuid.uuid4())) self.test_course_data = '<html>{}</html>'.format(str(uuid.uuid4()))
self.course = CourseFactory.create() self.course = CourseFactory.create()
self.test_course_id = self.course.id self.test_course_id = unicode(self.course.id)
self.course_content = ItemFactory.create( self.course_content = ItemFactory.create(
category="videosequence", category="videosequence",
parent_location=self.course.location, parent_location=self.course.location,
data=self.test_course_data, data=self.test_course_data,
due="2016-05-16T14:30:00Z", due=datetime(2016, 5, 16, 14, 30),
display_name="View_Sequence" display_name="View_Sequence"
) )
self.test_organization = Organization.objects.create( self.test_organization = Organization.objects.create(
name="Test Organization", name="Test Organization",
display_name = 'Test Org', display_name='Test Org',
contact_name = 'John Org', contact_name='John Org',
contact_email = 'john@test.org', contact_email='john@test.org',
contact_phone = '+1 332 232 24234' contact_phone='+1 332 232 24234'
) )
self.test_project = Project.objects.create( self.test_project = Project.objects.create(
course_id=self.course.id, course_id=unicode(self.course.id),
content_id=self.course_content.id content_id=unicode(self.course_content.scope_ids.usage_id)
) )
self.client = SecureClient() self.client = SecureClient()
...@@ -820,7 +822,7 @@ class GroupsApiTests(ModuleStoreTestCase): ...@@ -820,7 +822,7 @@ class GroupsApiTests(ModuleStoreTestCase):
data = {'course_id': self.test_course_id} data = {'course_id': self.test_course_id}
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
confirm_uri = test_uri + '/' + self.course.id confirm_uri = test_uri + '/' + unicode(self.course.id)
self.assertEqual(response.data['uri'], confirm_uri) self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['group_id'], str(group_id)) self.assertEqual(response.data['group_id'], str(group_id))
self.assertEqual(response.data['course_id'], self.test_course_id) self.assertEqual(response.data['course_id'], self.test_course_id)
...@@ -847,7 +849,13 @@ class GroupsApiTests(ModuleStoreTestCase): ...@@ -847,7 +849,13 @@ class GroupsApiTests(ModuleStoreTestCase):
response = self.do_post(self.base_groups_uri, data) response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
test_uri = response.data['uri'] + '/courses' test_uri = response.data['uri'] + '/courses'
data = {'course_id': "987/23/896"} data = {'course_id': "slashes:invalid+course+id"}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 404)
data = {'course_id': "invalid/course/id"}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 404)
data = {'course_id': "really-invalid-course-id"}
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
...@@ -860,7 +868,7 @@ class GroupsApiTests(ModuleStoreTestCase): ...@@ -860,7 +868,7 @@ class GroupsApiTests(ModuleStoreTestCase):
data = {'course_id': self.test_course_id} data = {'course_id': self.test_course_id}
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
confirm_uri = test_uri + '/' + self.course.id confirm_uri = test_uri + '/' + unicode(self.course.id)
self.assertEqual(response.data['uri'], confirm_uri) self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['group_id'], str(group_id)) self.assertEqual(response.data['group_id'], str(group_id))
self.assertEqual(response.data['course_id'], self.test_course_id) self.assertEqual(response.data['course_id'], self.test_course_id)
...@@ -885,6 +893,7 @@ class GroupsApiTests(ModuleStoreTestCase): ...@@ -885,6 +893,7 @@ class GroupsApiTests(ModuleStoreTestCase):
response = self.do_post(test_uri, data) response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
test_uri = '{}/{}/courses/{}'.format(self.base_groups_uri, group_id, self.test_course_id) test_uri = '{}/{}/courses/{}'.format(self.base_groups_uri, group_id, self.test_course_id)
print test_uri
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
confirm_uri = '{}{}/{}/courses/{}'.format( confirm_uri = '{}{}/{}/courses/{}'.format(
...@@ -930,7 +939,7 @@ class GroupsApiTests(ModuleStoreTestCase): ...@@ -930,7 +939,7 @@ class GroupsApiTests(ModuleStoreTestCase):
data = {'name': self.test_group_name, 'type': 'test'} data = {'name': self.test_group_name, 'type': 'test'}
response = self.do_post(self.base_groups_uri, data) response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
test_uri = '{}/courses/{}'.format(response.data['uri'], self.course.id) test_uri = '{}/courses/{}'.format(response.data['uri'], unicode(self.course.id))
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
...@@ -941,14 +950,13 @@ class GroupsApiTests(ModuleStoreTestCase): ...@@ -941,14 +950,13 @@ class GroupsApiTests(ModuleStoreTestCase):
group_id = response.data['id'] group_id = response.data['id']
self.test_organization.groups.add(group_id) self.test_organization.groups.add(group_id)
test_uri = response.data['uri'] + '/organizations/' test_uri = response.data['uri'] + '/organizations/'
confirm_uri = test_uri + '/' + str(self.test_organization.id)
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 1) self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]['id'], self.test_organization.id) self.assertEqual(response.data[0]['id'], self.test_organization.id)
self.assertEqual(response.data[0]['name'], self.test_organization.name) self.assertEqual(response.data[0]['name'], self.test_organization.name)
def test_group_courses_list_get_invalid_group(self): def test_group_organizations_list_get_invalid_group(self):
test_uri = self.base_groups_uri + '/1231241/organizations/' test_uri = self.base_groups_uri + '/1231241/organizations/'
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
...@@ -981,7 +989,8 @@ class GroupsApiTests(ModuleStoreTestCase): ...@@ -981,7 +989,8 @@ class GroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.data['num_pages'], 2) self.assertEqual(response.data['num_pages'], 2)
# test with course_id filter # test with course_id filter
response = self.do_get('/api/groups/{}/workgroups/?course_id={}'.format(group_id, self.course.id)) course_id = {'course_id': unicode(self.course.id)}
response = self.do_get('/api/groups/{}/workgroups/?{}'.format(group_id, urlencode(course_id)))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 11) self.assertEqual(response.data['count'], 11)
self.assertIsNotNone(response.data['results'][0]['name']) self.assertIsNotNone(response.data['results'][0]['name'])
......
...@@ -8,15 +8,15 @@ from api_manager.groups import views as groups_views ...@@ -8,15 +8,15 @@ from api_manager.groups import views as groups_views
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url(r'/*$^', groups_views.GroupsList.as_view()), url(r'/*$^', groups_views.GroupsList.as_view()),
url(r'^(?P<group_id>[0-9]+)$', groups_views.GroupsDetail.as_view()), url(r'^(?P<group_id>[0-9]+)/courses/(?P<course_id>[a-zA-Z0-9_+\/:]+)$', groups_views.GroupsCoursesDetail.as_view()),
url(r'^(?P<group_id>[0-9]+)/courses/*$', groups_views.GroupsCoursesList.as_view()), url(r'^(?P<group_id>[0-9]+)/courses/*$', groups_views.GroupsCoursesList.as_view()),
url(r'^(?P<group_id>[0-9]+)/courses/(?P<course_id>[a-zA-Z0-9/_:]+)$', groups_views.GroupsCoursesDetail.as_view()),
url(r'^(?P<group_id>[0-9]+)/organizations/*$', groups_views.GroupsOrganizationsList.as_view()), url(r'^(?P<group_id>[0-9]+)/organizations/*$', groups_views.GroupsOrganizationsList.as_view()),
url(r'^(?P<group_id>[0-9]+)/workgroups/*$', groups_views.GroupsWorkgroupsList.as_view()), url(r'^(?P<group_id>[0-9]+)/workgroups/*$', groups_views.GroupsWorkgroupsList.as_view()),
url(r'^(?P<group_id>[0-9]+)/users/*$', groups_views.GroupsUsersList.as_view()), url(r'^(?P<group_id>[0-9]+)/users/*$', groups_views.GroupsUsersList.as_view()),
url(r'^(?P<group_id>[0-9]+)/users/(?P<user_id>[0-9]+)$', groups_views.GroupsUsersDetail.as_view()), url(r'^(?P<group_id>[0-9]+)/users/(?P<user_id>[0-9]+)$', groups_views.GroupsUsersDetail.as_view()),
url(r'^(?P<group_id>[0-9]+)/groups/*$', groups_views.GroupsGroupsList.as_view()), url(r'^(?P<group_id>[0-9]+)/groups/*$', groups_views.GroupsGroupsList.as_view()),
url(r'^(?P<group_id>[0-9]+)/groups/(?P<related_group_id>[0-9]+)$', groups_views.GroupsGroupsDetail.as_view()), url(r'^(?P<group_id>[0-9]+)/groups/(?P<related_group_id>[0-9]+)$', groups_views.GroupsGroupsDetail.as_view()),
url(r'^(?P<group_id>[0-9]+)$', groups_views.GroupsDetail.as_view()),
) )
urlpatterns = format_suffix_patterns(urlpatterns) urlpatterns = format_suffix_patterns(urlpatterns)
""" API implementation for group-oriented interactions. """ """ API implementation for group-oriented interactions. """
import uuid import uuid
import json import json
from collections import OrderedDict
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.http import Http404 from django.http import Http404
from django.utils import timezone
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from api_manager.courseware_access import get_course
from api_manager.models import GroupRelationship, CourseGroupRelationship, GroupProfile, APIUser as User from api_manager.models import GroupRelationship, CourseGroupRelationship, GroupProfile, APIUser as User
from api_manager.permissions import SecureAPIView, SecureListAPIView from api_manager.permissions import SecureAPIView, SecureListAPIView
from api_manager.utils import str2bool, generate_base_uri from api_manager.utils import str2bool, generate_base_uri
from api_manager.organizations import serializers from api_manager.organizations import serializers
from projects.serializers import BasicWorkgroupSerializer from projects.serializers import BasicWorkgroupSerializer
from xmodule.modulestore import Location, InvalidLocationError
from xmodule.modulestore.django import modulestore
RELATIONSHIP_TYPES = {'hierarchical': 'h', 'graph': 'g'} RELATIONSHIP_TYPES = {'hierarchical': 'h', 'graph': 'g'}
...@@ -317,7 +314,7 @@ class GroupsUsersDetail(SecureAPIView): ...@@ -317,7 +314,7 @@ class GroupsUsersDetail(SecureAPIView):
response_status = status.HTTP_404_NOT_FOUND response_status = status.HTTP_404_NOT_FOUND
return Response(response_data, status=response_status) return Response(response_data, status=response_status)
def delete(self, request, group_id, user_id): def delete(self, request, group_id, user_id): # pylint: disable=W0612,W0613
""" """
DELETE removes/inactivates/etc. an existing group-user relationship DELETE removes/inactivates/etc. an existing group-user relationship
""" """
...@@ -470,7 +467,7 @@ class GroupsGroupsDetail(SecureAPIView): ...@@ -470,7 +467,7 @@ class GroupsGroupsDetail(SecureAPIView):
response_status = status.HTTP_200_OK response_status = status.HTTP_200_OK
return Response(response_data, response_status) return Response(response_data, response_status)
def delete(self, request, group_id, related_group_id): def delete(self, request, group_id, related_group_id): # pylint: disable=W0613
""" """
DELETE /api/groups/{group_id}/groups/{related_group_id} DELETE /api/groups/{group_id}/groups/{related_group_id}
""" """
...@@ -481,7 +478,6 @@ class GroupsGroupsDetail(SecureAPIView): ...@@ -481,7 +478,6 @@ class GroupsGroupsDetail(SecureAPIView):
try: try:
to_group_relationship = GroupRelationship.objects.get(group__id=related_group_id) to_group_relationship = GroupRelationship.objects.get(group__id=related_group_id)
except ObjectDoesNotExist: except ObjectDoesNotExist:
to_group = None
to_group_relationship = None to_group_relationship = None
if from_group_relationship: if from_group_relationship:
if to_group_relationship: if to_group_relationship:
...@@ -524,13 +520,12 @@ class GroupsCoursesList(SecureAPIView): ...@@ -524,13 +520,12 @@ class GroupsCoursesList(SecureAPIView):
existing_group = Group.objects.get(id=group_id) existing_group = Group.objects.get(id=group_id)
except ObjectDoesNotExist: except ObjectDoesNotExist:
return Response({}, status.HTTP_404_NOT_FOUND) return Response({}, status.HTTP_404_NOT_FOUND)
store = modulestore()
course_id = request.DATA['course_id'] course_id = request.DATA['course_id']
base_uri = generate_base_uri(request) base_uri = generate_base_uri(request)
response_data['uri'] = '{}/{}'.format(base_uri, course_id) response_data['uri'] = '{}/{}'.format(base_uri, course_id)
existing_course = store.get_course(course_id) existing_course, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
if not existing_course: if not existing_course:
return Response({}, status.HTTP_404_NOT_FOUND) return Response({}, status.HTTP_404_NOT_FOUND)
...@@ -558,11 +553,10 @@ class GroupsCoursesList(SecureAPIView): ...@@ -558,11 +553,10 @@ class GroupsCoursesList(SecureAPIView):
existing_group = Group.objects.get(id=group_id) existing_group = Group.objects.get(id=group_id)
except ObjectDoesNotExist: except ObjectDoesNotExist:
return Response({}, status.HTTP_404_NOT_FOUND) return Response({}, status.HTTP_404_NOT_FOUND)
store = modulestore()
members = CourseGroupRelationship.objects.filter(group=existing_group) members = CourseGroupRelationship.objects.filter(group=existing_group)
response_data = [] response_data = []
for member in members: for member in members:
course = store.get_course(member.course_id) course, course_key, course_content = get_course(request, request.user, member.course_id) # pylint: disable=W0612
course_data = { course_data = {
'course_id': member.course_id, 'course_id': member.course_id,
'display_name': course.display_name 'display_name': course.display_name
...@@ -607,7 +601,7 @@ class GroupsCoursesDetail(SecureAPIView): ...@@ -607,7 +601,7 @@ class GroupsCoursesDetail(SecureAPIView):
response_status = status.HTTP_404_NOT_FOUND response_status = status.HTTP_404_NOT_FOUND
return Response(response_data, status=response_status) return Response(response_data, status=response_status)
def delete(self, request, group_id, course_id): def delete(self, request, group_id, course_id): # pylint: disable=W0613
""" """
DELETE /api/groups/{group_id}/courses/{course_id} DELETE /api/groups/{group_id}/courses/{course_id}
""" """
...@@ -630,7 +624,7 @@ class GroupsOrganizationsList(SecureAPIView): ...@@ -630,7 +624,7 @@ class GroupsOrganizationsList(SecureAPIView):
* View all of the Organizations related to a particular Program (currently modeled as a Group entity) * View all of the Organizations related to a particular Program (currently modeled as a Group entity)
""" """
def get(self, request, group_id): def get(self, request, group_id): # pylint: disable=W0613
""" """
GET /api/groups/{group_id}/organizations/ GET /api/groups/{group_id}/organizations/
""" """
...@@ -642,7 +636,7 @@ class GroupsOrganizationsList(SecureAPIView): ...@@ -642,7 +636,7 @@ class GroupsOrganizationsList(SecureAPIView):
response_data = [] response_data = []
for org in existing_group.organizations.all(): for org in existing_group.organizations.all():
serializer = serializers.OrganizationSerializer(org) serializer = serializers.OrganizationSerializer(org)
response_data.append(serializer.data) response_data.append(serializer.data) # pylint: disable=E1101
return Response(response_data, status=status.HTTP_200_OK) return Response(response_data, status=status.HTTP_200_OK)
......
"""
One-time data migration script -- shoulen't need to run it again
"""
import json import json
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
...@@ -5,6 +8,8 @@ from django.core.management.base import BaseCommand ...@@ -5,6 +8,8 @@ from django.core.management.base import BaseCommand
from api_manager.models import GroupProfile, Organization from api_manager.models import GroupProfile, Organization
class Command(BaseCommand): class Command(BaseCommand):
""" """
Migrates legacy organization data and user relationships from older Group model approach to newer concrete Organization model Migrates legacy organization data and user relationships from older Group model approach to newer concrete Organization model
...@@ -40,6 +45,6 @@ class Command(BaseCommand): ...@@ -40,6 +45,6 @@ class Command(BaseCommand):
migrated_org.users.add(user) migrated_org.users.add(user)
linked_groups = group.grouprelationship.get_linked_group_relationships() linked_groups = group.grouprelationship.get_linked_group_relationships()
for linked_group in linked_groups: for linked_group in linked_groups:
if linked_group.to_group_relationship_id is not org.id: # Don't need to carry the symmetrical component if linked_group.to_group_relationship_id is not org.id: # Don't need to carry the symmetrical component
actual_group = Group.objects.get(id=linked_group.to_group_relationship_id) actual_group = Group.objects.get(id=linked_group.to_group_relationship_id)
migrated_org.groups.add(actual_group) migrated_org.groups.add(actual_group)
"""
Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/management/commands/tests/test_migrate_orgdata.py]
"""
import json import json
import uuid import uuid
...@@ -9,9 +13,9 @@ from api_manager.models import GroupProfile, GroupRelationship, Organization ...@@ -9,9 +13,9 @@ from api_manager.models import GroupProfile, GroupRelationship, Organization
class MigrateOrgDataTests(TestCase): class MigrateOrgDataTests(TestCase):
"""
def setUp(self): Test suite for data migration script
setup = True """
def test_migrate_orgdata(self): def test_migrate_orgdata(self):
""" """
...@@ -20,7 +24,6 @@ class MigrateOrgDataTests(TestCase): ...@@ -20,7 +24,6 @@ class MigrateOrgDataTests(TestCase):
# Create some old-style Group organizations to migrate # Create some old-style Group organizations to migrate
group_name = str(uuid.uuid4()) group_name = str(uuid.uuid4())
group_profile_name = "Group 1 Name"
group_type = "organization" group_type = "organization"
groupdata = {} groupdata = {}
groupdata['name'] = "Group 1 Data Name" groupdata['name'] = "Group 1 Data Name"
...@@ -29,8 +32,8 @@ class MigrateOrgDataTests(TestCase): ...@@ -29,8 +32,8 @@ class MigrateOrgDataTests(TestCase):
groupdata['contact_email'] = "Group 1 Data Contact Email" groupdata['contact_email'] = "Group 1 Data Contact Email"
groupdata['contact_phone'] = "Group 1 Data Contact Phone" groupdata['contact_phone'] = "Group 1 Data Contact Phone"
group = Group.objects.create(name=group_name) group = Group.objects.create(name=group_name)
group_relationship = GroupRelationship.objects.create(group_id=group.id) GroupRelationship.objects.create(group_id=group.id)
profile, _ = GroupProfile.objects.get_or_create( GroupProfile.objects.get_or_create(
group_id=group.id, group_id=group.id,
group_type=group_type, group_type=group_type,
name=groupdata['name'], name=groupdata['name'],
...@@ -43,7 +46,6 @@ class MigrateOrgDataTests(TestCase): ...@@ -43,7 +46,6 @@ class MigrateOrgDataTests(TestCase):
group.grouprelationship.add_linked_group_relationship(linked_group_relationship) group.grouprelationship.add_linked_group_relationship(linked_group_relationship)
group2_name = str(uuid.uuid4()) group2_name = str(uuid.uuid4())
group2_profile_name = "Group 2 Name"
group2_type = "organization" group2_type = "organization"
groupdata = {} groupdata = {}
groupdata['name'] = "Group 2 Data Name" groupdata['name'] = "Group 2 Data Name"
...@@ -52,8 +54,8 @@ class MigrateOrgDataTests(TestCase): ...@@ -52,8 +54,8 @@ class MigrateOrgDataTests(TestCase):
groupdata['contact_email'] = "Group 2 Data Contact Email" groupdata['contact_email'] = "Group 2 Data Contact Email"
groupdata['contact_phone'] = "Group 2 Data Contact Phone" groupdata['contact_phone'] = "Group 2 Data Contact Phone"
group2 = Group.objects.create(name=group2_name) group2 = Group.objects.create(name=group2_name)
grouprelattionship2 = GroupRelationship.objects.create(group_id=group2.id) GroupRelationship.objects.create(group_id=group2.id)
profile2, _ = GroupProfile.objects.get_or_create( GroupProfile.objects.get_or_create(
group_id=group2.id, group_id=group2.id,
group_type=group2_type, group_type=group2_type,
name=groupdata['name'], name=groupdata['name'],
......
...@@ -70,7 +70,6 @@ class Migration(SchemaMigration): ...@@ -70,7 +70,6 @@ class Migration(SchemaMigration):
self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now), self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now),
keep_default=False) keep_default=False)
def backwards(self, orm): def backwards(self, orm):
# Adding field 'GroupRelationship.record_date_created' # Adding field 'GroupRelationship.record_date_created'
db.add_column('api_manager_grouprelationship', 'record_date_created', db.add_column('api_manager_grouprelationship', 'record_date_created',
...@@ -122,7 +121,6 @@ class Migration(SchemaMigration): ...@@ -122,7 +121,6 @@ class Migration(SchemaMigration):
# Deleting field 'LinkedGroupRelationship.modified' # Deleting field 'LinkedGroupRelationship.modified'
db.delete_column('api_manager_linkedgrouprelationship', 'modified') db.delete_column('api_manager_linkedgrouprelationship', 'modified')
models = { models = {
'api_manager.coursegrouprelationship': { 'api_manager.coursegrouprelationship': {
'Meta': {'object_name': 'CourseGroupRelationship'}, 'Meta': {'object_name': 'CourseGroupRelationship'},
...@@ -184,4 +182,4 @@ class Migration(SchemaMigration): ...@@ -184,4 +182,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['api_manager'] complete_apps = ['api_manager']
\ No newline at end of file
...@@ -11,12 +11,10 @@ class Migration(SchemaMigration): ...@@ -11,12 +11,10 @@ class Migration(SchemaMigration):
# Adding unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group'] # Adding unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group']
db.create_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_id']) db.create_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_id'])
def backwards(self, orm): def backwards(self, orm):
# Removing unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group'] # Removing unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group']
db.delete_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_id']) db.delete_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_id'])
models = { models = {
'api_manager.coursecontentgrouprelationship': { 'api_manager.coursecontentgrouprelationship': {
'Meta': {'unique_together': "(('course_id', 'content_id', 'group'),)", 'object_name': 'CourseContentGroupRelationship'}, 'Meta': {'unique_together': "(('course_id', 'content_id', 'group'),)", 'object_name': 'CourseContentGroupRelationship'},
...@@ -88,4 +86,4 @@ class Migration(SchemaMigration): ...@@ -88,4 +86,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['api_manager'] complete_apps = ['api_manager']
\ No newline at end of file
...@@ -22,7 +22,6 @@ class Migration(SchemaMigration): ...@@ -22,7 +22,6 @@ class Migration(SchemaMigration):
# Adding unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group_profile'] # Adding unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group_profile']
db.create_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_profile_id']) db.create_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_profile_id'])
def backwards(self, orm): def backwards(self, orm):
# Removing unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group_profile'] # Removing unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group_profile']
db.delete_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_profile_id']) db.delete_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_profile_id'])
...@@ -38,7 +37,6 @@ class Migration(SchemaMigration): ...@@ -38,7 +37,6 @@ class Migration(SchemaMigration):
# Adding unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group'] # Adding unique constraint on 'CourseContentGroupRelationship', fields ['course_id', 'content_id', 'group']
db.create_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_id']) db.create_unique('api_manager_coursecontentgrouprelationship', ['course_id', 'content_id', 'group_id'])
models = { models = {
'api_manager.coursecontentgrouprelationship': { 'api_manager.coursecontentgrouprelationship': {
'Meta': {'unique_together': "(('course_id', 'content_id', 'group_profile'),)", 'object_name': 'CourseContentGroupRelationship'}, 'Meta': {'unique_together': "(('course_id', 'content_id', 'group_profile'),)", 'object_name': 'CourseContentGroupRelationship'},
...@@ -110,4 +108,4 @@ class Migration(SchemaMigration): ...@@ -110,4 +108,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['api_manager'] complete_apps = ['api_manager']
\ No newline at end of file
...@@ -14,12 +14,10 @@ class Migration(SchemaMigration): ...@@ -14,12 +14,10 @@ class Migration(SchemaMigration):
# Adding unique constraint on 'GroupProfile', fields ['group'] # Adding unique constraint on 'GroupProfile', fields ['group']
db.create_unique('auth_groupprofile', ['group_id']) db.create_unique('auth_groupprofile', ['group_id'])
def backwards(self, orm): def backwards(self, orm):
# Removing unique constraint on 'GroupProfile', fields ['group'] # Removing unique constraint on 'GroupProfile', fields ['group']
db.delete_unique('auth_groupprofile', ['group_id']) db.delete_unique('auth_groupprofile', ['group_id'])
# Changing field 'GroupProfile.group' # Changing field 'GroupProfile.group'
db.alter_column('auth_groupprofile', 'group_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.Group'])) db.alter_column('auth_groupprofile', 'group_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.Group']))
...@@ -128,4 +126,4 @@ class Migration(SchemaMigration): ...@@ -128,4 +126,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['api_manager'] complete_apps = ['api_manager']
\ No newline at end of file
...@@ -19,12 +19,10 @@ class Migration(SchemaMigration): ...@@ -19,12 +19,10 @@ class Migration(SchemaMigration):
)) ))
db.send_create_signal('api_manager', ['CourseModuleCompletion']) db.send_create_signal('api_manager', ['CourseModuleCompletion'])
def backwards(self, orm): def backwards(self, orm):
# Deleting model 'CourseModuleCompletion' # Deleting model 'CourseModuleCompletion'
db.delete_table('api_manager_coursemodulecompletion') db.delete_table('api_manager_coursemodulecompletion')
models = { models = {
'api_manager.coursecontentgrouprelationship': { 'api_manager.coursecontentgrouprelationship': {
'Meta': {'unique_together': "(('course_id', 'content_id', 'group_profile'),)", 'object_name': 'CourseContentGroupRelationship'}, 'Meta': {'unique_together': "(('course_id', 'content_id', 'group_profile'),)", 'object_name': 'CourseContentGroupRelationship'},
...@@ -139,4 +137,4 @@ class Migration(SchemaMigration): ...@@ -139,4 +137,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['api_manager'] complete_apps = ['api_manager']
\ No newline at end of file
...@@ -28,7 +28,6 @@ class Migration(SchemaMigration): ...@@ -28,7 +28,6 @@ class Migration(SchemaMigration):
self.gf('django.db.models.fields.CharField')(max_length=50, null=True, blank=True), self.gf('django.db.models.fields.CharField')(max_length=50, null=True, blank=True),
keep_default=False) keep_default=False)
def backwards(self, orm): def backwards(self, orm):
# Deleting field 'Organization.display_name' # Deleting field 'Organization.display_name'
db.delete_column('api_manager_organization', 'display_name') db.delete_column('api_manager_organization', 'display_name')
...@@ -42,7 +41,6 @@ class Migration(SchemaMigration): ...@@ -42,7 +41,6 @@ class Migration(SchemaMigration):
# Deleting field 'Organization.contact_phone' # Deleting field 'Organization.contact_phone'
db.delete_column('api_manager_organization', 'contact_phone') db.delete_column('api_manager_organization', 'contact_phone')
models = { models = {
'api_manager.coursecontentgrouprelationship': { 'api_manager.coursecontentgrouprelationship': {
'Meta': {'unique_together': "(('course_id', 'content_id', 'group_profile'),)", 'object_name': 'CourseContentGroupRelationship'}, 'Meta': {'unique_together': "(('course_id', 'content_id', 'group_profile'),)", 'object_name': 'CourseContentGroupRelationship'},
...@@ -161,4 +159,4 @@ class Migration(SchemaMigration): ...@@ -161,4 +159,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['api_manager'] complete_apps = ['api_manager']
\ No newline at end of file
...@@ -7,7 +7,7 @@ from django.db import models ...@@ -7,7 +7,7 @@ from django.db import models
class Migration(SchemaMigration): class Migration(SchemaMigration):
def forwards(self, orm): def forwards(self, orm):
# Adding M2M table for field groups on 'Organization' # Adding M2M table for field groups on 'Organization'
db.create_table('api_manager_organization_groups', ( db.create_table('api_manager_organization_groups', (
...@@ -17,13 +17,11 @@ class Migration(SchemaMigration): ...@@ -17,13 +17,11 @@ class Migration(SchemaMigration):
)) ))
db.create_unique('api_manager_organization_groups', ['organization_id', 'group_id']) db.create_unique('api_manager_organization_groups', ['organization_id', 'group_id'])
def backwards(self, orm): def backwards(self, orm):
# Removing M2M table for field groups on 'Organization' # Removing M2M table for field groups on 'Organization'
db.delete_table('api_manager_organization_groups') db.delete_table('api_manager_organization_groups')
models = { models = {
'api_manager.coursecontentgrouprelationship': { 'api_manager.coursecontentgrouprelationship': {
'Meta': {'unique_together': "(('course_id', 'content_id', 'group_profile'),)", 'object_name': 'CourseContentGroupRelationship'}, 'Meta': {'unique_together': "(('course_id', 'content_id', 'group_profile'),)", 'object_name': 'CourseContentGroupRelationship'},
...@@ -153,4 +151,4 @@ class Migration(SchemaMigration): ...@@ -153,4 +151,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['api_manager'] complete_apps = ['api_manager']
\ No newline at end of file
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.db import models from django.db import models
from django.utils import timezone
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
from .utils import is_int from .utils import is_int
...@@ -102,6 +102,9 @@ class GroupProfile(TimeStampedModel): ...@@ -102,6 +102,9 @@ class GroupProfile(TimeStampedModel):
""" """
class Meta: class Meta:
"""
Meta class for modifying things like table name
"""
db_table = "auth_groupprofile" db_table = "auth_groupprofile"
group = models.OneToOneField(Group, db_index=True) group = models.OneToOneField(Group, db_index=True)
...@@ -155,6 +158,7 @@ class CourseModuleCompletion(TimeStampedModel): ...@@ -155,6 +158,7 @@ class CourseModuleCompletion(TimeStampedModel):
user = models.ForeignKey(User, db_index=True, related_name="course_completions") user = models.ForeignKey(User, db_index=True, related_name="course_completions")
course_id = models.CharField(max_length=255, db_index=True) course_id = models.CharField(max_length=255, db_index=True)
content_id = models.CharField(max_length=255, db_index=True) content_id = models.CharField(max_length=255, db_index=True)
stage = models.CharField(max_length=255, null=True, blank=True)
class APIUserQuerySet(models.query.QuerySet): # pylint: disable=R0924 class APIUserQuerySet(models.query.QuerySet): # pylint: disable=R0924
......
...@@ -94,7 +94,6 @@ class OrganizationsApiTests(TestCase): ...@@ -94,7 +94,6 @@ class OrganizationsApiTests(TestCase):
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
users.append(response.data['id']) users.append(response.data['id'])
data = { data = {
'name': self.test_organization_name, 'name': self.test_organization_name,
'display_name': self.test_organization_display_name, 'display_name': self.test_organization_display_name,
......
# pylint: disable=C0103
""" ORGANIZATIONS API VIEWS """ """ ORGANIZATIONS API VIEWS """
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
...@@ -29,7 +31,7 @@ class OrganizationsViewSet(viewsets.ModelViewSet): ...@@ -29,7 +31,7 @@ class OrganizationsViewSet(viewsets.ModelViewSet):
if users: if users:
for user in users: for user in users:
serializer = UserSerializer(user) serializer = UserSerializer(user)
response_data.append(serializer.data) response_data.append(serializer.data) # pylint: disable=E1101
return Response(response_data, status=status.HTTP_200_OK) return Response(response_data, status=status.HTTP_200_OK)
else: else:
user_id = request.DATA.get('id') user_id = request.DATA.get('id')
......
...@@ -105,6 +105,9 @@ class CustomPaginationSerializer(pagination.PaginationSerializer): ...@@ -105,6 +105,9 @@ class CustomPaginationSerializer(pagination.PaginationSerializer):
class SecureAPIView(APIView): class SecureAPIView(APIView):
"""
View used for protecting access to specific workflows
"""
permission_classes = (ApiKeyHeaderPermission, ) permission_classes = (ApiKeyHeaderPermission, )
......
# pylint: disable=W0612
"""
Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/sessions/test_login_ratelimit.py]
"""
import json import json
import uuid import uuid
import unittest
from mock import patch from mock import patch
from datetime import datetime, timedelta from datetime import datetime, timedelta
from freezegun import freeze_time from freezegun import freeze_time
...@@ -10,7 +14,6 @@ from django.test import TestCase ...@@ -10,7 +14,6 @@ from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.test.utils import override_settings from django.test.utils import override_settings
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
......
# pylint: disable=W0612
""" """
Tests for session api with advance security features Tests for session api with advance security features
""" """
......
...@@ -6,10 +6,8 @@ Run these tests @ Devstack: ...@@ -6,10 +6,8 @@ Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/tests/test_session_views.py] rake fasttest_lms[common/djangoapps/api_manager/tests/test_session_views.py]
""" """
from random import randint from random import randint
import unittest
import uuid import uuid
from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.cache import cache from django.core.cache import cache
from django.test import TestCase, Client from django.test import TestCase, Client
......
...@@ -24,14 +24,35 @@ from student.models import ( ...@@ -24,14 +24,35 @@ from student.models import (
) )
AUDIT_LOG = logging.getLogger("audit") AUDIT_LOG = logging.getLogger("audit")
class SessionsList(SecureAPIView): class SessionsList(SecureAPIView):
""" Inherit with SecureAPIView """ """
**Use Case**
SessionsList creates a new session with the edX LMS.
**Example Request**
POST {"username": "staff", "password": "edx"}
**Response Values**
* token: A unique token value for the session created.
* expires: The number of seconds until the new session expires.
* user: The following data about the user for whom the session is
created.
* id: The unique user identifier.
* email: The user's email address.
* username: The user's edX username.
* first_name: The user's first name, if defined.
* last_name: The user's last name, if defined.
* creaed: The time and date the user account was created.
* organizations: An array of organizations the user is associated
with.
* uri: The URI to use to get details about the new session.
"""
def post(self, request): def post(self, request):
"""
POST creates a new system session, supported authentication modes:
1. Open edX username/password
"""
response_data = {} response_data = {}
# Add some rate limiting here by re-using the RateLimitMixin as a helper class # Add some rate limiting here by re-using the RateLimitMixin as a helper class
limiter = BadRequestRateLimiter() limiter = BadRequestRateLimiter()
...@@ -98,12 +119,28 @@ class SessionsList(SecureAPIView): ...@@ -98,12 +119,28 @@ class SessionsList(SecureAPIView):
class SessionsDetail(SecureAPIView): class SessionsDetail(SecureAPIView):
""" Inherit with SecureAPIView """ """
**Use Case**
SessionsDetail gets a details about a specific API session, as well as
enables you to delete an API session.
**Example Requests**
GET /api/session/{session_id}
DELETE /api/session/{session_id}/delete
**GET Response Values**
* token: A unique token value for the session.
* expires: The number of seconds until the session expires.
* user_id: The unique user identifier.
* uri: The URI to use to get details about the session.
"""
def get(self, request, session_id): def get(self, request, session_id):
"""
GET retrieves an existing system session
"""
response_data = {} response_data = {}
base_uri = generate_base_uri(request) base_uri = generate_base_uri(request)
engine = import_module(settings.SESSION_ENGINE) engine = import_module(settings.SESSION_ENGINE)
...@@ -125,9 +162,6 @@ class SessionsDetail(SecureAPIView): ...@@ -125,9 +162,6 @@ class SessionsDetail(SecureAPIView):
return Response(response_data, status=status.HTTP_404_NOT_FOUND) return Response(response_data, status=status.HTTP_404_NOT_FOUND)
def delete(self, request, session_id): def delete(self, request, session_id):
"""
DELETE flushes an existing system session from the system
"""
response_data = {} response_data = {}
engine = import_module(settings.SESSION_ENGINE) engine = import_module(settings.SESSION_ENGINE)
session = engine.SessionStore(session_id) session = engine.SessionStore(session_id)
......
...@@ -4,10 +4,8 @@ ...@@ -4,10 +4,8 @@
Run these tests @ Devstack: Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/tests/test_views.py] rake fasttest_lms[common/djangoapps/api_manager/tests/test_views.py]
""" """
import unittest
import uuid import uuid
from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.test import TestCase, Client from django.test import TestCase, Client
from django.test.utils import override_settings from django.test.utils import override_settings
...@@ -77,4 +75,3 @@ class SystemApiTests(TestCase): ...@@ -77,4 +75,3 @@ class SystemApiTests(TestCase):
self.assertIsNotNone(response.data['description']) self.assertIsNotNone(response.data['description'])
self.assertGreater(len(response.data['description']), 0) self.assertGreater(len(response.data['description']), 0)
self.assertIsNotNone(response.data['resources']) self.assertIsNotNone(response.data['resources'])
...@@ -12,6 +12,9 @@ class SystemDetail(SecureAPIView): ...@@ -12,6 +12,9 @@ class SystemDetail(SecureAPIView):
"""Manages system-level information about the Open edX API""" """Manages system-level information about the Open edX API"""
def get(self, request): def get(self, request):
"""
GET /api/system/
"""
base_uri = generate_base_uri(request) base_uri = generate_base_uri(request)
response_data = {} response_data = {}
response_data['name'] = "Open edX System API" response_data['name'] = "Open edX System API"
...@@ -25,6 +28,9 @@ class ApiDetail(SecureAPIView): ...@@ -25,6 +28,9 @@ class ApiDetail(SecureAPIView):
"""Manages top-level information about the Open edX API""" """Manages top-level information about the Open edX API"""
def get(self, request): def get(self, request):
"""
GET /api/
"""
base_uri = generate_base_uri(request) base_uri = generate_base_uri(request)
response_data = {} response_data = {}
response_data['name'] = "Open edX API" response_data['name'] = "Open edX API"
......
...@@ -3,10 +3,8 @@ Run these tests @ Devstack: ...@@ -3,10 +3,8 @@ Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/tests/test_permissions.py] rake fasttest_lms[common/djangoapps/api_manager/tests/test_permissions.py]
""" """
from random import randint from random import randint
import unittest
import uuid import uuid
from django.conf import settings
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
...@@ -78,4 +76,3 @@ class PermissionsTests(TestCase): ...@@ -78,4 +76,3 @@ class PermissionsTests(TestCase):
ip_address = kwargs.get('ip_address', {}) ip_address = kwargs.get('ip_address', {})
response = self.client.post(uri, headers=headers, data=data, **ip_address) response = self.client.post(uri, headers=headers, data=data, **ip_address)
return response return response
# pylint: disable=C0103
""" """
The URI scheme for resources is as follows: The URI scheme for resources is as follows:
Resource type: /api/{resource_type} Resource type: /api/{resource_type}
......
...@@ -153,7 +153,6 @@ class UserPasswordResetTest(TestCase): ...@@ -153,7 +153,6 @@ class UserPasswordResetTest(TestCase):
) )
self._assert_response(response, status=403, message=message) self._assert_response(response, status=403, message=message)
@override_settings(ADVANCED_SECURITY_CONFIG={'MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS': 1}) @override_settings(ADVANCED_SECURITY_CONFIG={'MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS': 1})
def test_is_password_reset_too_frequent(self): def test_is_password_reset_too_frequent(self):
""" """
......
...@@ -7,19 +7,24 @@ from api_manager.users import views as users_views ...@@ -7,19 +7,24 @@ from api_manager.users import views as users_views
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url(r'/*$^', users_views.UsersList.as_view(), name='apimgr-users-list'),
url(r'^metrics/cities/$', users_views.UsersMetricsCitiesList.as_view(), name='apimgr-users-metrics-cities-list'), url(r'^metrics/cities/$', users_views.UsersMetricsCitiesList.as_view(), name='apimgr-users-metrics-cities-list'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)$', users_views.UsersDetail.as_view(), name='apimgr-users-detail'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/*$', users_views.UsersCoursesList.as_view(), name='users-courses-list'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)$', users_views.UsersCoursesDetail.as_view(), name='users-courses-detail'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/grades$', users_views.UsersCoursesGradesDetail.as_view(), name='users-courses-grades-detail'), url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/grades$', users_views.UsersCoursesGradesDetail.as_view(), name='users-courses-grades-detail'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/metrics/social/$', users_views.UsersSocialMetrics.as_view(), name='users-social-metrics'), url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/metrics/social/$', users_views.UsersSocialMetrics.as_view(), name='users-social-metrics'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/completions/$', users_views.UsersCoursesCompletionsList.as_view(), name='users-courses-completions-list'), url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/completions/$', users_views.UsersCoursesCompletionsList.as_view(), name='users-courses-completions-list'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)$', users_views.UsersCoursesDetail.as_view(), name='users-courses-detail'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/(?P<course_id>[a-zA-Z0-9_+\/:]+)/grades$', users_views.UsersCoursesGradesDetail.as_view(), name='users-courses-grades-detail'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/(?P<course_id>[a-zA-Z0-9_+\/:]+)/metrics/social/$', users_views.UsersSocialMetrics.as_view(), name='users-social-metrics'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/(?P<course_id>[a-zA-Z0-9_+\/:]+)/completions/$', users_views.UsersCoursesCompletionsList.as_view(), name='users-courses-completions-list'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/(?P<course_id>[a-zA-Z0-9_+\/:]+)$', users_views.UsersCoursesDetail.as_view(), name='users-courses-detail'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/courses/*$', users_views.UsersCoursesList.as_view(), name='users-courses-list'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/groups/*$', users_views.UsersGroupsList.as_view(), name='users-groups-list'), url(r'^(?P<user_id>[a-zA-Z0-9]+)/groups/*$', users_views.UsersGroupsList.as_view(), name='users-groups-list'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/groups/(?P<group_id>[0-9]+)$', users_views.UsersGroupsDetail.as_view(), name='users-groups-detail'), url(r'^(?P<user_id>[a-zA-Z0-9]+)/groups/(?P<group_id>[0-9]+)$', users_views.UsersGroupsDetail.as_view(), name='users-groups-detail'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/preferences$', users_views.UsersPreferences.as_view(), name='users-preferences-list'), url(r'^(?P<user_id>[a-zA-Z0-9]+)/preferences$', users_views.UsersPreferences.as_view(), name='users-preferences-list'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/preferences/(?P<preference_id>[a-zA-Z0-9_]+)$', users_views.UsersPreferencesDetail.as_view(), name='users-preferences-detail'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/organizations/$', users_views.UsersOrganizationsList.as_view(), name='users-organizations-list'), url(r'^(?P<user_id>[a-zA-Z0-9]+)/organizations/$', users_views.UsersOrganizationsList.as_view(), name='users-organizations-list'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/workgroups/$', users_views.UsersWorkgroupsList.as_view(), name='users-workgroups-list'), url(r'^(?P<user_id>[a-zA-Z0-9]+)/workgroups/$', users_views.UsersWorkgroupsList.as_view(), name='users-workgroups-list'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)$', users_views.UsersDetail.as_view(), name='apimgr-users-detail'),
url(r'/*$^', users_views.UsersList.as_view(), name='apimgr-users-list'),
) )
urlpatterns = format_suffix_patterns(urlpatterns) urlpatterns = format_suffix_patterns(urlpatterns)
""" API implementation for Secure api calls. """ """ API implementation for Secure api calls. """
import socket import socket
import struct import struct
...@@ -55,3 +54,5 @@ def is_int(value): ...@@ -55,3 +54,5 @@ def is_int(value):
return True return True
except ValueError: except ValueError:
return False return False
...@@ -70,6 +70,8 @@ from xmodule.lti_module import LTIModule ...@@ -70,6 +70,8 @@ from xmodule.lti_module import LTIModule
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from xmodule.mixin import wrap_with_license from xmodule.mixin import wrap_with_license
from api_manager.models import CourseModuleCompletion
from util.json_request import JsonResponse from util.json_request import JsonResponse
from util.sandboxing import can_execute_unsafe_code, get_python_lib_zip from util.sandboxing import can_execute_unsafe_code, get_python_lib_zip
from util import milestones_helpers from util import milestones_helpers
...@@ -484,10 +486,20 @@ def get_module_system_for_user(user, field_data_cache, # TODO # pylint: disabl ...@@ -484,10 +486,20 @@ def get_module_system_for_user(user, field_data_cache, # TODO # pylint: disabl
usage_id=unicode(descriptor.location) usage_id=unicode(descriptor.location)
) )
CourseModuleCompletion.objects.get_or_create(
user_id=user_id,
course_id=course_id,
content_id=unicode(descriptor.location)
)
def publish(block, event_type, event): def publish(block, event_type, event):
"""A function that allows XModules to publish events.""" """A function that allows XModules to publish events."""
if event_type == 'grade': if event_type == 'grade':
handle_grade_event(block, event_type, event) handle_grade_event(block, event_type, event)
elif event_type == 'progress':
# expose another special case event type which gets sent
# into the CourseCompletions models
handle_progress_event(block, event_type, event)
else: else:
track_function(event_type, event) track_function(event_type, event)
......
...@@ -245,7 +245,7 @@ def save_child_position(seq_module, child_name): ...@@ -245,7 +245,7 @@ def save_child_position(seq_module, child_name):
""" """
child_name: url_name of the child child_name: url_name of the child
""" """
print child_name
for position, c in enumerate(seq_module.get_display_items(), start=1): for position, c in enumerate(seq_module.get_display_items(), start=1):
if c.location.name == child_name: if c.location.name == child_name:
# Only save if position changed # Only save if position changed
......
...@@ -97,7 +97,6 @@ class Migration(SchemaMigration): ...@@ -97,7 +97,6 @@ class Migration(SchemaMigration):
)) ))
db.send_create_signal('projects', ['WorkgroupPeerReview']) db.send_create_signal('projects', ['WorkgroupPeerReview'])
def backwards(self, orm): def backwards(self, orm):
# Removing unique constraint on 'Project', fields ['course_id', 'content_id'] # Removing unique constraint on 'Project', fields ['course_id', 'content_id']
db.delete_unique('projects_project', ['course_id', 'content_id']) db.delete_unique('projects_project', ['course_id', 'content_id'])
...@@ -126,7 +125,6 @@ class Migration(SchemaMigration): ...@@ -126,7 +125,6 @@ class Migration(SchemaMigration):
# Deleting model 'WorkgroupPeerReview' # Deleting model 'WorkgroupPeerReview'
db.delete_table('projects_workgrouppeerreview') db.delete_table('projects_workgrouppeerreview')
models = { models = {
'auth.group': { 'auth.group': {
'Meta': {'object_name': 'Group'}, 'Meta': {'object_name': 'Group'},
...@@ -226,4 +224,4 @@ class Migration(SchemaMigration): ...@@ -226,4 +224,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['projects'] complete_apps = ['projects']
\ No newline at end of file
...@@ -13,12 +13,10 @@ class Migration(SchemaMigration): ...@@ -13,12 +13,10 @@ class Migration(SchemaMigration):
self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='projects', null=True, on_delete=models.SET_NULL, to=orm['api_manager.Organization']), self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='projects', null=True, on_delete=models.SET_NULL, to=orm['api_manager.Organization']),
keep_default=False) keep_default=False)
def backwards(self, orm): def backwards(self, orm):
# Deleting field 'Project.organization' # Deleting field 'Project.organization'
db.delete_column('projects_project', 'organization_id') db.delete_column('projects_project', 'organization_id')
models = { models = {
'api_manager.organization': { 'api_manager.organization': {
'Meta': {'object_name': 'Organization'}, 'Meta': {'object_name': 'Organization'},
...@@ -132,4 +130,4 @@ class Migration(SchemaMigration): ...@@ -132,4 +130,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['projects'] complete_apps = ['projects']
\ No newline at end of file
...@@ -13,12 +13,10 @@ class Migration(SchemaMigration): ...@@ -13,12 +13,10 @@ class Migration(SchemaMigration):
self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True), self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
keep_default=False) keep_default=False)
def backwards(self, orm): def backwards(self, orm):
# Deleting field 'WorkgroupSubmission.document_filename' # Deleting field 'WorkgroupSubmission.document_filename'
db.delete_column('projects_workgroupsubmission', 'document_filename') db.delete_column('projects_workgroupsubmission', 'document_filename')
models = { models = {
'api_manager.organization': { 'api_manager.organization': {
'Meta': {'object_name': 'Organization'}, 'Meta': {'object_name': 'Organization'},
...@@ -134,4 +132,4 @@ class Migration(SchemaMigration): ...@@ -134,4 +132,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['projects'] complete_apps = ['projects']
\ No newline at end of file
...@@ -4,7 +4,6 @@ from django.contrib.auth.models import Group, User ...@@ -4,7 +4,6 @@ from django.contrib.auth.models import Group, User
from django.db import models from django.db import models
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
from student.models import AnonymousUserId
class Project(TimeStampedModel): class Project(TimeStampedModel):
...@@ -14,8 +13,13 @@ class Project(TimeStampedModel): ...@@ -14,8 +13,13 @@ class Project(TimeStampedModel):
""" """
course_id = models.CharField(max_length=255) course_id = models.CharField(max_length=255)
content_id = models.CharField(max_length=255) content_id = models.CharField(max_length=255)
organization = models.ForeignKey('api_manager.Organization', blank=True, null=True, related_name="projects" organization = models.ForeignKey(
, on_delete=models.SET_NULL) 'api_manager.Organization',
blank=True,
null=True,
related_name="projects",
on_delete=models.SET_NULL
)
class Meta: class Meta:
""" Meta class for defining additional model characteristics """ """ Meta class for defining additional model characteristics """
...@@ -45,6 +49,7 @@ class WorkgroupReview(TimeStampedModel): ...@@ -45,6 +49,7 @@ class WorkgroupReview(TimeStampedModel):
reviewer = models.CharField(max_length=255) # AnonymousUserId reviewer = models.CharField(max_length=255) # AnonymousUserId
question = models.CharField(max_length=255) question = models.CharField(max_length=255)
answer = models.CharField(max_length=255) answer = models.CharField(max_length=255)
content_id = models.CharField(max_length=255, null=True, blank=True)
class WorkgroupSubmission(TimeStampedModel): class WorkgroupSubmission(TimeStampedModel):
...@@ -72,6 +77,7 @@ class WorkgroupSubmissionReview(TimeStampedModel): ...@@ -72,6 +77,7 @@ class WorkgroupSubmissionReview(TimeStampedModel):
reviewer = models.CharField(max_length=255) # AnonymousUserId reviewer = models.CharField(max_length=255) # AnonymousUserId
question = models.CharField(max_length=255) question = models.CharField(max_length=255)
answer = models.CharField(max_length=255) answer = models.CharField(max_length=255)
content_id = models.CharField(max_length=255, null=True, blank=True)
class WorkgroupPeerReview(TimeStampedModel): class WorkgroupPeerReview(TimeStampedModel):
......
...@@ -83,7 +83,7 @@ class WorkgroupReviewSerializer(serializers.HyperlinkedModelSerializer): ...@@ -83,7 +83,7 @@ class WorkgroupReviewSerializer(serializers.HyperlinkedModelSerializer):
model = WorkgroupReview model = WorkgroupReview
fields = ( fields = (
'id', 'url', 'created', 'modified', 'question', 'answer', 'id', 'url', 'created', 'modified', 'question', 'answer',
'workgroup', 'reviewer' 'workgroup', 'reviewer', 'content_id'
) )
...@@ -96,7 +96,7 @@ class WorkgroupSubmissionReviewSerializer(serializers.HyperlinkedModelSerializer ...@@ -96,7 +96,7 @@ class WorkgroupSubmissionReviewSerializer(serializers.HyperlinkedModelSerializer
model = WorkgroupSubmissionReview model = WorkgroupSubmissionReview
fields = ( fields = (
'id', 'url', 'created', 'modified', 'question', 'answer', 'id', 'url', 'created', 'modified', 'question', 'answer',
'submission', 'reviewer' 'submission', 'reviewer', 'content_id'
) )
......
...@@ -9,11 +9,13 @@ import uuid ...@@ -9,11 +9,13 @@ import uuid
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.cache import cache from django.core.cache import cache
from django.test import TestCase, Client from django.test import Client
from django.test.utils import override_settings from django.test.utils import override_settings
from projects.models import Project, Workgroup from projects.models import Project, Workgroup
from student.models import anonymous_id_for_user from student.models import anonymous_id_for_user
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
TEST_API_KEY = str(uuid.uuid4()) TEST_API_KEY = str(uuid.uuid4())
...@@ -29,7 +31,7 @@ class SecureClient(Client): ...@@ -29,7 +31,7 @@ class SecureClient(Client):
@override_settings(EDX_API_KEY=TEST_API_KEY) @override_settings(EDX_API_KEY=TEST_API_KEY)
class PeerReviewsApiTests(TestCase): class PeerReviewsApiTests(ModuleStoreTestCase):
""" Test suite for Users API views """ """ Test suite for Users API views """
...@@ -40,7 +42,17 @@ class PeerReviewsApiTests(TestCase): ...@@ -40,7 +42,17 @@ class PeerReviewsApiTests(TestCase):
self.test_projects_uri = '/api/projects/' self.test_projects_uri = '/api/projects/'
self.test_peer_reviews_uri = '/api/peer_reviews/' self.test_peer_reviews_uri = '/api/peer_reviews/'
self.test_course_id = 'edx/demo/course' self.course = CourseFactory.create()
self.test_data = '<html>{}</html>'.format(str(uuid.uuid4()))
self.chapter = ItemFactory.create(
category="chapter",
parent_location=self.course.location,
data=self.test_data,
display_name="Overview"
)
self.test_course_id = unicode(self.course.id)
self.test_bogus_course_id = 'foo/bar/baz' self.test_bogus_course_id = 'foo/bar/baz'
self.test_course_content_id = "i4x://blah" self.test_course_content_id = "i4x://blah"
self.test_bogus_course_content_id = "14x://foo/bar/baz" self.test_bogus_course_content_id = "14x://foo/bar/baz"
...@@ -59,7 +71,7 @@ class PeerReviewsApiTests(TestCase): ...@@ -59,7 +71,7 @@ class PeerReviewsApiTests(TestCase):
username="reviewer", username="reviewer",
is_active=True is_active=True
) )
self.anonymous_user_id = anonymous_id_for_user(self.test_reviewer_user, self.test_course_id) self.anonymous_user_id = anonymous_id_for_user(self.test_reviewer_user, self.course.id)
self.test_project = Project.objects.create( self.test_project = Project.objects.create(
course_id=self.test_course_id, course_id=self.test_course_id,
......
...@@ -9,11 +9,13 @@ import uuid ...@@ -9,11 +9,13 @@ import uuid
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.cache import cache from django.core.cache import cache
from django.test import TestCase, Client from django.test import Client
from django.test.utils import override_settings from django.test.utils import override_settings
from projects.models import Project, Workgroup, WorkgroupSubmission from projects.models import Project, Workgroup, WorkgroupSubmission
from student.models import anonymous_id_for_user from student.models import anonymous_id_for_user
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
TEST_API_KEY = str(uuid.uuid4()) TEST_API_KEY = str(uuid.uuid4())
...@@ -29,7 +31,7 @@ class SecureClient(Client): ...@@ -29,7 +31,7 @@ class SecureClient(Client):
@override_settings(EDX_API_KEY=TEST_API_KEY) @override_settings(EDX_API_KEY=TEST_API_KEY)
class SubmissionReviewsApiTests(TestCase): class SubmissionReviewsApiTests(ModuleStoreTestCase):
""" Test suite for Users API views """ """ Test suite for Users API views """
...@@ -40,7 +42,17 @@ class SubmissionReviewsApiTests(TestCase): ...@@ -40,7 +42,17 @@ class SubmissionReviewsApiTests(TestCase):
self.test_projects_uri = '/api/projects/' self.test_projects_uri = '/api/projects/'
self.test_submission_reviews_uri = '/api/submission_reviews/' self.test_submission_reviews_uri = '/api/submission_reviews/'
self.test_course_id = 'edx/demo/course' self.course = CourseFactory.create()
self.test_data = '<html>{}</html>'.format(str(uuid.uuid4()))
self.chapter = ItemFactory.create(
category="chapter",
parent_location=self.course.location,
data=self.test_data,
display_name="Overview"
)
self.test_course_id = unicode(self.course.id)
self.test_bogus_course_id = 'foo/bar/baz' self.test_bogus_course_id = 'foo/bar/baz'
self.test_course_content_id = "i4x://blah" self.test_course_content_id = "i4x://blah"
self.test_bogus_course_content_id = "14x://foo/bar/baz" self.test_bogus_course_content_id = "14x://foo/bar/baz"
...@@ -53,7 +65,7 @@ class SubmissionReviewsApiTests(TestCase): ...@@ -53,7 +65,7 @@ class SubmissionReviewsApiTests(TestCase):
username="testing", username="testing",
is_active=True is_active=True
) )
self.anonymous_user_id = anonymous_id_for_user(self.test_user, self.test_course_id) self.anonymous_user_id = anonymous_id_for_user(self.test_user, self.course.id)
self.test_project = Project.objects.create( self.test_project = Project.objects.create(
course_id=self.test_course_id, course_id=self.test_course_id,
...@@ -113,6 +125,7 @@ class SubmissionReviewsApiTests(TestCase): ...@@ -113,6 +125,7 @@ class SubmissionReviewsApiTests(TestCase):
'reviewer': self.anonymous_user_id, 'reviewer': self.anonymous_user_id,
'question': self.test_question, 'question': self.test_question,
'answer': self.test_answer, 'answer': self.test_answer,
'content_id': self.test_course_content_id,
} }
response = self.do_post(self.test_submission_reviews_uri, data) response = self.do_post(self.test_submission_reviews_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
...@@ -128,6 +141,7 @@ class SubmissionReviewsApiTests(TestCase): ...@@ -128,6 +141,7 @@ class SubmissionReviewsApiTests(TestCase):
self.assertEqual(response.data['submission'], self.test_submission.id) self.assertEqual(response.data['submission'], self.test_submission.id)
self.assertEqual(response.data['question'], self.test_question) self.assertEqual(response.data['question'], self.test_question)
self.assertEqual(response.data['answer'], self.test_answer) self.assertEqual(response.data['answer'], self.test_answer)
self.assertEqual(response.data['content_id'], self.test_course_content_id)
self.assertIsNotNone(response.data['created']) self.assertIsNotNone(response.data['created'])
self.assertIsNotNone(response.data['modified']) self.assertIsNotNone(response.data['modified'])
...@@ -137,6 +151,7 @@ class SubmissionReviewsApiTests(TestCase): ...@@ -137,6 +151,7 @@ class SubmissionReviewsApiTests(TestCase):
'reviewer': self.anonymous_user_id, 'reviewer': self.anonymous_user_id,
'question': self.test_question, 'question': self.test_question,
'answer': self.test_answer, 'answer': self.test_answer,
'content_id': self.test_course_content_id,
} }
response = self.do_post(self.test_submission_reviews_uri, data) response = self.do_post(self.test_submission_reviews_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
...@@ -154,6 +169,7 @@ class SubmissionReviewsApiTests(TestCase): ...@@ -154,6 +169,7 @@ class SubmissionReviewsApiTests(TestCase):
self.assertEqual(response.data['submission'], self.test_submission.id) self.assertEqual(response.data['submission'], self.test_submission.id)
self.assertEqual(response.data['question'], self.test_question) self.assertEqual(response.data['question'], self.test_question)
self.assertEqual(response.data['answer'], self.test_answer) self.assertEqual(response.data['answer'], self.test_answer)
self.assertEqual(response.data['content_id'], self.test_course_content_id)
self.assertIsNotNone(response.data['created']) self.assertIsNotNone(response.data['created'])
self.assertIsNotNone(response.data['modified']) self.assertIsNotNone(response.data['modified'])
......
...@@ -14,6 +14,8 @@ from django.test.utils import override_settings ...@@ -14,6 +14,8 @@ from django.test.utils import override_settings
from projects.models import Project, Workgroup, WorkgroupSubmission from projects.models import Project, Workgroup, WorkgroupSubmission
from student.models import anonymous_id_for_user from student.models import anonymous_id_for_user
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
TEST_API_KEY = str(uuid.uuid4()) TEST_API_KEY = str(uuid.uuid4())
...@@ -29,18 +31,29 @@ class SecureClient(Client): ...@@ -29,18 +31,29 @@ class SecureClient(Client):
@override_settings(EDX_API_KEY=TEST_API_KEY) @override_settings(EDX_API_KEY=TEST_API_KEY)
class WorkgroupReviewsApiTests(TestCase): class WorkgroupReviewsApiTests(ModuleStoreTestCase):
""" Test suite for Users API views """ """ Test suite for Users API views """
def setUp(self): def setUp(self):
super(WorkgroupReviewsApiTests, self).setUp()
self.test_server_prefix = 'https://testserver' self.test_server_prefix = 'https://testserver'
self.test_users_uri = '/api/users/' self.test_users_uri = '/api/users/'
self.test_workgroups_uri = '/api/workgroups/' self.test_workgroups_uri = '/api/workgroups/'
self.test_projects_uri = '/api/projects/' self.test_projects_uri = '/api/projects/'
self.test_workgroup_reviews_uri = '/api/workgroup_reviews/' self.test_workgroup_reviews_uri = '/api/workgroup_reviews/'
self.test_course_id = 'edx/demo/course' self.course = CourseFactory.create()
self.test_data = '<html>{}</html>'.format(str(uuid.uuid4()))
self.chapter = ItemFactory.create(
category="chapter",
parent_location=self.course.location,
data=self.test_data,
display_name="Overview"
)
self.test_course_id = unicode(self.course.id)
self.test_bogus_course_id = 'foo/bar/baz' self.test_bogus_course_id = 'foo/bar/baz'
self.test_course_content_id = "i4x://blah" self.test_course_content_id = "i4x://blah"
self.test_bogus_course_content_id = "14x://foo/bar/baz" self.test_bogus_course_content_id = "14x://foo/bar/baz"
...@@ -53,7 +66,7 @@ class WorkgroupReviewsApiTests(TestCase): ...@@ -53,7 +66,7 @@ class WorkgroupReviewsApiTests(TestCase):
username="testing", username="testing",
is_active=True is_active=True
) )
self.anonymous_user_id = anonymous_id_for_user(self.test_user, self.test_course_id) self.anonymous_user_id = anonymous_id_for_user(self.test_user, self.course.id)
self.test_project = Project.objects.create( self.test_project = Project.objects.create(
course_id=self.test_course_id, course_id=self.test_course_id,
content_id=self.test_course_content_id, content_id=self.test_course_content_id,
...@@ -112,6 +125,7 @@ class WorkgroupReviewsApiTests(TestCase): ...@@ -112,6 +125,7 @@ class WorkgroupReviewsApiTests(TestCase):
'reviewer': self.anonymous_user_id, 'reviewer': self.anonymous_user_id,
'question': self.test_question, 'question': self.test_question,
'answer': self.test_answer, 'answer': self.test_answer,
'content_id': self.test_course_content_id,
} }
response = self.do_post(self.test_workgroup_reviews_uri, data) response = self.do_post(self.test_workgroup_reviews_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
...@@ -127,6 +141,7 @@ class WorkgroupReviewsApiTests(TestCase): ...@@ -127,6 +141,7 @@ class WorkgroupReviewsApiTests(TestCase):
self.assertEqual(response.data['workgroup'], self.test_workgroup.id) self.assertEqual(response.data['workgroup'], self.test_workgroup.id)
self.assertEqual(response.data['question'], self.test_question) self.assertEqual(response.data['question'], self.test_question)
self.assertEqual(response.data['answer'], self.test_answer) self.assertEqual(response.data['answer'], self.test_answer)
self.assertEqual(response.data['content_id'], self.test_course_content_id)
self.assertIsNotNone(response.data['created']) self.assertIsNotNone(response.data['created'])
self.assertIsNotNone(response.data['modified']) self.assertIsNotNone(response.data['modified'])
...@@ -136,6 +151,7 @@ class WorkgroupReviewsApiTests(TestCase): ...@@ -136,6 +151,7 @@ class WorkgroupReviewsApiTests(TestCase):
'reviewer': self.anonymous_user_id, 'reviewer': self.anonymous_user_id,
'question': self.test_question, 'question': self.test_question,
'answer': self.test_answer, 'answer': self.test_answer,
'content_id': self.test_course_content_id,
} }
response = self.do_post(self.test_workgroup_reviews_uri, data) response = self.do_post(self.test_workgroup_reviews_uri, data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
...@@ -153,6 +169,7 @@ class WorkgroupReviewsApiTests(TestCase): ...@@ -153,6 +169,7 @@ class WorkgroupReviewsApiTests(TestCase):
self.assertEqual(response.data['workgroup'], self.test_workgroup.id) self.assertEqual(response.data['workgroup'], self.test_workgroup.id)
self.assertEqual(response.data['question'], self.test_question) self.assertEqual(response.data['question'], self.test_question)
self.assertEqual(response.data['answer'], self.test_answer) self.assertEqual(response.data['answer'], self.test_answer)
self.assertEqual(response.data['content_id'], self.test_course_content_id)
self.assertIsNotNone(response.data['created']) self.assertIsNotNone(response.data['created'])
self.assertIsNotNone(response.data['modified']) self.assertIsNotNone(response.data['modified'])
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
Run these tests @ Devstack: Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/projects/tests/test_workgroups.py] rake fasttest_lms[common/djangoapps/projects/tests/test_workgroups.py]
""" """
from datetime import datetime
import json import json
import uuid import uuid
...@@ -45,14 +46,14 @@ class WorkgroupsApiTests(ModuleStoreTestCase): ...@@ -45,14 +46,14 @@ class WorkgroupsApiTests(ModuleStoreTestCase):
self.test_server_prefix = 'https://testserver' self.test_server_prefix = 'https://testserver'
self.test_workgroups_uri = '/api/workgroups/' self.test_workgroups_uri = '/api/workgroups/'
self.test_bogus_course_id = 'foo/bar/baz' self.test_bogus_course_id = 'foo/bar/baz'
self.test_bogus_course_content_id = "14x://foo/bar/baz" self.test_bogus_course_content_id = "i4x://foo/bar/baz"
self.test_group_id = '1' self.test_group_id = '1'
self.test_bogus_group_id = "2131241123" self.test_bogus_group_id = "2131241123"
self.test_workgroup_name = str(uuid.uuid4()) self.test_workgroup_name = str(uuid.uuid4())
self.test_course = CourseFactory.create( self.test_course = CourseFactory.create(
start="2014-06-16T14:30:00Z", start=datetime(2014, 6, 16, 14, 30),
end="2015-01-16T14:30:00Z" end=datetime(2015, 1, 16, 14, 30)
) )
self.test_data = '<html>{}</html>'.format(str(uuid.uuid4())) self.test_data = '<html>{}</html>'.format(str(uuid.uuid4()))
...@@ -60,12 +61,12 @@ class WorkgroupsApiTests(ModuleStoreTestCase): ...@@ -60,12 +61,12 @@ class WorkgroupsApiTests(ModuleStoreTestCase):
category="group_project", category="group_project",
parent_location=self.test_course.location, parent_location=self.test_course.location,
data=self.test_data, data=self.test_data,
due="2014-05-16T14:30:00Z", due=datetime(2014, 5, 16, 14, 30),
display_name="Group Project" display_name="Group Project"
) )
self.test_course_id = self.test_course.id self.test_course_id = unicode(self.test_course.id)
self.test_course_content_id = self.test_group_project.id self.test_course_content_id = unicode(self.test_group_project.scope_ids.usage_id)
self.test_group_name = str(uuid.uuid4()) self.test_group_name = str(uuid.uuid4())
self.test_group = Group.objects.create( self.test_group = Group.objects.create(
...@@ -137,7 +138,6 @@ class WorkgroupsApiTests(ModuleStoreTestCase): ...@@ -137,7 +138,6 @@ class WorkgroupsApiTests(ModuleStoreTestCase):
response = self.client.delete_with_data(uri, data, headers=headers) response = self.client.delete_with_data(uri, data, headers=headers)
return response return response
def test_workgroups_list_post(self): def test_workgroups_list_post(self):
data = { data = {
'name': self.test_workgroup_name, 'name': self.test_workgroup_name,
...@@ -403,6 +403,68 @@ class WorkgroupsApiTests(ModuleStoreTestCase): ...@@ -403,6 +403,68 @@ class WorkgroupsApiTests(ModuleStoreTestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data['grades']), 0) self.assertGreater(len(response.data['grades']), 0)
def test_workgroups_grades_post_invalid_course(self):
data = {
'name': self.test_workgroup_name,
'project': self.test_project.id
}
response = self.do_post(self.test_workgroups_uri, data)
self.assertEqual(response.status_code, 201)
workgroup_id = response.data['id']
users_uri = '{}{}/users/'.format(self.test_workgroups_uri, workgroup_id)
data = {"id": self.test_user.id}
response = self.do_post(users_uri, data)
self.assertEqual(response.status_code, 201)
data = {"id": self.test_user2.id}
response = self.do_post(users_uri, data)
self.assertEqual(response.status_code, 201)
grade_data = {
'course_id': self.test_bogus_course_id,
'content_id': self.test_course_content_id,
'grade': 0.85,
'max_grade': 0.75,
}
grades_uri = '{}{}/grades/'.format(self.test_workgroups_uri, workgroup_id)
response = self.do_post(grades_uri, grade_data)
self.assertEqual(response.status_code, 400)
grade_data = {
'course_id': "really-invalid-course-id",
'content_id': self.test_course_content_id,
'grade': 0.85,
'max_grade': 0.75,
}
grades_uri = '{}{}/grades/'.format(self.test_workgroups_uri, workgroup_id)
response = self.do_post(grades_uri, grade_data)
self.assertEqual(response.status_code, 400)
def test_workgroups_grades_post_invalid_course_content(self):
data = {
'name': self.test_workgroup_name,
'project': self.test_project.id
}
response = self.do_post(self.test_workgroups_uri, data)
self.assertEqual(response.status_code, 201)
workgroup_id = response.data['id']
users_uri = '{}{}/users/'.format(self.test_workgroups_uri, workgroup_id)
data = {"id": self.test_user.id}
response = self.do_post(users_uri, data)
self.assertEqual(response.status_code, 201)
data = {"id": self.test_user2.id}
response = self.do_post(users_uri, data)
self.assertEqual(response.status_code, 201)
grade_data = {
'course_id': self.test_course_id,
'content_id': self.test_bogus_course_content_id,
'grade': 0.85,
'max_grade': 0.75,
}
grades_uri = '{}{}/grades/'.format(self.test_workgroups_uri, workgroup_id)
response = self.do_post(grades_uri, grade_data)
self.assertEqual(response.status_code, 400)
def test_workgroups_grades_post_invalid_requests(self): def test_workgroups_grades_post_invalid_requests(self):
data = { data = {
'name': self.test_workgroup_name, 'name': self.test_workgroup_name,
......
...@@ -13,9 +13,15 @@ from rest_framework.response import Response ...@@ -13,9 +13,15 @@ from rest_framework.response import Response
from xblock.fields import Scope from xblock.fields import Scope
from xblock.runtime import KeyValueStore from xblock.runtime import KeyValueStore
from courseware import module_render
from courseware.courses import get_course from courseware.courses import get_course
from courseware.model_data import FieldDataCache from courseware.model_data import FieldDataCache
from xmodule.modulestore import Location from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
from xmodule.modulestore import Location, InvalidLocationError
from xmodule.modulestore.django import modulestore
from .models import Project, Workgroup, WorkgroupSubmission from .models import Project, Workgroup, WorkgroupSubmission
from .models import WorkgroupReview, WorkgroupSubmissionReview, WorkgroupPeerReview from .models import WorkgroupReview, WorkgroupSubmissionReview, WorkgroupPeerReview
...@@ -24,6 +30,64 @@ from .serializers import ProjectSerializer, WorkgroupSerializer, WorkgroupSubmis ...@@ -24,6 +30,64 @@ from .serializers import ProjectSerializer, WorkgroupSerializer, WorkgroupSubmis
from .serializers import WorkgroupReviewSerializer, WorkgroupSubmissionReviewSerializer, WorkgroupPeerReviewSerializer from .serializers import WorkgroupReviewSerializer, WorkgroupSubmissionReviewSerializer, WorkgroupPeerReviewSerializer
def _get_course(request, user, course_id, depth=0):
"""
Utility method to obtain course components
"""
course_descriptor = None
course_key = None
course_content = None
try:
course_key = CourseKey.from_string(course_id)
except InvalidKeyError:
try:
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
except InvalidKeyError:
pass
if course_key:
try:
course_descriptor = get_course(course_key, depth=depth)
except ValueError:
pass
if course_descriptor:
field_data_cache = FieldDataCache([course_descriptor], course_key, user)
course_content = module_render.get_module(
user,
request,
course_descriptor.location,
field_data_cache,
course_key)
return course_descriptor, course_key, course_content
def _get_course_child(request, user, course_key, content_id):
"""
Return a course xmodule/xblock to the caller
"""
content_descriptor = None
content_key = None
content = None
try:
content_key = UsageKey.from_string(content_id)
except InvalidKeyError:
try:
content_key = Location.from_deprecated_string(content_id)
except (InvalidKeyError, InvalidLocationError):
pass
if content_key:
store = modulestore()
content_descriptor = store.get_item(content_key)
if content_descriptor:
field_data_cache = FieldDataCache([content_descriptor], course_key, user)
content = module_render.get_module(
user,
request,
content_key,
field_data_cache,
course_key)
return content_descriptor, content_key, content
class GroupViewSet(viewsets.ModelViewSet): class GroupViewSet(viewsets.ModelViewSet):
""" """
Django Rest Framework ViewSet for the Group model (auth_group). Django Rest Framework ViewSet for the Group model (auth_group).
...@@ -58,7 +122,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet): ...@@ -58,7 +122,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
if groups: if groups:
for group in groups: for group in groups:
serializer = GroupSerializer(group) serializer = GroupSerializer(group)
response_data.append(serializer.data) response_data.append(serializer.data) # pylint: disable=E1101
return Response(response_data, status=status.HTTP_200_OK) return Response(response_data, status=status.HTTP_200_OK)
else: else:
group_id = request.DATA.get('id') group_id = request.DATA.get('id')
...@@ -84,7 +148,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet): ...@@ -84,7 +148,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
if users: if users:
for user in users: for user in users:
serializer = UserSerializer(user) serializer = UserSerializer(user)
response_data.append(serializer.data) response_data.append(serializer.data) # pylint: disable=E1101
return Response(response_data, status=status.HTTP_200_OK) return Response(response_data, status=status.HTTP_200_OK)
elif request.method == 'POST': elif request.method == 'POST':
user_id = request.DATA.get('id') user_id = request.DATA.get('id')
...@@ -118,7 +182,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet): ...@@ -118,7 +182,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
if peer_reviews: if peer_reviews:
for peer_review in peer_reviews: for peer_review in peer_reviews:
serializer = WorkgroupPeerReviewSerializer(peer_review) serializer = WorkgroupPeerReviewSerializer(peer_review)
response_data.append(serializer.data) response_data.append(serializer.data) # pylint: disable=E1101
return Response(response_data, status=status.HTTP_200_OK) return Response(response_data, status=status.HTTP_200_OK)
@link() @link()
...@@ -131,7 +195,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet): ...@@ -131,7 +195,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
if workgroup_reviews: if workgroup_reviews:
for workgroup_review in workgroup_reviews: for workgroup_review in workgroup_reviews:
serializer = WorkgroupReviewSerializer(workgroup_review) serializer = WorkgroupReviewSerializer(workgroup_review)
response_data.append(serializer.data) response_data.append(serializer.data) # pylint: disable=E1101
return Response(response_data, status=status.HTTP_200_OK) return Response(response_data, status=status.HTTP_200_OK)
@link() @link()
...@@ -144,7 +208,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet): ...@@ -144,7 +208,7 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
if submissions: if submissions:
for submission in submissions: for submission in submissions:
serializer = WorkgroupSubmissionSerializer(submission) serializer = WorkgroupSubmissionSerializer(submission)
response_data.append(serializer.data) response_data.append(serializer.data) # pylint: disable=E1101
return Response(response_data, status=status.HTTP_200_OK) return Response(response_data, status=status.HTTP_200_OK)
@action() @action()
...@@ -156,16 +220,16 @@ class WorkgroupsViewSet(viewsets.ModelViewSet): ...@@ -156,16 +220,16 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
course_id = request.DATA.get('course_id') course_id = request.DATA.get('course_id')
if course_id is None: if course_id is None:
return Response({}, status=status.HTTP_400_BAD_REQUEST) return Response({}, status=status.HTTP_400_BAD_REQUEST)
try: course_descriptor, course_key, course_content = _get_course(request, request.user, course_id) # pylint: disable=W0612
course_descriptor = get_course(course_id)
except ValueError:
course_descriptor = None
if not course_descriptor: if not course_descriptor:
return Response({}, status=status.HTTP_400_BAD_REQUEST) return Response({}, status=status.HTTP_400_BAD_REQUEST)
content_id = request.DATA.get('content_id') content_id = request.DATA.get('content_id')
if content_id is None: if content_id is None:
return Response({}, status=status.HTTP_400_BAD_REQUEST) return Response({}, status=status.HTTP_400_BAD_REQUEST)
content_descriptor, content_key, content = _get_course_child(request, request.user, course_key, content_id) # pylint: disable=W0612
if content_descriptor is None:
return Response({}, status=status.HTTP_400_BAD_REQUEST)
grade = request.DATA.get('grade') grade = request.DATA.get('grade')
if grade is None: if grade is None:
...@@ -182,10 +246,10 @@ class WorkgroupsViewSet(viewsets.ModelViewSet): ...@@ -182,10 +246,10 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
key = KeyValueStore.Key( key = KeyValueStore.Key(
scope=Scope.user_state, scope=Scope.user_state,
user_id=user.id, user_id=user.id,
block_scope_id=Location(content_id), block_scope_id=content_key,
field_name='grade' field_name='grade'
) )
field_data_cache = FieldDataCache([course_descriptor], course_id, user) field_data_cache = FieldDataCache([course_descriptor], course_key, user)
student_module = field_data_cache.find_or_create(key) student_module = field_data_cache.find_or_create(key)
student_module.grade = grade student_module.grade = grade
student_module.max_grade = max_grade student_module.max_grade = max_grade
...@@ -211,7 +275,7 @@ class ProjectsViewSet(viewsets.ModelViewSet): ...@@ -211,7 +275,7 @@ class ProjectsViewSet(viewsets.ModelViewSet):
if workgroups: if workgroups:
for workgroup in workgroups: for workgroup in workgroups:
serializer = WorkgroupSerializer(workgroup) serializer = WorkgroupSerializer(workgroup)
response_data.append(serializer.data) response_data.append(serializer.data) # pylint: disable=E1101
return Response(response_data, status=status.HTTP_200_OK) return Response(response_data, status=status.HTTP_200_OK)
else: else:
workgroup_id = request.DATA.get('id') workgroup_id = request.DATA.get('id')
......
...@@ -421,6 +421,9 @@ FEATURES = { ...@@ -421,6 +421,9 @@ FEATURES = {
# The block types to disable need to be specified in "x block disable config" in django admin. # The block types to disable need to be specified in "x block disable config" in django admin.
'ENABLE_DISABLING_XBLOCK_TYPES': True, 'ENABLE_DISABLING_XBLOCK_TYPES': True,
# Whether an xBlock publishing a 'grade' event should be considered a 'progress' event as well
'MARK_PROGRESS_ON_GRADING_EVENT': False
} }
# Ignore static asset files on import which match this pattern # Ignore static asset files on import which match this pattern
......
...@@ -100,6 +100,9 @@ CC_PROCESSOR = { ...@@ -100,6 +100,9 @@ CC_PROCESSOR = {
FEATURES['API'] = True FEATURES['API'] = True
########################## USER API ########################
EDX_API_KEY = None
########################### External REST APIs ################################# ########################### External REST APIs #################################
FEATURES['ENABLE_OAUTH2_PROVIDER'] = True FEATURES['ENABLE_OAUTH2_PROVIDER'] = True
......
...@@ -161,6 +161,17 @@ class User(models.Model): ...@@ -161,6 +161,17 @@ class User(models.Model):
self._update_from_response(response) self._update_from_response(response)
def get_course_social_stats(course_id):
url = _url_for_course_social_stats()
params = {'course_id': course_id}
response = perform_request(
'get',
url,
params
)
return response
def _url_for_vote_comment(comment_id): def _url_for_vote_comment(comment_id):
return "{prefix}/comments/{comment_id}/votes".format(prefix=settings.PREFIX, comment_id=comment_id) return "{prefix}/comments/{comment_id}/votes".format(prefix=settings.PREFIX, comment_id=comment_id)
...@@ -186,3 +197,6 @@ def _url_for_user_stats(user_id,course_id): ...@@ -186,3 +197,6 @@ def _url_for_user_stats(user_id,course_id):
def _url_for_user_social_stats(user_id): def _url_for_user_social_stats(user_id):
return "{prefix}/users/{user_id}/social_stats".format(prefix=settings.PREFIX, user_id=user_id) return "{prefix}/users/{user_id}/social_stats".format(prefix=settings.PREFIX, user_id=user_id)
def _url_for_course_social_stats():
return "{prefix}/users/*/social_stats".format(prefix=settings.PREFIX)
...@@ -20,20 +20,22 @@ class UserTagsEventContextMiddleware(object): ...@@ -20,20 +20,22 @@ class UserTagsEventContextMiddleware(object):
""" """
Add a user's tags to the tracking event context. Add a user's tags to the tracking event context.
""" """
match = COURSE_REGEX.match(request.build_absolute_uri()) match = COURSE_REGEX.match(request.path)
course_id = None course_key = None
if match: if match:
course_id = match.group('course_id') course_key = match.group('course_id')
try: try:
course_key = CourseKey.from_string(course_id) course_key = CourseKey.from_string(course_key)
except InvalidKeyError: except InvalidKeyError:
course_id = None
course_key = None course_key = None
context = {} context = {}
if course_id: if course_key:
context['course_id'] = course_id try:
context['course_id'] = course_key.to_deprecated_string()
except AttributeError:
context['course_id'] = unicode(course_key)
if request.user.is_authenticated(): if request.user.is_authenticated():
context['course_user_tags'] = dict( context['course_user_tags'] = dict(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment