Commit 1e1c05c4 by Matt Drayer Committed by Jonathan Piacenti

api-v1: Initial API implementation & group relationships

Includes:
* Initial API implementation
* API_KEY header fix
* Fixed indentation error
* move api from common to lms
* Course-Group Relationships
* wip
* add parsing of overview content blob
* initial implementation
* get course updates API method
* get course static tabs API methods
* add unit tests
* fix some merge conflicts and remove some extra print statements picked up in rebase
* better constrain some of the course url regexs to expect a triple for courseId
* add ability to enroll users into courses
* wip
* add queryable group lists
* add a GET endpoint to /api/groups/{groupid}/courses to get the list of courses associated with the group (program)
* return the display names when listing all courses in a group (program)
* create optimized course tree query API endpoint
* make sure group profile data is properly serialized and deserialized. There appears to be a difference between how the Django test client handles things. Disabling unit test for now to unblock UI devs
* null guard
* fix broken unit test
* add GET endpoint to group/user relationships
* Added Security to session api as requested in #785
* Storing passoword history of user and validation for user's email and username
* Moved security tests to lms also
* add two FEATURE flag overrides to enable the security features
* remove unnecessary settings overrides (because they are 'feature flags')
* Adding ratelimiting on login api
* Migrated from Function-Based Views to Class-Based-Views
* Create new Account/Login Audit Log
* The API should provide for an audit log when a user creates a new account or logs into the system.
* UserList.post() now creates UserProfile, UserPreference records
* Group Profile fix
* Filter group subgroups by group type
* Application reorganization
* Name is now required during group creation
* Added null check for profile name
* User must reset password functionality added
* User must reset password dunctionality added
* Added user password reset functionality
* Add password reset api and implemented Password history
* Remove unused imports, password reset message text
parent 0e5c81c0
"""
Some test content strings. Best to keep them out of the test files because they take up a lot of
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_STATIC_TAB1_CONTENT = dedent("""
<div>This is static tab1</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>
<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="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="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>
""")
# pylint: disable=E1103
"""
Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/tests/test_group_views.py]
"""
import simplejson as json
import unittest
import uuid
from random import randint
from django.core.cache import cache
from django.test import TestCase, Client
from django.test.utils import override_settings
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from .content import TEST_COURSE_OVERVIEW_CONTENT, TEST_COURSE_UPDATES_CONTENT
from .content import TEST_STATIC_TAB1_CONTENT, TEST_STATIC_TAB2_CONTENT
TEST_API_KEY = str(uuid.uuid4())
class SecureClient(Client):
""" Django test client using a "secure" connection. """
def __init__(self, *args, **kwargs):
kwargs = kwargs.copy()
kwargs.update({'SERVER_PORT': 443, 'wsgi.url_scheme': 'https'})
super(SecureClient, self).__init__(*args, **kwargs)
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
@override_settings(EDX_API_KEY=TEST_API_KEY)
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'
self.test_group_name = 'Alpha Group'
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.module = ItemFactory.create(
category="videosequence",
parent_location=self.chapter.location,
data=self.test_data,
display_name="Video_Sequence"
)
self.submodule = ItemFactory.create(
category="video",
parent_location=self.module.location,
data=self.test_data,
display_name="Video_Resources"
)
self.overview = ItemFactory.create(
category="about",
parent_location=self.course.location,
data=TEST_COURSE_OVERVIEW_CONTENT,
display_name="overview"
)
self.updates = ItemFactory.create(
category="course_info",
parent_location=self.course.location,
data=TEST_COURSE_UPDATES_CONTENT,
display_name="updates"
)
self.static_tab1 = ItemFactory.create(
category="static_tab",
parent_location=self.course.location,
data=TEST_STATIC_TAB1_CONTENT,
display_name="syllabus"
)
self.static_tab2 = ItemFactory.create(
category="static_tab",
parent_location=self.course.location,
data=TEST_STATIC_TAB2_CONTENT,
display_name="readings"
)
self.test_course_id = self.course.id
self.test_bogus_course_id = 'foo/bar/baz'
self.test_course_name = self.course.display_name
self.test_course_number = self.course.number
self.test_course_org = self.course.org
self.test_chapter_id = self.chapter.id
self.test_module_id = self.module.id
self.test_submodule_id = self.submodule.id
self.base_modules_uri = '/api/courses/' + self.test_course_id + '/modules'
self.base_chapters_uri = self.base_modules_uri + '?type=chapter'
self.client = SecureClient()
cache.clear()
def do_get(self, uri):
"""Submit an HTTP GET request"""
headers = {
'Content-Type': 'application/json',
'X-Edx-Api-Key': str(TEST_API_KEY),
}
response = self.client.get(uri, headers=headers)
return response
def do_post(self, uri, data):
"""Submit an HTTP POST request"""
headers = {
'X-Edx-Api-Key': str(TEST_API_KEY),
}
json_data = json.dumps(data)
response = self.client.post(uri, headers=headers, content_type='application/json', data=json_data)
return response
def do_delete(self, uri):
"""Submit an HTTP DELETE request"""
headers = {
'Content-Type': 'application/json',
'X-Edx-Api-Key': str(TEST_API_KEY),
}
response = self.client.delete(uri, headers=headers)
return response
def _find_item_by_class(self, items, class_name):
for item in items:
if item['class'] == class_name:
return item
return None
def test_courses_list_get(self):
test_uri = self.base_courses_uri
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
matched_course = False
for course in response.data:
if matched_course is False and course['id'] == self.test_course_id:
self.assertEqual(course['name'], self.test_course_name)
self.assertEqual(course['number'], self.test_course_number)
self.assertEqual(course['org'], self.test_course_org)
confirm_uri = self.test_server_prefix + test_uri + '/' + course['id']
self.assertEqual(course['uri'], confirm_uri)
matched_course = True
self.assertTrue(matched_course)
def test_courses_detail_get(self):
test_uri = self.base_courses_uri + '/' + self.test_course_id
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
self.assertEqual(response.data['id'], self.test_course_id)
self.assertEqual(response.data['name'], self.test_course_name)
self.assertEqual(response.data['number'], self.test_course_number)
self.assertEqual(response.data['org'], self.test_course_org)
confirm_uri = self.test_server_prefix + test_uri
self.assertEqual(response.data['uri'], confirm_uri)
def test_courses_detail_get_with_submodules(self):
test_uri = self.base_courses_uri + '/' + self.test_course_id + '?depth=100'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
self.assertEqual(response.data['id'], self.test_course_id)
self.assertEqual(response.data['name'], self.test_course_name)
self.assertEqual(response.data['number'], self.test_course_number)
self.assertEqual(response.data['org'], self.test_course_org)
confirm_uri = self.test_server_prefix + test_uri
self.assertEqual(response.data['uri'], confirm_uri)
self.assertGreater(len(response.data['modules']), 0)
def test_courses_detail_get_notfound(self):
test_uri = self.base_courses_uri + '/' + self.test_bogus_course_id
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_courses_tree_get(self):
# query the course tree to quickly get naviation information
test_uri = self.base_courses_uri + '/' + self.test_course_id + '?depth=2'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
self.assertEqual(response.data['category'], 'course')
self.assertEqual(response.data['name'], self.course.display_name)
self.assertEqual(len(response.data['modules']), 1)
chapter = response.data['modules'][0]
self.assertEqual(chapter['category'], 'chapter')
self.assertEqual(chapter['name'], 'Overview')
self.assertEqual(len(chapter['modules']), 1)
sequence = chapter['modules'][0]
self.assertEqual(sequence['category'], 'videosequence')
self.assertEqual(sequence['name'], 'Video_Sequence')
self.assertNotIn('modules', sequence)
def test_courses_tree_get_root(self):
# query the course tree to quickly get naviation information
test_uri = self.base_courses_uri + '/' + self.test_course_id + '?depth=0'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
self.assertEqual(response.data['category'], 'course')
self.assertEqual(response.data['name'], self.course.display_name)
self.assertNotIn('modules', response.data)
def test_chapter_list_get(self):
test_uri = self.base_chapters_uri
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
matched_chapter = False
for chapter in response.data:
if matched_chapter is False and chapter['id'] == self.test_chapter_id:
self.assertIsNotNone(chapter['uri'])
self.assertGreater(len(chapter['uri']), 0)
confirm_uri = self.test_server_prefix + self.base_modules_uri + '/' + chapter['id']
self.assertEqual(chapter['uri'], confirm_uri)
matched_chapter = True
self.assertTrue(matched_chapter)
def test_chapter_detail_get(self):
test_uri = self.base_modules_uri + '/' + self.test_chapter_id
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data['id']), 0)
self.assertEqual(response.data['id'], self.test_chapter_id)
confirm_uri = self.test_server_prefix + test_uri
self.assertEqual(response.data['uri'], confirm_uri)
self.assertGreater(len(response.data['modules']), 0)
def test_modules_list_get(self):
test_uri = self.base_modules_uri + '/' + self.test_module_id
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
matched_submodule = False
for submodule in response.data['modules']:
if matched_submodule is False and submodule['id'] == self.test_submodule_id:
self.assertIsNotNone(submodule['uri'])
self.assertGreater(len(submodule['uri']), 0)
confirm_uri = self.test_server_prefix + self.base_modules_uri + '/' + submodule['id']
self.assertEqual(submodule['uri'], confirm_uri)
matched_submodule = True
self.assertTrue(matched_submodule)
def test_modules_detail_get(self):
test_uri = self.base_modules_uri + '/' + self.test_module_id
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
self.assertEqual(response.data['id'], self.test_module_id)
confirm_uri = self.test_server_prefix + test_uri
self.assertEqual(response.data['uri'], confirm_uri)
self.assertGreater(len(response.data['modules']), 0)
def test_modules_detail_get_course(self):
test_uri = self.base_modules_uri + '/' + self.test_course_id
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
self.assertEqual(response.data['id'], self.test_course_id)
confirm_uri = self.test_server_prefix + self.base_courses_uri + '/' + self.test_course_id
self.assertEqual(response.data['uri'], confirm_uri)
self.assertGreater(len(response.data['modules']), 0)
def test_modules_detail_get_notfound(self):
test_uri = self.base_modules_uri + '/' + '2p38fp2hjfp9283'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_modules_list_get_filtered_submodules_for_module(self):
test_uri = self.base_modules_uri + '/' + self.test_module_id + '/submodules?type=video'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
matched_submodule = False
for submodule in response.data:
if matched_submodule is False and submodule['id'] == self.test_submodule_id:
confirm_uri = self.test_server_prefix + self.base_modules_uri + '/' + submodule['id']
self.assertEqual(submodule['uri'], confirm_uri)
matched_submodule = True
self.assertTrue(matched_submodule)
def test_modules_list_get_notfound(self):
test_uri = self.base_modules_uri + '/2p38fp2hjfp9283/submodules?type=video'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_courses_groups_list_post(self):
data = {'name': self.test_group_name}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = '{}/{}/groups'.format(self.base_courses_uri, self.test_course_id)
data = {'group_id': group_id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
confirm_uri = self.test_server_prefix + test_uri + '/' + str(group_id)
self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['course_id'], str(self.test_course_id))
self.assertEqual(response.data['group_id'], str(group_id))
def test_courses_groups_list_post_duplicate(self):
data = {'name': self.test_group_name}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = '{}/{}/groups'.format(self.base_courses_uri, self.test_course_id)
data = {'group_id': group_id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 409)
def test_courses_groups_list_post_invalid_resources(self):
test_uri = self.base_courses_uri + '/1239/87/8976/groups'
data = {'group_id': "98723896"}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 404)
def test_courses_groups_detail_get(self):
data = {'name': self.test_group_name}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = '{}/{}/groups'.format(self.base_courses_uri, self.test_course_id)
data = {'group_id': response.data['id']}
response = self.do_post(test_uri, data)
test_uri = response.data['uri']
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['uri'], test_uri)
self.assertEqual(response.data['course_id'], self.test_course_id)
self.assertEqual(response.data['group_id'], str(group_id))
def test_courses_groups_detail_get_invalid_resources(self):
course_id = 'asd/fas/vcsadfaf'
group_id = '12343'
test_uri = '{}/{}/groups/{}'.format(self.base_courses_uri, course_id, group_id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
confirm_uri = self.test_server_prefix + test_uri
self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['course_id'], course_id)
self.assertEqual(response.data['group_id'], group_id)
def test_courses_groups_detail_delete(self):
data = {'name': self.test_group_name}
response = self.do_post(self.base_groups_uri, data)
test_uri = '{}/{}/groups'.format(self.base_courses_uri, self.test_course_id)
data = {'group_id': response.data['id']}
response = self.do_post(test_uri, data)
test_uri = response.data['uri']
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204)
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204) # Idempotent
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_courses_groups_detail_delete_invalid_course(self):
test_uri = '{}/{}/groups/123124'.format(self.base_courses_uri, self.test_bogus_course_id)
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204)
def test_courses_groups_detail_delete_invalid_group(self):
test_uri = '{}/{}/groups/123124'.format(self.base_courses_uri, self.test_course_id)
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204)
def test_courses_groups_detail_get_undefined(self):
data = {'name': self.test_group_name}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = '{}/{}/groups/{}'.format(self.base_courses_uri, self.test_course_id, group_id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_courses_overview_get_unparsed(self):
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/overview'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
self.assertEqual(response.data['overview_html'], self.overview.data)
def test_courses_overview_get_parsed(self):
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/overview?parse=true'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
sections = response.data['sections']
self.assertEqual(len(sections), 4)
self.assertIsNotNone(self._find_item_by_class(sections, 'about'))
self.assertIsNotNone(self._find_item_by_class(sections, 'prerequisites'))
self.assertIsNotNone(self._find_item_by_class(sections, 'course-staff'))
self.assertIsNotNone(self._find_item_by_class(sections, 'faq'))
course_staff = self._find_item_by_class(sections, 'course-staff')
staff = course_staff['articles']
self.assertEqual(len(staff), 3)
self.assertEqual(staff[0]['class'], "teacher")
self.assertEqual(staff[0]['name'], "Staff Member #1")
self.assertEqual(staff[0]['image_src'], "/images/pl-faculty.png")
self.assertIn("<p>Biography of instructor/staff member #1</p>", staff[0]['bio'])
self.assertEqual(staff[1]['class'], "teacher")
self.assertEqual(staff[1]['name'], "Staff Member #2")
self.assertEqual(staff[1]['image_src'], "/images/pl-faculty.png")
self.assertIn("<p>Biography of instructor/staff member #2</p>", staff[1]['bio'])
self.assertEqual(staff[2]['class'], "author")
body = staff[2]['body']
self.assertGreater(len(body), 0)
about = self._find_item_by_class(sections, 'about')
self.assertGreater(len(about['body']), 0)
prerequisites = self._find_item_by_class(sections, 'prerequisites')
self.assertGreater(len(prerequisites['body']), 0)
faq = self._find_item_by_class(sections, 'faq')
self.assertGreater(len(faq['body']), 0)
invalid_tab = self._find_item_by_class(sections, 'invalid_tab')
self.assertFalse(invalid_tab)
def test_courses_overview_get_invalid_course(self):
#try a bogus course_id to test failure case
test_uri = '{}/{}/overview'.format(self.base_courses_uri, self.test_bogus_course_id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_courses_overview_get_invalid_content(self):
#try a bogus course_id to test failure case
test_course = CourseFactory.create()
test_uri = '{}/{}/overview'.format(self.base_courses_uri, test_course.id)
test_updates = ItemFactory.create(
category="about",
parent_location=test_course.location,
data='',
display_name="overview"
)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_courses_updates_get(self):
# first try raw without any parsing
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/updates'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
self.assertEqual(response.data['content'], self.updates.data)
# then try parsed
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/updates?parse=True'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
postings = response.data['postings']
self.assertEqual(len(postings), 4)
self.assertEqual(postings[0]['date'], 'April 18, 2014')
self.assertEqual(postings[0]['content'], 'This does not have a paragraph tag around it')
self.assertEqual(postings[1]['date'], 'April 17, 2014')
self.assertEqual(postings[1]['content'], 'Some text before paragraph tag<p>This is inside paragraph tag</p>Some text after tag')
self.assertEqual(postings[2]['date'], 'April 16, 2014')
self.assertEqual(postings[2]['content'], 'Some text before paragraph tag<p>This is inside paragraph tag</p>Some text after tag<p>one more</p>')
self.assertEqual(postings[3]['date'], 'April 15, 2014')
self.assertEqual(postings[3]['content'], '<p>A perfectly</p><p>formatted piece</p><p>of HTML</p>')
def test_courses_updates_get_invalid_course(self):
#try a bogus course_id to test failure case
test_uri = '{}/{}/updates'.format(self.base_courses_uri, self.test_bogus_course_id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
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(
category="course_info",
parent_location=test_course.location,
data='',
display_name="updates"
)
test_uri = '{}/{}/updates'.format(self.base_courses_uri, test_course.id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_static_tab_list_get(self):
test_uri = '{}/{}/static_tabs'.format(self.base_courses_uri, self.test_course_id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
tabs = response.data['tabs']
self.assertEqual(len(tabs), 2)
self.assertEqual(tabs[0]['name'], u'syllabus')
self.assertEqual(tabs[0]['id'], u'syllabus')
self.assertEqual(tabs[1]['name'], u'readings')
self.assertEqual(tabs[1]['id'], u'readings')
# now try when we get the details on the tabs
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/static_tabs?detail=true'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
tabs = response.data['tabs']
self.assertEqual(tabs[0]['name'], u'syllabus')
self.assertEqual(tabs[0]['id'], u'syllabus')
self.assertEqual(tabs[0]['content'], self.static_tab1.data)
self.assertEqual(tabs[1]['name'], u'readings')
self.assertEqual(tabs[1]['id'], u'readings')
self.assertEqual(tabs[1]['content'], self.static_tab2.data)
def test_static_tab_list_get_invalid_course(self):
#try a bogus course_id to test failure case
test_uri = self.base_courses_uri + '/' + self.test_bogus_course_id + '/static_tabs'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_static_tab_detail_get(self):
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/static_tabs/syllabus'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
tab = response.data
self.assertEqual(tab['name'], u'syllabus')
self.assertEqual(tab['id'], u'syllabus')
self.assertEqual(tab['content'], self.static_tab1.data)
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/static_tabs/readings'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
tab = response.data
self.assertEqual(tab['name'], u'readings')
self.assertEqual(tab['id'], u'readings')
self.assertEqual(tab['content'], self.static_tab2.data)
def test_static_tab_detail_get_invalid_course(self):
# try a bogus courseId
test_uri = self.base_courses_uri + '/' + self.test_bogus_course_id + '/static_tabs/syllabus'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_static_tab_detail_get_invalid_item(self):
# try a not found item
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/static_tabs/bogus'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_courses_users_list_get_no_students(self):
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/users'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
# assert that there is no enrolled students
enrollments = response.data['enrollments']
self.assertEqual(len(enrollments), 0)
self.assertNotIn('pending_enrollments', response.data)
def test_courses_users_list_invalid_course(self):
test_uri = self.base_courses_uri + '/' + self.test_bogus_course_id + '/users'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_courses_users_list_post_nonexisting_user_deny(self):
# enroll a non-existing student
# first, don't allow non-existing
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/users'
post_data = {
'email': 'test+pending@tester.com',
'allow_pending': False,
}
response = self.do_post(test_uri, post_data)
self.assertEqual(response.status_code, 400)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
def test_courses_users_list_post_nonexisting_user_allow(self):
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/users'
post_data = {}
post_data['email'] = 'test+pending@tester.com'
post_data['allow_pending'] = True
response = self.do_post(test_uri, post_data)
self.assertEqual(response.status_code, 201)
response = self.do_get(test_uri)
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'
test_user_uri = '/api/users'
local_username = "some_test_user" + str(randint(11, 99))
local_email = "test+notpending@tester.com"
data = {
'email': local_email,
'username': local_username,
'password': 'fooabr',
'first_name': 'Joe',
'last_name': 'Brown'
}
response = self.do_post(test_user_uri, data)
self.assertEqual(response.status_code, 201)
self.assertGreater(response.data['id'], 0)
created_user_id = response.data['id']
# now enroll this user in the course
post_data = {}
post_data['user_id'] = created_user_id
response = self.do_post(test_uri, post_data)
self.assertEqual(response.status_code, 201)
def test_courses_users_list_post_invalid_course(self):
test_uri = self.base_courses_uri + '/' + self.test_bogus_course_id + '/users'
post_data = {}
post_data['email'] = 'test+pending@tester.com'
post_data['allow_pending'] = True
response = self.do_post(test_uri, post_data)
self.assertEqual(response.status_code, 404)
def test_courses_users_list_post_invalid_user(self):
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/users'
post_data = {}
post_data['user_id'] = '123123124'
post_data['allow_pending'] = True
response = self.do_post(test_uri, post_data)
self.assertEqual(response.status_code, 404)
def test_courses_users_list_post_invalid_payload(self):
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/users'
post_data = {}
response = self.do_post(test_uri, post_data)
self.assertEqual(response.status_code, 400)
def test_courses_users_list_get(self):
# create a new user (note, this calls into the /users/ subsystem)
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/users'
test_user_uri = '/api/users'
local_username = "some_test_user" + str(randint(11, 99))
local_email = "test+notpending@tester.com"
data = {
'email': local_email,
'username': local_username,
'password': 'fooabr',
'first_name': 'Joe',
'last_name': 'Brown'
}
response = self.do_post(test_user_uri, data)
self.assertEqual(response.status_code, 201)
self.assertGreater(response.data['id'], 0)
created_user_id = response.data['id']
post_data = {}
post_data['user_id'] = created_user_id
response = self.do_post(test_uri, post_data)
self.assertEqual(response.status_code, 201)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
def test_courses_users_detail_get(self):
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/users'
test_user_uri = '/api/users'
local_username = "some_test_user" + str(randint(11, 99))
local_email = "test+notpending@tester.com"
data = {
'email': local_email,
'username': local_username,
'password': 'fooabr',
'first_name': 'Joe',
'last_name': 'Brown'
}
response = self.do_post(test_user_uri, data)
self.assertEqual(response.status_code, 201)
self.assertGreater(response.data['id'], 0)
created_user_id = response.data['id']
# now enroll this user in the course
post_data = {}
post_data['user_id'] = created_user_id
response = self.do_post(test_uri, post_data)
self.assertEqual(response.status_code, 201)
confirm_uri = '{}/{}'.format(test_uri, created_user_id)
response = self.do_get(confirm_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
def test_courses_users_detail_get_invalid_course(self):
test_uri = self.base_courses_uri + '/' + self.test_bogus_course_id + '/users/213432'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
self.assertGreater(len(response.data), 0)
def test_courses_users_detail_get_invalid_user(self):
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/users/213432'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
self.assertGreater(len(response.data), 0)
def test_courses_users_detail_delete(self):
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/users'
test_user_uri = '/api/users'
local_username = "some_test_user" + str(randint(11, 99))
local_email = "test+notpending@tester.com"
data = {
'email': local_email,
'username': local_username,
'password': 'fooabr',
'first_name': 'Joe',
'last_name': 'Brown'
}
response = self.do_post(test_user_uri, data)
self.assertEqual(response.status_code, 201)
self.assertGreater(response.data['id'], 0)
created_user_id = response.data['id']
# now enroll this user in the course
post_data = {}
post_data['user_id'] = created_user_id
response = self.do_post(test_uri, post_data)
self.assertEqual(response.status_code, 201)
confirm_uri = '{}/{}'.format(test_uri, created_user_id)
response = self.do_get(confirm_uri)
self.assertEqual(response.status_code, 200)
response = self.do_delete(confirm_uri)
self.assertEqual(response.status_code, 204)
def test_courses_users_detail_delete_invalid_course(self):
test_uri = self.base_courses_uri + '/' + self.test_bogus_course_id + '/users/213432'
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 404)
def test_courses_users_detail_delete_invalid_user(self):
test_uri = self.base_courses_uri + '/' + self.test_course_id + '/users/213432'
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204)
"""
Courses API URI specification
The order of the URIs really matters here, due to the slash characters present in the identifiers
"""
from django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns
from api_manager.courses import views as courses_views
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()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/modules/(?P<module_id>[a-zA-Z0-9/_:]+)$', courses_views.ModulesDetail.as_view()),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/modules/*$', courses_views.ModulesList.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()),
)
urlpatterns = format_suffix_patterns(urlpatterns)
""" API implementation for course-oriented interactions. """
from collections import OrderedDict
import logging
from lxml import etree
from StringIO import StringIO
from django.contrib.auth.models import Group, User
from django.core.exceptions import ObjectDoesNotExist
from django.http import Http404
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
from api_manager.permissions import ApiKeyHeaderPermission
from api_manager.models import CourseGroupRelationship
from courseware import module_render
from courseware.courses import get_course, get_course_about_section, get_course_info_section
from courseware.model_data import FieldDataCache
from courseware.views import get_static_tab_contents
from student.models import CourseEnrollment, CourseEnrollmentAllowed
from xmodule.modulestore.django import modulestore
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
"""
protocol = 'http'
if request.is_secure():
protocol = protocol + 's'
resource_uri = '{}://{}{}'.format(
protocol,
request.get_host(),
request.get_full_path()
)
return resource_uri
def _get_module_submodules(module, submodule_type=None):
"""
Parses the provided module looking for child modules
Matches on submodule type (category) when specified
"""
submodules = []
if hasattr(module, 'children'):
child_modules = module.get_children()
for child_module in child_modules:
if submodule_type:
if getattr(child_module, 'category') == submodule_type:
submodules.append(child_module)
else:
submodules.append(child_module)
return submodules
def _serialize_module(request, course_id, module):
"""
Loads the specified module data into the response dict
This should probably evolve to use DRF serializers
"""
data = {}
if getattr(module, 'id') == course_id:
module_id = module.id
else:
module_id = module.location.url()
data['id'] = module_id
if hasattr(module, 'display_name'):
data['name'] = module.display_name
data['category'] = module.location.category
protocol = 'http'
if request.is_secure():
protocol = protocol + 's'
module_uri = '{}://{}/api/courses/{}'.format(
protocol,
request.get_host(),
course_id.encode('utf-8')
)
# Some things we do only if the module is a course
if (course_id == module_id):
data['number'] = module.location.course
data['org'] = module.location.org
# Other things we do only if the module is not a course
else:
module_uri = '{}/modules/{}'.format(module_uri, module_id)
data['uri'] = module_uri
return data
def _serialize_module_submodules(request, course_id, submodules):
"""
Loads the specified module submodule data into the response dict
This should probably evolve to use DRF serializers
"""
data = []
if submodules:
for submodule in submodules:
submodule_data = _serialize_module(
request,
course_id,
submodule
)
data.append(submodule_data)
return data
def _serialize_module_with_children(request, course_descriptor, descriptor, depth):
data = _serialize_module(
request,
course_descriptor.id,
descriptor
)
if depth > 0:
data['modules'] = []
for child in descriptor.get_children():
data['modules'].append(_serialize_module_with_children(
request,
course_descriptor,
child,
depth-1
))
return data
def _inner_content(tag):
"""
Helper method
"""
inner_content = None
if tag is not None:
inner_content = tag.text if tag.text else u''
inner_content += u''.join(etree.tostring(e) for e in tag)
inner_content += tag.tail if tag.tail else u''
return inner_content
def _parse_overview_html(html):
"""
Helper method to break up the course about HTML into components
"""
result = {}
parser = etree.HTMLParser()
tree = etree.parse(StringIO(html), parser)
sections = tree.findall('/body/section')
result = []
for section in sections:
section_class = section.get('class')
if section_class:
section_data = OrderedDict()
section_data['class'] = section_class
articles = section.findall('article')
if articles:
section_data['articles'] = []
for article in articles:
article_class = article.get('class')
if article_class:
article_data = OrderedDict()
article_data['class'] = article_class
if article_class == "teacher":
name_element = article.find('h3')
if name_element is not None:
article_data['name'] = name_element.text
image_element = article.find("./div[@class='teacher-image']/img")
if image_element is not None:
article_data['image_src'] = image_element.get('src')
bios = article.findall('p')
bio_html = ''
for bio in bios:
bio_html += etree.tostring(bio)
if bio_html:
article_data['bio'] = bio_html
else:
article_data['body'] = _inner_content(article)
section_data['articles'].append(article_data)
else:
section_data['body'] = _inner_content(section)
result.append(section_data)
return result
def _parse_updates_html(html):
"""
Helper method to break up the course updates HTML into components
"""
result = {}
parser = etree.HTMLParser()
tree = etree.parse(StringIO(html), parser)
# get all of the individual postings
postings = tree.findall('/body/ol/li')
result = []
for posting in postings:
posting_data = {}
posting_date_element = posting.find('h2')
if posting_date_element is not None:
posting_data['date'] = posting_date_element.text
content = u''
for el in posting:
# note, we can't delete or skip over the date element in
# the HTML tree because there might be some tailing content
if el != posting_date_element:
content += etree.tostring(el)
else:
content += el.tail if el.tail else u''
posting_data['content'] = content.strip()
result.append(posting_data)
return result
class ModulesList(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def get(self, request, course_id, module_id=None, format=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
"""
if module_id is None:
module_id = course_id
response_data = []
submodule_type = request.QUERY_PARAMS.get('type', None)
store = modulestore()
if course_id != module_id:
try:
module = store.get_instance(course_id, Location(module_id))
except InvalidLocationError:
module = None
else:
module = get_course(course_id)
if module:
submodules = _get_module_submodules(module, submodule_type)
response_data = _serialize_module_submodules(
request,
course_id,
submodules
)
status_code = status.HTTP_200_OK
else:
status_code = status.HTTP_404_NOT_FOUND
return Response(response_data, status=status_code)
class ModulesDetail(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def get(self, request, course_id, module_id, format=None):
"""
GET retrieves an existing module from the system
"""
store = modulestore()
response_data = {}
submodule_type = request.QUERY_PARAMS.get('type', None)
if course_id != module_id:
try:
module = store.get_instance(course_id, Location(module_id))
except InvalidLocationError:
module = None
else:
module = get_course(course_id)
if module:
response_data = _serialize_module(
request,
course_id,
module
)
submodules = _get_module_submodules(module, submodule_type)
response_data['modules'] = _serialize_module_submodules(
request,
course_id,
submodules
)
status_code = status.HTTP_200_OK
else:
status_code = status.HTTP_404_NOT_FOUND
return Response(response_data, status=status_code)
class CoursesList(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def get(self, request, format=None):
"""
GET returns the list of available courses
"""
response_data = []
store = modulestore()
course_descriptors = store.get_courses()
for course_descriptor in course_descriptors:
course_data = _serialize_module(
request,
course_descriptor.id,
course_descriptor
)
response_data.append(course_data)
return Response(response_data, status=status.HTTP_200_OK)
class CoursesDetail(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def get(self, request, course_id, format=None):
"""
GET retrieves an existing course from the system and returns summary information about the submodules
to the specified depth
"""
depth = request.QUERY_PARAMS.get('depth', 0)
depth_int = int(depth)
# get_course_by_id raises an Http404 if the requested course is invalid
# Rather than catching it, we just let it bubble up
try:
course_descriptor = get_course(course_id)
except ValueError:
course_descriptor = None
if course_descriptor:
if depth_int > 0:
response_data = _serialize_module_with_children(
request,
course_descriptor,
course_descriptor, # Primer for recursive function
depth_int
)
else:
response_data = _serialize_module(
request,
course_descriptor.id,
course_descriptor
)
status_code = status.HTTP_200_OK
response_data['uri'] = _generate_base_uri(request)
return Response(response_data, status=status_code)
else:
return Response({}, status=status.HTTP_404_NOT_FOUND)
class CoursesGroupsList(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def post(self, request, course_id, format=None):
"""
POST creates a new course-group relationship in the system
"""
response_data = {}
group_id = request.DATA['group_id']
base_uri = _generate_base_uri(request)
try:
existing_course = get_course(course_id)
except ValueError:
existing_course = None
try:
existing_group = Group.objects.get(id=group_id)
except ObjectDoesNotExist:
existing_group = None
if existing_course and existing_group:
try:
existing_relationship = CourseGroupRelationship.objects.get(course_id=course_id, group=existing_group)
except ObjectDoesNotExist:
existing_relationship = None
if existing_relationship is None:
CourseGroupRelationship.objects.create(course_id=course_id, group=existing_group)
response_data['course_id'] = str(existing_course.id)
response_data['group_id'] = str(existing_group.id)
response_data['uri'] = '{}/{}'.format(base_uri, existing_group.id)
response_status = status.HTTP_201_CREATED
else:
response_data['message'] = "Relationship already exists."
response_status = status.HTTP_409_CONFLICT
else:
response_status = status.HTTP_404_NOT_FOUND
return Response(response_data, status=response_status)
class CoursesGroupsDetail(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def get(self, request, course_id, group_id, format=None):
"""
GET retrieves an existing course-group relationship from the system
"""
response_data = {}
base_uri = _generate_base_uri(request)
response_data['uri'] = base_uri
response_data['course_id'] = course_id
response_data['group_id'] = group_id
try:
existing_course = get_course(course_id)
except ValueError:
existing_course = None
try:
existing_group = Group.objects.get(id=group_id)
except ObjectDoesNotExist:
existing_group = None
if existing_course and existing_group:
try:
existing_relationship = CourseGroupRelationship.objects.get(course_id=course_id, group=existing_group)
except ObjectDoesNotExist:
existing_relationship = None
if existing_relationship:
response_status = status.HTTP_200_OK
else:
response_status = status.HTTP_404_NOT_FOUND
else:
response_status = status.HTTP_404_NOT_FOUND
return Response(response_data, status=response_status)
def delete(self, request, course_id, group_id, format=None):
"""
DELETE removes/inactivates/etc. an existing course-group relationship
"""
try:
existing_group = Group.objects.get(id=group_id)
existing_relationship = CourseGroupRelationship.objects.get(course_id=course_id, group=existing_group).delete()
except ObjectDoesNotExist:
pass
return Response({}, status=status.HTTP_204_NO_CONTENT)
class CoursesOverview(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def get(self, request, course_id, format=None):
"""
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"}
"""
response_data = OrderedDict()
try:
existing_course = get_course(course_id)
except ValueError:
existing_course = None
if existing_course:
existing_content = get_course_about_section(existing_course, 'overview')
if existing_content:
if request.GET.get('parse') and request.GET.get('parse') in ['True', 'true']:
response_data['sections'] = _parse_overview_html(existing_content)
else:
response_data['overview_html'] = existing_content
return Response(response_data, status=status.HTTP_200_OK)
else:
return Response({}, status=status.HTTP_404_NOT_FOUND)
else:
return Response({}, status=status.HTTP_404_NOT_FOUND)
class CoursesUpdates(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def get(self, request, course_id, format=None):
"""
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"}
"""
response_data = OrderedDict()
try:
existing_course = get_course(course_id)
except ValueError:
existing_course = None
if not existing_course:
return Response({}, status=status.HTTP_404_NOT_FOUND)
content = get_course_info_section(request, existing_course, 'updates')
if not content:
return Response({}, status=status.HTTP_404_NOT_FOUND)
if request.GET.get('parse') and request.GET.get('parse') in ['True', 'true']:
response_data['postings'] = _parse_updates_html(content)
else:
response_data['content'] = content
return Response(response_data)
class CoursesStaticTabsList(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def get(self, request, course_id):
"""
GET returns an array of Static Tabs inside of a course
"""
try:
existing_course = get_course(course_id)
except ValueError:
existing_course = None
if not existing_course:
return Response({}, status=status.HTTP_404_NOT_FOUND)
response_data = OrderedDict()
tabs = []
for tab in existing_course.tabs:
if tab.type == 'static_tab':
tab_data = OrderedDict()
tab_data['id'] = tab.url_slug
tab_data['name'] = tab.name
if request.GET.get('detail') and request.GET.get('detail') in ['True', 'true']:
tab_data['content'] = get_static_tab_contents(
request,
existing_course,
tab,
wrap_xmodule_display=False
)
tabs.append(tab_data)
response_data['tabs'] = tabs
return Response(response_data)
class CoursesStaticTabsDetail(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def get(self, request, course_id, tab_id):
"""
GET returns the specified static tab for the specified course
"""
try:
existing_course = get_course(course_id)
except ValueError:
existing_course = None
if existing_course:
response_data = OrderedDict()
for tab in existing_course.tabs:
if tab.type == 'static_tab' and tab.url_slug == tab_id:
response_data['id'] = tab.url_slug
response_data['name'] = tab.name
response_data['content'] = get_static_tab_contents(
request,
existing_course,
tab,
wrap_xmodule_display=False
)
if not response_data:
return Response({}, status=status.HTTP_404_NOT_FOUND)
return Response(response_data, status=status.HTTP_200_OK)
else:
return Response({}, status=status.HTTP_404_NOT_FOUND)
class CoursesUsersList(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def post(self, request, course_id, format=None):
"""
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
"""
response_data = OrderedDict()
try:
existing_course = get_course(course_id)
except ValueError:
existing_course = None
if not existing_course:
return Response({}, status=status.HTTP_404_NOT_FOUND)
if 'user_id' in request.DATA:
user_id = request.DATA['user_id']
try:
existing_user = User.objects.get(id=user_id)
except ObjectDoesNotExist:
existing_user = None
if existing_user:
CourseEnrollment.enroll(existing_user, course_id)
return Response({}, status=status.HTTP_201_CREATED)
else:
return Response({}, status=status.HTTP_404_NOT_FOUND)
elif 'email' in request.DATA:
try:
email = request.DATA['email']
existing_user = User.objects.get(email=email)
except ObjectDoesNotExist:
if request.DATA.get('allow_pending'):
# If the email doesn't exist we assume the student does not exist
# and the instructor is pre-enrolling them
# Store the pre-enrollment data in the CourseEnrollmentAllowed table
# NOTE: This logic really should live in CourseEnrollment.....
cea, _ = CourseEnrollmentAllowed.objects.get_or_create(course_id=course_id, email=email)
cea.auto_enroll = True
cea.save()
return Response({}, status.HTTP_201_CREATED)
else:
return Response({}, status.HTTP_400_BAD_REQUEST)
else:
return Response({}, status=status.HTTP_400_BAD_REQUEST)
def get(self, request, course_id, format=None):
"""
GET returns a list of users enrolled in the course_id
"""
response_data = OrderedDict()
try:
existing_course = get_course(course_id)
except ValueError:
existing_course = None
if not existing_course:
return Response({}, status=status.HTTP_404_NOT_FOUND)
# Get a list of all enrolled students
users = CourseEnrollment.users_enrolled_in(course_id)
response_data['enrollments'] = []
for user in users:
user_data = OrderedDict()
user_data['id'] = user.id
user_data['email'] = user.email
user_data['username'] = user.username
# @TODO: Should we create a URI resourse that points to user?!? But that's in a different URL subpath
response_data['enrollments'].append(user_data)
# Then list all enrollments which are pending. These are enrollments for students that have not yet
# created an account
pending_enrollments = CourseEnrollmentAllowed.objects.filter(course_id=course_id)
if pending_enrollments:
response_data['pending_enrollments'] = []
for cea in pending_enrollments:
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):
"""
GET identifies an ACTIVE course enrollment for the specified user
"""
base_uri = _generate_base_uri(request)
response_data = {
'course_id': course_id,
'user_id': user_id,
'uri': base_uri,
}
try:
course_descriptor = get_course(course_id)
except ValueError:
course_descriptor = None
if not course_descriptor:
return Response(response_data, status=status.HTTP_404_NOT_FOUND)
try:
user = User.objects.get(id=user_id, is_active=True)
except ObjectDoesNotExist:
user = None
if user and CourseEnrollment.is_enrolled(user, course_id):
field_data_cache = FieldDataCache([course_descriptor], course_id, user)
course_module = module_render.get_module(
user,
request,
course_descriptor.location,
field_data_cache,
course_id)
response_data['position'] = course_module.position
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, course_id, user_id, format=None):
"""
DELETE unenrolls the specified user from the specified course
"""
response_data = OrderedDict()
try:
existing_course = get_course(course_id)
except ValueError:
existing_course = None
if not existing_course:
return Response({}, status=status.HTTP_404_NOT_FOUND)
try:
user = User.objects.get(id=user_id, is_active=True)
except ObjectDoesNotExist:
user = None
if user:
CourseEnrollment.unenroll(user, course_id)
return Response({}, status=status.HTTP_204_NO_CONTENT)
# pylint: disable=E1103
"""
Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/tests/test_group_views.py]
"""
from random import randint
import uuid
import json
from django.core.cache import cache
from django.test import TestCase, Client
from django.test.utils import override_settings
from api_manager.models import GroupRelationship, GroupProfile
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
TEST_API_KEY = str(uuid.uuid4())
class SecureClient(Client):
""" Django test client using a "secure" connection. """
def __init__(self, *args, **kwargs):
kwargs = kwargs.copy()
kwargs.update({'SERVER_PORT': 443, 'wsgi.url_scheme': 'https'})
super(SecureClient, self).__init__(*args, **kwargs)
@override_settings(EDX_API_KEY=TEST_API_KEY)
class GroupsApiTests(ModuleStoreTestCase):
""" Test suite for Groups API views """
def setUp(self):
self.test_server_prefix = 'https://testserver'
self.test_username = str(uuid.uuid4())
self.test_password = str(uuid.uuid4())
self.test_email = str(uuid.uuid4()) + '@test.org'
self.test_group_name = str(uuid.uuid4())
self.base_users_uri = '/api/users'
self.base_groups_uri = '/api/groups'
self.course = CourseFactory.create()
self.test_course_id = self.course.id
self.client = SecureClient()
cache.clear()
def do_post(self, uri, data):
"""Submit an HTTP POST request"""
headers = {
'Content-Type': 'application/json',
'X-Edx-Api-Key': str(TEST_API_KEY),
}
response = self.client.post(uri, headers=headers, data=json.dumps(data), content_type='application/json')
return response
def do_get(self, uri):
"""Submit an HTTP GET request"""
headers = {
'Content-Type': 'application/json',
'X-Edx-Api-Key': str(TEST_API_KEY),
}
response = self.client.get(uri, headers=headers)
return response
def do_delete(self, uri):
"""Submit an HTTP DELETE request"""
headers = {
'Content-Type': 'application/json',
'X-Edx-Api-Key': str(TEST_API_KEY),
}
response = self.client.delete(uri, headers=headers)
return response
def test_group_list_post(self):
data = {'name': self.test_group_name}
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201)
self.assertGreater(response.data['id'], 0)
confirm_uri = self.test_server_prefix + self.base_groups_uri + '/' + str(response.data['id'])
self.assertEqual(response.data['uri'], confirm_uri)
self.assertGreater(len(response.data['name']), 0)
def test_group_list_get_with_profile(self):
data = {
'name': self.test_group_name,
'group_type': 'series',
'data': {
'display_name': 'My first series'
}
}
response = self.do_post(self.base_groups_uri, data)
self.assertGreater(response.data['id'], 0)
group_id = response.data['id']
# query for list of groups, but don't put the type filter
test_uri = self.base_groups_uri
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
# try again with filter
test_uri = self.base_groups_uri + '?type=series'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]['group_id'], group_id)
self.assertEqual(response.data[0]['group_type'], 'series')
self.assertEqual(response.data[0]['name'], self.test_group_name)
self.assertEqual(response.data[0]['data']['display_name'], 'My first series')
# query the group detail
test_uri = self.base_groups_uri + '/' + str(group_id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['id'], group_id)
confirm_uri = self.test_server_prefix + test_uri
self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['name'], self.test_group_name)
self.assertEqual(response.data['group_type'], 'series')
self.assertEqual(response.data['data']['display_name'], 'My first series')
# update the profile
# first with missing data
response = self.do_post(test_uri, {})
self.assertEqual(response.status_code, 400)
data = {
'name': self.test_group_name,
'group_type': 'seriesX',
'data': {
'display_name': 'My updated series'
}
}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
# requery the filter
test_uri = self.base_groups_uri + '?type=series'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 0)
test_uri = self.base_groups_uri + '?type=seriesX'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]['group_id'], group_id)
self.assertEqual(response.data[0]['group_type'], 'seriesX')
self.assertEqual(response.data[0]['name'], self.test_group_name)
self.assertEqual(response.data[0]['data']['display_name'], 'My updated series')
def test_group_list_post_invalid_name(self):
data = {'name': ''}
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 400)
def test_group_list_get_uses_base_group_name(self):
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']
profile = GroupProfile.objects.get(group_id=group_id)
profile.name = ''
profile.save()
response = self.do_get(self.base_groups_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data[0]['name'], '{:04d}: {}'.format(group_id, self.test_group_name))
profile.name = None
profile.save()
response = self.do_get(self.base_groups_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data[0]['name'], '{:04d}: {}'.format(group_id, self.test_group_name))
def test_group_detail_get(self):
data = {'name': self.test_group_name}
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201)
self.assertGreater(response.data['id'], 0)
group_id = response.data['id']
test_uri = self.base_groups_uri + '/' + str(group_id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['id'], group_id)
confirm_uri = self.test_server_prefix + test_uri
self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['name'], self.test_group_name)
def test_group_detail_get_uses_base_group_name(self):
data = {'name': self.test_group_name}
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201)
self.assertGreater(response.data['id'], 0)
group_id = response.data['id']
profile = GroupProfile.objects.get(group_id=group_id)
profile.name = ''
profile.save()
test_uri = self.base_groups_uri + '/' + str(group_id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['id'], group_id)
confirm_uri = self.test_server_prefix + test_uri
self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['name'], '{:04d}: {}'.format(group_id, self.test_group_name))
def test_group_detail_get_with_missing_profile(self):
data = {'name': self.test_group_name}
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201)
self.assertGreater(response.data['id'], 0)
group_id = response.data['id']
GroupProfile.objects.get(group_id=group_id).delete()
test_uri = self.base_groups_uri + '/' + str(group_id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['id'], group_id)
confirm_uri = self.test_server_prefix + test_uri
self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['name'], '{:04d}: {}'.format(group_id, self.test_group_name))
def test_group_detail_get_undefined(self):
test_uri = self.base_groups_uri + '/123456789'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_group_detail_post(self):
data = {'name': self.test_group_name}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = response.data['uri']
self.assertEqual(response.status_code, 201)
group_type = 'seriesX'
data = {
'name': self.test_group_name,
'group_type': group_type,
'data': {
'display_name': 'My updated series'
}
}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
self.assertEqual(response.data['id'], group_id)
self.assertEqual(response.data['name'], self.test_group_name)
self.assertEqual(response.data['uri'], test_uri)
def test_group_detail_post_invalid_group(self):
test_uri = '{}/23209232'.format(self.base_groups_uri)
group_type = 'seriesX'
data = {
'name': self.test_group_name,
'group_type': group_type,
'data': {
'display_name': 'My updated series'
}
}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 404)
def test_group_users_list_post(self):
local_username = self.test_username + str(randint(11, 99))
data = {
'email': self.test_email,
'username': local_username,
'password': self.test_password,
'first_name': 'Joe',
'last_name': 'Smith'
}
response = self.do_post(self.base_users_uri, data)
user_id = response.data['id']
data = {'name': 'Alpha Group'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = self.base_groups_uri + '/' + str(group_id)
response = self.do_get(test_uri)
test_uri = test_uri + '/users'
data = {'user_id': user_id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
confirm_uri = self.test_server_prefix + test_uri + '/' + str(response.data['user_id'])
self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['group_id'], str(group_id))
self.assertEqual(response.data['user_id'], str(user_id))
def test_group_users_list_post_duplicate(self):
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password}
response = self.do_post(self.base_users_uri, data)
user_id = response.data['id']
data = {'name': 'Alpha Group'}
response = self.do_post(self.base_groups_uri, data)
test_uri = self.base_groups_uri + '/' + str(response.data['id'])
response = self.do_get(test_uri)
test_uri = test_uri + '/users'
data = {'user_id': user_id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 409)
def test_group_users_list_post_invalid_group(self):
test_uri = self.base_groups_uri + '/1239878976'
test_uri = test_uri + '/users'
data = {'user_id': "98723896"}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 404)
def test_group_users_list_post_invalid_user(self):
data = {'name': 'Alpha Group'}
response = self.do_post(self.base_groups_uri, data)
test_uri = '{}/{}/users'.format(self.base_groups_uri, str(response.data['id']))
data = {'user_id': "98723896"}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 404)
def test_group_users_list_get(self):
local_username = self.test_username + str(randint(11, 99))
data = {
'email': self.test_email,
'username': local_username,
'password': self.test_password,
'first_name': 'Joe',
'last_name': 'Smith'
}
response = self.do_post(self.base_users_uri, data)
user_id = response.data['id']
data = {'name': 'Alpha Group'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = self.base_groups_uri + '/' + str(group_id)
response = self.do_get(test_uri)
test_uri = test_uri + '/users'
data = {'user_id': user_id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
users = response.data['users']
self.assertEqual(len(users), 1)
self.assertEqual(users[0]['id'], user_id)
self.assertEqual(users[0]['username'], local_username)
self.assertEqual(users[0]['email'], self.test_email)
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)
self.assertEqual(response.status_code, 404)
def test_group_users_detail_get(self):
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password}
response = self.do_post(self.base_users_uri, data)
user_id = response.data['id']
data = {'name': 'Alpha Group'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = self.base_groups_uri + '/' + str(response.data['id'])
response = self.do_get(test_uri)
test_uri = test_uri + '/users'
data = {'user_id': user_id}
response = self.do_post(test_uri, data)
test_uri = test_uri + '/' + str(user_id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data['uri']), 0)
confirm_uri = self.test_server_prefix + test_uri
self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['group_id'], group_id)
self.assertEqual(response.data['user_id'], user_id)
def test_group_users_detail_delete(self):
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password}
response = self.do_post(self.base_users_uri, data)
user_id = response.data['id']
data = {'name': 'Alpha Group'}
response = self.do_post(self.base_groups_uri, data)
test_uri = self.base_groups_uri + '/' + str(response.data['id'])
response = self.do_get(test_uri)
test_uri = test_uri + '/users'
data = {'user_id': user_id}
response = self.do_post(test_uri, data)
test_uri = test_uri + '/' + str(user_id)
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204)
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204) # Idempotent
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_group_users_detail_delete_invalid_group(self):
test_uri = self.base_groups_uri + '/123987102/users/123124'
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204)
def test_group_users_detail_delete_invalid_user(self):
data = {'name': self.test_group_name}
response = self.do_post(self.base_groups_uri, data)
test_uri = self.base_groups_uri + '/' + str(response.data['id'])
test_uri = test_uri + '/users/123124'
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204)
def test_group_users_detail_get_undefined(self):
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password}
response = self.do_post(self.base_users_uri, data)
user_id = response.data['id']
data = {'name': 'Alpha Group'}
response = self.do_post(self.base_groups_uri, data)
group_id = response.data['id']
test_uri = self.base_groups_uri + '/' + str(group_id) + '/users/' + str(user_id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_group_groups_list_post_hierarchical(self):
data = {'name': 'Alpha Group'}
alpha_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(alpha_response.status_code, 201)
data = {'name': 'Beta Group'}
beta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(beta_response.status_code, 201)
data = {'name': 'Delta Group'}
delta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(delta_response.status_code, 201)
test_uri = alpha_response.data['uri'] + '/groups'
group_id = delta_response.data['id']
relationship_type = 'h' # Hierarchical
data = {'group_id': group_id, 'relationship_type': relationship_type}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
self.assertGreater(len(response.data['uri']), 0)
confirm_uri = test_uri + '/' + str(response.data['group_id'])
self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['group_id'], str(group_id))
self.assertEqual(response.data['relationship_type'], relationship_type)
def test_group_groups_list_post_linked(self):
data = {'name': 'Alpha Group'}
alpha_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(alpha_response.status_code, 201)
data = {'name': 'Beta Group'}
beta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(beta_response.status_code, 201)
data = {'name': 'Delta Group'}
delta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(delta_response.status_code, 201)
test_uri = alpha_response.data['uri'] + '/groups'
group_id = delta_response.data['id']
relationship_type = 'g' # Graph
data = {'group_id': group_id, 'relationship_type': relationship_type}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
self.assertGreater(len(response.data['uri']), 0)
confirm_uri = test_uri + '/' + str(response.data['group_id'])
self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['group_id'], str(group_id))
self.assertEqual(response.data['relationship_type'], relationship_type)
def test_group_groups_list_post_linked_duplicate(self):
data = {'name': 'Alpha Group'}
alpha_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(alpha_response.status_code, 201)
data = {'name': 'Beta Group'}
beta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(beta_response.status_code, 201)
data = {'name': 'Delta Group'}
delta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(delta_response.status_code, 201)
test_uri = alpha_response.data['uri'] + '/groups'
group_id = delta_response.data['id']
relationship_type = 'g' # Graph
data = {'group_id': group_id, 'relationship_type': relationship_type}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
response = self.do_post(test_uri, data)
# Duplicate responses are idemnotent in this case
self.assertEqual(response.status_code, 201)
def test_group_groups_list_post_invalid_group(self):
test_uri = self.base_groups_uri + '/123098/groups'
relationship_type = 'g' # Graph
data = {'group_id': '232987', 'relationship_type': relationship_type}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 404)
def test_group_groups_list_post_invalid_relationship_type(self):
data = {'name': 'Alpha Group'}
alpha_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(alpha_response.status_code, 201)
data = {'name': 'Beta Group'}
beta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(beta_response.status_code, 201)
data = {'name': 'Delta Group'}
delta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(delta_response.status_code, 201)
test_uri = alpha_response.data['uri'] + '/groups'
group_id = delta_response.data['id']
relationship_type = "z" # Graph
data = {'group_id': group_id, 'relationship_type': relationship_type}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 406)
def test_group_groups_list_get(self):
data = {'name': 'Bravo Group'}
bravo_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(bravo_response.status_code, 201)
bravo_group_id = bravo_response.data['id']
bravo_groups_uri = bravo_response.data['uri'] + '/groups'
data = {'name': 'Charlie Group'}
charlie_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(charlie_response.status_code, 201)
charlie_group_id = charlie_response.data['id']
relationship_type = 'h' # Hierarchical
data = {'group_id': charlie_group_id, 'relationship_type': relationship_type}
response = self.do_post(bravo_groups_uri, data)
self.assertEqual(response.status_code, 201)
data = {'name': 'Foxtrot Group'}
foxtrot_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(foxtrot_response.status_code, 201)
foxtrot_group_id = foxtrot_response.data['id']
relationship_type = 'g' # Graph
data = {'group_id': foxtrot_group_id, 'relationship_type': relationship_type}
response = self.do_post(bravo_groups_uri, data)
self.assertEqual(response.status_code, 201)
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'
response = self.do_post(tango_groups_uri, data)
self.assertEqual(response.status_code, 201)
response = self.do_get(bravo_groups_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
group_idlist = (charlie_group_id, foxtrot_group_id, tango_group_id)
relationship_count = 0
for relationship in response.data:
relationship_count = relationship_count + 1
group_id = relationship['id']
self.assertGreater(group_id, 0)
self.assertFalse(bravo_group_id == group_id)
self.assertTrue(relationship['relationship_type'] in ["h", "g"])
self.assertGreater(len(relationship['uri']), 0)
self.assertEqual(relationship_count, len(group_idlist))
def test_group_groups_list_get_with_profile_type(self):
data = {'name': 'Bravo Group'}
bravo_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(bravo_response.status_code, 201)
bravo_group_id = bravo_response.data['id']
bravo_groups_uri = bravo_response.data['uri'] + '/groups?type=test_group'
data = {'name': 'Charlie Group', 'group_type': 'test_group'}
charlie_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(charlie_response.status_code, 201)
charlie_group_id = charlie_response.data['id']
relationship_type = 'h' # Hierarchical
data = {'group_id': charlie_group_id, 'relationship_type': relationship_type}
response = self.do_post(bravo_groups_uri, data)
self.assertEqual(response.status_code, 201)
data = {'name': 'Foxtrot Group', 'group_type': 'test_group'}
foxtrot_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(foxtrot_response.status_code, 201)
foxtrot_group_id = foxtrot_response.data['id']
relationship_type = 'g' # Graph
data = {'group_id': foxtrot_group_id, 'relationship_type': relationship_type}
response = self.do_post(bravo_groups_uri, data)
self.assertEqual(response.status_code, 201)
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'
response = self.do_post(tango_groups_uri, data)
self.assertEqual(response.status_code, 201)
response = self.do_get(bravo_groups_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data), 0)
group_idlist = (charlie_group_id, foxtrot_group_id)
relationship_count = 0
for relationship in response.data:
relationship_count = relationship_count + 1
group_id = relationship['id']
self.assertGreater(group_id, 0)
self.assertFalse(bravo_group_id == group_id)
self.assertTrue(relationship['relationship_type'] in ["h", "g"])
self.assertGreater(len(relationship['uri']), 0)
self.assertEqual(relationship_count, len(group_idlist))
def test_group_groups_list_get_notfound(self):
test_uri = self.base_groups_uri + '/213213123/groups'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_group_groups_detail_get_hierarchical(self):
data = {'name': 'Alpha Group'}
alpha_response = self.do_post(self.base_groups_uri, data)
alpha_group_id = alpha_response.data['id']
self.assertEqual(alpha_response.status_code, 201)
data = {'name': 'Beta Group'}
beta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(beta_response.status_code, 201)
data = {'name': 'Delta Group'}
delta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(delta_response.status_code, 201)
test_uri = alpha_response.data['uri'] + '/groups'
delta_group_id = delta_response.data['id']
relationship_type = 'h' # Hierarchical
data = {'group_id': delta_group_id, 'relationship_type': relationship_type}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
test_uri = response.data['uri']
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data['uri']), 0)
confirm_uri = test_uri
self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['from_group_id'], str(alpha_group_id))
self.assertEqual(response.data['to_group_id'], str(delta_group_id))
self.assertEqual(response.data['relationship_type'], relationship_type)
def test_group_groups_detail_get_linked(self):
data = {'name': 'Alpha Group'}
alpha_response = self.do_post(self.base_groups_uri, data)
alpha_group_id = alpha_response.data['id']
self.assertEqual(alpha_response.status_code, 201)
data = {'name': 'Beta Group'}
beta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(beta_response.status_code, 201)
data = {'name': 'Delta Group'}
delta_response = self.do_post(self.base_groups_uri, data)
delta_group_id = delta_response.data['id']
self.assertEqual(delta_response.status_code, 201)
test_uri = alpha_response.data['uri'] + '/groups'
relationship_type = 'g' # Graph
data = {'group_id': delta_group_id, 'relationship_type': relationship_type}
delta_group = GroupRelationship.objects.get(group_id=delta_group_id)
delta_group.parent_group_id = None
delta_group.save()
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
test_uri = response.data['uri']
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data['uri']), 0)
confirm_uri = test_uri
self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['from_group_id'], str(alpha_group_id))
self.assertEqual(response.data['to_group_id'], str(delta_group_id))
self.assertEqual(response.data['relationship_type'], relationship_type)
def test_group_groups_detail_get_notfound(self):
data = {'name': 'Alpha Group'}
alpha_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(alpha_response.status_code, 201)
test_uri = alpha_response.data['uri'] + '/groups/gaois89sdf98'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_group_groups_detail_delete_hierarchical(self):
data = {'name': 'Alpha Group'}
alpha_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(alpha_response.status_code, 201)
data = {'name': 'Beta Group'}
beta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(beta_response.status_code, 201)
data = {'name': 'Delta Group'}
delta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(delta_response.status_code, 201)
data = {'name': 'Gamma Group'}
gamma_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(gamma_response.status_code, 201)
test_uri = alpha_response.data['uri'] + '/groups'
group_id = gamma_response.data['id']
relationship_type = 'h'
data = {'group_id': group_id, 'relationship_type': relationship_type}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
test_uri = response.data['uri']
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204)
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204)
try:
self.assertIsNone(response.data['message'])
except KeyError:
pass
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_group_groups_detail_delete_linked(self):
data = {'name': 'Alpha Group'}
alpha_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(alpha_response.status_code, 201)
data = {'name': 'Beta Group'}
beta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(beta_response.status_code, 201)
data = {'name': 'Delta Group'}
delta_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(delta_response.status_code, 201)
data = {'name': 'Gamma Group'}
gamma_response = self.do_post(self.base_groups_uri, data)
self.assertEqual(gamma_response.status_code, 201)
test_uri = alpha_response.data['uri'] + '/groups'
group_id = gamma_response.data['id']
relationship_type = 'g'
data = {'group_id': group_id, 'relationship_type': relationship_type}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
test_uri = response.data['uri']
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204)
try:
self.assertIsNone(response.data['message'])
except KeyError:
pass
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_group_groups_detail_delete_invalid(self):
test_uri = self.base_groups_uri + '/1231234232/groups/1'
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 404)
def test_group_courses_list_post(self):
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)
self.assertEqual(response.status_code, 201)
confirm_uri = test_uri + '/' + 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)
def test_group_courses_list_post_duplicate(self):
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)
self.assertEqual(response.status_code, 201)
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 409)
def test_group_courses_list_post_invalid_group(self):
test_uri = self.base_groups_uri + '/1239878976/courses'
data = {'course_id': "98723896"}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 404)
def test_group_courses_list_post_invalid_course(self):
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)
self.assertEqual(response.status_code, 404)
def test_group_courses_list_get(self):
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)
self.assertEqual(response.status_code, 201)
confirm_uri = test_uri + '/' + 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)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['courses']), 1)
self.assertEqual(response.data['courses'][0]['course_id'], self.test_course_id)
self.assertEqual(response.data['courses'][0]['display_name'], self.course.display_name)
def test_group_courses_list_get_invalid_group(self):
test_uri = self.base_groups_uri + '/1231241/courses'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_group_courses_detail_get(self):
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)
self.assertEqual(response.status_code, 201)
test_uri = '{}/{}/courses/{}'.format(self.base_groups_uri, group_id, self.test_course_id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
confirm_uri = '{}{}/{}/courses/{}'.format(
self.test_server_prefix,
self.base_groups_uri,
group_id,
self.test_course_id
)
self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['group_id'], group_id)
self.assertEqual(response.data['course_id'], self.test_course_id)
def test_group_courses_detail_delete(self):
data = {'name': self.test_group_name}
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201)
test_uri = response.data['uri'] + '/courses'
data = {'course_id': self.test_course_id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
test_uri = response.data['uri']
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204)
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204) # Idempotent
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_group_courses_detail_delete_invalid_group(self):
test_uri = self.base_groups_uri + '/123987102/courses/123124'
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204)
def test_group_courses_detail_delete_invalid_course(self):
data = {'name': self.test_group_name}
response = self.do_post(self.base_groups_uri, data)
self.assertEqual(response.status_code, 201)
test_uri = response.data['uri'] + '/courses/123124'
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204)
def test_group_courses_detail_get_undefined(self):
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)
""" Groups API URI specification """
from django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns
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/*$', 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]+)/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()),
)
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, User
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone
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
from api_manager.permissions import ApiKeyHeaderPermission
from api_manager.models import GroupRelationship, CourseGroupRelationship, GroupProfile
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location, InvalidLocationError
RELATIONSHIP_TYPES = {'hierarchical': 'h', 'graph': 'g'}
def _generate_base_uri(request):
"""
Constructs the protocol:host:path component of the resource uri
"""
protocol = 'http'
if request.is_secure():
protocol = protocol + 's'
resource_uri = '{}://{}{}'.format(
protocol,
request.get_host(),
request.get_full_path()
)
return resource_uri
class GroupsList(APIView):
permissions_classes = (ApiKeyHeaderPermission,)
def post(self, request, format=None):
"""
POST creates a new group in the system
"""
response_data = {}
base_uri = _generate_base_uri(request)
# Group name must be unique, but we need to support dupes
group = Group.objects.create(name=str(uuid.uuid4()))
if request.DATA.get('name'):
original_group_name = request.DATA.get('name')
else:
return Response(response_data, status=status.HTTP_400_BAD_REQUEST)
group.name = '{:04d}: {}'.format(group.id, original_group_name)
group.record_active = True
group.record_date_created = timezone.now()
group.record_date_modified = timezone.now()
group.save()
# Create a corresponding relationship management record
GroupRelationship.objects.create(group_id=group.id, parent_group=None)
# Create a corresponding profile record (for extra meta info)
group_type = request.DATA.get('group_type', None)
data = json.dumps(request.DATA.get('data')) if request.DATA.get('data') else {}
profile, _ = GroupProfile.objects.get_or_create(group_id=group.id, group_type=group_type, name=original_group_name, data=data)
response_data = {
'id': group.id,
'name': original_group_name,
'type': group_type,
}
base_uri = _generate_base_uri(request)
response_data['uri'] = '{}/{}'.format(base_uri, group.id)
response_status = status.HTTP_201_CREATED
return Response(response_data, status=response_status)
def get(self, request, format=None):
"""
GET retrieves a list of groups in the system filtered by type
"""
response_data = []
if 'type' in request.GET:
profiles = GroupProfile.objects.filter(group_type=request.GET['type'])
else:
profiles = GroupProfile.objects.all()
for profile in profiles:
item_data = {}
item_data['group_id'] = profile.group_id
if profile.name and len(profile.name):
group_name = profile.name
else:
group = Group.objects.get(id=profile.group_id)
group_name = group.name
item_data['name'] = group_name
if profile.group_type:
item_data['group_type'] = profile.group_type
if profile.data:
item_data['data'] = json.loads(profile.data)
response_data.append(item_data)
return Response(response_data, status=status.HTTP_200_OK)
class GroupsDetail(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def post(self, request, group_id, format=None):
response_data = {}
base_uri = _generate_base_uri(request)
print base_uri
try:
existing_group = Group.objects.get(id=group_id)
except ObjectDoesNotExist:
return Response({}, status.HTTP_404_NOT_FOUND)
group_type = request.DATA.get('group_type')
data = json.dumps(request.DATA.get('data')) if request.DATA.get('data') else None
if not group_type and not data:
return Response({}, status.HTTP_400_BAD_REQUEST)
profile, _ = GroupProfile.objects.get_or_create(group_id=group_id)
profile.group_type = group_type
profile.data = data
profile.save()
response_data['id'] = existing_group.id
response_data['name'] = profile.name
response_data['uri'] = _generate_base_uri(request)
return Response(response_data, status=status.HTTP_201_CREATED)
def get(self, request, group_id, format=None):
"""
GET retrieves an existing group from the system
"""
response_data = {}
base_uri = _generate_base_uri(request)
try:
existing_group = Group.objects.get(id=group_id)
except ObjectDoesNotExist:
return Response({}, status.HTTP_404_NOT_FOUND)
response_data['id'] = existing_group.id
response_data['uri'] = base_uri
response_data['resources'] = []
resource_uri = '{}/users'.format(base_uri)
response_data['resources'].append({'uri': resource_uri})
resource_uri = '{}/groups'.format(base_uri)
response_data['resources'].append({'uri': resource_uri})
try:
group_profile = GroupProfile.objects.get(group_id=group_id)
except ObjectDoesNotExist:
group_profile = None
if group_profile:
if group_profile.name:
response_data['name'] = group_profile.name
else:
response_data['name'] = existing_group.name
if group_profile.group_type:
response_data['group_type'] = group_profile.group_type
data = group_profile.data
if data:
response_data['data'] = json.loads(data)
else:
response_data['name'] = existing_group.name
return Response(response_data, status=status.HTTP_200_OK)
class GroupsUsersList(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def post(self, request, group_id, format=None):
"""
POST creates a new group-user relationship in the system
"""
base_uri = _generate_base_uri(request)
try:
existing_group = Group.objects.get(id=group_id)
except ObjectDoesNotExist:
return Response({}, status.HTTP_404_NOT_FOUND)
user_id = request.DATA['user_id']
try:
existing_user = User.objects.get(id=user_id)
except ObjectDoesNotExist:
return Response({}, status.HTTP_404_NOT_FOUND)
try:
existing_relationship = Group.objects.get(user=existing_user)
except ObjectDoesNotExist:
existing_relationship = None
response_data = {}
if existing_relationship is None:
existing_group.user_set.add(existing_user.id)
response_data['uri'] = '{}/{}'.format(base_uri, existing_user.id)
response_data['group_id'] = str(existing_group.id)
response_data['user_id'] = str(existing_user.id)
response_status = status.HTTP_201_CREATED
else:
response_data['uri'] = '{}/{}'.format(base_uri, existing_user.id)
response_data['message'] = "Relationship already exists."
response_status = status.HTTP_409_CONFLICT
return Response(response_data, status=response_status)
def get(self, request, group_id, format=None):
"""
GET retrieves the list of users related to the specified group
"""
try:
existing_group = Group.objects.get(id=group_id)
except ObjectDoesNotExist:
return Response({}, status.HTTP_404_NOT_FOUND)
users = existing_group.user_set.all()
response_data = {}
response_data['users'] = []
for user in users:
user_data = {}
user_data['id'] = user.id
user_data['email'] = user.email
user_data['username'] = user.username
user_data['first_name'] = user.first_name
user_data['last_name'] = user.last_name
response_data['users'].append(user_data)
response_status = status.HTTP_200_OK
return Response(response_data, status=response_status)
class GroupsUsersDetail(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def get(self, request, group_id, user_id, format=None):
"""
GET retrieves an existing group-user relationship from the system
"""
response_data = {}
base_uri = _generate_base_uri(request)
try:
existing_group = Group.objects.get(id=group_id)
existing_relationship = existing_group.user_set.get(id=user_id)
except ObjectDoesNotExist:
existing_group = None
existing_relationship = None
if existing_group and existing_relationship:
response_data['group_id'] = existing_group.id
response_data['user_id'] = existing_relationship.id
response_data['uri'] = base_uri
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, group_id, user_id, format=None):
"""
DELETE removes/inactivates/etc. an existing group-user relationship
"""
try:
existing_group = Group.objects.get(id=group_id)
existing_group.user_set.remove(user_id)
existing_group.save()
except ObjectDoesNotExist:
pass
return Response({}, status=status.HTTP_204_NO_CONTENT)
class GroupsGroupsList(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def post(self, request, group_id, format=None):
"""
POST creates a new group-group relationship in the system
"""
response_data = {}
to_group_id = request.DATA['group_id']
relationship_type = request.DATA['relationship_type']
base_uri = _generate_base_uri(request)
response_data['uri'] = '{}/{}'.format(base_uri, to_group_id)
response_data['group_id'] = str(to_group_id)
response_data['relationship_type'] = relationship_type
try:
from_group_relationship = GroupRelationship.objects.get(group__id=group_id)
to_group_relationship = GroupRelationship.objects.get(group__id=to_group_id)
except ObjectDoesNotExist:
from_group_relationship = None
to_group_relationship = None
if from_group_relationship and to_group_relationship:
response_status = status.HTTP_201_CREATED
if relationship_type == RELATIONSHIP_TYPES['hierarchical']:
to_group_relationship.parent_group = from_group_relationship
to_group_relationship.save()
elif relationship_type == RELATIONSHIP_TYPES['graph']:
from_group_relationship.add_linked_group_relationship(to_group_relationship)
else:
response_data['message'] = "Relationship type '%s' not currently supported" % relationship_type
response_data['field_conflict'] = 'relationship_type'
response_status = status.HTTP_406_NOT_ACCEPTABLE
else:
response_status = status.HTTP_404_NOT_FOUND
return Response(response_data, status=response_status)
def get(self, request, group_id, format=None):
"""
GET retrieves the existing group-group relationships for the specified group
"""
try:
from_group_relationship = GroupRelationship.objects.get(group__id=group_id)
except ObjectDoesNotExist:
from_group_relationship = None
response_data = []
if from_group_relationship:
base_uri = _generate_base_uri(request)
group_type = request.QUERY_PARAMS.get('type', None)
child_groups = GroupRelationship.objects.filter(parent_group_id=group_id)
linked_groups = from_group_relationship.get_linked_group_relationships()
if group_type:
profiles = GroupProfile.objects.filter(group_type=request.GET['type']).values_list('group_id', flat=True)
if profiles:
child_groups = child_groups.filter(group_id__in=profiles)
linked_groups = linked_groups.filter(to_group_relationship__in=profiles)
if child_groups:
for group in child_groups:
response_data.append({
"id": group.group_id,
"relationship_type": RELATIONSHIP_TYPES['hierarchical'],
"uri": '{}/{}'.format(base_uri, group.group.id)
})
if linked_groups:
for group in linked_groups:
response_data.append({
"id": group.to_group_relationship_id,
"relationship_type": RELATIONSHIP_TYPES['graph'],
"uri": '{}/{}'.format(base_uri, group.to_group_relationship_id)
})
response_status = status.HTTP_200_OK
else:
response_status = status.HTTP_404_NOT_FOUND
return Response(response_data, status=response_status)
class GroupsGroupsDetail(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def get(self, request, group_id, related_group_id, format=None):
"""
GET retrieves an existing group-group relationship from the system
"""
response_data = {}
base_uri = _generate_base_uri(request)
response_data['uri'] = base_uri
response_data['from_group_id'] = group_id
response_data['to_group_id'] = related_group_id
response_status = status.HTTP_404_NOT_FOUND
from_group_relationship = GroupRelationship.objects.get(group__id=group_id)
if from_group_relationship:
to_group_relationship = GroupRelationship.objects.get(group__id=related_group_id)
if to_group_relationship and str(to_group_relationship.parent_group_id) == str(group_id):
response_data['relationship_type'] = RELATIONSHIP_TYPES['hierarchical']
response_status = status.HTTP_200_OK
else:
to_group = Group.objects.get(id=to_group_relationship.group_id)
linked_group_exists = from_group_relationship.check_linked_group_relationship(to_group, symmetrical=True)
if linked_group_exists:
response_data['relationship_type'] = RELATIONSHIP_TYPES['graph']
response_status = status.HTTP_200_OK
return Response(response_data, response_status)
def delete(self, request, group_id, related_group_id, format=None):
"""
DELETE removes/inactivates/etc. an existing group-group relationship
"""
try:
from_group_relationship = GroupRelationship.objects.get(group__id=group_id)
except ObjectDoesNotExist:
from_group_relationship = None
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:
if to_group_relationship.parent_group_id is from_group_relationship.group_id:
to_group_relationship.parent_group_id = None
to_group_relationship.save()
else:
from_group_relationship.remove_linked_group_relationship(to_group_relationship)
from_group_relationship.save()
# No 'else' clause here -> It's ok if we didn't find a match
response_status = status.HTTP_204_NO_CONTENT
else:
response_status = status.HTTP_404_NOT_FOUND
return Response({}, status=response_status)
class GroupsCoursesList(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def post(self, request, group_id, format=None):
"""
POST creates a new group-course relationship in the system
"""
response_data = {}
try:
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)
if not existing_course:
return Response({}, status.HTTP_404_NOT_FOUND)
try:
existing_relationship = CourseGroupRelationship.objects.get(course_id=course_id, group=existing_group)
except ObjectDoesNotExist:
existing_relationship = None
if existing_relationship is None:
new_relationship = CourseGroupRelationship.objects.create(course_id=course_id, group=existing_group)
response_data['group_id'] = str(new_relationship.group_id)
response_data['course_id'] = str(new_relationship.course_id)
response_status = status.HTTP_201_CREATED
else:
response_data['message'] = "Relationship already exists."
response_status = status.HTTP_409_CONFLICT
return Response(response_data, status=response_status)
def get(self, request, group_id, format=None):
"""
GET returns all courses that has a relationship to the group
"""
response_data = {}
try:
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['courses'] = []
for member in members:
course = store.get_course(member.course_id)
course_data = {
'course_id': member.course_id,
'display_name': course.display_name
}
response_data['courses'].append(course_data)
response_status = status.HTTP_200_OK
return Response(response_data, status=response_status)
class GroupsCoursesDetail(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def get(self, request, group_id, course_id, format=None):
"""
GET retrieves an existing group-course relationship from the system
"""
response_data = {}
base_uri = _generate_base_uri(request)
response_data['uri'] = base_uri
try:
existing_group = Group.objects.get(id=group_id)
existing_relationship = CourseGroupRelationship.objects.get(course_id=course_id, group=existing_group)
except ObjectDoesNotExist:
existing_group = None
existing_relationship = None
if existing_group and existing_relationship:
response_data['group_id'] = existing_group.id
response_data['course_id'] = existing_relationship.course_id
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, group_id, course_id, format=None):
"""
DELETE removes/inactivates/etc. an existing group-course relationship
"""
try:
existing_group = Group.objects.get(id=group_id)
existing_group.coursegrouprelationship_set.get(course_id=course_id).delete()
existing_group.save()
except ObjectDoesNotExist:
pass
return Response({}, status=status.HTTP_204_NO_CONTENT)
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'GroupRelationship'
db.create_table('api_manager_grouprelationship', (
('group', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['auth.Group'], unique=True, primary_key=True)),
('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
('parent_group', self.gf('django.db.models.fields.related.ForeignKey')(default=0, related_name='child_groups', null=True, blank=True, to=orm['api_manager.GroupRelationship'])),
('record_active', self.gf('django.db.models.fields.BooleanField')(default=True)),
('record_date_created', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2014, 3, 27, 0, 0))),
('record_date_modified', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
))
db.send_create_signal('api_manager', ['GroupRelationship'])
# Adding model 'LinkedGroupRelationship'
db.create_table('api_manager_linkedgrouprelationship', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('from_group_relationship', self.gf('django.db.models.fields.related.ForeignKey')(related_name='from_group_relationships', to=orm['api_manager.GroupRelationship'])),
('to_group_relationship', self.gf('django.db.models.fields.related.ForeignKey')(related_name='to_group_relationships', to=orm['api_manager.GroupRelationship'])),
('record_active', self.gf('django.db.models.fields.BooleanField')(default=True)),
('record_date_created', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2014, 3, 27, 0, 0))),
('record_date_modified', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
))
db.send_create_signal('api_manager', ['LinkedGroupRelationship'])
def backwards(self, orm):
# Deleting model 'GroupRelationship'
db.delete_table('api_manager_grouprelationship')
# Deleting model 'LinkedGroupRelationship'
db.delete_table('api_manager_linkedgrouprelationship')
models = {
'api_manager.grouprelationship': {
'Meta': {'object_name': 'GroupRelationship'},
'group': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.Group']", 'unique': 'True', 'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'parent_group': ('django.db.models.fields.related.ForeignKey', [], {'default': '0', 'related_name': "'child_groups'", 'null': 'True', 'blank': 'True', 'to': "orm['api_manager.GroupRelationship']"}),
'record_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'record_date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 3, 27, 0, 0)'}),
'record_date_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
},
'api_manager.linkedgrouprelationship': {
'Meta': {'object_name': 'LinkedGroupRelationship'},
'from_group_relationship': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'from_group_relationships'", 'to': "orm['api_manager.GroupRelationship']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'record_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'record_date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 3, 27, 0, 0)'}),
'record_date_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'to_group_relationship': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'to_group_relationships'", 'to': "orm['api_manager.GroupRelationship']"})
},
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}
}
complete_apps = ['api_manager']
\ No newline at end of file
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'CourseGroupRelationship'
db.create_table('api_manager_coursegrouprelationship', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('group', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.Group'])),
))
db.send_create_signal('api_manager', ['CourseGroupRelationship'])
# Adding model 'GroupProfile'
db.create_table('auth_groupprofile', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('group', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.Group'])),
('group_type', self.gf('django.db.models.fields.CharField')(max_length=32, null=True, db_index=True)),
('data', self.gf('django.db.models.fields.TextField')(blank=True)),
))
db.send_create_signal('api_manager', ['GroupProfile'])
def backwards(self, orm):
# Deleting model 'CourseGroupRelationship'
db.delete_table('api_manager_coursegrouprelationship')
# Deleting model 'GroupProfile'
db.delete_table('auth_groupprofile')
models = {
'api_manager.coursegrouprelationship': {
'Meta': {'object_name': 'CourseGroupRelationship'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'api_manager.groupprofile': {
'Meta': {'object_name': 'GroupProfile', 'db_table': "'auth_groupprofile'"},
'data': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}),
'group_type': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'api_manager.grouprelationship': {
'Meta': {'object_name': 'GroupRelationship'},
'group': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.Group']", 'unique': 'True', 'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'parent_group': ('django.db.models.fields.related.ForeignKey', [], {'default': '0', 'related_name': "'child_groups'", 'null': 'True', 'blank': 'True', 'to': "orm['api_manager.GroupRelationship']"}),
'record_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'record_date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 4, 21, 0, 0)'}),
'record_date_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
},
'api_manager.linkedgrouprelationship': {
'Meta': {'object_name': 'LinkedGroupRelationship'},
'from_group_relationship': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'from_group_relationships'", 'to': "orm['api_manager.GroupRelationship']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'record_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'record_date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 4, 21, 0, 0)'}),
'record_date_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'to_group_relationship': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'to_group_relationships'", 'to': "orm['api_manager.GroupRelationship']"})
},
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}
}
complete_apps = ['api_manager']
\ No newline at end of file
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'GroupProfile.name'
db.add_column('auth_groupprofile', 'name',
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'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'api_manager.groupprofile': {
'Meta': {'object_name': 'GroupProfile', 'db_table': "'auth_groupprofile'"},
'data': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}),
'group_type': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
},
'api_manager.grouprelationship': {
'Meta': {'object_name': 'GroupRelationship'},
'group': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.Group']", 'unique': 'True', 'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'parent_group': ('django.db.models.fields.related.ForeignKey', [], {'default': '0', 'related_name': "'child_groups'", 'null': 'True', 'blank': 'True', 'to': "orm['api_manager.GroupRelationship']"}),
'record_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'record_date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 4, 30, 0, 0)'}),
'record_date_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
},
'api_manager.linkedgrouprelationship': {
'Meta': {'object_name': 'LinkedGroupRelationship'},
'from_group_relationship': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'from_group_relationships'", 'to': "orm['api_manager.GroupRelationship']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'record_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'record_date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 4, 30, 0, 0)'}),
'record_date_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'to_group_relationship': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'to_group_relationships'", 'to': "orm['api_manager.GroupRelationship']"})
},
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}
}
complete_apps = ['api_manager']
\ No newline at end of file
# pylint: disable=E1101
""" Database ORM models managed by this Django app """
from django.contrib.auth.models import Group
from django.db import models
from django.utils import timezone
class GroupRelationship(models.Model):
"""
The GroupRelationship model contains information describing the relationships of a group,
which allows us to utilize Django's user/group/permission
models and features instead of rolling our own.
"""
group = models.OneToOneField(Group, primary_key=True)
name = models.CharField(max_length=255)
parent_group = models.ForeignKey('self',
related_name="child_groups",
blank=True, null=True, default=0)
linked_groups = models.ManyToManyField('self',
through="LinkedGroupRelationship",
symmetrical=False,
related_name="linked_to+"),
record_active = models.BooleanField(default=True)
record_date_created = models.DateTimeField(default=timezone.now())
record_date_modified = models.DateTimeField(auto_now=True)
def add_linked_group_relationship(self, to_group_relationship, symmetrical=True):
""" Create a new group-group relationship """
relationship = LinkedGroupRelationship.objects.get_or_create(
from_group_relationship=self,
to_group_relationship=to_group_relationship)
if symmetrical:
# avoid recursion by passing `symm=False`
to_group_relationship.add_linked_group_relationship(self, False)
return relationship
def remove_linked_group_relationship(self, to_group_relationship, symmetrical=True):
""" Remove an existing group-group relationship """
LinkedGroupRelationship.objects.filter(
from_group_relationship=self,
to_group_relationship=to_group_relationship).delete()
if symmetrical:
# avoid recursion by passing `symm=False`
to_group_relationship.remove_linked_group_relationship(self, False)
return
def get_linked_group_relationships(self):
""" Retrieve an existing group-group relationship """
efferent_relationships = LinkedGroupRelationship.objects.filter(from_group_relationship=self)
matching_relationships = efferent_relationships
return matching_relationships
def check_linked_group_relationship(self, relationship_to_check, symmetrical=False):
""" Confirm the existence of a possibly-existing group-group relationship """
query = dict(
to_group_relationships__from_group_relationship=self,
to_group_relationships__to_group_relationship=relationship_to_check,
)
if symmetrical:
query.update(
from_group_relationships__to_group_relationship=self,
from_group_relationships__from_group_relationship=relationship_to_check,
)
return GroupRelationship.objects.filter(**query).exists()
class LinkedGroupRelationship(models.Model):
"""
The LinkedGroupRelationship model manages self-referential two-way
relationships between group entities via the GroupRelationship model.
Specifying the intermediary table allows for the definition of additional
relationship information
"""
from_group_relationship = models.ForeignKey(GroupRelationship,
related_name="from_group_relationships",
verbose_name="From Group")
to_group_relationship = models.ForeignKey(GroupRelationship,
related_name="to_group_relationships",
verbose_name="To Group")
record_active = models.BooleanField(default=True)
record_date_created = models.DateTimeField(default=timezone.now())
record_date_modified = models.DateTimeField(auto_now=True)
class CourseGroupRelationship(models.Model):
"""
The CourseGroupRelationship model contains information describing the
link between a course and a group. A typical use case for this table
is to manage the courses for an XSeries or other sort of program.
"""
course_id = models.CharField(max_length=255, db_index=True)
group = models.ForeignKey(Group, db_index=True)
class GroupProfile(models.Model):
"""
This table will provide additional tables regarding groups. This has a foreign key to
the auth_groups table
"""
class Meta:
db_table = "auth_groupprofile"
group = models.ForeignKey(Group, db_index=True)
group_type = models.CharField(null=True, max_length=32, db_index=True)
name = models.CharField(max_length=255, null=True, blank=True)
data = models.TextField(blank=True) # JSON dictionary for generic key/value pairs
""" Permissions classes utilized by Django REST Framework """
import logging
from django.conf import settings
from rest_framework import permissions
log = logging.getLogger(__name__)
class ApiKeyHeaderPermission(permissions.BasePermission):
"""
Check for permissions by matching the configured API key and header
"""
def has_permission(self, request, view):
"""
If settings.DEBUG is True and settings.EDX_API_KEY is not set or None,
then allow the request. Otherwise, allow the request if and only if
settings.EDX_API_KEY is set and the X-Edx-Api-Key HTTP header is
present in the request and matches the setting.
"""
debug_enabled = settings.DEBUG
api_key = getattr(settings, "EDX_API_KEY", None)
# DEBUG mode rules over all else
# Including the api_key check here ensures we don't break the feature locally
if debug_enabled and api_key is None:
log.warn("EDX_API_KEY Override: Debug Mode")
return True
# If we're not DEBUG, we need a local api key
if api_key is None:
return False
# The client needs to present the same api key
header_key = request.META.get('HTTP_X_EDX_API_KEY')
if header_key is None:
try:
header_key = request.META['headers'].get('X-Edx-Api-Key')
except KeyError:
return False
if header_key is None:
return False
# The api key values need to be the same
if header_key != api_key:
return False
# Allow the request to take place
return True
import json
import uuid
import unittest
from mock import patch
from datetime import datetime, timedelta
from freezegun import freeze_time
from pytz import UTC
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
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
"""
def setUp(self):
"""
Create one user and save it to the database
"""
self.user = UserFactory.build(username='test', email='test@edx.org')
self.user.set_password('test_password')
self.user.save()
# Create the test client
self.client = Client()
cache.clear()
self.session_url = '/api/sessions'
def test_login_ratelimiting_protection(self):
""" Try (and fail) login user 30 times on invalid password """
for i in xrange(30):
password = u'test_password{0}'.format(i)
response = self._do_post_request(self.session_url, 'test', password, secure=True)
self.assertEqual(response.status_code, 401)
# then the rate limiter should kick in and give a HttpForbidden response
response = self._do_post_request(self.session_url, 'test', 'test_password', secure=True)
message = _('Rate limit exceeded in api login.')
self._assert_response(response, status=403, message=message)
def test_login_ratelimiting_unblock(self):
""" Try (and fail) login user 30 times on invalid password """
for i in xrange(30):
password = u'test_password{0}'.format(i)
response = self._do_post_request(self.session_url, 'test', password, secure=True)
self.assertEqual(response.status_code, 401)
# then the rate limiter should kick in and give a HttpForbidden response
response = self._do_post_request(self.session_url, 'test', 'test_password', secure=True)
message = _('Rate limit exceeded in api login.')
self._assert_response(response, status=403, message=message)
# now reset the time to 5 mins from now in future in order to unblock
reset_time = datetime.now(UTC) + timedelta(seconds=300)
with freeze_time(reset_time):
response = self._do_post_request(self.session_url, 'test', 'test_password', secure=True)
self._assert_response(response, status=201)
def _do_post_request(self, url, username, password, **kwargs):
"""
Post the login info
"""
post_params, extra = {'username': username, 'password': password}, {}
headers = {'X-Edx-Api-Key': TEST_API_KEY, 'Content-Type': 'application/json'}
if kwargs.get('secure', False):
extra['wsgi.url_scheme'] = 'https'
return self.client.post(url, post_params, headers=headers, **extra)
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.
"""
self.assertEqual(response.status_code, status)
response_dict = json.loads(response.content)
if message is not None:
msg = ("'%s' did not contain '%s'" %
(response_dict['message'], message))
self.assertTrue(message in response_dict['message'], msg)
"""
Tests for session api with advance security features
"""
import json
import uuid
from mock import patch
from datetime import datetime, timedelta
from freezegun import freeze_time
from pytz import UTC
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.core.cache import cache
from student.tests.factories import UserFactory
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", {'ENABLE_MAX_FAILED_LOGIN_ATTEMPTS': True})
class SessionApiSecurityTest(TestCase):
"""
Test api_manager.session.session_list view
"""
def setUp(self):
"""
Create one user and save it to the database
"""
self.user = UserFactory.build(username='test', email='test@edx.org')
self.user.set_password('test_password')
self.user.save()
# Create the test client
self.client = Client()
cache.clear()
self.session_url = '/api/sessions'
self.user_url = '/api/users'
@override_settings(MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED=10)
def test_login_ratelimited_success(self):
"""
Try (and fail) logging in with fewer attempts than the limit of 10
and verify that you can still successfully log in afterwards.
"""
for i in xrange(9):
password = u'test_password{0}'.format(i)
response, mock_audit_log = self._do_request(self.session_url, 'test', password, secure=True)
self.assertEqual(response.status_code, 401)
# now try logging in with a valid password and check status
response, mock_audit_log = self._do_request(self.session_url, 'test', 'test_password', secure=True)
self._assert_response(response, status=201)
@override_settings(MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED=10)
def test_login_blockout(self):
"""
Try (and fail) logging in with 10 attempts
and verify that user is blocked out.
"""
for i in xrange(10):
password = u'test_password{0}'.format(i)
response, mock_audit_log = self._do_request(self.session_url, 'test', password, secure=True)
self.assertEqual(response.status_code, 401)
# check to see if this response indicates blockout
response, mock_audit_log = self._do_request(self.session_url, 'test', 'test_password', secure=True)
message = _('This account has been temporarily locked due to excessive login failures. Try again later.')
self._assert_response(response, status=403, message=message)
@override_settings(MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED=10,
MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS=1800)
def test_blockout_reset_time_period(self):
"""
Try logging in 10 times to block user and then login with right
credentials(after 30 minutes) to verify blocked out time expired and
user can login successfully.
"""
for i in xrange(10):
password = u'test_password{0}'.format(i)
response, mock_audit_log = self._do_request(self.session_url, 'test', password, secure=True)
self.assertEqual(response.status_code, 401)
self._assert_audit_log(mock_audit_log, 'warn',
[u"API::User authentication failed with user-id - {0}".format(self.user.id)])
self._assert_not_in_audit_log(mock_audit_log, 'warn', [u'test'])
# check to see if this response indicates blockout
response, mock_audit_log = self._do_request(self.session_url, 'test', 'test_password', secure=True)
message = _('This account has been temporarily locked due to excessive login failures. Try again later.')
self._assert_response(response, status=403, message=message)
# now reset the time to 30 from now in future
reset_time = datetime.now(UTC) + timedelta(seconds=1800)
with freeze_time(reset_time):
response, mock_audit_log = self._do_request(self.session_url, 'test', 'test_password', secure=True)
self._assert_response(response, status=201)
@override_settings(PASSWORD_MIN_LENGTH=4)
def test_with_short_password(self):
"""
Try (and fail) user creation with shorter password
"""
response, mock_audit_log = self._do_request(self.user_url, 'test', 'abc', email='test@edx.org',
first_name='John', last_name='Doe', secure=True)
message = _('Password: Invalid Length (must be 4 characters or more)')
self._assert_response(response, status=400, message=message)
@override_settings(PASSWORD_MAX_LENGTH=12)
def test_with_long_password(self):
"""
Try (and fail) user creation with longer password
"""
response, mock_audit_log = self._do_request(self.user_url, 'test', 'test_password', email='test@edx.org',
first_name='John', last_name='Doe', secure=True)
message = _('Password: Invalid Length (must be 12 characters or less)')
self._assert_response(response, status=400, message=message)
@override_settings(PASSWORD_COMPLEXITY={'UPPER': 2, 'LOWER': 2, 'PUNCTUATION': 2, 'DIGITS': 2})
def test_password_without_uppercase(self):
"""
Try (and fail) user creation since password should have atleast
2 upper characters
"""
response, mock_audit_log = self._do_request(self.user_url, 'test', 'test.pa64!', email='test@edx.org',
first_name='John', last_name='Doe', secure=True)
message = _('Password: Must be more complex (must contain 2 or more uppercase characters)')
self._assert_response(response, status=400, message=message)
@override_settings(PASSWORD_COMPLEXITY={'UPPER': 2, 'LOWER': 2, 'PUNCTUATION': 2, 'DIGITS': 2})
def test_password_without_lowercase(self):
"""
Try (and fail) user creation without any numeric characters
in password
"""
response, mock_audit_log = self._do_request(self.user_url, 'test', 'TEST.PA64!', email='test@edx.org',
first_name='John', last_name='Doe', secure=True)
message = _('Password: Must be more complex (must contain 2 or more lowercase characters)')
self._assert_response(response, status=400, message=message)
@override_settings(PASSWORD_COMPLEXITY={'UPPER': 2, 'LOWER': 2, 'PUNCTUATION': 2, 'DIGITS': 2})
def test_password_without_punctuation(self):
"""
Try (and fail) user creation without any punctuation in password
"""
response, mock_audit_log = self._do_request(self.user_url, 'test', 'test64SS', email='test@edx.org',
first_name='John', last_name='Doe', secure=True)
message = _('Password: Must be more complex (must contain 2 or more uppercase characters,'
' must contain 2 or more punctuation characters)')
self._assert_response(response, status=400, message=message)
@override_settings(PASSWORD_COMPLEXITY={'UPPER': 2, 'LOWER': 2, 'PUNCTUATION': 2, 'DIGITS': 2})
def test_password_without_numeric(self):
"""
Try (and fail) user creation without any numeric characters in password
"""
response, mock_audit_log = self._do_request(self.user_url, 'test', 'test.paSS!', email='test@edx.org',
first_name='John', last_name='Doe', secure=True)
message = _('Password: Must be more complex (must contain 2 or more uppercase characters,'
' must contain 2 or more digits)')
self._assert_response(response, status=400, message=message)
@override_settings(PASSWORD_COMPLEXITY={'UPPER': 2, 'LOWER': 2, 'PUNCTUATION': 2, 'DIGITS': 2})
def test_password_with_complexity(self):
"""
This should pass since it has everything needed for a complex password
"""
response, mock_audit_log = self._do_request(self.user_url, str(uuid.uuid4()), 'Test.Me64!',
email='test@edx.org', first_name='John',
last_name='Doe', secure=True,
patched_audit_log='api_manager.users.views.AUDIT_LOG')
self._assert_response(response, status=201)
self._assert_audit_log(mock_audit_log, 'info', [u'API::New account created with user-id'])
self._assert_not_in_audit_log(mock_audit_log, 'info', [u'test@edx.org'])
def test_user_with_invalid_email(self):
"""
Try (and fail) user creation with invalid email address
"""
response, mock_audit_log = self._do_request(self.user_url, 'test', 'Test.Me64!', email='test-edx.org',
first_name='John', last_name='Doe', secure=True)
message = _('Valid e-mail is required.')
self._assert_response(response, status=400, message=message)
def test_user_with_invalid_username(self):
"""
Try (and fail) user creation with invalid username
"""
response, mock_audit_log = self._do_request(self.user_url, 'user name', 'Test.Me64!', email='test@edx.org',
first_name='John', last_name='Doe', secure=True)
message = _('Username should only consist of A-Z and 0-9, with no spaces.')
self._assert_response(response, status=400, message=message)
def test_user_with_unknown_username(self):
"""
Try (and fail) user login with unknown credentials
"""
response, mock_audit_log = self._do_request(self.session_url, 'unknown', 'UnKnown.Pass', secure=True)
self._assert_response(response, status=404)
self._assert_audit_log(mock_audit_log, 'warn', [u'API::Failed login attempt with unknown email/username'])
def test_successful_logout(self):
"""
Try login of user first and then logout user successfully and test audit log
"""
response, mock_audit_log = self._do_request(self.session_url, 'test', 'test_password', secure=True)
self._assert_response(response, status=201)
self._assert_audit_log(mock_audit_log, 'info',
[u"API::User logged in successfully with user-id - {0}".format(self.user.id)])
self._assert_not_in_audit_log(mock_audit_log, 'info', [u'test'])
response_dict = json.loads(response.content)
response, mock_audit_log = self._do_request(self.session_url + '/' + response_dict['token'], 'test',
'test_password', secure=True, request_method='DELETE')
self._assert_response(response, status=204)
self._assert_audit_log(mock_audit_log, 'info',
[u'API::User session terminated for user-id - {0}'.format(self.user.id)])
def _do_request(self, url, username, password, **kwargs):
"""
Make Post/Delete/Get requests with params
"""
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'):
post_params['email'] = kwargs.get('email')
if kwargs.get('first_name'):
post_params['first_name'] = kwargs.get('first_name')
if kwargs.get('last_name'):
post_params['last_name'] = kwargs.get('last_name')
if kwargs.get('secure', False):
extra['wsgi.url_scheme'] = 'https'
if kwargs.get('patched_audit_log'):
patched_audit_log = kwargs.get('patched_audit_log')
headers = {'X-Edx-Api-Key': TEST_API_KEY, 'Content-Type': 'application/json'}
with patch(patched_audit_log) as mock_audit_log:
if request_method == 'POST':
result = self.client.post(url, post_params, headers=headers, **extra)
elif request_method == 'DELETE':
result = self.client.delete(url, post_params, headers=headers, **extra)
return result, mock_audit_log
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.
"""
self.assertEqual(response.status_code, status)
# Return if response has not content
if response.status_code == 204:
return
response_dict = json.loads(response.content)
if message is not None:
msg = ("'%s' did not contain '%s'" %
(response_dict['message'], message))
self.assertTrue(message in response_dict['message'], msg)
def _assert_audit_log(self, mock_audit_log, level, log_strings):
"""
Check that the audit log has received the expected call as its last call.
"""
method_calls = mock_audit_log.method_calls
name, args, _kwargs = method_calls[-1]
self.assertEquals(name, level)
self.assertEquals(len(args), 1)
format_string = args[0]
for log_string in log_strings:
self.assertIn(log_string, format_string)
def _assert_not_in_audit_log(self, mock_audit_log, level, log_strings):
"""
Check that the audit log has received the expected call as its last call.
"""
method_calls = mock_audit_log.method_calls
name, args, _kwargs = method_calls[-1]
self.assertEquals(name, level)
self.assertEquals(len(args), 1)
format_string = args[0]
for log_string in log_strings:
self.assertNotIn(log_string, format_string)
# pylint: disable=E1101
# pylint: disable=E1103
"""
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
from django.test.utils import override_settings
TEST_API_KEY = str(uuid.uuid4())
class SecureClient(Client):
""" Django test client using a "secure" connection. """
def __init__(self, *args, **kwargs):
kwargs = kwargs.copy()
kwargs.update({'SERVER_PORT': 443, 'wsgi.url_scheme': 'https'})
super(SecureClient, self).__init__(*args, **kwargs)
@override_settings(EDX_API_KEY=TEST_API_KEY)
class SessionsApiTests(TestCase):
""" Test suite for Sessions API views """
def setUp(self):
self.test_server_prefix = 'https://testserver'
self.test_username = str(uuid.uuid4())
self.test_password = str(uuid.uuid4())
self.test_email = str(uuid.uuid4()) + '@test.org'
self.base_users_uri = '/api/users'
self.base_sessions_uri = '/api/sessions'
self.client = SecureClient()
cache.clear()
def do_post(self, uri, data):
"""Submit an HTTP POST request"""
headers = {
'Content-Type': 'application/json',
'X-Edx-Api-Key': str(TEST_API_KEY),
}
response = self.client.post(uri, headers=headers, data=data)
return response
def do_get(self, uri):
"""Submit an HTTP GET request"""
headers = {
'Content-Type': 'application/json',
'X-Edx-Api-Key': str(TEST_API_KEY),
}
response = self.client.get(uri, headers=headers)
return response
def do_delete(self, uri):
"""Submit an HTTP DELETE request"""
headers = {
'Content-Type': 'application/json',
'X-Edx-Api-Key': str(TEST_API_KEY),
}
response = self.client.delete(uri, headers=headers)
return response
def test_session_list_post_valid(self):
local_username = self.test_username + str(randint(11, 99))
local_username = local_username[3:-1] # username is a 32-character field
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password}
response = self.do_post(self.base_users_uri, data)
user_id = response.data['id']
data = {'username': local_username, 'password': self.test_password}
response = self.do_post(self.base_sessions_uri, data)
self.assertEqual(response.status_code, 201)
self.assertGreater(len(response.data['token']), 0)
confirm_uri = self.test_server_prefix + self.base_sessions_uri + '/' + response.data['token']
self.assertEqual(response.data['uri'], confirm_uri)
self.assertGreater(response.data['expires'], 0)
self.assertGreater(len(response.data['user']), 0)
self.assertEqual(str(response.data['user']['username']), local_username)
self.assertEqual(response.data['user']['id'], user_id)
def test_session_list_post_invalid(self):
local_username = self.test_username + str(randint(11, 99))
local_username = local_username[3:-1] # username is a 32-character field
bad_password = "12345"
data = {'email': self.test_email, 'username': local_username, 'password': bad_password}
response = self.do_post(self.base_users_uri, data)
data = {'username': local_username, 'password': self.test_password}
response = self.do_post(self.base_sessions_uri, data)
self.assertEqual(response.status_code, 401)
def test_session_list_post_valid_inactive(self):
local_username = self.test_username + str(randint(11, 99))
local_username = local_username[3:-1] # username is a 32-character field
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password}
response = self.do_post(self.base_users_uri, data)
user = User.objects.get(username=local_username)
user.is_active = False
user.save()
data = {'username': local_username, 'password': self.test_password}
response = self.do_post(self.base_sessions_uri, data)
self.assertEqual(response.status_code, 401)
def test_session_list_post_invalid_notfound(self):
data = {'username': 'user_12321452334', 'password': self.test_password}
response = self.do_post(self.base_sessions_uri, data)
self.assertEqual(response.status_code, 404)
def test_session_detail_get(self):
local_username = self.test_username + str(randint(11, 99))
local_username = local_username[3:-1] # username is a 32-character field
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password}
response = self.do_post(self.base_users_uri, data)
data = {'username': local_username, 'password': self.test_password}
response = self.do_post(self.base_sessions_uri, data)
test_uri = self.base_sessions_uri + '/' + response.data['token']
post_token = response.data['token']
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['token'], post_token)
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_session_detail_get_undefined(self):
test_uri = self.base_sessions_uri + "/123456789"
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_session_detail_delete(self):
local_username = self.test_username + str(randint(11, 99))
local_username = local_username[3:-1] # username is a 32-character field
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password}
response = self.do_post(self.base_users_uri, data)
self.assertEqual(response.status_code, 201)
data = {'username': local_username, 'password': self.test_password}
response = self.do_post(self.base_sessions_uri, data)
self.assertEqual(response.status_code, 201)
test_uri = self.base_users_uri + str(response.data['user']['id'])
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
""" Sessions API URI specification """
from django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns
from api_manager.sessions import views as sessions_views
urlpatterns = patterns('',
url(r'/*$^', sessions_views.SessionsList.as_view()),
url(r'^(?P<session_id>[a-z0-9]+)$', sessions_views.SessionsDetail.as_view()),
)
urlpatterns = format_suffix_patterns(urlpatterns)
# pylint: disable=E1101
""" API implementation for session-oriented interactions. """
import logging
from django.conf import settings
from django.contrib.auth import authenticate, login
from django.contrib.auth import SESSION_KEY, BACKEND_SESSION_KEY, load_backend
from django.contrib.auth.models import AnonymousUser, User
from django.core.exceptions import ObjectDoesNotExist
from django.utils.importlib import import_module
from django.utils.translation import ugettext as _
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from util.bad_request_rate_limiter import BadRequestRateLimiter
from api_manager.permissions import ApiKeyHeaderPermission
from api_manager.users.serializers import UserSerializer
from student.models import (
LoginFailures, PasswordHistory
)
AUDIT_LOG = logging.getLogger("audit")
def _generate_base_uri(request):
"""
Constructs the protocol:host:path component of the resource uri
"""
protocol = 'http'
if request.is_secure():
protocol = protocol + 's'
resource_uri = '{}://{}{}'.format(
protocol,
request.get_host(),
request.get_full_path()
)
return resource_uri
class SessionsList(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def post(self, request, format=None):
"""
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()
if limiter.is_rate_limit_exceeded(request):
response_data['message'] = _('Rate limit exceeded in api login.')
return Response(response_data, status=status.HTTP_403_FORBIDDEN)
base_uri = _generate_base_uri(request)
try:
existing_user = User.objects.get(username=request.DATA['username'])
except ObjectDoesNotExist:
existing_user = None
# see if account has been locked out due to excessive login failures
if existing_user and LoginFailures.is_feature_enabled():
if LoginFailures.is_user_locked_out(existing_user):
response_status = status.HTTP_403_FORBIDDEN
response_data['message'] = _('This account has been temporarily locked due to excessive login failures. '
'Try again later.')
return Response(response_data, status=response_status)
# see if the user must reset his/her password due to any policy settings
if existing_user and PasswordHistory.should_user_reset_password_now(existing_user):
response_status = status.HTTP_403_FORBIDDEN
response_data['message'] = _(
'Your password has expired due to password policy on this account. '
'You must reset your password before you can log in again.'
)
return Response(response_data, status=response_status)
if existing_user:
user = authenticate(username=existing_user.username, password=request.DATA['password'])
if user is not None:
# successful login, clear failed login attempts counters, if applicable
if LoginFailures.is_feature_enabled():
LoginFailures.clear_lockout_counter(user)
if user.is_active:
login(request, user)
response_data['token'] = request.session.session_key
response_data['expires'] = request.session.get_expiry_age()
user_dto = UserSerializer(user)
response_data['user'] = user_dto.data
response_data['uri'] = '{}/{}'.format(base_uri, request.session.session_key)
response_status = status.HTTP_201_CREATED
# add to audit log
AUDIT_LOG.info(u"API::User logged in successfully with user-id - {0}".format(user.id))
else:
response_status = status.HTTP_401_UNAUTHORIZED
else:
limiter.tick_bad_request_counter(request)
# tick the failed login counters if the user exists in the database
if LoginFailures.is_feature_enabled():
LoginFailures.increment_lockout_counter(existing_user)
response_status = status.HTTP_401_UNAUTHORIZED
AUDIT_LOG.warn(u"API::User authentication failed with user-id - {0}".format(existing_user.id))
else:
AUDIT_LOG.warn(u"API::Failed login attempt with unknown email/username")
response_status = status.HTTP_404_NOT_FOUND
return Response(response_data, status=response_status)
class SessionsDetail(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def get(self, request, session_id, format=None):
"""
GET retrieves an existing system session
"""
response_data = {}
base_uri = _generate_base_uri(request)
engine = import_module(settings.SESSION_ENGINE)
session = engine.SessionStore(session_id)
try:
user_id = session[SESSION_KEY]
backend_path = session[BACKEND_SESSION_KEY]
backend = load_backend(backend_path)
user = backend.get_user(user_id) or AnonymousUser()
except KeyError:
user = AnonymousUser()
if user.is_authenticated():
response_data['token'] = session.session_key
response_data['expires'] = session.get_expiry_age()
response_data['uri'] = base_uri
response_data['user_id'] = user.id
return Response(response_data, status=status.HTTP_200_OK)
else:
return Response(response_data, status=status.HTTP_404_NOT_FOUND)
def delete(self, request, session_id, format=None):
"""
DELETE flushes an existing system session from the system
"""
response_data = {}
base_uri = _generate_base_uri(request)
engine = import_module(settings.SESSION_ENGINE)
session = engine.SessionStore(session_id)
user_id = session[SESSION_KEY]
AUDIT_LOG.info(u"API::User session terminated for user-id - {0}".format(user_id))
session.flush()
return Response(response_data, status=status.HTTP_204_NO_CONTENT)
\ No newline at end of file
# pylint: disable=E1103
"""
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
TEST_API_KEY = str(uuid.uuid4())
class SecureClient(Client):
""" Django test client using a "secure" connection. """
def __init__(self, *args, **kwargs):
kwargs = kwargs.copy()
kwargs.update({'SERVER_PORT': 443, 'wsgi.url_scheme': 'https'})
super(SecureClient, self).__init__(*args, **kwargs)
@override_settings(EDX_API_KEY=TEST_API_KEY)
class SystemApiTests(TestCase):
""" Test suite for base API views """
def setUp(self):
self.test_server_prefix = "https://testserver/api"
self.test_username = str(uuid.uuid4())
self.test_password = str(uuid.uuid4())
self.test_email = str(uuid.uuid4()) + '@test.org'
self.test_group_name = str(uuid.uuid4())
self.client = SecureClient()
cache.clear()
def do_get(self, uri):
"""Submit an HTTP GET request"""
headers = {
'Content-Type': 'application/json',
'X-Edx-Api-Key': str(TEST_API_KEY),
}
response = self.client.get(uri, headers=headers)
return response
def test_system_detail_get(self):
""" Ensure the system returns base data about the system """
test_uri = self.test_server_prefix + '/system'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertIsNotNone(response.data['uri'])
self.assertGreater(len(response.data['uri']), 0)
self.assertEqual(response.data['uri'], test_uri)
self.assertIsNotNone(response.data['documentation'])
self.assertGreater(len(response.data['documentation']), 0)
self.assertIsNotNone(response.data['name'])
self.assertGreater(len(response.data['name']), 0)
self.assertIsNotNone(response.data['description'])
self.assertGreater(len(response.data['description']), 0)
def test_system_detail_api_get(self):
""" Ensure the system returns base data about the API """
test_uri = self.test_server_prefix
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertIsNotNone(response.data['uri'])
self.assertGreater(len(response.data['uri']), 0)
self.assertEqual(response.data['uri'], test_uri)
self.assertIsNotNone(response.data['documentation'])
self.assertGreater(len(response.data['documentation']), 0)
self.assertIsNotNone(response.data['name'])
self.assertGreater(len(response.data['name']), 0)
self.assertIsNotNone(response.data['description'])
self.assertGreater(len(response.data['description']), 0)
""" 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
from api_manager.permissions import ApiKeyHeaderPermission
def _generate_base_uri(request):
"""
Constructs the protocol:host:path component of the resource uri
"""
protocol = 'http'
if request.is_secure():
protocol = protocol + 's'
resource_uri = '{}://{}{}'.format(
protocol,
request.get_host(),
request.get_full_path()
)
return resource_uri
class SystemDetail(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def get(self, request, format=None):
"""Returns top-level descriptive information about the Open edX API"""
base_uri = _generate_base_uri(request)
response_data = {}
response_data['name'] = "Open edX System API"
response_data['description'] = "System interface for managing groups, users, and sessions."
response_data['documentation'] = "http://docs.openedxapi.apiary.io/#get-%2Fapi%2Fsystem"
response_data['uri'] = base_uri
return Response(response_data, status=status.HTTP_200_OK)
class ApiDetail(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def get(self, request, format=None):
"""Returns top-level descriptive information about the Open edX API"""
base_uri = _generate_base_uri(request)
response_data = {}
response_data['name'] = "Open edX API"
response_data['description'] = "Machine interface for interactions with Open edX."
response_data['documentation'] = "http://docs.openedxapi.apiary.io"
response_data['uri'] = base_uri
response_data['resources'] = []
response_data['resources'].append({'uri': base_uri + 'courses'})
response_data['resources'].append({'uri': base_uri + 'groups'})
response_data['resources'].append({'uri': base_uri + 'sessions'})
response_data['resources'].append({'uri': base_uri + 'system'})
response_data['resources'].append({'uri': base_uri + 'users'})
return Response(response_data, status=status.HTTP_200_OK)
"""
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
TEST_API_KEY = "123456ABCDEF"
@override_settings(DEBUG=True, EDX_API_KEY=None)
class PermissionsTestsDebug(TestCase):
""" Test suite for Permissions helper classes """
def setUp(self):
self.test_username = str(uuid.uuid4())
self.test_password = str(uuid.uuid4())
self.test_email = str(uuid.uuid4()) + '@test.org'
def do_post(self, uri, data):
"""Submit an HTTP POST request"""
headers = {
'Content-Type': 'application/json',
'X-Edx-Api-Key': str(TEST_API_KEY),
}
response = self.client.post(uri, headers=headers, data=data)
return response
def test_has_permission_debug_enabled(self):
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
local_username = local_username[3:-1] # username is a 32-character field
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
@override_settings(DEBUG=False, EDX_API_KEY="123456ABCDEF")
class PermissionsTestsApiKey(TestCase):
""" Test suite for Permissions helper classes """
def setUp(self):
self.test_username = str(uuid.uuid4())
self.test_password = str(uuid.uuid4())
self.test_email = str(uuid.uuid4()) + '@test.org'
def do_post(self, uri, data):
"""Submit an HTTP POST request"""
headers = {
'Content-Type': 'application/json',
'X-Edx-Api-Key': str(TEST_API_KEY),
}
response = self.client.post(uri, headers=headers, data=data)
return response
def test_has_permission_valid_api_key(self):
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
local_username = local_username[3:-1] # username is a 32-character field
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
@override_settings(DEBUG=False, EDX_API_KEY=None)
class PermissionsTestDeniedMissingServerKey(TestCase):
""" Test suite for Permissions helper classes """
def setUp(self):
self.test_username = str(uuid.uuid4())
self.test_password = str(uuid.uuid4())
self.test_email = str(uuid.uuid4()) + '@test.org'
def do_post(self, uri, data):
"""Submit an HTTP POST request"""
headers = {
'Content-Type': 'application/json',
'X-Edx-Api-Key': str(TEST_API_KEY),
}
response = self.client.post(uri, headers=headers, data=data)
return response
def test_has_permission_missing_server_key(self):
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
local_username = local_username[3:-1] # username is a 32-character field
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 403)
@override_settings(DEBUG=False, EDX_API_KEY="67890VWXYZ")
class PermissionsTestDeniedMissingClientKey(TestCase):
""" Test suite for Permissions helper classes """
def setUp(self):
self.test_username = str(uuid.uuid4())
self.test_password = str(uuid.uuid4())
self.test_email = str(uuid.uuid4()) + '@test.org'
def do_post(self, uri, data):
"""Submit an HTTP POST request"""
headers = {
'Content-Type': 'application/json',
}
response = self.client.post(uri, headers=headers, data=data)
return response
def test_has_permission_invalid_client_key(self):
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
local_username = local_username[3:-1] # username is a 32-character field
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 403)
@override_settings(DEBUG=False, EDX_API_KEY="67890VWXYZ")
class PermissionsTestDeniedInvalidClientKey(TestCase):
""" Test suite for Permissions helper classes """
def setUp(self):
self.test_username = str(uuid.uuid4())
self.test_password = str(uuid.uuid4())
self.test_email = str(uuid.uuid4()) + '@test.org'
def do_post(self, uri, data):
"""Submit an HTTP POST request"""
headers = {
'Content-Type': 'application/json',
'X-Edx-Api-Key': str(TEST_API_KEY),
}
response = self.client.post(uri, headers=headers, data=data)
return response
def test_has_permission_invalid_client_key(self):
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
local_username = local_username[3:-1] # username is a 32-character field
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 403)
"""
The URI scheme for resources is as follows:
Resource type: /api/{resource_type}
Specific resource: /api/{resource_type}/{resource_id}
The remaining URIs provide information about the API and/or module
System: General context and intended usage
API: Top-level description of overall API (must live somewhere)
"""
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')),
)
""" Django REST Framework Serializers """
from django.contrib.auth.models import User
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
""" Serializer for User model interactions """
class Meta:
""" Serializer/field specification """
model = User
fields = ("id", "email", "username")
read_only_fields = ("id", "email", "username")
"""
Tests for session api with advance security features
"""
import json
import uuid
from mock import patch
from django.test import TestCase
from django.test.utils import override_settings
from django.utils import timezone
from django.utils.translation import ugettext as _
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})
@override_settings(PASSWORD_MIN_LENGTH=4, PASSWORD_MAX_LENGTH=12,
PASSWORD_COMPLEXITY={'UPPER': 2, 'LOWER': 2, 'PUNCTUATION': 2, 'DIGITS': 2})
class UserPasswordResetTest(TestCase):
"""
Test api_manager.session.session_list view
"""
def setUp(self):
"""
setup the api urls
"""
self.session_url = '/api/sessions'
self.user_url = '/api/users'
cache.clear()
@override_settings(ADVANCED_SECURITY_CONFIG={'MIN_DAYS_FOR_STUDENT_ACCOUNTS_PASSWORD_RESETS': 5})
def test_user_must_reset_password_after_n_days(self):
"""
Test to ensure that User session login fails
after N days. User must reset his/her
password after N days to login again
"""
response = self._do_post_request(
self.user_url, 'test2', 'Test.Me64!', email='test@edx.org',
first_name='John', last_name='Doe', secure=True
)
self._assert_response(response, status=201)
user_id = response.data['id']
response = self._do_post_request(self.session_url, 'test2', 'Test.Me64!', secure=True)
self.assertEqual(response.status_code, 201)
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 =_(
'Your password has expired due to password policy on this account. '
'You must reset your password before you can log in again.'
)
self._assert_response(response, status=403, message=message)
#reset the password and then try login
pass_reset_url = "%s/%s" % (self.user_url, str(user_id))
response = self._do_post_pass_reset_request(
pass_reset_url, password='Test.Me64@', secure=True
)
self.assertEqual(response.status_code, 201)
#login successful after reset password
response = self._do_post_request(self.session_url, 'test2', 'Test.Me64@', secure=True)
self.assertEqual(response.status_code, 201)
@override_settings(ADVANCED_SECURITY_CONFIG={'MIN_DIFFERENT_STUDENT_PASSWORDS_BEFORE_REUSE': 4,
'MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS': 0})
def test_password_reset_not_allowable_reuse(self):
"""
Try resetting user password < 4 and > 4 times and
then use one of the passwords that you have used
before
"""
response = self._do_post_request(
self.user_url, 'test2', 'Test.Me64!', email='test@edx.org',
first_name='John', last_name='Doe', secure=True
)
self._assert_response(response, status=201)
user_id = response.data['id']
pass_reset_url = "%s/%s" % (self.user_url, str(user_id))
response = self._do_post_pass_reset_request(
pass_reset_url, password='Test.Me64#', secure=True
)
message = 'Password Reset Successful'
self._assert_response(response, status=201, message=message)
response = self._do_post_pass_reset_request(
pass_reset_url, password='Test.Me64@', secure=True
)
message = 'Password Reset Successful'
self._assert_response(response, status=201, message=message)
response = self._do_post_pass_reset_request(
pass_reset_url, password='Test.Me64^', secure=True
)
message = 'Password Reset Successful'
self._assert_response(response, status=201, message=message)
#now use previously used password
response = self._do_post_pass_reset_request(
pass_reset_url, password='Test.Me64!', secure=True
)
message = _(
"You are re-using a password that you have used recently. You must "
"have 4 distinct password(s) before reusing a previous password."
)
self._assert_response(response, status=403, message=message)
response = self._do_post_pass_reset_request(
pass_reset_url, password='Test.Me64&', secure=True
)
message = 'Password Reset Successful'
self._assert_response(response, status=201, message=message)
#now use previously used password
response = self._do_post_pass_reset_request(
pass_reset_url, password='Test.Me64!', secure=True
)
message = 'Password Reset Successful'
self._assert_response(response, status=201, message=message)
@override_settings(ADVANCED_SECURITY_CONFIG={'MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS': 1})
def test_is_password_reset_too_frequent(self):
"""
Try reset user password before
and after the MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS
"""
response = self._do_post_request(
self.user_url, 'test2', 'Test.Me64!', email='test@edx.org',
first_name='John', last_name='Doe', secure=True
)
self._assert_response(response, status=201)
user_id = response.data['id']
pass_reset_url = "%s/%s" % (self.user_url, str(user_id))
response = self._do_post_pass_reset_request(
pass_reset_url, password='NewP@ses34!', secure=True
)
message = _(
"You are resetting passwords too frequently. Due to security policies, "
"1 day(s) must elapse between password resets"
)
self._assert_response(response, status=403, message=message)
reset_time = timezone.now() + timedelta(days=1)
with patch.object(timezone, 'now', return_value=reset_time):
response = self._do_post_pass_reset_request(
pass_reset_url, password='NewP@ses34!', secure=True
)
message = 'Password Reset Successful'
self._assert_response(response, status=201, message=message)
@override_settings(ADVANCED_SECURITY_CONFIG={'MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS': 0})
def test_password_reset_rate_limiting_unblock(self):
"""
Try (and fail) login user 30 times on invalid password
and then unblock it after 5 minutes
"""
response = self._do_post_request(
self.user_url, 'test2', 'Test.Me64!', email='test@edx.org',
first_name='John', last_name='Doe', secure=True
)
self._assert_response(response, status=201)
user_id = response.data['id']
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)
response = self._do_post_pass_reset_request(
'{}/{}'.format(self.user_url, '31'), password='Test.Me64@', secure=True
)
message = _('Rate limit exceeded in password_reset.')
self._assert_response(response, status=403, message=message)
# now reset the time to 5 mins from now in future in order to unblock
reset_time = datetime.now(UTC) + timedelta(seconds=300)
with freeze_time(reset_time):
response = self._do_post_pass_reset_request(
pass_reset_url, password='Test.Me64@', secure=True
)
self._assert_response(response, status=201)
def _do_post_request(self, url, username, password, **kwargs):
"""
Post the login info
"""
post_params, extra = {'username': username, 'password': password}, {}
if kwargs.get('email'):
post_params['email'] = kwargs.get('email')
if kwargs.get('first_name'):
post_params['first_name'] = kwargs.get('first_name')
if kwargs.get('last_name'):
post_params['last_name'] = kwargs.get('last_name')
headers = {'X-Edx-Api-Key': TEST_API_KEY, 'Content-Type': 'application/json'}
if kwargs.get('secure', False):
extra['wsgi.url_scheme'] = 'https'
return self.client.post(url, post_params, headers=headers, **extra)
def _do_post_pass_reset_request(self, url, password, **kwargs):
"""
Post the Password Reset info
"""
post_params, extra = {'password': password}, {}
headers = {'X-Edx-Api-Key': TEST_API_KEY, 'Content-Type': 'application/json'}
if kwargs.get('secure', False):
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):
"""
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.
"""
self.assertEqual(response.status_code, status)
response_dict = json.loads(response.content)
if message is not None:
msg = ("'%s' did not contain '%s'" %
(response_dict['message'], message))
self.assertTrue(message in response_dict['message'], msg)
# pylint: disable=E1103
"""
Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/tests/test_user_views.py]
"""
from random import randint
import simplejson as json
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
from django.test.utils import override_settings
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
TEST_API_KEY = str(uuid.uuid4())
class SecureClient(Client):
""" Django test client using a "secure" connection. """
def __init__(self, *args, **kwargs):
kwargs = kwargs.copy()
kwargs.update({'SERVER_PORT': 443, 'wsgi.url_scheme': 'https'})
super(SecureClient, self).__init__(*args, **kwargs)
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
@override_settings(EDX_API_KEY=TEST_API_KEY)
class UsersApiTests(TestCase):
""" Test suite for Users API views """
def setUp(self):
self.test_server_prefix = 'https://testserver'
self.test_username = str(uuid.uuid4())
self.test_password = str(uuid.uuid4())
self.test_email = str(uuid.uuid4()) + '@test.org'
self.test_first_name = str(uuid.uuid4())
self.test_last_name = str(uuid.uuid4())
self.client = SecureClient()
cache.clear()
def do_post(self, uri, data):
"""Submit an HTTP POST request"""
headers = {
'X-Edx-Api-Key': str(TEST_API_KEY),
}
json_data = json.dumps(data)
response = self.client.post(uri, headers=headers, content_type='application/json', data=json_data)
return response
def do_get(self, uri):
"""Submit an HTTP GET request"""
headers = {
'Content-Type': 'application/json',
'X-Edx-Api-Key': str(TEST_API_KEY),
}
response = self.client.get(uri, headers=headers)
return response
def do_delete(self, uri):
"""Submit an HTTP DELETE request"""
headers = {
'Content-Type': 'application/json',
'X-Edx-Api-Key': str(TEST_API_KEY),
}
response = self.client.delete(uri, headers=headers)
return response
def test_user_list_post(self):
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
self.assertGreater(response.data['id'], 0)
confirm_uri = self.test_server_prefix + test_uri + '/' + str(response.data['id'])
self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['email'], self.test_email)
self.assertEqual(response.data['username'], local_username)
self.assertEqual(response.data['first_name'], self.test_first_name)
self.assertEqual(response.data['last_name'], self.test_last_name)
def test_user_list_post_duplicate(self):
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data)
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 409)
self.assertGreater(response.data['message'], 0)
self.assertEqual(response.data['field_conflict'], 'username')
def test_user_detail_get(self):
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data)
test_uri = test_uri + '/' + str(response.data['id'])
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(response.data['id'], 0)
confirm_uri = self.test_server_prefix + test_uri
self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['email'], self.test_email)
self.assertEqual(response.data['username'], local_username)
self.assertEqual(response.data['first_name'], self.test_first_name)
self.assertEqual(response.data['last_name'], self.test_last_name)
self.assertEqual(len(response.data['resources']), 2)
def test_user_detail_delete(self):
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data)
test_uri = test_uri + '/' + str(response.data['id'])
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
response = self.do_delete(test_uri) # User no longer exists, should get a 204 all the same
self.assertEqual(response.status_code, 204)
def test_user_detail_get_undefined(self):
test_uri = '/api/users/123456789'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_user_groups_list_post(self):
test_uri = '/api/groups'
data = {'name': 'Alpha Group'}
response = self.do_post(test_uri, data)
group_id = response.data['id']
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data)
user_id = response.data['id']
test_uri = test_uri + '/' + str(response.data['id'])
response = self.do_get(test_uri)
test_uri = test_uri + '/groups'
data = {'group_id': group_id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
self.assertGreater(len(response.data['uri']), 0)
confirm_uri = self.test_server_prefix + test_uri + '/' + str(group_id)
self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['group_id'], str(group_id))
self.assertEqual(response.data['user_id'], str(user_id))
def test_user_groups_list_post_duplicate(self):
test_uri = '/api/groups'
data = {'name': 'Alpha Group'}
response = self.do_post(test_uri, data)
group_id = response.data['id']
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data)
test_uri = test_uri + '/' + str(response.data['id'])
response = self.do_get(test_uri)
test_uri = test_uri + '/groups'
data = {'group_id': group_id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 409)
def test_user_groups_list_post_invalid_user(self):
test_uri = '/api/groups'
data = {'name': 'Alpha Group'}
response = self.do_post(test_uri, data)
group_id = response.data['id']
test_uri = '/api/users/897698769/groups'
data = {'group_id': group_id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 404)
def test_user_groups_list_get(self):
test_uri = '/api/groups'
group_name = 'Alpha Group'
data = {'name': group_name}
response = self.do_post(test_uri, data)
group_id = response.data['id']
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data)
user_id = response.data['id']
test_uri = test_uri + '/' + str(response.data['id'])
response = self.do_get(test_uri)
test_uri = test_uri + '/groups'
data = {'group_id': group_id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data['groups']), 0)
self.assertEqual(response.data['groups'][0]['id'], group_id)
self.assertEqual(response.data['groups'][0]['name'], str(group_name))
def test_user_groups_list_get_invalid_user(self):
test_uri = '/api/users/123124/groups'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_user_groups_detail_get(self):
test_uri = '/api/groups'
data = {'name': 'Alpha Group'}
response = self.do_post(test_uri, data)
group_id = response.data['id']
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data)
user_id = response.data['id']
test_uri = test_uri + '/' + str(response.data['id']) + '/groups'
data = {'group_id': group_id}
response = self.do_post(test_uri, data)
test_uri = test_uri + '/' + str(group_id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.data['uri']), 0)
confirm_uri = self.test_server_prefix + test_uri
self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['group_id'], group_id)
self.assertEqual(response.data['user_id'], user_id)
def test_user_groups_detail_delete(self):
test_uri = '/api/groups'
data = {'name': 'Alpha Group'}
response = self.do_post(test_uri, data)
group_id = response.data['id']
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data)
test_uri = test_uri + '/' + str(response.data['id']) + '/groups'
data = {'group_id': group_id}
response = self.do_post(test_uri, data)
test_uri = test_uri + '/' + str(group_id)
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
response = self.do_delete(test_uri) # Relationship no longer exists, should get a 204 all the same
self.assertEqual(response.status_code, 204)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_user_groups_detail_get_invalid_user(self):
test_uri = '/api/users/123124/groups/12321'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_user_groups_detail_get_undefined(self):
test_uri = '/api/groups'
data = {'name': 'Alpha Group'}
response = self.do_post(test_uri, data)
group_id = response.data['id']
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data)
user_id = response.data['id']
test_uri = '/api/users/' + str(user_id) + '/groups/' + str(group_id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_user_courses_list_post(self):
course = CourseFactory.create()
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data)
user_id = response.data['id']
test_uri = '{}/{}/courses'.format(test_uri, str(user_id))
data = {'course_id': course.id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
confirm_uri = self.test_server_prefix + test_uri + '/' + course.id
self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['id'], course.id)
self.assertTrue(response.data['is_active'])
def test_user_courses_list_post_undefined_user(self):
course = CourseFactory.create()
test_uri = '/api/users'
user_id = '234234'
test_uri = '{}/{}/courses'.format(test_uri, str(user_id))
data = {'course_id': course.id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 404)
def test_user_courses_list_post_undefined_course(self):
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data)
user_id = response.data['id']
test_uri = '{}/{}/courses'.format(test_uri, str(user_id))
data = {'course_id': '234asdfapsdf'}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 404)
def test_user_courses_list_get(self):
course = CourseFactory.create(display_name="TEST COURSE")
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data)
user_id = response.data['id']
test_uri = '{}/{}/courses'.format(test_uri, str(user_id))
data = {'course_id': course.id}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
confirm_uri = self.test_server_prefix + test_uri + '/' + course.id
self.assertEqual(response.data[0]['uri'], confirm_uri)
self.assertEqual(response.data[0]['id'], course.id)
self.assertTrue(response.data[0]['is_active'])
self.assertEqual(response.data[0]['name'], course.display_name)
def test_user_courses_list_get_undefined_user(self):
test_uri = '/api/users/2134234/courses'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_user_courses_detail_post_position_course_as_descriptor(self):
course = CourseFactory.create()
test_data = '<html>{}</html>'.format(str(uuid.uuid4()))
chapter1 = ItemFactory.create(
category="chapter",
parent_location=course.location,
data=test_data,
display_name="Chapter 1"
)
chapter2 = ItemFactory.create(
category="chapter",
parent_location=course.location,
data=test_data,
display_name="Chapter 2"
)
chapter3 = ItemFactory.create(
category="chapter",
parent_location=course.location,
data=test_data,
display_name="Chapter 3"
)
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data)
user_id = response.data['id']
test_uri = test_uri + '/' + str(user_id) + '/courses'
data = {'course_id': course.id}
response = self.do_post(test_uri, data)
test_uri = test_uri + '/' + str(course.id)
self.assertEqual(response.status_code, 201)
position_data = {
'position': {
'parent_module_id': str(course.id),
'child_module_id': str(chapter3.location)
}
}
response = self.do_post(test_uri, data=position_data)
self.assertEqual(response.data['position'], chapter3.id)
def test_user_courses_detail_post_position_invalid_user(self):
course = CourseFactory.create()
test_data = '<html>{}</html>'.format(str(uuid.uuid4()))
chapter1 = ItemFactory.create(
category="chapter",
parent_location=course.location,
data=test_data,
display_name="Chapter 1"
)
user_id = 2342334
course_id = 'asdfa9sd8fasdf'
test_uri = '/api/users/{}/courses/{}'.format(str(user_id), course_id)
position_data = {
'position': {
'parent_module_id': course_id,
'child_module_id': str(chapter1.location)
}
}
response = self.do_post(test_uri, data=position_data)
self.assertEqual(response.status_code, 404)
def test_user_courses_detail_post_position_course_as_module(self):
course = CourseFactory.create()
test_data = '<html>{}</html>'.format(str(uuid.uuid4()))
chapter1 = ItemFactory.create(
category="chapter",
parent_location=course.location,
data=test_data,
display_name="Chapter 1"
)
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data)
user_id = response.data['id']
test_uri = test_uri + '/' + str(user_id) + '/courses'
data = {'course_id': course.id}
response = self.do_post(test_uri, data)
test_uri = test_uri + '/' + str(course.id)
self.assertEqual(response.status_code, 201)
position_data = {
'position': {
'parent_module_id': str(course.location),
'child_module_id': str(chapter1.location)
}
}
response = self.do_post(test_uri, data=position_data)
self.assertEqual(response.data['position'], chapter1.id)
def test_user_courses_detail_get(self):
course = CourseFactory.create()
test_data = '<html>{}</html>'.format(str(uuid.uuid4()))
chapter1 = ItemFactory.create(
category="chapter",
parent_location=course.location,
data=test_data,
display_name="Overview"
)
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data)
user_id = response.data['id']
test_uri = test_uri + '/' + str(user_id) + '/courses'
data = {'course_id': course.id}
response = self.do_post(test_uri, data)
test_uri = test_uri + '/' + str(course.id)
self.assertEqual(response.status_code, 201)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
confirm_uri = self.test_server_prefix + test_uri
self.assertEqual(response.data['uri'], confirm_uri)
self.assertEqual(response.data['course_id'], course.id)
self.assertEqual(response.data['user_id'], user_id)
position_data = {
'position': {
'parent_module_id': str(course.location),
'child_module_id': str(chapter1.location)
}
}
response = self.do_post(confirm_uri, data=position_data)
self.assertEqual(response.data['position'], chapter1.id)
def test_user_courses_detail_get_undefined_user(self):
test_uri = '/api/users/2134234/courses/a8df7asvd98'
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_user_courses_detail_get_undefined_enrollment(self):
course = CourseFactory.create()
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data)
user_id = response.data['id']
test_uri = '/api/users/' + str(user_id) + '/courses/' + str(course.id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_user_courses_detail_delete(self):
course = CourseFactory.create()
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data)
user_id = response.data['id']
post_uri = test_uri + '/' + str(user_id) + '/courses'
data = {'course_id': course.id}
response = self.do_post(post_uri, data)
self.assertEqual(response.status_code, 201)
test_uri = post_uri + '/' + str(course.id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
response = self.do_post(post_uri, data) # Re-enroll the student in the course
self.assertEqual(response.status_code, 201)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 404)
def test_user_courses_detail_delete_undefined_user(self):
course = CourseFactory.create()
user_id = '2134234'
test_uri = '/api/users/{}/courses/{}'.format(str(user_id), str(course.id))
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 204)
def test_user_courses_detail_delete_undefined_course(self):
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password': self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data)
user_id = response.data['id']
test_uri = '{}/{}/oasdf987sdf'.format(test_uri, str(user_id))
response = self.do_delete(test_uri)
self.assertEqual(response.status_code, 404)
""" Users API URI specification """
from django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns
from api_manager.users import views as users_views
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()),
url(r'^(?P<user_id>[0-9]+)/courses/(?P<course_id>[a-zA-Z0-9/_:]+)$', users_views.UsersCoursesDetail.as_view()),
url(r'^(?P<user_id>[0-9]+)/groups/*$', users_views.UsersGroupsList.as_view()),
url(r'^(?P<user_id>[0-9]+)/groups/(?P<group_id>[0-9]+)$', users_views.UsersGroupsDetail.as_view()),
)
urlpatterns = format_suffix_patterns(urlpatterns)
""" API implementation for user-oriented interactions. """
import logging
from django.contrib.auth.models import User, Group
from django.core.exceptions import ObjectDoesNotExist
from django.db import IntegrityError
from django.core.validators import validate_email, validate_slug, ValidationError
from django.conf import settings
from django.utils.translation import get_language, ugettext_lazy as _
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from api_manager.models import GroupProfile
from api_manager.permissions import ApiKeyHeaderPermission
from courseware import module_render
from courseware.model_data import FieldDataCache
from courseware.views import get_module_for_descriptor, save_child_position, get_current_child
from lang_pref import LANGUAGE_KEY
from student.models import CourseEnrollment, PasswordHistory, UserProfile
from openedx.core.djangoapps.user_api.models import UserPreference
from xmodule.modulestore.django import modulestore
from util.password_policy_validators import (
validate_password_length, validate_password_complexity,
validate_password_dictionary
)
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
"""
protocol = 'http'
if request.is_secure():
protocol = protocol + 's'
resource_uri = '{}://{}{}'.format(
protocol,
request.get_host(),
request.get_full_path()
)
return resource_uri
def _serialize_user(response_data, user):
"""
Loads the object data into the response dict
This should probably evolve to use DRF serializers
"""
response_data['email'] = user.email
response_data['username'] = user.username
response_data['first_name'] = user.first_name
response_data['last_name'] = user.last_name
response_data['id'] = user.id
return response_data
def _save_module_position(request, user, course_id, course_descriptor, position):
"""
Records the indicated position for the specified course
Really no reason to generalize this out of user_courses_detail aside from pylint complaining
"""
field_data_cache = FieldDataCache([course_descriptor], course_id, user)
if course_id == position['parent_module_id']:
parent_module = get_module_for_descriptor(
user,
request,
course_descriptor,
field_data_cache,
course_id
)
else:
parent_module = module_render.get_module(
user,
request,
position['parent_module_id'],
field_data_cache,
course_id
)
child_module = module_render.get_module(
user,
request,
position['child_module_id'],
field_data_cache,
course_id
)
save_child_position(parent_module, child_module.location.name)
saved_module = get_current_child(parent_module)
return saved_module.id
class UsersList(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def post(self, request, format=None):
"""
POST creates a new user in the system
"""
response_data = {}
base_uri = _generate_base_uri(request)
email = request.DATA['email']
username = request.DATA['username']
password = request.DATA['password']
first_name = request.DATA.get('first_name', '')
last_name = request.DATA.get('last_name', '')
# enforce password complexity as an optional feature
if settings.FEATURES.get('ENFORCE_PASSWORD_POLICY', False):
try:
validate_password_length(password)
validate_password_complexity(password)
validate_password_dictionary(password)
except ValidationError, err:
status_code = status.HTTP_400_BAD_REQUEST
response_data['message'] = _('Password: ') + '; '.join(err.messages)
return Response(response_data, status=status_code)
try:
validate_email(email)
except ValidationError:
status_code = status.HTTP_400_BAD_REQUEST
response_data['message'] = _('Valid e-mail is required.')
return Response(response_data, status=status_code)
try:
validate_slug(username)
except ValidationError:
status_code = status.HTTP_400_BAD_REQUEST
response_data['message'] = _('Username should only consist of A-Z and 0-9, with no spaces.')
return Response(response_data, status=status_code)
# Create the User, UserProfile, and UserPreference records
try:
user = User.objects.create(email=email, username=username)
except IntegrityError:
user = None
else:
user.set_password(password)
user.first_name = first_name
user.last_name = last_name
user.save()
profile = UserProfile(user=user)
profile.name = '{} {}'.format(first_name, last_name)
profile.save()
UserPreference.set_preference(user, LANGUAGE_KEY, get_language())
# add this account creation to password history
# NOTE, this will be a NOP unless the feature has been turned on in configuration
password_history_entry = PasswordHistory()
password_history_entry.create(user)
# add to audit log
AUDIT_LOG.info(u"API::New account created with user-id - {0}".format(user.id))
# CDODGE: @TODO: We will have to extend this to look in the CourseEnrollmentAllowed table and
# auto-enroll students when they create a new account. Also be sure to remove from
# the CourseEnrollmentAllow table after the auto-registration has taken place
if user:
status_code = status.HTTP_201_CREATED
response_data = _serialize_user(response_data, user)
response_data['uri'] = '{}/{}'.format(base_uri, str(user.id))
else:
status_code = status.HTTP_409_CONFLICT
response_data['message'] = "User '%s' already exists", username
response_data['field_conflict'] = "username"
return Response(response_data, status=status_code)
class UsersDetail(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def get(self, request, user_id, format=None):
"""
GET retrieves an existing user from the system
"""
response_data = {}
base_uri = _generate_base_uri(request)
try:
existing_user = User.objects.get(id=user_id, is_active=True)
_serialize_user(response_data, existing_user)
response_data['uri'] = base_uri
response_data['resources'] = []
resource_uri = '{}/groups'.format(base_uri)
response_data['resources'].append({'uri': resource_uri})
resource_uri = '{}/courses'.format(base_uri)
response_data['resources'].append({'uri': resource_uri})
return Response(response_data, status=status.HTTP_200_OK)
except ObjectDoesNotExist:
return Response(response_data, status=status.HTTP_404_NOT_FOUND)
def delete(self, request, user_id, format=None):
"""
DELETE removes/inactivates/etc. an existing user
"""
response_data = {}
try:
existing_user = User.objects.get(id=user_id, is_active=True)
existing_user.is_active = False
existing_user.save()
except ObjectDoesNotExist:
# It's ok if we don't find a match
pass
return Response(response_data, status=status.HTTP_204_NO_CONTENT)
def post(self, request, user_id, format=None):
response_data = {}
base_uri = _generate_base_uri(request)
# Add some rate limiting here by re-using the RateLimitMixin as a helper class
limiter = BadRequestRateLimiter()
if limiter.is_rate_limit_exceeded(request):
AUDIT_LOG.warning("API::Rate limit exceeded in password_reset")
response_data['message'] = _('Rate limit exceeded in password_reset.')
status_code = status.HTTP_403_FORBIDDEN
return Response(response_data, status=status_code)
try:
existing_user = User.objects.get(id=user_id)
old_password_hash = existing_user.password
_serialize_user(response_data, existing_user)
password = request.DATA['password']
if existing_user:
if settings.FEATURES.get('ENFORCE_PASSWORD_POLICY', False):
try:
validate_password_length(password)
validate_password_complexity(password)
validate_password_dictionary(password)
except ValidationError, err:
# bad user? tick the rate limiter counter
AUDIT_LOG.warning("API::Bad password in password_reset.")
status_code = status.HTTP_400_BAD_REQUEST
response_data['message'] = _('Password: ') + '; '.join(err.messages)
return Response(response_data, status=status_code)
# also, check the password reuse policy
err_msg = None
if not PasswordHistory.is_allowable_password_reuse(existing_user, password):
if existing_user.is_staff:
num_distinct = settings.ADVANCED_SECURITY_CONFIG['MIN_DIFFERENT_STAFF_PASSWORDS_BEFORE_REUSE']
else:
num_distinct = settings.ADVANCED_SECURITY_CONFIG['MIN_DIFFERENT_STUDENT_PASSWORDS_BEFORE_REUSE']
err_msg = _(
"You are re-using a password that you have used recently. You must "
"have {0} distinct password(s) before reusing a previous password."
).format(num_distinct)
# also, check to see if passwords are getting reset too frequent
if PasswordHistory.is_password_reset_too_soon(existing_user):
num_days = settings.ADVANCED_SECURITY_CONFIG['MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS']
err_msg = _(
"You are resetting passwords too frequently. Due to security policies, "
"{0} day(s) must elapse between password resets"
).format(num_days)
if err_msg:
# We have an password reset attempt which violates some security policy,
status_code = status.HTTP_403_FORBIDDEN
response_data['message'] = err_msg
return Response(response_data, status=status_code)
existing_user.is_active = True
existing_user.set_password(password)
existing_user.save()
update_user_password_hash = existing_user.password
if update_user_password_hash != old_password_hash:
# add this account creation to password history
# NOTE, this will be a NOP unless the feature has been turned on in configuration
password_history_entry = PasswordHistory()
password_history_entry.create(existing_user)
status_code = status.HTTP_201_CREATED
response_data['uri'] = base_uri
response_data['message'] = 'Password Reset Successful'
else:
status_code = status.HTTP_404_NOT_FOUND
response_data['message'] = 'User not exist'
except ObjectDoesNotExist:
limiter.tick_bad_request_counter(request)
return Response(response_data, status=status.HTTP_404_NOT_FOUND)
return Response(response_data, status=status_code)
class UsersGroupsList(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def post(self, request, user_id, format=None):
"""
POST creates a new user-group relationship in the system
"""
response_data = {}
group_id = request.DATA['group_id']
base_uri = _generate_base_uri(request)
response_data['uri'] = '{}/{}'.format(base_uri, str(group_id))
try:
existing_user = User.objects.get(id=user_id)
existing_group = Group.objects.get(id=group_id)
except ObjectDoesNotExist:
existing_user = None
existing_group = None
if existing_user and existing_group:
try:
existing_relationship = existing_user.groups.get(id=existing_group.id)
except ObjectDoesNotExist:
existing_relationship = None
if existing_relationship is None:
existing_user.groups.add(existing_group.id)
response_data['uri'] = '{}/{}'.format(base_uri, existing_user.id)
response_data['group_id'] = str(existing_group.id)
response_data['user_id'] = str(existing_user.id)
response_status = status.HTTP_201_CREATED
else:
response_data['uri'] = '{}/{}'.format(base_uri, existing_group.id)
response_data['message'] = "Relationship already exists."
response_status = status.HTTP_409_CONFLICT
else:
response_status = status.HTTP_404_NOT_FOUND
return Response(response_data, status=response_status)
def get(self, request, user_id, format=None):
"""
GET retrieves the list of groups related to the specified user
"""
try:
existing_user = User.objects.get(id=user_id)
except ObjectDoesNotExist:
return Response({}, status.HTTP_404_NOT_FOUND)
groups = existing_user.groups.all()
response_data = {}
response_data['groups'] = []
for group in groups:
group_profile = GroupProfile.objects.get(group_id=group.id)
group_data = {}
group_data['id'] = group.id
group_data['name'] = group_profile.name
response_data['groups'].append(group_data)
response_status = status.HTTP_200_OK
return Response(response_data, status=response_status)
class UsersGroupsDetail(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def get(self, request, user_id, group_id, format=None):
"""
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)
except ObjectDoesNotExist:
existing_user = None
existing_relationship = None
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_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):
"""
DELETE removes/inactivates/etc. an existing user-group relationship
"""
existing_user = User.objects.get(id=user_id, is_active=True)
existing_user.groups.remove(group_id)
existing_user.save()
return Response({}, status=status.HTTP_204_NO_CONTENT)
class UsersCoursesList(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def post(self, request, user_id, format=None):
"""
POST creates a new course enrollment for a user
"""
store = modulestore()
response_data = {}
user_id = user_id
course_id = request.DATA['course_id']
try:
user = User.objects.get(id=user_id)
course_descriptor = store.get_course(course_id)
except (ObjectDoesNotExist, ValueError):
user = None
course_descriptor = None
if user and course_descriptor:
base_uri = _generate_base_uri(request)
course_enrollment = CourseEnrollment.enroll(user, course_id)
response_data['uri'] = '{}/{}'.format(base_uri, course_id)
response_data['id'] = course_id
response_data['name'] = course_descriptor.display_name
response_data['is_active'] = course_enrollment.is_active
status_code = status.HTTP_201_CREATED
else:
status_code = status.HTTP_404_NOT_FOUND
return Response(response_data, status=status_code)
def get(self, request, user_id, format=None):
"""
GET creates the list of enrolled courses for a user
"""
store = modulestore()
response_data = []
base_uri = _generate_base_uri(request)
try:
user = User.objects.get(id=user_id)
except ObjectDoesNotExist:
user = None
if user:
enrollments = CourseEnrollment.enrollments_for_user(user=user)
for enrollment in enrollments:
descriptor = store.get_course(enrollment.course_id)
course_data = {
"id": enrollment.course_id,
"uri": '{}/{}'.format(base_uri, enrollment.course_id),
"is_active": enrollment.is_active,
"name": descriptor.display_name
}
response_data.append(course_data)
return Response(response_data, status=status.HTTP_200_OK)
else:
status_code = status.HTTP_404_NOT_FOUND
return Response(response_data, status=status_code)
class UsersCoursesDetail(APIView):
permission_classes = (ApiKeyHeaderPermission,)
def post(self, request, user_id, course_id, format=None):
"""
POST creates an ACTIVE course enrollment for the specified user
"""
store = modulestore()
base_uri = _generate_base_uri(request)
response_data = {}
response_data['uri'] = base_uri
try:
user = User.objects.get(id=user_id)
course_descriptor = store.get_course(course_id)
except (ObjectDoesNotExist, ValueError):
user = None
course_descriptor = None
if user and course_descriptor:
response_data['user_id'] = user.id
response_data['course_id'] = course_id
response_status = status.HTTP_201_CREATED
if request.DATA['position']:
response_data['position'] = _save_module_position(
request,
user,
course_id,
course_descriptor,
request.DATA['position']
)
else:
response_status = status.HTTP_404_NOT_FOUND
return Response(response_data, status=response_status)
def get(self, request, user_id, course_id, format=None):
"""
GET identifies an ACTIVE course enrollment for the specified user
"""
store = modulestore()
response_data = {}
base_uri = _generate_base_uri(request)
try:
user = User.objects.get(id=user_id, is_active=True)
course_descriptor = store.get_course(course_id)
except (ObjectDoesNotExist, ValueError):
user = None
course_descriptor = None
if user and CourseEnrollment.is_enrolled(user, course_id):
response_data['user_id'] = user.id
response_data['course_id'] = course_id
response_data['uri'] = base_uri
field_data_cache = FieldDataCache([course_descriptor], course_id, user)
course_module = module_render.get_module(
user,
request,
course_descriptor.location,
field_data_cache,
course_id)
response_data['position'] = course_module.position
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, course_id, format=None):
"""
DELETE unenrolls the specified user from a course
"""
try:
user = User.objects.get(id=user_id, is_active=True)
except ObjectDoesNotExist:
user = None
if user:
CourseEnrollment.unenroll(user, course_id)
return Response({}, status=status.HTTP_204_NO_CONTENT)
\ No newline at end of file
......@@ -245,6 +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
......@@ -1221,7 +1222,7 @@ def notification_image_for_tab(course_tab, user, course):
return None
def get_static_tab_contents(request, course, tab):
def get_static_tab_contents(request, course, tab, wrap_xmodule_display=True):
"""
Returns the contents for the given static tab
"""
......@@ -1233,7 +1234,8 @@ def get_static_tab_contents(request, course, tab):
course.id, request.user, modulestore().get_item(loc), depth=0
)
tab_module = get_module(
request.user, request, loc, field_data_cache, static_asset_path=course.static_asset_path, course=course
request.user, request, loc, field_data_cache, static_asset_path=course.static_asset_path,
course=course, wrap_xmodule_display=wrap_xmodule_display,
)
logging.debug('course_module = {0}'.format(tab_module))
......
......@@ -1926,6 +1926,9 @@ INSTALLED_APPS = (
'teams',
'xblock_django',
# EDX API application
'api_manager',
)
######################### CSRF #########################################
......@@ -2092,7 +2095,8 @@ if FEATURES.get('ENABLE_CORS_HEADERS'):
'cors_csrf.middleware.CorsCSRFMiddleware',
) + MIDDLEWARE_CLASSES
CORS_ALLOW_CREDENTIALS = True
CORS_ORIGIN_WHITELIST = ()
CORS_ORIGIN_WHITELIST = ('devstack.local', 'apros.devstack.local')
CORS_ORIGIN_REGEX_WHITELIST = ('^http?://(\w+\.)?devstack\.local$',)
###################### Registration ##################################
......
......@@ -96,6 +96,11 @@ CC_PROCESSOR = {
}
}
########################### EDX API #################################
FEATURES['API'] = True
########################### External REST APIs #################################
FEATURES['ENABLE_OAUTH2_PROVIDER'] = True
OAUTH_OIDC_ISSUER = 'http://127.0.0.1:8000/oauth2'
......@@ -181,6 +186,11 @@ FEATURES['ENABLE_COSMETIC_DISPLAY_PRICE'] = True
if FEATURES.get('ENABLE_THIRD_PARTY_AUTH') and 'third_party_auth.dummy.DummyBackend' not in AUTHENTICATION_BACKENDS:
AUTHENTICATION_BACKENDS = ['third_party_auth.dummy.DummyBackend'] + list(AUTHENTICATION_BACKENDS)
########################### EDX API #################################
FEATURES['API'] = True
#####################################################################
# See if the developer has any local overrides.
try:
......
......@@ -70,8 +70,14 @@ FEATURES['ALLOW_COURSE_STAFF_GRADE_DOWNLOADS'] = True
# Toggles embargo on for testing
FEATURES['EMBARGO'] = True
# Toggles API on for testing
FEATURES['API'] = True
FEATURES['ENABLE_COMBINED_LOGIN_REGISTRATION'] = True
# Toggles API on for testing
FEATURES['API'] = True
# Need wiki for courseware views to work. TODO (vshnayder): shouldn't need it.
WIKI_ENABLED = True
......
......@@ -122,6 +122,12 @@ if settings.FEATURES["ENABLE_MOBILE_REST_API"]:
url(r'^api/mobile/v0.5/', include('mobile_api.urls')),
)
# OPEN EDX API
if settings.FEATURES["API"]:
urlpatterns += (
url(r'^api/*', include('api_manager.urls')),
)
# if settings.FEATURES.get("MULTIPLE_ENROLLMENT_ROLES"):
urlpatterns += (
# TODO Namespace these!
......
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