Skip to content
This repository has been archived by the owner on Jan 28, 2020. It is now read-only.

Commit

Permalink
Added REST api view to get and delete a course
Browse files Browse the repository at this point in the history
  • Loading branch information
Giovanni Di Milia committed Sep 18, 2015
1 parent c2a0aa5 commit 7d89927
Show file tree
Hide file tree
Showing 6 changed files with 387 additions and 19 deletions.
23 changes: 23 additions & 0 deletions apiary.apib
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,29 @@ Create a repository by providing its JSON representation.
]
}

## Course [/repositories/{repo_slug}/courses/{course_id}]

+ Parameters
+ repo_slug: `physics-1` (string, required) - slug for the repository
+ course_id: `1` (number, required) - ID of the course

### Retrieve a Course [GET]

+ Response 200 (application/json)

{
"id": 1,
"org": "MITx",
"course_number": "1.Mech",
"run": "1T2010"
}

### Delete Course [DELETE]

Deletes a course, all learning resources and static assets in it.

+ Response 204 (application/json)

## Group SearchResults

## SearchResult Collection [/repositories/{repo_slug}/search/{?q,sortby,selected_facets}]
Expand Down
17 changes: 17 additions & 0 deletions rest/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,20 @@ def has_permission(self, request, view):
username = view.kwargs['username']

return request.user.username == username


class ImportCoursePermission(BasePermission):
"""
Checks that the user has permission to import a course.
The same permission is used to check if the user can delete a course.
"""
def has_permission(self, request, view):
try:
repo = get_repo(view.kwargs['repo_slug'], request.user.id)
except NotFound:
raise Http404()
except PermissionDenied:
return False
if request.method in SAFE_METHODS:
return True
return RepoPermission.import_course[0] in get_perms(request.user, repo)
28 changes: 27 additions & 1 deletion rest/tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ class RESTTestCase(LoreTestCase):
def assert_options_head(self, url, expected_status):
"""Assert OPTIONS and HEAD."""
resp = self.client.options(url)
self.assertEqual(expected_status, resp.status_code)
if expected_status == HTTP_200_OK:
self.assertEqual(expected_status, resp.status_code)
# verify output is proper JSON
as_json(resp)
self.assertIn("HEAD", resp['ALLOW'])
Expand Down Expand Up @@ -193,6 +193,32 @@ def get_courses(self, repo_slug, expected_status=HTTP_200_OK):
if expected_status == HTTP_200_OK:
return as_json(resp)

def get_course(self, repo_slug, course_id, expected_status=HTTP_200_OK):
"""Get a course"""
url = '{repo_base}{slug}/courses/{course_id}/'.format(
slug=repo_slug,
repo_base=REPO_BASE,
course_id=course_id
)
self.assert_options_head(url, expected_status=expected_status)

resp = self.client.get(url)
self.assertEqual(expected_status, resp.status_code)
if expected_status == HTTP_200_OK:
return as_json(resp)

def delete_course(self, repo_slug, course_id,
expected_status=HTTP_204_NO_CONTENT):
"""Delete a course."""
resp = self.client.delete(
'{repo_base}{slug}/courses/{course_id}/'.format(
slug=repo_slug,
repo_base=REPO_BASE,
course_id=course_id
)
)
self.assertEqual(expected_status, resp.status_code)

def get_vocabularies(self, repo_slug, expected_status=HTTP_200_OK):
"""Get list of vocabularies."""
url = '{repo_base}{slug}/vocabularies/'.format(
Expand Down
263 changes: 263 additions & 0 deletions rest/tests/test_course.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@
)

from learningresources.api import create_course
from learningresources.models import (
Course,
LearningResource,
LearningResourceType,
Repository,
StaticAsset,
)
from taxonomy.models import Term, Vocabulary


class TestCourse(RESTTestCase):
Expand Down Expand Up @@ -53,6 +61,169 @@ def test_list_courses(self):
self.assertEqual(course_2['id'], second_course.id)
self.assertTrue(course_1['id'] < course_2['id'])

def test_get_course(self):
"""
REST tests to get a Course.
"""
res = self.get_course(
repo_slug=self.repo.slug,
course_id=self.course.id
)
self.assertDictEqual(
res,
{
'org': self.course.org,
'run': self.course.run,
'course_number': self.course.course_number,
'id': self.course.id
}
)
self.get_course(
repo_slug=self.repo.slug,
course_id='foo',
expected_status=404
)
self.get_course(
repo_slug=self.repo.slug,
course_id=1234567,
expected_status=404
)
# try to get the right course but non existing repo
self.get_course(
repo_slug='nonsense',
course_id=self.course.id,
expected_status=404
)

# create another repo
another_repo_dict = {
'name': 'another_repo_name',
'description': 'description',
}
repo_res = self.create_repository(another_repo_dict)

# try to get the right course but on wrong repo
self.get_course(
repo_slug=repo_res['slug'],
course_id=self.course.id,
expected_status=404
)

def count_resources(self, repositories=0, courses=0, learning_resources=0,
learning_resource_types=0, static_assetts=0, terms=0,
vocabularies=0):
"""
Helper function to count resources in the database
"""
# pylint: disable=too-many-arguments
self.assertEqual(Repository.objects.count(), repositories)
self.assertEqual(Course.objects.count(), courses)
self.assertEqual(LearningResource.objects.count(), learning_resources)
self.assertEqual(LearningResourceType.objects.count(),
learning_resource_types)
self.assertEqual(StaticAsset.objects.count(), static_assetts)
self.assertEqual(Term.objects.count(), terms)
self.assertEqual(Vocabulary.objects.count(), vocabularies)

def test_delete_course(self):
"""
REST tests to delete a Course.
All learning resources and static assets connected to the course are
deleted as well.
Other courses, learning resources types, terms, vocabularies,
repositories are not deleted.
Note: learning resources types are created during the import of the
course but they are related to the repository.
"""
# trying to delete non existing courses
self.delete_course(
repo_slug=self.repo.slug,
course_id='foo',
expected_status=404
)
self.delete_course(
repo_slug=self.repo.slug,
course_id=1234567,
expected_status=404
)

# environment before importing a course
self.count_resources(
repositories=1,
courses=1,
learning_resources=1,
learning_resource_types=1
)

# Import the course tarball
self.import_course_tarball(self.repo)
# create a vocabulary and term
vocabulary = self.create_vocabulary(self.repo.slug)
self.create_term(self.repo.slug, vocabulary['slug'])

# environment before deleting the course
self.count_resources(
repositories=1,
courses=2,
learning_resources=19,
learning_resource_types=8,
static_assetts=5,
terms=1,
vocabularies=1
)

# make sure of the course that is going to be deleted
courses = Course.objects.all()
self.assertEqual(courses[0].id, self.course.id)
self.assertNotEqual(courses[1].id, self.course.id)

# try to get the right course but non existing repo
self.delete_course(
repo_slug='nonsense',
course_id=courses[1].id,
expected_status=404
)

# delete the course
self.delete_course(self.repo.slug, courses[1].id)

# environment after deleting the course
self.count_resources(
repositories=1,
courses=1,
learning_resources=1,
learning_resource_types=8,
terms=1,
vocabularies=1
)

# the course remaining is the one not deleted
courses = Course.objects.all()
self.assertEqual(courses[0].id, self.course.id)

# and the learning resource remaining is associated with the remaining
# course
learning_resources = LearningResource.objects.all()
self.assertEqual(
learning_resources[0].course.id,
self.course.id
)

# try to delete the remaining course but with the wrong repo
# create another repo
another_repo_dict = {
'name': 'another_repo_name',
'description': 'description',
}
repo_res = self.create_repository(another_repo_dict)

# try to get the right course but on wrong repo
self.delete_course(
repo_slug=repo_res['slug'],
course_id=self.course.id,
expected_status=404
)


class TestCourseAuthorization(RESTAuthTestCase):
"""
Expand Down Expand Up @@ -85,3 +256,95 @@ def test_get_courses(self):

# anonymous
self.get_courses(repo_slug=self.repo.slug, expected_status=403)

def test_get_course(self):
"""
Authorizations for get a course
Any user of the repo can see courses
"""
# administrator of the repo (already logged in)
self.get_course(repo_slug=self.repo.slug, course_id=self.course.id)
self.logout()

# curator
self.login(self.curator_user.username)
self.get_course(repo_slug=self.repo.slug, course_id=self.course.id)
self.logout()

# author
self.login(self.author_user.username)
self.get_course(repo_slug=self.repo.slug, course_id=self.course.id)
self.logout()

# user without repo
self.login(self.user_norepo)
self.get_course(
repo_slug=self.repo.slug,
course_id=self.course.id,
expected_status=403
)
self.logout()

# anonymous
self.get_course(
repo_slug=self.repo.slug,
course_id=self.course.id,
expected_status=403
)

def test_delete_course(self):
"""
Authorizations for delete a course
Only user with import permission (admin and curator) can delete a repo
"""
course_params = {
'org': 'my org',
'repo_id': self.repo.id,
'course_number': 'second course number',
'run': "second course run",
'user_id': self.user.id
}

# anonymous user
self.logout()
self.delete_course(
self.repo.slug,
self.course.id,
expected_status=403
)

# user not belonging to repo
self.login(self.user_norepo.username)
self.delete_course(
self.repo.slug,
self.course.id,
expected_status=403
)
self.logout()

# author
self.login(self.author_user.username)
self.delete_course(
self.repo.slug,
self.course.id,
expected_status=403
)
self.logout()

# curator
self.login(self.curator_user.username)
self.delete_course(
self.repo.slug,
self.course.id
)
self.logout()

# recreate course
self.course = create_course(**course_params)

# administrator
self.login(self.user.username)
self.delete_course(
self.repo.slug,
self.course.id
)
4 changes: 4 additions & 0 deletions rest/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.conf.urls import url, include

from .views import (
CourseDetail,
CourseList,
LearningResourceDetail,
LearningResourceExportDetail,
Expand Down Expand Up @@ -56,6 +57,9 @@
url(r'^repositories/(?P<repo_slug>[-\w]+)/courses/$',
CourseList.as_view(),
name='course-list'),
url(r'^repositories/(?P<repo_slug>[-\w]+)/courses/(?P<course_id>\d+)/$',
CourseDetail.as_view(),
name='course-detail'),
url(REPOSITORY_VOCAB_URL + r'$',
VocabularyList.as_view(),
name='vocabulary-list'),
Expand Down
Loading

0 comments on commit 7d89927

Please sign in to comment.