Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
913252d4
Commit
913252d4
authored
May 06, 2014
by
Matt Drayer
Committed by
Jonathan Piacenti
Aug 20, 2015
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
mattdrayer/api-quality: Quality updates
parent
af299af1
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
177 additions
and
178 deletions
+177
-178
lms/djangoapps/api_manager/courses/content.py
+69
-65
lms/djangoapps/api_manager/courses/tests.py
+4
-7
lms/djangoapps/api_manager/courses/urls.py
+2
-1
lms/djangoapps/api_manager/courses/views.py
+25
-17
lms/djangoapps/api_manager/groups/tests.py
+0
-5
lms/djangoapps/api_manager/groups/urls.py
+2
-1
lms/djangoapps/api_manager/groups/views.py
+16
-18
lms/djangoapps/api_manager/migrations/0001_initial.py
+1
-4
lms/djangoapps/api_manager/migrations/0002_auto__add_coursegrouprelationship__add_groupprofile.py
+1
-4
lms/djangoapps/api_manager/migrations/0003_move_name_to_profile.py
+1
-4
lms/djangoapps/api_manager/sessions/test_login_ratelimit.py
+0
-2
lms/djangoapps/api_manager/sessions/test_security.py
+1
-1
lms/djangoapps/api_manager/sessions/urls.py
+2
-1
lms/djangoapps/api_manager/sessions/views.py
+3
-3
lms/djangoapps/api_manager/system/views.py
+5
-5
lms/djangoapps/api_manager/urls.py
+9
-8
lms/djangoapps/api_manager/users/test_user_password_reset.py
+12
-12
lms/djangoapps/api_manager/users/tests.py
+3
-4
lms/djangoapps/api_manager/users/urls.py
+2
-1
lms/djangoapps/api_manager/users/views.py
+19
-15
No files found.
lms/djangoapps/api_manager/courses/content.py
View file @
913252d4
...
...
@@ -5,80 +5,84 @@ text space
from
textwrap
import
dedent
TEST_COURSE_UPDATES_CONTENT
=
dedent
(
"""
<ol>
<li>
<h2>April 18, 2014</h2>
This does not have a paragraph tag around it
</li>
<li>
<h2>April 17, 2014</h2>
Some text before paragraph tag<p>This is inside paragraph tag</p>Some text after tag
</li>
<li>
<h2>April 16, 2014</h2>
Some text before paragraph tag<p>This is inside paragraph tag</p>Some text after tag<p>one more</p>
</li>
<li>
<h2>April 15, 2014</h2>
<p>A perfectly</p><p>formatted piece</p><p>of HTML</p>
</li>
</ol>
"""
TEST_COURSE_UPDATES_CONTENT
=
dedent
(
"""
<ol>
<li>
<h2>April 18, 2014</h2>
This does not have a paragraph tag around it
</li>
<li>
<h2>April 17, 2014</h2>
Some text before paragraph tag<p>This is inside paragraph tag</p>Some text after tag
</li>
<li>
<h2>April 16, 2014</h2>
Some text before paragraph tag<p>This is inside paragraph tag</p>Some text after tag<p>one more</p>
</li>
<li>
<h2>April 15, 2014</h2>
<p>A perfectly</p><p>formatted piece</p><p>of HTML</p>
</li>
</ol>
"""
)
TEST_STATIC_TAB1_CONTENT
=
dedent
(
"""
<div>This is static tab1</div>
"""
TEST_STATIC_TAB1_CONTENT
=
dedent
(
"""
<div>This is static tab1</div>
"""
)
TEST_STATIC_TAB2_CONTENT
=
dedent
(
"""
<div>This is static tab2</div>
"""
TEST_STATIC_TAB2_CONTENT
=
dedent
(
"""
<div>This is static tab2</div>
"""
)
TEST_COURSE_OVERVIEW_CONTENT
=
dedent
(
"""
<section class="about">
<h2>About This Course</h2>
<p>Include your long course description here. The long course description should contain 150-400 words.</p>
TEST_COURSE_OVERVIEW_CONTENT
=
dedent
(
"""
<section class="about">
<h2>About This Course</h2>
<p>Include your long course description here. The long course description should contain 150-400 words.</p>
<p>This is paragraph 2 of the long course description. Add more paragraphs as needed. Make sure to enclose them in paragraph tags.</p>
</section>
<p>This is paragraph 2 of the long course description. Add more paragraphs as needed. Make sure to enclose them in paragraph tags.</p>
</section>
<section class="prerequisites">
<h2>Prerequisites</h2>
<p>Add information about course prerequisites here.</p>
</section>
<section class="prerequisites">
<h2>Prerequisites</h2>
<p>Add information about course prerequisites here.</p>
</section>
<section class="course-staff">
<h2>Course Staff</h2>
<article class="teacher">
<div class="teacher-image">
<img src="/images/pl-faculty.png" align="left" style="margin:0 20 px 0" alt="Course Staff Image #1">
</div>
<h3>Staff Member #1</h3>
<p>Biography of instructor/staff member #1</p>
</article>
<section class="course-staff">
<h2>Course Staff</h2>
<article class="teacher">
<div class="teacher-image">
<img src="/images/pl-faculty.png" align="left" style="margin:0 20 px 0" alt="Course Staff Image #1">
</div>
<h3>Staff Member #1</h3>
<p>Biography of instructor/staff member #1</p>
</article>
<article class="teacher">
<div class="teacher-image">
<img src="/images/pl-faculty.png" align="left" style="margin:0 20 px 0" alt="Course Staff Image #2">
</div>
<h3>Staff Member #2</h3>
<p>Biography of instructor/staff member #2</p>
</article>
<article class="teacher">
<div class="teacher-image">
<img src="/images/pl-faculty.png" align="left" style="margin:0 20 px 0" alt="Course Staff Image #2">
</div>
<h3>Staff Member #2</h3>
<p>Biography of instructor/staff member #2</p>
</article>
<article class="author">
<div class="author-image">
<img src="/images/pl-author.png" align="left" style="margin:0 20 px 0" alt="Author Name">
</div>
<h3>Author Name</h3>
<p>Biography of Author Name</p>
</article>
</section>
<section class="faq">
<p>Some text here</p>
</section>
"""
)
<article class="author">
<div class="author-image">
<img src="/images/pl-author.png" align="left" style="margin:0 20 px 0" alt="Author Name">
</div>
<h3>Author Name</h3>
<p>Biography of Author Name</p>
</article>
</section>
<section class="faq">
<p>Some text here</p>
</section>
"""
)
lms/djangoapps/api_manager/courses/tests.py
View file @
913252d4
...
...
@@ -2,10 +2,9 @@
"""
Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/
tests/test_group_view
s.py]
rake fasttest_lms[common/djangoapps/api_manager/
courses/test
s.py]
"""
import
simplejson
as
json
import
unittest
import
uuid
from
random
import
randint
...
...
@@ -36,7 +35,6 @@ class CoursesApiTests(TestCase):
""" Test suite for Courses API views """
def
setUp
(
self
):
self
.
maxDiff
=
3000
self
.
test_server_prefix
=
'https://testserver'
self
.
base_courses_uri
=
'/api/courses'
self
.
base_groups_uri
=
'/api/groups'
...
...
@@ -137,6 +135,7 @@ class CoursesApiTests(TestCase):
return
response
def
_find_item_by_class
(
self
,
items
,
class_name
):
"""Helper method to match a single matching item"""
for
item
in
items
:
if
item
[
'class'
]
==
class_name
:
return
item
...
...
@@ -444,7 +443,7 @@ class CoursesApiTests(TestCase):
#try a bogus course_id to test failure case
test_course
=
CourseFactory
.
create
()
test_uri
=
'{}/{}/overview'
.
format
(
self
.
base_courses_uri
,
test_course
.
id
)
test_updates
=
ItemFactory
.
create
(
ItemFactory
.
create
(
category
=
"about"
,
parent_location
=
test_course
.
location
,
data
=
''
,
...
...
@@ -487,8 +486,7 @@ class CoursesApiTests(TestCase):
def
test_courses_updates_get_invalid_content
(
self
):
#try a bogus course_id to test failure case
test_course
=
CourseFactory
.
create
()
test_course_data
=
'<html>{}</html>'
.
format
(
str
(
uuid
.
uuid4
()))
test_updates
=
ItemFactory
.
create
(
ItemFactory
.
create
(
category
=
"course_info"
,
parent_location
=
test_course
.
location
,
data
=
''
,
...
...
@@ -604,7 +602,6 @@ class CoursesApiTests(TestCase):
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
len
(
response
.
data
[
'enrollments'
]),
0
)
def
test_courses_users_list_post_existing_user
(
self
):
# create a new user (note, this calls into the /users/ subsystem)
test_uri
=
self
.
base_courses_uri
+
'/'
+
self
.
test_course_id
+
'/users'
...
...
lms/djangoapps/api_manager/courses/urls.py
View file @
913252d4
...
...
@@ -8,7 +8,8 @@ from rest_framework.urlpatterns import format_suffix_patterns
from
api_manager.courses
import
views
as
courses_views
urlpatterns
=
patterns
(
''
,
urlpatterns
=
patterns
(
''
,
url
(
r'/*$^'
,
courses_views
.
CoursesList
.
as_view
()),
url
(
r'^(?P<course_id>[^/]+/[^/]+/[^/]+)$'
,
courses_views
.
CoursesDetail
.
as_view
()),
url
(
r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/modules/(?P<module_id>[a-zA-Z0-9/_:]+)/submodules/*$'
,
courses_views
.
ModulesList
.
as_view
()),
...
...
lms/djangoapps/api_manager/courses/views.py
View file @
913252d4
...
...
@@ -27,6 +27,7 @@ from xmodule.modulestore import Location, InvalidLocationError
log
=
logging
.
getLogger
(
__name__
)
def
_generate_base_uri
(
request
):
"""
Constructs the protocol:host:path component of the resource uri
...
...
@@ -129,7 +130,7 @@ def _serialize_module_with_children(request, course_descriptor, descriptor, dept
request
,
course_descriptor
,
child
,
depth
-
1
depth
-
1
))
return
data
...
...
@@ -240,7 +241,7 @@ def _parse_updates_html(html):
class
ModulesList
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
get
(
self
,
request
,
course_id
,
module_id
=
None
,
format
=
None
):
def
get
(
self
,
request
,
course_id
,
module_id
=
None
):
"""
GET retrieves the list of submodules for a given module
We don't know where in the module hierarchy we are -- could even be the top
...
...
@@ -273,7 +274,7 @@ class ModulesList(APIView):
class
ModulesDetail
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
get
(
self
,
request
,
course_id
,
module_id
,
format
=
None
):
def
get
(
self
,
request
,
course_id
,
module_id
):
"""
GET retrieves an existing module from the system
"""
...
...
@@ -308,7 +309,7 @@ class ModulesDetail(APIView):
class
CoursesList
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
get
(
self
,
request
,
format
=
None
):
def
get
(
self
,
request
):
"""
GET returns the list of available courses
"""
...
...
@@ -328,7 +329,7 @@ class CoursesList(APIView):
class
CoursesDetail
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
get
(
self
,
request
,
course_id
,
format
=
None
):
def
get
(
self
,
request
,
course_id
):
"""
GET retrieves an existing course from the system and returns summary information about the submodules
to the specified depth
...
...
@@ -365,7 +366,7 @@ class CoursesDetail(APIView):
class
CoursesGroupsList
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
post
(
self
,
request
,
course_id
,
format
=
None
):
def
post
(
self
,
request
,
course_id
):
"""
POST creates a new course-group relationship in the system
"""
...
...
@@ -402,7 +403,7 @@ class CoursesGroupsList(APIView):
class
CoursesGroupsDetail
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
get
(
self
,
request
,
course_id
,
group_id
,
format
=
None
):
def
get
(
self
,
request
,
course_id
,
group_id
):
"""
GET retrieves an existing course-group relationship from the system
"""
...
...
@@ -432,7 +433,7 @@ class CoursesGroupsDetail(APIView):
response_status
=
status
.
HTTP_404_NOT_FOUND
return
Response
(
response_data
,
status
=
response_status
)
def
delete
(
self
,
request
,
course_id
,
group_id
,
format
=
None
):
def
delete
(
self
,
request
,
course_id
,
group_id
):
"""
DELETE removes/inactivates/etc. an existing course-group relationship
"""
...
...
@@ -441,13 +442,15 @@ class CoursesGroupsDetail(APIView):
existing_relationship
=
CourseGroupRelationship
.
objects
.
get
(
course_id
=
course_id
,
group
=
existing_group
)
.
delete
()
except
ObjectDoesNotExist
:
pass
return
Response
({},
status
=
status
.
HTTP_204_NO_CONTENT
)
response_data
=
{}
response_data
[
'uri'
]
=
_generate_base_uri
(
request
)
return
Response
(
response_data
,
status
=
status
.
HTTP_204_NO_CONTENT
)
class
CoursesOverview
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
get
(
self
,
request
,
course_id
,
format
=
None
):
def
get
(
self
,
request
,
course_id
):
"""
GET retrieves the course overview module, which - in MongoDB - is stored with the following
naming convention {"_id.org":"i4x", "_id.course":<course_num>, "_id.category":"about", "_id.name":"overview"}
...
...
@@ -474,7 +477,7 @@ class CoursesOverview(APIView):
class
CoursesUpdates
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
get
(
self
,
request
,
course_id
,
format
=
None
):
def
get
(
self
,
request
,
course_id
):
"""
GET retrieves the course overview module, which - in MongoDB - is stored with the following
naming convention {"_id.org":"i4x", "_id.course":<course_num>, "_id.category":"course_info", "_id.name":"updates"}
...
...
@@ -561,7 +564,7 @@ class CoursesStaticTabsDetail(APIView):
class
CoursesUsersList
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
post
(
self
,
request
,
course_id
,
format
=
None
):
def
post
(
self
,
request
,
course_id
):
"""
POST enrolls a student in the course. Note, this can be a user_id or
just an email, in case the user does not exist in the system
...
...
@@ -603,11 +606,13 @@ class CoursesUsersList(APIView):
else
:
return
Response
({},
status
=
status
.
HTTP_400_BAD_REQUEST
)
def
get
(
self
,
request
,
course_id
,
format
=
None
):
def
get
(
self
,
request
,
course_id
):
"""
GET returns a list of users enrolled in the course_id
"""
response_data
=
OrderedDict
()
base_uri
=
_generate_base_uri
(
request
)
response_data
[
'uri'
]
=
base_uri
try
:
existing_course
=
get_course
(
course_id
)
except
ValueError
:
...
...
@@ -634,10 +639,11 @@ class CoursesUsersList(APIView):
response_data
[
'pending_enrollments'
]
.
append
(
cea
.
email
)
return
Response
(
response_data
)
class
CoursesUsersDetail
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
get
(
self
,
request
,
course_id
,
user_id
,
format
=
None
):
def
get
(
self
,
request
,
course_id
,
user_id
):
"""
GET identifies an ACTIVE course enrollment for the specified user
"""
...
...
@@ -671,11 +677,10 @@ class CoursesUsersDetail(APIView):
response_status
=
status
.
HTTP_404_NOT_FOUND
return
Response
(
response_data
,
status
=
response_status
)
def
delete
(
self
,
request
,
course_id
,
user_id
,
format
=
None
):
def
delete
(
self
,
request
,
course_id
,
user_id
):
"""
DELETE unenrolls the specified user from the specified course
"""
response_data
=
OrderedDict
()
try
:
existing_course
=
get_course
(
course_id
)
except
ValueError
:
...
...
@@ -688,4 +693,7 @@ class CoursesUsersDetail(APIView):
user
=
None
if
user
:
CourseEnrollment
.
unenroll
(
user
,
course_id
)
return
Response
({},
status
=
status
.
HTTP_204_NO_CONTENT
)
response_data
=
{}
base_uri
=
_generate_base_uri
(
request
)
response_data
[
'uri'
]
=
base_uri
return
Response
(
response_data
,
status
=
status
.
HTTP_204_NO_CONTENT
)
lms/djangoapps/api_manager/groups/tests.py
View file @
913252d4
...
...
@@ -344,7 +344,6 @@ class GroupsApiTests(ModuleStoreTestCase):
self
.
assertEqual
(
users
[
0
][
'first_name'
],
'Joe'
)
self
.
assertEqual
(
users
[
0
][
'last_name'
],
'Smith'
)
def
test_group_users_list_get_invalid_group
(
self
):
test_uri
=
self
.
base_groups_uri
+
'/1231241/users'
response
=
self
.
do_get
(
test_uri
)
...
...
@@ -582,7 +581,6 @@ class GroupsApiTests(ModuleStoreTestCase):
data
=
{
'name'
:
'Tango Group'
}
tango_response
=
self
.
do_post
(
self
.
base_groups_uri
,
data
)
self
.
assertEqual
(
tango_response
.
status_code
,
201
)
tango_group_id
=
tango_response
.
data
[
'id'
]
tango_uri
=
tango_response
.
data
[
'uri'
]
data
=
{
'group_id'
:
bravo_group_id
,
'relationship_type'
:
relationship_type
}
tango_groups_uri
=
tango_uri
+
'/groups'
...
...
@@ -756,7 +754,6 @@ class GroupsApiTests(ModuleStoreTestCase):
data
=
{
'name'
:
self
.
test_group_name
}
response
=
self
.
do_post
(
self
.
base_groups_uri
,
data
)
self
.
assertEqual
(
response
.
status_code
,
201
)
group_id
=
response
.
data
[
'id'
]
test_uri
=
response
.
data
[
'uri'
]
+
'/courses'
data
=
{
'course_id'
:
self
.
test_course_id
}
response
=
self
.
do_post
(
test_uri
,
data
)
...
...
@@ -774,7 +771,6 @@ class GroupsApiTests(ModuleStoreTestCase):
data
=
{
'name'
:
self
.
test_group_name
}
response
=
self
.
do_post
(
self
.
base_groups_uri
,
data
)
self
.
assertEqual
(
response
.
status_code
,
201
)
group_id
=
response
.
data
[
'id'
]
test_uri
=
response
.
data
[
'uri'
]
+
'/courses'
data
=
{
'course_id'
:
"987/23/896"
}
response
=
self
.
do_post
(
test_uri
,
data
)
...
...
@@ -859,7 +855,6 @@ class GroupsApiTests(ModuleStoreTestCase):
data
=
{
'name'
:
self
.
test_group_name
}
response
=
self
.
do_post
(
self
.
base_groups_uri
,
data
)
self
.
assertEqual
(
response
.
status_code
,
201
)
group_id
=
response
.
data
[
'id'
]
test_uri
=
'{}/courses/{}'
.
format
(
response
.
data
[
'uri'
],
self
.
course
.
id
)
response
=
self
.
do_get
(
test_uri
)
self
.
assertEqual
(
response
.
status_code
,
404
)
lms/djangoapps/api_manager/groups/urls.py
View file @
913252d4
...
...
@@ -5,7 +5,8 @@ from rest_framework.urlpatterns import format_suffix_patterns
from
api_manager.groups
import
views
as
groups_views
urlpatterns
=
patterns
(
''
,
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/*$'
,
groups_views
.
GroupsCoursesList
.
as_view
()),
...
...
lms/djangoapps/api_manager/groups/views.py
View file @
913252d4
...
...
@@ -38,7 +38,7 @@ def _generate_base_uri(request):
class
GroupsList
(
APIView
):
permissions_classes
=
(
ApiKeyHeaderPermission
,)
def
post
(
self
,
request
,
format
=
None
):
def
post
(
self
,
request
):
"""
POST creates a new group in the system
"""
...
...
@@ -74,7 +74,7 @@ class GroupsList(APIView):
response_status
=
status
.
HTTP_201_CREATED
return
Response
(
response_data
,
status
=
response_status
)
def
get
(
self
,
request
,
format
=
None
):
def
get
(
self
,
request
):
"""
GET retrieves a list of groups in the system filtered by type
"""
...
...
@@ -103,7 +103,7 @@ class GroupsList(APIView):
class
GroupsDetail
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
post
(
self
,
request
,
group_id
,
format
=
None
):
def
post
(
self
,
request
,
group_id
):
response_data
=
{}
base_uri
=
_generate_base_uri
(
request
)
print
base_uri
...
...
@@ -124,7 +124,7 @@ class GroupsDetail(APIView):
response_data
[
'uri'
]
=
_generate_base_uri
(
request
)
return
Response
(
response_data
,
status
=
status
.
HTTP_201_CREATED
)
def
get
(
self
,
request
,
group_id
,
format
=
None
):
def
get
(
self
,
request
,
group_id
):
"""
GET retrieves an existing group from the system
"""
...
...
@@ -163,7 +163,7 @@ class GroupsDetail(APIView):
class
GroupsUsersList
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
post
(
self
,
request
,
group_id
,
format
=
None
):
def
post
(
self
,
request
,
group_id
):
"""
POST creates a new group-user relationship in the system
"""
...
...
@@ -194,7 +194,7 @@ class GroupsUsersList(APIView):
response_status
=
status
.
HTTP_409_CONFLICT
return
Response
(
response_data
,
status
=
response_status
)
def
get
(
self
,
request
,
group_id
,
format
=
None
):
def
get
(
self
,
request
,
group_id
):
"""
GET retrieves the list of users related to the specified group
"""
...
...
@@ -220,7 +220,7 @@ class GroupsUsersList(APIView):
class
GroupsUsersDetail
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
get
(
self
,
request
,
group_id
,
user_id
,
format
=
None
):
def
get
(
self
,
request
,
group_id
,
user_id
):
"""
GET retrieves an existing group-user relationship from the system
"""
...
...
@@ -241,8 +241,7 @@ class GroupsUsersDetail(APIView):
response_status
=
status
.
HTTP_404_NOT_FOUND
return
Response
(
response_data
,
status
=
response_status
)
def
delete
(
self
,
request
,
group_id
,
user_id
,
format
=
None
):
def
delete
(
self
,
request
,
group_id
,
user_id
):
"""
DELETE removes/inactivates/etc. an existing group-user relationship
"""
...
...
@@ -258,7 +257,7 @@ class GroupsUsersDetail(APIView):
class
GroupsGroupsList
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
post
(
self
,
request
,
group_id
,
format
=
None
):
def
post
(
self
,
request
,
group_id
):
"""
POST creates a new group-group relationship in the system
"""
...
...
@@ -290,8 +289,7 @@ class GroupsGroupsList(APIView):
response_status
=
status
.
HTTP_404_NOT_FOUND
return
Response
(
response_data
,
status
=
response_status
)
def
get
(
self
,
request
,
group_id
,
format
=
None
):
def
get
(
self
,
request
,
group_id
):
"""
GET retrieves the existing group-group relationships for the specified group
"""
...
...
@@ -333,7 +331,7 @@ class GroupsGroupsList(APIView):
class
GroupsGroupsDetail
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
get
(
self
,
request
,
group_id
,
related_group_id
,
format
=
None
):
def
get
(
self
,
request
,
group_id
,
related_group_id
):
"""
GET retrieves an existing group-group relationship from the system
"""
...
...
@@ -357,7 +355,7 @@ class GroupsGroupsDetail(APIView):
response_status
=
status
.
HTTP_200_OK
return
Response
(
response_data
,
response_status
)
def
delete
(
self
,
request
,
group_id
,
related_group_id
,
format
=
None
):
def
delete
(
self
,
request
,
group_id
,
related_group_id
):
"""
DELETE removes/inactivates/etc. an existing group-group relationship
"""
...
...
@@ -388,7 +386,7 @@ class GroupsGroupsDetail(APIView):
class
GroupsCoursesList
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
post
(
self
,
request
,
group_id
,
format
=
None
):
def
post
(
self
,
request
,
group_id
):
"""
POST creates a new group-course relationship in the system
"""
...
...
@@ -422,7 +420,7 @@ class GroupsCoursesList(APIView):
response_status
=
status
.
HTTP_409_CONFLICT
return
Response
(
response_data
,
status
=
response_status
)
def
get
(
self
,
request
,
group_id
,
format
=
None
):
def
get
(
self
,
request
,
group_id
):
"""
GET returns all courses that has a relationship to the group
"""
...
...
@@ -448,7 +446,7 @@ class GroupsCoursesList(APIView):
class
GroupsCoursesDetail
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
get
(
self
,
request
,
group_id
,
course_id
,
format
=
None
):
def
get
(
self
,
request
,
group_id
,
course_id
):
"""
GET retrieves an existing group-course relationship from the system
"""
...
...
@@ -469,7 +467,7 @@ class GroupsCoursesDetail(APIView):
response_status
=
status
.
HTTP_404_NOT_FOUND
return
Response
(
response_data
,
status
=
response_status
)
def
delete
(
self
,
request
,
group_id
,
course_id
,
format
=
None
):
def
delete
(
self
,
request
,
group_id
,
course_id
):
"""
DELETE removes/inactivates/etc. an existing group-course relationship
"""
...
...
lms/djangoapps/api_manager/migrations/0001_initial.py
View file @
913252d4
...
...
@@ -30,7 +30,6 @@ class Migration(SchemaMigration):
))
db
.
send_create_signal
(
'api_manager'
,
[
'LinkedGroupRelationship'
])
def
backwards
(
self
,
orm
):
# Deleting model 'GroupRelationship'
db
.
delete_table
(
'api_manager_grouprelationship'
)
...
...
@@ -38,7 +37,6 @@ class Migration(SchemaMigration):
# Deleting model 'LinkedGroupRelationship'
db
.
delete_table
(
'api_manager_linkedgrouprelationship'
)
models
=
{
'api_manager.grouprelationship'
:
{
'Meta'
:
{
'object_name'
:
'GroupRelationship'
},
...
...
@@ -80,4 +78,4 @@ class Migration(SchemaMigration):
}
}
complete_apps
=
[
'api_manager'
]
\ No newline at end of file
complete_apps
=
[
'api_manager'
]
lms/djangoapps/api_manager/migrations/0002_auto__add_coursegrouprelationship__add_groupprofile.py
View file @
913252d4
...
...
@@ -25,7 +25,6 @@ class Migration(SchemaMigration):
))
db
.
send_create_signal
(
'api_manager'
,
[
'GroupProfile'
])
def
backwards
(
self
,
orm
):
# Deleting model 'CourseGroupRelationship'
db
.
delete_table
(
'api_manager_coursegrouprelationship'
)
...
...
@@ -33,7 +32,6 @@ class Migration(SchemaMigration):
# Deleting model 'GroupProfile'
db
.
delete_table
(
'auth_groupprofile'
)
models
=
{
'api_manager.coursegrouprelationship'
:
{
'Meta'
:
{
'object_name'
:
'CourseGroupRelationship'
},
...
...
@@ -88,4 +86,4 @@ class Migration(SchemaMigration):
}
}
complete_apps
=
[
'api_manager'
]
\ No newline at end of file
complete_apps
=
[
'api_manager'
]
lms/djangoapps/api_manager/migrations/0003_move_name_to_profile.py
View file @
913252d4
...
...
@@ -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 'GroupProfile.name'
db
.
delete_column
(
'auth_groupprofile'
,
'name'
)
models
=
{
'api_manager.coursegrouprelationship'
:
{
'Meta'
:
{
'object_name'
:
'CourseGroupRelationship'
},
...
...
@@ -74,4 +72,4 @@ class Migration(SchemaMigration):
}
}
complete_apps
=
[
'api_manager'
]
\ No newline at end of file
complete_apps
=
[
'api_manager'
]
lms/djangoapps/api_manager/sessions/test_login_ratelimit.py
View file @
913252d4
...
...
@@ -19,7 +19,6 @@ TEST_API_KEY = str(uuid.uuid4())
@override_settings
(
EDX_API_KEY
=
TEST_API_KEY
)
@patch.dict
(
"django.conf.settings.FEATURES"
,
{
'ENABLE_MAX_FAILED_LOGIN_ATTEMPTS'
:
False
})
class
SessionApiRateLimitingProtectionTest
(
TestCase
):
"""
Test api_manager.session.login.ratelimit
...
...
@@ -37,7 +36,6 @@ class SessionApiRateLimitingProtectionTest(TestCase):
cache
.
clear
()
self
.
session_url
=
'/api/sessions'
def
test_login_ratelimiting_protection
(
self
):
""" Try (and fail) login user 30 times on invalid password """
...
...
lms/djangoapps/api_manager/sessions/test_security.py
View file @
913252d4
...
...
@@ -222,7 +222,7 @@ class SessionApiSecurityTest(TestCase):
"""
Make Post/Delete/Get requests with params
"""
post_params
,
extra
,
=
{
'username'
:
username
,
'password'
:
password
},
{}
post_params
,
extra
,
=
{
'username'
:
username
,
'password'
:
password
},
{}
patched_audit_log
=
'api_manager.sessions.views.AUDIT_LOG'
request_method
=
kwargs
.
get
(
'request_method'
,
'POST'
)
if
kwargs
.
get
(
'email'
):
...
...
lms/djangoapps/api_manager/sessions/urls.py
View file @
913252d4
...
...
@@ -5,7 +5,8 @@ from rest_framework.urlpatterns import format_suffix_patterns
from
api_manager.sessions
import
views
as
sessions_views
urlpatterns
=
patterns
(
''
,
urlpatterns
=
patterns
(
''
,
url
(
r'/*$^'
,
sessions_views
.
SessionsList
.
as_view
()),
url
(
r'^(?P<session_id>[a-z0-9]+)$'
,
sessions_views
.
SessionsDetail
.
as_view
()),
)
...
...
lms/djangoapps/api_manager/sessions/views.py
View file @
913252d4
...
...
@@ -43,7 +43,7 @@ def _generate_base_uri(request):
class
SessionsList
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
post
(
self
,
request
,
format
=
None
):
def
post
(
self
,
request
):
"""
POST creates a new system session, supported authentication modes:
1. Open edX username/password
...
...
@@ -116,7 +116,7 @@ class SessionsList(APIView):
class
SessionsDetail
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
get
(
self
,
request
,
session_id
,
format
=
None
):
def
get
(
self
,
request
,
session_id
):
"""
GET retrieves an existing system session
"""
...
...
@@ -140,7 +140,7 @@ class SessionsDetail(APIView):
else
:
return
Response
(
response_data
,
status
=
status
.
HTTP_404_NOT_FOUND
)
def
delete
(
self
,
request
,
session_id
,
format
=
None
):
def
delete
(
self
,
request
,
session_id
):
"""
DELETE flushes an existing system session from the system
"""
...
...
lms/djangoapps/api_manager/system/views.py
View file @
913252d4
""" BASE API VIEWS """
from
rest_framework
import
status
from
rest_framework.decorators
import
api_view
,
permission_classes
from
rest_framework.response
import
Response
from
rest_framework.views
import
APIView
...
...
@@ -22,11 +21,12 @@ def _generate_base_uri(request):
)
return
resource_uri
class
SystemDetail
(
APIView
):
"""Manages system-level information about the Open edX API"""
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
get
(
self
,
request
,
format
=
None
):
"""Returns top-level descriptive information about the Open edX API"""
def
get
(
self
,
request
):
base_uri
=
_generate_base_uri
(
request
)
response_data
=
{}
response_data
[
'name'
]
=
"Open edX System API"
...
...
@@ -37,10 +37,10 @@ class SystemDetail(APIView):
class
ApiDetail
(
APIView
):
"""Manages top-level information about the Open edX API"""
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
get
(
self
,
request
,
format
=
None
):
"""Returns top-level descriptive information about the Open edX API"""
def
get
(
self
,
request
):
base_uri
=
_generate_base_uri
(
request
)
response_data
=
{}
response_data
[
'name'
]
=
"Open edX API"
...
...
lms/djangoapps/api_manager/urls.py
View file @
913252d4
...
...
@@ -12,11 +12,12 @@ from django.conf.urls import include, patterns, url
from
api_manager.system
import
views
as
system_views
urlpatterns
=
patterns
(
''
,
url
(
r'^$'
,
system_views
.
ApiDetail
.
as_view
()),
url
(
r'^system$'
,
system_views
.
SystemDetail
.
as_view
()),
url
(
r'^users/*'
,
include
(
'api_manager.users.urls'
)),
url
(
r'^groups/*'
,
include
(
'api_manager.groups.urls'
)),
url
(
r'^sessions/*'
,
include
(
'api_manager.sessions.urls'
)),
url
(
r'^courses/*'
,
include
(
'api_manager.courses.urls'
)),
)
urlpatterns
=
patterns
(
''
,
url
(
r'^$'
,
system_views
.
ApiDetail
.
as_view
()),
url
(
r'^system$'
,
system_views
.
SystemDetail
.
as_view
()),
url
(
r'^users/*'
,
include
(
'api_manager.users.urls'
)),
url
(
r'^groups/*'
,
include
(
'api_manager.groups.urls'
)),
url
(
r'^sessions/*'
,
include
(
'api_manager.sessions.urls'
)),
url
(
r'^courses/*'
,
include
(
'api_manager.courses.urls'
)),
)
lms/djangoapps/api_manager/users/test_user_password_reset.py
View file @
913252d4
...
...
@@ -12,8 +12,10 @@ from django.core.cache import cache
from
datetime
import
datetime
,
timedelta
from
freezegun
import
freeze_time
from
pytz
import
UTC
TEST_API_KEY
=
str
(
uuid
.
uuid4
())
@override_settings
(
EDX_API_KEY
=
TEST_API_KEY
)
@patch.dict
(
"django.conf.settings.FEATURES"
,
{
'ENFORCE_PASSWORD_POLICY'
:
True
})
@patch.dict
(
"django.conf.settings.FEATURES"
,
{
'ADVANCED_SECURITY'
:
True
})
...
...
@@ -52,7 +54,7 @@ class UserPasswordResetTest(TestCase):
reset_time
=
timezone
.
now
()
+
timedelta
(
days
=
5
)
with
patch
.
object
(
timezone
,
'now'
,
return_value
=
reset_time
):
response
=
self
.
_do_post_request
(
self
.
session_url
,
'test2'
,
'Test.Me64!'
,
secure
=
True
)
message
=
_
(
message
=
_
(
'Your password has expired due to password policy on this account. '
'You must reset your password before you can log in again.'
)
...
...
@@ -63,7 +65,7 @@ class UserPasswordResetTest(TestCase):
response
=
self
.
_do_post_pass_reset_request
(
pass_reset_url
,
password
=
'Test.Me64@'
,
secure
=
True
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
#login successful after reset password
response
=
self
.
_do_post_request
(
self
.
session_url
,
'test2'
,
'Test.Me64@'
,
secure
=
True
)
...
...
@@ -167,11 +169,13 @@ class UserPasswordResetTest(TestCase):
pass_reset_url
=
'{}/{}'
.
format
(
self
.
user_url
,
user_id
)
for
i
in
xrange
(
30
):
password
=
u'test_password{0}'
.
format
(
i
)
response
=
self
.
_do_post_pass_reset_request
(
'{}/{}'
.
format
(
self
.
user_url
,
i
+
200
),
password
=
password
,
secure
=
True
)
self
.
_assert_response
(
response
,
status
=
404
)
password
=
u'test_password{0}'
.
format
(
i
)
response
=
self
.
_do_post_pass_reset_request
(
'{}/{}'
.
format
(
self
.
user_url
,
i
+
200
),
password
=
password
,
secure
=
True
)
self
.
_assert_response
(
response
,
status
=
404
)
response
=
self
.
_do_post_pass_reset_request
(
'{}/{}'
.
format
(
self
.
user_url
,
'31'
),
password
=
'Test.Me64@'
,
secure
=
True
...
...
@@ -215,14 +219,11 @@ class UserPasswordResetTest(TestCase):
extra
[
'wsgi.url_scheme'
]
=
'https'
return
self
.
client
.
post
(
url
,
post_params
,
headers
=
headers
,
**
extra
)
def
_assert_response
(
self
,
response
,
status
=
200
,
success
=
None
,
message
=
None
):
def
_assert_response
(
self
,
response
,
status
=
200
,
message
=
None
):
"""
Assert that the response had status 200 and returned a valid
JSON-parseable dict.
If success is provided, assert that the response had that
value for 'success' in the JSON dict.
If message is provided, assert that the response contained that
value for 'message' in the JSON dict.
"""
...
...
@@ -233,4 +234,3 @@ class UserPasswordResetTest(TestCase):
msg
=
(
"'
%
s' did not contain '
%
s'"
%
(
response_dict
[
'message'
],
message
))
self
.
assertTrue
(
message
in
response_dict
[
'message'
],
msg
)
lms/djangoapps/api_manager/users/tests.py
View file @
913252d4
...
...
@@ -90,12 +90,11 @@ class UsersApiTests(TestCase):
def
test_user_list_post_inactive
(
self
):
test_uri
=
'/api/users'
local_username
=
self
.
test_username
+
str
(
randint
(
11
,
99
))
data
=
{
'email'
:
self
.
test_email
,
'username'
:
local_username
,
'password'
:
self
.
test_password
,
'first_name'
:
self
.
test_first_name
,
'last_name'
:
self
.
test_last_name
,
'is_active'
:
False
}
data
=
{
'email'
:
self
.
test_email
,
'username'
:
local_username
,
'password'
:
self
.
test_password
,
'first_name'
:
self
.
test_first_name
,
'last_name'
:
self
.
test_last_name
,
'is_active'
:
False
}
response
=
self
.
do_post
(
test_uri
,
data
)
self
.
assertEqual
(
response
.
status_code
,
201
)
self
.
assertEqual
(
response
.
data
[
'is_active'
],
False
)
def
test_user_list_post_duplicate
(
self
):
test_uri
=
'/api/users'
local_username
=
self
.
test_username
+
str
(
randint
(
11
,
99
))
...
...
@@ -135,7 +134,7 @@ class UsersApiTests(TestCase):
data
=
{
'email'
:
self
.
test_email
,
'username'
:
local_username
,
'password'
:
self
.
test_password
,
'first_name'
:
self
.
test_first_name
,
'last_name'
:
self
.
test_last_name
}
response
=
self
.
do_post
(
test_uri
,
data
)
test_uri
=
test_uri
+
'/'
+
str
(
response
.
data
[
'id'
])
data
=
{
'is_active'
:
False
}
data
=
{
'is_active'
:
False
}
response
=
self
.
do_post
(
test_uri
,
data
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
data
[
'is_active'
],
False
)
...
...
@@ -145,7 +144,7 @@ class UsersApiTests(TestCase):
def
test_user_detail_post_invalid_user
(
self
):
test_uri
=
'/api/users/123124124'
data
=
{
'is_active'
:
False
}
data
=
{
'is_active'
:
False
}
response
=
self
.
do_post
(
test_uri
,
data
)
self
.
assertEqual
(
response
.
status_code
,
404
)
...
...
lms/djangoapps/api_manager/users/urls.py
View file @
913252d4
...
...
@@ -5,7 +5,8 @@ from rest_framework.urlpatterns import format_suffix_patterns
from
api_manager.users
import
views
as
users_views
urlpatterns
=
patterns
(
''
,
urlpatterns
=
patterns
(
''
,
url
(
r'/*$^'
,
users_views
.
UsersList
.
as_view
()),
url
(
r'^(?P<user_id>[0-9]+)$'
,
users_views
.
UsersDetail
.
as_view
()),
url
(
r'^(?P<user_id>[0-9]+)/courses/*$'
,
users_views
.
UsersCoursesList
.
as_view
()),
...
...
lms/djangoapps/api_manager/users/views.py
View file @
913252d4
...
...
@@ -30,6 +30,7 @@ from util.bad_request_rate_limiter import BadRequestRateLimiter
log
=
logging
.
getLogger
(
__name__
)
AUDIT_LOG
=
logging
.
getLogger
(
"audit"
)
def
_generate_base_uri
(
request
):
"""
Constructs the protocol:host:path component of the resource uri
...
...
@@ -44,6 +45,7 @@ def _generate_base_uri(request):
)
return
resource_uri
def
_serialize_user
(
response_data
,
user
):
"""
Loads the object data into the response dict
...
...
@@ -57,6 +59,7 @@ def _serialize_user(response_data, user):
response_data
[
'is_active'
]
=
user
.
is_active
return
response_data
def
_save_module_position
(
request
,
user
,
course_id
,
course_descriptor
,
position
):
"""
Records the indicated position for the specified course
...
...
@@ -94,7 +97,7 @@ def _save_module_position(request, user, course_id, course_descriptor, position)
class
UsersList
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
post
(
self
,
request
,
format
=
None
):
def
post
(
self
,
request
):
"""
POST creates a new user in the system
"""
...
...
@@ -176,7 +179,7 @@ class UsersList(APIView):
class
UsersDetail
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
get
(
self
,
request
,
user_id
,
format
=
None
):
def
get
(
self
,
request
,
user_id
):
"""
GET retrieves an existing user from the system
"""
...
...
@@ -195,7 +198,7 @@ class UsersDetail(APIView):
except
ObjectDoesNotExist
:
return
Response
(
response_data
,
status
=
status
.
HTTP_404_NOT_FOUND
)
def
post
(
self
,
request
,
user_id
,
format
=
None
):
def
post
(
self
,
request
,
user_id
):
"""
POST provides the ability to update information about an existing user
"""
...
...
@@ -287,7 +290,7 @@ class UsersDetail(APIView):
class
UsersGroupsList
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
post
(
self
,
request
,
user_id
,
format
=
None
):
def
post
(
self
,
request
,
user_id
):
"""
POST creates a new user-group relationship in the system
"""
...
...
@@ -320,7 +323,7 @@ class UsersGroupsList(APIView):
response_status
=
status
.
HTTP_404_NOT_FOUND
return
Response
(
response_data
,
status
=
response_status
)
def
get
(
self
,
request
,
user_id
,
format
=
None
):
def
get
(
self
,
request
,
user_id
):
"""
GET retrieves the list of groups related to the specified user
"""
...
...
@@ -328,8 +331,10 @@ class UsersGroupsList(APIView):
existing_user
=
User
.
objects
.
get
(
id
=
user_id
)
except
ObjectDoesNotExist
:
return
Response
({},
status
.
HTTP_404_NOT_FOUND
)
groups
=
existing_user
.
groups
.
all
()
response_data
=
{}
base_uri
=
_generate_base_uri
(
request
)
response_data
[
'uri'
]
=
base_uri
groups
=
existing_user
.
groups
.
all
()
response_data
[
'groups'
]
=
[]
for
group
in
groups
:
group_profile
=
GroupProfile
.
objects
.
get
(
group_id
=
group
.
id
)
...
...
@@ -344,12 +349,11 @@ class UsersGroupsList(APIView):
class
UsersGroupsDetail
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
get
(
self
,
request
,
user_id
,
group_id
,
format
=
None
):
def
get
(
self
,
request
,
user_id
,
group_id
):
"""
GET retrieves an existing user-group relationship from the system
"""
response_data
=
{}
base_uri
=
_generate_base_uri
(
request
)
try
:
existing_user
=
User
.
objects
.
get
(
id
=
user_id
,
is_active
=
True
)
existing_relationship
=
existing_user
.
groups
.
get
(
id
=
group_id
)
...
...
@@ -359,13 +363,13 @@ class UsersGroupsDetail(APIView):
if
existing_user
and
existing_relationship
:
response_data
[
'user_id'
]
=
existing_user
.
id
response_data
[
'group_id'
]
=
existing_relationship
.
id
response_data
[
'uri'
]
=
base_uri
response_data
[
'uri'
]
=
_generate_base_uri
(
request
)
response_status
=
status
.
HTTP_200_OK
else
:
response_status
=
status
.
HTTP_404_NOT_FOUND
return
Response
(
response_data
,
status
=
response_status
)
def
delete
(
self
,
request
,
user_id
,
group_id
,
format
=
None
):
def
delete
(
self
,
request
,
user_id
,
group_id
):
"""
DELETE removes/inactivates/etc. an existing user-group relationship
"""
...
...
@@ -378,7 +382,7 @@ class UsersGroupsDetail(APIView):
class
UsersCoursesList
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
post
(
self
,
request
,
user_id
,
format
=
None
):
def
post
(
self
,
request
,
user_id
):
"""
POST creates a new course enrollment for a user
"""
...
...
@@ -404,7 +408,7 @@ class UsersCoursesList(APIView):
status_code
=
status
.
HTTP_404_NOT_FOUND
return
Response
(
response_data
,
status
=
status_code
)
def
get
(
self
,
request
,
user_id
,
format
=
None
):
def
get
(
self
,
request
,
user_id
):
"""
GET creates the list of enrolled courses for a user
"""
...
...
@@ -435,7 +439,7 @@ class UsersCoursesList(APIView):
class
UsersCoursesDetail
(
APIView
):
permission_classes
=
(
ApiKeyHeaderPermission
,)
def
post
(
self
,
request
,
user_id
,
course_id
,
format
=
None
):
def
post
(
self
,
request
,
user_id
,
course_id
):
"""
POST creates an ACTIVE course enrollment for the specified user
"""
...
...
@@ -465,7 +469,7 @@ class UsersCoursesDetail(APIView):
response_status
=
status
.
HTTP_404_NOT_FOUND
return
Response
(
response_data
,
status
=
response_status
)
def
get
(
self
,
request
,
user_id
,
course_id
,
format
=
None
):
def
get
(
self
,
request
,
user_id
,
course_id
):
"""
GET identifies an ACTIVE course enrollment for the specified user
"""
...
...
@@ -495,7 +499,7 @@ class UsersCoursesDetail(APIView):
response_status
=
status
.
HTTP_404_NOT_FOUND
return
Response
(
response_data
,
status
=
response_status
)
def
delete
(
self
,
request
,
user_id
,
course_id
,
format
=
None
):
def
delete
(
self
,
request
,
user_id
,
course_id
):
"""
DELETE unenrolls the specified user from a course
"""
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment