Commit bf5bf27b by Matt Drayer Committed by Jonathan Piacenti

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

Also contains:

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