diff --git a/cms/djangoapps/contentstore/tests/test_assets.py b/cms/djangoapps/contentstore/tests/test_assets.py new file mode 100644 index 0000000000..a11db50141 --- /dev/null +++ b/cms/djangoapps/contentstore/tests/test_assets.py @@ -0,0 +1,83 @@ +import json +from datetime import datetime +from io import BytesIO +from pytz import UTC +from unittest import TestCase +from .utils import CourseTestCase +from django.core.urlresolvers import reverse +from contentstore.views import assets + + +class AssetsTestCase(CourseTestCase): + def setUp(self): + super(AssetsTestCase, self).setUp() + self.url = reverse("asset_index", kwargs={ + 'org': self.course.location.org, + 'course': self.course.location.course, + 'name': self.course.location.name, + }) + + def test_basic(self): + resp = self.client.get(self.url) + self.assertEquals(resp.status_code, 200) + + def test_json(self): + resp = self.client.get( + self.url, + HTTP_ACCEPT="application/json", + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + self.assertEquals(resp.status_code, 200) + content = json.loads(resp.content) + self.assertIsInstance(content, list) + + +class UploadTestCase(CourseTestCase): + def setUp(self): + super(UploadTestCase, self).setUp() + self.url = reverse("upload_asset", kwargs={ + 'org': self.course.location.org, + 'course': self.course.location.course, + 'coursename': self.course.location.name, + }) + + def test_happy_path(self): + f = BytesIO("sample content") + f.name = "sample.txt" + resp = self.client.post(self.url, {"name": "my-name", "file": f}) + self.assert2XX(resp.status_code) + + def test_no_file(self): + resp = self.client.post(self.url, {"name": "file.txt"}) + self.assert4XX(resp.status_code) + + def test_get(self): + resp = self.client.get(self.url) + self.assertEquals(resp.status_code, 405) + + +class AssetsToJsonTestCase(TestCase): + def test_basic(self): + upload_date = datetime(2013, 6, 1, 10, 30, tzinfo=UTC) + asset = { + "displayname": "foo", + "chunkSize": 512, + "filename": "foo.png", + "length": 100, + "uploadDate": upload_date, + "_id": { + "course": "course", + "org": "org", + "revision": 12, + "category": "category", + "name": "name", + "tag": "tag", + } + } + output = assets.assets_to_json_dict([asset]) + self.assertEquals(len(output), 1) + compare = output[0] + self.assertEquals(compare["name"], "foo") + self.assertEquals(compare["path"], "foo.png") + self.assertEquals(compare["uploaded"], upload_date.isoformat()) + self.assertEquals(compare["id"], "/tag/org/course/12/category/name") diff --git a/cms/djangoapps/contentstore/tests/test_textbooks.py b/cms/djangoapps/contentstore/tests/test_textbooks.py index a2cb3d7dc3..7264891285 100644 --- a/cms/djangoapps/contentstore/tests/test_textbooks.py +++ b/cms/djangoapps/contentstore/tests/test_textbooks.py @@ -1,17 +1,17 @@ import json -import mock from unittest import TestCase from .utils import CourseTestCase from django.core.urlresolvers import reverse from contentstore.utils import get_modulestore +from xmodule.modulestore.inheritance import own_metadata from contentstore.views.course import ( - validate_textbook_json, TextbookValidationError) + validate_textbooks_json, validate_textbook_json, TextbookValidationError) -class TextbookTestCase(CourseTestCase): +class TextbookIndexTestCase(CourseTestCase): def setUp(self): - super(TextbookTestCase, self).setUp() + super(TextbookIndexTestCase, self).setUp() self.url = reverse('textbook_index', kwargs={ 'org': self.course.location.org, 'course': self.course.location.course, @@ -20,7 +20,7 @@ class TextbookTestCase(CourseTestCase): def test_view_index(self): resp = self.client.get(self.url) - self.assertEqual(resp.status_code, 200) + self.assert2XX(resp.status_code) # we don't have resp.context right now, # due to bugs in our testing harness :( if resp.context: @@ -32,7 +32,7 @@ class TextbookTestCase(CourseTestCase): HTTP_ACCEPT="application/json", HTTP_X_REQUESTED_WITH='XMLHttpRequest' ) - self.assertEqual(resp.status_code, 200) + self.assert2XX(resp.status_code) obj = json.loads(resp.content) self.assertEqual(self.course.pdf_textbooks, obj) @@ -48,13 +48,17 @@ class TextbookTestCase(CourseTestCase): HTTP_ACCEPT="application/json", HTTP_X_REQUESTED_WITH='XMLHttpRequest' ) - self.assertEqual(resp.status_code, 204) - self.assertEqual(resp.content, "") + self.assert2XX(resp.status_code) # reload course store = get_modulestore(self.course.location) course = store.get_item(self.course.location) - self.assertEqual(course.pdf_textbooks, textbooks) + # should be the same, except for added ID + no_ids = [] + for textbook in course.pdf_textbooks: + del textbook["id"] + no_ids.append(textbook) + self.assertEqual(no_ids, textbooks) def test_view_index_xhr_post_invalid(self): resp = self.client.post( @@ -64,43 +68,271 @@ class TextbookTestCase(CourseTestCase): HTTP_ACCEPT="application/json", HTTP_X_REQUESTED_WITH='XMLHttpRequest' ) - self.assertEqual(resp.status_code, 400) + self.assert4XX(resp.status_code) obj = json.loads(resp.content) self.assertIn("error", obj) -class TextbookValidationTestCase(TestCase): - def test_happy_path(self): - textbooks = [ - { - "tab_title": "Hi, mom!", - "url": "/mom.pdf" - }, - { - "tab_title": "Textbook 2", - "chapters": [ - { - "title": "Chapter 1", - "url": "/ch1.pdf" - }, { - "title": "Chapter 2", - "url": "/ch2.pdf" - } - ] +class TextbookCreateTestCase(CourseTestCase): + def setUp(self): + super(TextbookCreateTestCase, self).setUp() + self.url = reverse('create_textbook', kwargs={ + 'org': self.course.location.org, + 'course': self.course.location.course, + 'name': self.course.location.name, + }) + self.textbook = { + "tab_title": "Economics", + "chapters": { + "title": "Chapter 1", + "url": "/a/b/c/ch1.pdf", } - ] + } - result = validate_textbook_json(json.dumps(textbooks)) - self.assertEqual(textbooks, result) + def test_happy_path(self): + resp = self.client.post( + self.url, + data=json.dumps(self.textbook), + content_type="application/json", + HTTP_ACCEPT="application/json", + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + self.assertEqual(resp.status_code, 201) + self.assertIn("Location", resp) + textbook = json.loads(resp.content) + self.assertIn("id", textbook) + del textbook["id"] + self.assertEqual(self.textbook, textbook) - def test_invalid_json(self): + def test_get(self): + resp = self.client.get( + self.url, + HTTP_ACCEPT="application/json", + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + self.assertEqual(resp.status_code, 405) + + def test_valid_id(self): + self.textbook["id"] = "7x5" + resp = self.client.post( + self.url, + data=json.dumps(self.textbook), + content_type="application/json", + HTTP_ACCEPT="application/json", + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + self.assertEqual(resp.status_code, 201) + textbook = json.loads(resp.content) + self.assertEqual(self.textbook, textbook) + + def test_invalid_id(self): + self.textbook["id"] = "xxx" + resp = self.client.post( + self.url, + data=json.dumps(self.textbook), + content_type="application/json", + HTTP_ACCEPT="application/json", + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + self.assert4XX(resp.status_code) + self.assertNotIn("Location", resp) + + +class TextbookByIdTestCase(CourseTestCase): + def setUp(self): + super(TextbookByIdTestCase, self).setUp() + self.textbook1 = { + "tab_title": "Economics", + "id": 1, + "chapters": { + "title": "Chapter 1", + "url": "/a/b/c/ch1.pdf", + } + } + self.url1 = reverse('textbook_by_id', kwargs={ + 'org': self.course.location.org, + 'course': self.course.location.course, + 'name': self.course.location.name, + 'tid': 1, + }) + self.textbook2 = { + "tab_title": "Algebra", + "id": 2, + "chapters": { + "title": "Chapter 11", + "url": "/a/b/ch11.pdf", + } + } + self.url2 = reverse('textbook_by_id', kwargs={ + 'org': self.course.location.org, + 'course': self.course.location.course, + 'name': self.course.location.name, + 'tid': 2, + }) + self.course.pdf_textbooks = [self.textbook1, self.textbook2] + self.store = get_modulestore(self.course.location) + self.store.update_metadata(self.course.location, own_metadata(self.course)) + self.url_nonexist = reverse('textbook_by_id', kwargs={ + 'org': self.course.location.org, + 'course': self.course.location.course, + 'name': self.course.location.name, + 'tid': 20, + }) + + def test_get_1(self): + resp = self.client.get(self.url1) + self.assert2XX(resp.status_code) + compare = json.loads(resp.content) + self.assertEqual(compare, self.textbook1) + + def test_get_2(self): + resp = self.client.get(self.url2) + self.assert2XX(resp.status_code) + compare = json.loads(resp.content) + self.assertEqual(compare, self.textbook2) + + def test_get_nonexistant(self): + resp = self.client.get(self.url_nonexist) + self.assertEqual(resp.status_code, 404) + + def test_delete(self): + resp = self.client.delete(self.url1) + self.assert2XX(resp.status_code) + course = self.store.get_item(self.course.location) + self.assertEqual(course.pdf_textbooks, [self.textbook2]) + + def test_delete_nonexistant(self): + resp = self.client.delete(self.url_nonexist) + self.assertEqual(resp.status_code, 404) + course = self.store.get_item(self.course.location) + self.assertEqual(course.pdf_textbooks, [self.textbook1, self.textbook2]) + + def test_create_new_by_id(self): + textbook = { + "tab_title": "a new textbook", + "url": "supercool.pdf", + "id": "1supercool", + } + url = reverse("textbook_by_id", kwargs={ + 'org': self.course.location.org, + 'course': self.course.location.course, + 'name': self.course.location.name, + 'tid': "1supercool", + }) + resp = self.client.post( + url, + data=json.dumps(textbook), + content_type="application/json", + ) + self.assertEqual(resp.status_code, 201) + resp2 = self.client.get(url) + self.assert2XX(resp2.status_code) + compare = json.loads(resp2.content) + self.assertEqual(compare, textbook) + course = self.store.get_item(self.course.location) + self.assertEqual( + course.pdf_textbooks, + [self.textbook1, self.textbook2, textbook] + ) + + def test_replace_by_id(self): + replacement = { + "tab_title": "You've been replaced!", + "url": "supercool.pdf", + "id": "2", + } + resp = self.client.post( + self.url2, + data=json.dumps(replacement), + content_type="application/json", + ) + self.assertEqual(resp.status_code, 201) + resp2 = self.client.get(self.url2) + self.assert2XX(resp2.status_code) + compare = json.loads(resp2.content) + self.assertEqual(compare, replacement) + course = self.store.get_item(self.course.location) + self.assertEqual( + course.pdf_textbooks, + [self.textbook1, replacement] + ) + + +class TextbookValidationTestCase(TestCase): + def setUp(self): + self.tb1 = { + "tab_title": "Hi, mom!", + "url": "/mom.pdf" + } + self.tb2 = { + "tab_title": "Hi, dad!", + "chapters": [ + { + "title": "Baseball", + "url": "baseball.pdf", + }, { + "title": "Basketball", + "url": "crazypants.pdf", + } + ] + } + self.textbooks = [self.tb1, self.tb2] + + def test_happy_path_plural(self): + result = validate_textbooks_json(json.dumps(self.textbooks)) + self.assertEqual(self.textbooks, result) + + def test_happy_path_singular_1(self): + result = validate_textbook_json(json.dumps(self.tb1)) + self.assertEqual(self.tb1, result) + + def test_happy_path_singular_2(self): + result = validate_textbook_json(json.dumps(self.tb2)) + self.assertEqual(self.tb2, result) + + def test_valid_id(self): + self.tb1["id"] = 1 + result = validate_textbook_json(json.dumps(self.tb1)) + self.assertEqual(self.tb1, result) + + def test_invalid_id(self): + self.tb1["id"] = "abc" with self.assertRaises(TextbookValidationError): - validate_textbook_json("[{'abc'}]") + validate_textbook_json(json.dumps(self.tb1)) - def test_wrong_json(self): + def test_invalid_json_plural(self): with self.assertRaises(TextbookValidationError): - validate_textbook_json('{"tab_title": "Hi, mom!"}') + validate_textbooks_json("[{'abc'}]") - def test_no_tab_title(self): + def test_invalid_json_singular(self): with self.assertRaises(TextbookValidationError): - validate_textbook_json('[{"url": "/textbook.pdf"}') + validate_textbook_json("[{1]}") + + def test_wrong_json_plural(self): + with self.assertRaises(TextbookValidationError): + validate_textbooks_json('{"tab_title": "Hi, mom!"}') + + def test_wrong_json_singular(self): + with self.assertRaises(TextbookValidationError): + validate_textbook_json('[{"tab_title": "Hi, mom!"}, {"tab_title": "Hi, dad!"}]') + + def test_no_tab_title_plural(self): + with self.assertRaises(TextbookValidationError): + validate_textbooks_json('[{"url": "/textbook.pdf"}]') + + def test_no_tab_title_singular(self): + with self.assertRaises(TextbookValidationError): + validate_textbook_json('{"url": "/textbook.pdf"}') + + def test_duplicate_ids(self): + textbooks = [{ + "tab_title": "name one", + "url": "one.pdf", + "id": 1, + }, { + "tab_title": "name two", + "url": "two.pdf", + "id": 1, + }] + with self.assertRaises(TextbookValidationError): + validate_textbooks_json(json.dumps(textbooks)) diff --git a/cms/djangoapps/contentstore/views/assets.py b/cms/djangoapps/contentstore/views/assets.py index 64be433de1..902d5f3a07 100644 --- a/cms/djangoapps/contentstore/views/assets.py +++ b/cms/djangoapps/contentstore/views/assets.py @@ -13,6 +13,7 @@ from django_future.csrf import ensure_csrf_cookie from django.core.urlresolvers import reverse from django.core.servers.basehttp import FileWrapper from django.core.files.temp import NamedTemporaryFile +from django.views.decorators.http import require_POST from mitxmako.shortcuts import render_to_response from cache_toolbox.core import del_cached_content @@ -30,6 +31,7 @@ from xmodule.exceptions import NotFoundError from ..utils import get_url_reverse from .access import get_location_and_verify_access +from util.json_request import JsonResponse __all__ = ['asset_index', 'upload_asset', 'import_course', 'generate_export_course', 'export_course'] @@ -89,7 +91,7 @@ def asset_index(request, org, course, name): assets = sorted(assets, key=lambda asset: asset['uploadDate'], reverse=True) if request.META.get('HTTP_ACCEPT', "").startswith("application/json"): - return HttpResponse(json.dumps(assets_to_json_dict(assets)), content_type="application/json") + return JsonResponse(assets_to_json_dict(assets)) asset_display = [] for asset in assets: @@ -120,6 +122,7 @@ def asset_index(request, org, course, name): }) +@require_POST @ensure_csrf_cookie @login_required def upload_asset(request, org, course, coursename): @@ -127,10 +130,6 @@ def upload_asset(request, org, course, coursename): This method allows for POST uploading of files into the course asset library, which will be supported by GridFS in MongoDB. ''' - if request.method != 'POST': - # (cdodge) @todo: Is there a way to do a - say - 'raise Http400'? - return HttpResponseBadRequest() - # construct a location from the passed in path location = get_location_and_verify_access(request, org, course, coursename) diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index aa001a901d..e6b1732c95 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -8,7 +8,7 @@ import string from django.contrib.auth.decorators import login_required from django_future.csrf import ensure_csrf_cookie from django.conf import settings -from django.views.decorators.http import require_http_methods +from django.views.decorators.http import require_http_methods, require_POST from django.core.exceptions import PermissionDenied from django.core.urlresolvers import reverse from django.http import HttpResponse, HttpResponseBadRequest @@ -521,9 +521,9 @@ def textbook_index(request, org, course, name): }) +@require_POST @login_required @ensure_csrf_cookie -@require_http_methods(("POST",)) def create_textbook(request, org, course, name): location = get_location_and_verify_access(request, org, course, name) store = get_modulestore(location) @@ -531,7 +531,7 @@ def create_textbook(request, org, course, name): try: textbook = validate_textbook_json(request.body) - except TextbookValidationError: + except TextbookValidationError as e: return JsonResponse({"error": e.message}, status=400) if not textbook.get("id"): tids = set(t["id"] for t in course_module.pdf_textbooks if "id" in t) @@ -555,7 +555,8 @@ def textbook_by_id(request, org, course, name, tid): location = get_location_and_verify_access(request, org, course, name) store = get_modulestore(location) course_module = store.get_item(location, depth=3) - matching_id = [tb for tb in course_module.pdf_textbooks if tb.get("id") == tid] + matching_id = [tb for tb in course_module.pdf_textbooks + if str(tb.get("id")) == str(tid)] if matching_id: textbook = matching_id[0] else: @@ -589,4 +590,4 @@ def textbook_by_id(request, org, course, name, tid): new_textbooks.extend(course_module.pdf_textbooks[i+1:]) course_module.pdf_textbooks = new_textbooks store.update_metadata(course_module.location, own_metadata(course_module)) - return JsonResponse(new_textbook) + return JsonResponse() diff --git a/common/djangoapps/util/tests/test_json_request.py b/common/djangoapps/util/tests/test_json_request.py new file mode 100644 index 0000000000..13751baad8 --- /dev/null +++ b/common/djangoapps/util/tests/test_json_request.py @@ -0,0 +1,42 @@ +from django.http import HttpResponse +from util.json_request import JsonResponse +import json +import unittest + + +class JsonResponseTestCase(unittest.TestCase): + def test_empty(self): + resp = JsonResponse() + self.assertIsInstance(resp, HttpResponse) + self.assertEqual(resp.content, "") + self.assertEqual(resp.status_code, 204) + self.assertEqual(resp["content-type"], "application/json") + + def test_empty_string(self): + resp = JsonResponse("") + self.assertIsInstance(resp, HttpResponse) + self.assertEqual(resp.content, "") + self.assertEqual(resp.status_code, 204) + self.assertEqual(resp["content-type"], "application/json") + + def test_string(self): + resp = JsonResponse("foo") + self.assertEqual(resp.content, '"foo"') + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp["content-type"], "application/json") + + def test_dict(self): + obj = {"foo": "bar"} + resp = JsonResponse(obj) + compare = json.loads(resp.content) + self.assertEqual(obj, compare) + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp["content-type"], "application/json") + + def test_set_status(self): + obj = {"error": "resource not found"} + resp = JsonResponse(obj, status=404) + compare = json.loads(resp.content) + self.assertEqual(obj, compare) + self.assertEqual(resp.status_code, 404) + self.assertEqual(resp["content-type"], "application/json") diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index 1a3d2699cc..5d5bb9d18b 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -6,6 +6,7 @@ from django.test import TestCase from django.conf import settings import xmodule.modulestore.django from xmodule.templates import update_templates +from unittest.util import safe_repr def mongo_store_config(data_dir): @@ -183,3 +184,23 @@ class ModuleStoreTestCase(TestCase): # Call superclass implementation super(ModuleStoreTestCase, self)._post_teardown() + + def assert2XX(self, status_code, msg=None): + if not 200 <= status_code < 300: + msg = self._formatMessage(msg, "%s is not a success status" % safe_repr(status_code)) + raise self.failureExecption(msg) + + def assert3XX(self, status_code, msg=None): + if not 300 <= status_code < 400: + msg = self._formatMessage(msg, "%s is not a redirection status" % safe_repr(status_code)) + raise self.failureExecption(msg) + + def assert4XX(self, status_code, msg=None): + if not 400 <= status_code < 500: + msg = self._formatMessage(msg, "%s is not a client error status" % safe_repr(status_code)) + raise self.failureExecption(msg) + + def assert5XX(self, status_code, msg=None): + if not 500 <= status_code < 600: + msg = self._formatMessage(msg, "%s is not a server error status" % safe_repr(status_code)) + raise self.failureExecption(msg)