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>
""")
"""
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)
""" 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)
# -*- 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)
# 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")
""" 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)
......@@ -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