diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2a70c98abf..8e11779704 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,8 @@ LMS: Added *experimental* crowdsource hinting manager page. XModule: Added *experimental* crowdsource hinting module. +Studio: Added support for uploading and managing PDF textbooks + Common: Student information is now passed to the tracking log via POST instead of GET. Common: Add tests for documentation generation to test suite diff --git a/cms/djangoapps/contentstore/debug_file_uploader.py b/cms/djangoapps/contentstore/debug_file_uploader.py new file mode 100644 index 0000000000..d783e16192 --- /dev/null +++ b/cms/djangoapps/contentstore/debug_file_uploader.py @@ -0,0 +1,22 @@ +from django.core.files.uploadhandler import FileUploadHandler +import time + + +class DebugFileUploader(FileUploadHandler): + def __init__(self, request=None): + super(DebugFileUploader, self).__init__(request) + self.count = 0 + + def receive_data_chunk(self, raw_data, start): + time.sleep(1) + self.count = self.count + len(raw_data) + fail_at = None + if 'fail_at' in self.request.GET: + fail_at = int(self.request.GET.get('fail_at')) + if fail_at and self.count > fail_at: + raise Exception('Triggered fail') + + return raw_data + + def file_complete(self, file_size): + return None diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index 1661e1c391..37d5c12ecc 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -2,7 +2,7 @@ #pylint: disable=W0621 from lettuce import world, step -from nose.tools import assert_false, assert_equal, assert_regexp_matches, assert_true +from nose.tools import assert_false, assert_equal, assert_regexp_matches from common import type_in_codemirror KEY_CSS = '.key input.policy-key' @@ -36,7 +36,7 @@ def press_the_notification_button(step, name): error_showing = world.is_css_present('.is-shown.wrapper-notification-error') return confirmation_dismissed or error_showing - assert_true(world.css_click(css, success_condition=save_clicked), 'Save button not clicked after 5 attempts.') + world.css_click(css, success_condition=save_clicked) @step(u'I edit the value of a policy key$') diff --git a/cms/djangoapps/contentstore/features/textbooks.feature b/cms/djangoapps/contentstore/features/textbooks.feature new file mode 100644 index 0000000000..0758a0b57b --- /dev/null +++ b/cms/djangoapps/contentstore/features/textbooks.feature @@ -0,0 +1,47 @@ +Feature: Textbooks + + Scenario: No textbooks + Given I have opened a new course in Studio + When I go to the textbooks page + Then I should see a message telling me to create a new textbook + + Scenario: Create a textbook + Given I have opened a new course in Studio + And I go to the textbooks page + When I click on the New Textbook button + And I name my textbook "Economics" + And I name the first chapter "Chapter 1" + And I click the Upload Asset link for the first chapter + And I upload the textbook "textbook.pdf" + And I wait for "2" seconds + And I save the textbook + Then I should see a textbook named "Economics" with a chapter path containing "/c4x/MITx/999/asset/textbook.pdf" + And I reload the page + Then I should see a textbook named "Economics" with a chapter path containing "/c4x/MITx/999/asset/textbook.pdf" + + Scenario: Create a textbook with multiple chapters + Given I have opened a new course in Studio + And I go to the textbooks page + When I click on the New Textbook button + And I name my textbook "History" + And I name the first chapter "Britain" + And I type in "britain.pdf" for the first chapter asset + And I click Add a Chapter + And I name the second chapter "America" + And I type in "america.pdf" for the second chapter asset + And I save the textbook + Then I should see a textbook named "History" with 2 chapters + And I click the textbook chapters + Then I should see a textbook named "History" with 2 chapters + And the first chapter should be named "Britain" + And the first chapter should have an asset called "britain.pdf" + And the second chapter should be named "America" + And the second chapter should have an asset called "america.pdf" + And I reload the page + Then I should see a textbook named "History" with 2 chapters + And I click the textbook chapters + Then I should see a textbook named "History" with 2 chapters + And the first chapter should be named "Britain" + And the first chapter should have an asset called "britain.pdf" + And the second chapter should be named "America" + And the second chapter should have an asset called "america.pdf" diff --git a/cms/djangoapps/contentstore/features/textbooks.py b/cms/djangoapps/contentstore/features/textbooks.py new file mode 100644 index 0000000000..ca135d9725 --- /dev/null +++ b/cms/djangoapps/contentstore/features/textbooks.py @@ -0,0 +1,121 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + +from lettuce import world, step +from django.conf import settings +import os + +TEST_ROOT = settings.COMMON_TEST_DATA_ROOT + + +@step(u'I go to the textbooks page') +def go_to_uploads(_step): + world.click_course_content() + menu_css = 'li.nav-course-courseware-textbooks' + world.css_find(menu_css).click() + + +@step(u'I should see a message telling me to create a new textbook') +def assert_create_new_textbook_msg(_step): + css = ".wrapper-content .no-textbook-content" + assert world.is_css_present(css) + no_tb = world.css_find(css) + assert "You haven't added any textbooks" in no_tb.text + + +@step(u'I upload the textbook "([^"]*)"$') +def upload_file(_step, file_name): + file_css = '.upload-dialog input[type=file]' + upload = world.css_find(file_css) + # uploading the file itself + path = os.path.join(TEST_ROOT, 'uploads', file_name) + upload._element.send_keys(os.path.abspath(path)) + button_css = ".upload-dialog .action-upload" + world.css_click(button_css) + + +@step(u'I click (on )?the New Textbook button') +def click_new_textbook(_step, on): + button_css = ".nav-actions .new-button" + button = world.css_find(button_css) + button.click() + + +@step(u'I name my textbook "([^"]*)"') +def name_textbook(_step, name): + input_css = ".textbook input[name=textbook-name]" + world.css_fill(input_css, name) + + +@step(u'I name the (first|second|third) chapter "([^"]*)"') +def name_chapter(_step, ordinal, name): + index = ["first", "second", "third"].index(ordinal) + input_css = ".textbook .chapter{i} input.chapter-name".format(i=index+1) + world.css_fill(input_css, name) + + +@step(u'I type in "([^"]*)" for the (first|second|third) chapter asset') +def asset_chapter(_step, name, ordinal): + index = ["first", "second", "third"].index(ordinal) + input_css = ".textbook .chapter{i} input.chapter-asset-path".format(i=index+1) + world.css_fill(input_css, name) + + +@step(u'I click the Upload Asset link for the (first|second|third) chapter') +def click_upload_asset(_step, ordinal): + index = ["first", "second", "third"].index(ordinal) + button_css = ".textbook .chapter{i} .action-upload".format(i=index+1) + world.css_click(button_css) + + +@step(u'I click Add a Chapter') +def click_add_chapter(_step): + button_css = ".textbook .action-add-chapter" + world.css_click(button_css) + + +@step(u'I save the textbook') +def save_textbook(_step): + submit_css = "form.edit-textbook button[type=submit]" + world.css_click(submit_css) + + +@step(u'I should see a textbook named "([^"]*)" with a chapter path containing "([^"]*)"') +def check_textbook(_step, textbook_name, chapter_name): + title = world.css_find(".textbook h3.textbook-title") + chapter = world.css_find(".textbook .wrap-textbook p") + assert title.text == textbook_name, "{} != {}".format(title.text, textbook_name) + assert chapter.text == chapter_name, "{} != {}".format(chapter.text, chapter_name) + + +@step(u'I should see a textbook named "([^"]*)" with (\d+) chapters') +def check_textbook_chapters(_step, textbook_name, num_chapters_str): + num_chapters = int(num_chapters_str) + title = world.css_find(".textbook .view-textbook h3.textbook-title") + toggle = world.css_find(".textbook .view-textbook .chapter-toggle") + assert title.text == textbook_name, "{} != {}".format(title.text, textbook_name) + assert toggle.text == "{num} PDF Chapters".format(num=num_chapters), \ + "Expected {num} chapters, found {real}".format(num=num_chapters, real=toggle.text) + + +@step(u'I click the textbook chapters') +def click_chapters(_step): + world.css_click(".textbook a.chapter-toggle") + + +@step(u'the (first|second|third) chapter should be named "([^"]*)"') +def check_chapter_name(_step, ordinal, name): + index = ["first", "second", "third"].index(ordinal) + chapter = world.css_find(".textbook .view-textbook ol.chapters li")[index] + element = chapter.find_by_css(".chapter-name") + assert element.text == name, "Expected chapter named {expected}, found chapter named {actual}".format( + expected=name, actual=element.text) + + +@step(u'the (first|second|third) chapter should have an asset called "([^"]*)"') +def check_chapter_asset(_step, ordinal, name): + index = ["first", "second", "third"].index(ordinal) + chapter = world.css_find(".textbook .view-textbook ol.chapters li")[index] + element = chapter.find_by_css(".chapter-asset-path") + assert element.text == name, "Expected chapter with asset {expected}, found chapter with asset {actual}".format( + expected=name, actual=element.text) diff --git a/cms/djangoapps/contentstore/tests/test_assets.py b/cms/djangoapps/contentstore/tests/test_assets.py new file mode 100644 index 0000000000..58aee3c77d --- /dev/null +++ b/cms/djangoapps/contentstore/tests/test_assets.py @@ -0,0 +1,95 @@ +""" +Unit tests for the asset upload endpoint. +""" + +import json +from datetime import datetime +from io import BytesIO +from pytz import UTC +from unittest import TestCase, skip +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): + """ + Unit tests for uploading a file + """ + 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, + }) + + @skip("CorruptGridFile error on continuous integration server") + def test_happy_path(self): + file = BytesIO("sample content") + file.name = "sample.txt" + resp = self.client.post(self.url, {"name": "my-name", "file": file}) + 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): + """ + Unit tests for transforming the results of a database call into something + we can send out to the client via JSON. + """ + 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_checklists.py b/cms/djangoapps/contentstore/tests/test_checklists.py index 52e9ba14fe..99ffb8678d 100644 --- a/cms/djangoapps/contentstore/tests/test_checklists.py +++ b/cms/djangoapps/contentstore/tests/test_checklists.py @@ -1,10 +1,10 @@ """ Unit tests for checklist methods in views.py. """ from contentstore.utils import get_modulestore, get_url_reverse -from contentstore.tests.test_course_settings import CourseTestCase from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.tests.factories import CourseFactory from django.core.urlresolvers import reverse import json +from .utils import CourseTestCase class ChecklistTestCase(CourseTestCase): @@ -117,4 +117,4 @@ class ChecklistTestCase(CourseTestCase): 'name': self.course.location.name, 'checklist_index': 100}) response = self.client.delete(update_url) - self.assertContains(response, 'Unsupported request', status_code=400) + self.assertEqual(response.status_code, 405) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index dbfc797caa..8400f35171 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -1,3 +1,5 @@ +#pylint: disable=E1101 + import json import shutil import mock diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 40ec2ed3c7..972bc22dce 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -6,8 +6,6 @@ import json import copy import mock -from django.contrib.auth.models import User -from django.test.client import Client from django.core.urlresolvers import reverse from django.utils.timezone import UTC from django.test.utils import override_settings @@ -17,45 +15,12 @@ from models.settings.course_details import (CourseDetails, CourseSettingsEncoder from models.settings.course_grading import CourseGradingModel from contentstore.utils import get_modulestore -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory from models.settings.course_metadata import CourseMetadata from xmodule.modulestore.xml_importer import import_from_xml from xmodule.fields import Date - -class CourseTestCase(ModuleStoreTestCase): - """ - Base class for test classes below. - """ - def setUp(self): - """ - These tests need a user in the DB so that the django Test Client - can log them in. - They inherit from the ModuleStoreTestCase class so that the mongodb collection - will be cleared out before each test case execution and deleted - afterwards. - """ - uname = 'testuser' - email = 'test+courses@edx.org' - password = 'foo' - - # Create the use so we can log them in. - self.user = User.objects.create_user(uname, email, password) - - # Note that we do not actually need to do anything - # for registration if we directly mark them active. - self.user.is_active = True - # Staff has access to view all courses - self.user.is_staff = True - self.user.save() - - self.client = Client() - self.client.login(username=uname, password=password) - - course = CourseFactory.create(template='i4x://edx/templates/course/Empty', org='MITx', number='999', display_name='Robot Super Course') - self.course_location = course.location +from .utils import CourseTestCase class CourseDetailsTestCase(CourseTestCase): @@ -63,8 +28,8 @@ class CourseDetailsTestCase(CourseTestCase): Tests the first course settings page (course dates, overview, etc.). """ def test_virgin_fetch(self): - details = CourseDetails.fetch(self.course_location) - self.assertEqual(details.course_location, self.course_location, "Location not copied into") + details = CourseDetails.fetch(self.course.location) + self.assertEqual(details.course_location, self.course.location, "Location not copied into") self.assertIsNotNone(details.start_date.tzinfo) self.assertIsNone(details.end_date, "end date somehow initialized " + str(details.end_date)) self.assertIsNone(details.enrollment_start, "enrollment_start date somehow initialized " + str(details.enrollment_start)) @@ -75,10 +40,10 @@ class CourseDetailsTestCase(CourseTestCase): self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort)) def test_encoder(self): - details = CourseDetails.fetch(self.course_location) + details = CourseDetails.fetch(self.course.location) jsondetails = json.dumps(details, cls=CourseSettingsEncoder) jsondetails = json.loads(jsondetails) - self.assertTupleEqual(Location(jsondetails['course_location']), self.course_location, "Location !=") + self.assertTupleEqual(Location(jsondetails['course_location']), self.course.location, "Location !=") self.assertIsNone(jsondetails['end_date'], "end date somehow initialized ") self.assertIsNone(jsondetails['enrollment_start'], "enrollment_start date somehow initialized ") self.assertIsNone(jsondetails['enrollment_end'], "enrollment_end date somehow initialized ") @@ -91,10 +56,12 @@ class CourseDetailsTestCase(CourseTestCase): """ Test the encoder out of its original constrained purpose to see if it functions for general use """ - details = {'location': Location(['tag', 'org', 'course', 'category', 'name']), - 'number': 1, - 'string': 'string', - 'datetime': datetime.datetime.now(UTC())} + details = { + 'location': Location(['tag', 'org', 'course', 'category', 'name']), + 'number': 1, + 'string': 'string', + 'datetime': datetime.datetime.now(UTC()) + } jsondetails = json.dumps(details, cls=CourseSettingsEncoder) jsondetails = json.loads(jsondetails) @@ -105,7 +72,7 @@ class CourseDetailsTestCase(CourseTestCase): self.assertEqual(jsondetails['string'], 'string') def test_update_and_fetch(self): - jsondetails = CourseDetails.fetch(self.course_location) + jsondetails = CourseDetails.fetch(self.course.location) jsondetails.syllabus = "bar" # encode - decode to convert date fields and other data which changes form self.assertEqual( @@ -138,9 +105,9 @@ class CourseDetailsTestCase(CourseTestCase): settings_details_url = reverse( 'settings_details', kwargs={ - 'org': self.course_location.org, - 'name': self.course_location.name, - 'course': self.course_location.course + 'org': self.course.location.org, + 'name': self.course.location.name, + 'course': self.course.location.course } ) @@ -162,9 +129,9 @@ class CourseDetailsTestCase(CourseTestCase): settings_details_url = reverse( 'settings_details', kwargs={ - 'org': self.course_location.org, - 'name': self.course_location.name, - 'course': self.course_location.course + 'org': self.course.location.org, + 'name': self.course.location.name, + 'course': self.course.location.course } ) @@ -204,11 +171,12 @@ class CourseDetailsViewTest(CourseTestCase): return Date().to_json(dt) def test_update_and_fetch(self): - details = CourseDetails.fetch(self.course_location) + loc = self.course.location + details = CourseDetails.fetch(loc) # resp s/b json from here on - url = reverse('course_settings', kwargs={'org': self.course_location.org, 'course': self.course_location.course, - 'name': self.course_location.name, 'section': 'details'}) + url = reverse('course_settings', kwargs={'org': loc.org, 'course': loc.course, + 'name': loc.name, 'section': 'details'}) resp = self.client.get(url) self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, "virgin get") @@ -251,49 +219,49 @@ class CourseGradingTest(CourseTestCase): Tests for the course settings grading page. """ def test_initial_grader(self): - descriptor = get_modulestore(self.course_location).get_item(self.course_location) + descriptor = get_modulestore(self.course.location).get_item(self.course.location) test_grader = CourseGradingModel(descriptor) # ??? How much should this test bake in expectations about defaults and thus fail if defaults change? - self.assertEqual(self.course_location, test_grader.course_location, "Course locations") + self.assertEqual(self.course.location, test_grader.course_location, "Course locations") self.assertIsNotNone(test_grader.graders, "No graders") self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs") def test_fetch_grader(self): - test_grader = CourseGradingModel.fetch(self.course_location.url()) - self.assertEqual(self.course_location, test_grader.course_location, "Course locations") + test_grader = CourseGradingModel.fetch(self.course.location.url()) + self.assertEqual(self.course.location, test_grader.course_location, "Course locations") self.assertIsNotNone(test_grader.graders, "No graders") self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs") - test_grader = CourseGradingModel.fetch(self.course_location) - self.assertEqual(self.course_location, test_grader.course_location, "Course locations") + test_grader = CourseGradingModel.fetch(self.course.location) + self.assertEqual(self.course.location, test_grader.course_location, "Course locations") self.assertIsNotNone(test_grader.graders, "No graders") self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs") for i, grader in enumerate(test_grader.graders): - subgrader = CourseGradingModel.fetch_grader(self.course_location, i) + subgrader = CourseGradingModel.fetch_grader(self.course.location, i) self.assertDictEqual(grader, subgrader, str(i) + "th graders not equal") - subgrader = CourseGradingModel.fetch_grader(self.course_location.list(), 0) + subgrader = CourseGradingModel.fetch_grader(self.course.location.list(), 0) self.assertDictEqual(test_grader.graders[0], subgrader, "failed with location as list") def test_fetch_cutoffs(self): - test_grader = CourseGradingModel.fetch_cutoffs(self.course_location) + test_grader = CourseGradingModel.fetch_cutoffs(self.course.location) # ??? should this check that it's at least a dict? (expected is { "pass" : 0.5 } I think) self.assertIsNotNone(test_grader, "No cutoffs via fetch") - test_grader = CourseGradingModel.fetch_cutoffs(self.course_location.url()) + test_grader = CourseGradingModel.fetch_cutoffs(self.course.location.url()) self.assertIsNotNone(test_grader, "No cutoffs via fetch with url") def test_fetch_grace(self): - test_grader = CourseGradingModel.fetch_grace_period(self.course_location) + test_grader = CourseGradingModel.fetch_grace_period(self.course.location) # almost a worthless test self.assertIn('grace_period', test_grader, "No grace via fetch") - test_grader = CourseGradingModel.fetch_grace_period(self.course_location.url()) + test_grader = CourseGradingModel.fetch_grace_period(self.course.location.url()) self.assertIn('grace_period', test_grader, "No cutoffs via fetch with url") def test_update_from_json(self): - test_grader = CourseGradingModel.fetch(self.course_location) + test_grader = CourseGradingModel.fetch(self.course.location) altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__) self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Noop update") @@ -307,11 +275,10 @@ class CourseGradingTest(CourseTestCase): test_grader.grace_period = {'hours': 4, 'minutes': 5, 'seconds': 0} altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__) - print test_grader.grace_period, altered_grader.grace_period self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period") def test_update_grader_from_json(self): - test_grader = CourseGradingModel.fetch(self.course_location) + test_grader = CourseGradingModel.fetch(self.course.location) altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1]) self.assertDictEqual(test_grader.graders[1], altered_grader, "Noop update") @@ -331,11 +298,11 @@ class CourseMetadataEditingTest(CourseTestCase): def setUp(self): CourseTestCase.setUp(self) # add in the full class too - import_from_xml(get_modulestore(self.course_location), 'common/test/data/', ['full']) + import_from_xml(get_modulestore(self.course.location), 'common/test/data/', ['full']) self.fullcourse_location = Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]) def test_fetch_initial_fields(self): - test_model = CourseMetadata.fetch(self.course_location) + test_model = CourseMetadata.fetch(self.course.location) self.assertIn('display_name', test_model, 'Missing editable metadata field') self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value") @@ -348,17 +315,17 @@ class CourseMetadataEditingTest(CourseTestCase): self.assertIn('xqa_key', test_model, 'xqa_key field ') def test_update_from_json(self): - test_model = CourseMetadata.update_from_json(self.course_location, { + test_model = CourseMetadata.update_from_json(self.course.location, { "advertised_start": "start A", "testcenter_info": {"c": "test"}, "days_early_for_beta": 2 }) self.update_check(test_model) # try fresh fetch to ensure persistence - test_model = CourseMetadata.fetch(self.course_location) + test_model = CourseMetadata.fetch(self.course.location) self.update_check(test_model) # now change some of the existing metadata - test_model = CourseMetadata.update_from_json(self.course_location, { + test_model = CourseMetadata.update_from_json(self.course.location, { "advertised_start": "start B", "display_name": "jolly roger"} ) @@ -387,3 +354,35 @@ class CourseMetadataEditingTest(CourseTestCase): # check for deletion effectiveness self.assertEqual('closed', test_model['showanswer'], 'showanswer field still in') self.assertEqual(None, test_model['xqa_key'], 'xqa_key field still in') + + +class CourseGraderUpdatesTest(CourseTestCase): + def setUp(self): + super(CourseGraderUpdatesTest, self).setUp() + self.url = reverse("course_settings", kwargs={ + 'org': self.course.location.org, + 'course': self.course.location.course, + 'name': self.course.location.name, + 'grader_index': 0, + }) + + def test_get(self): + resp = self.client.get(self.url) + self.assert2XX(resp.status_code) + obj = json.loads(resp.content) + + def test_delete(self): + resp = self.client.delete(self.url) + self.assert2XX(resp.status_code) + + def test_post(self): + grader = { + "type": "manual", + "min_count": 5, + "drop_count": 10, + "short_label": "yo momma", + "weight": 17.3, + } + resp = self.client.post(self.url, grader) + self.assert2XX(resp.status_code) + obj = json.loads(resp.content) diff --git a/cms/djangoapps/contentstore/tests/test_course_updates.py b/cms/djangoapps/contentstore/tests/test_course_updates.py index ae14555b32..4f92806871 100644 --- a/cms/djangoapps/contentstore/tests/test_course_updates.py +++ b/cms/djangoapps/contentstore/tests/test_course_updates.py @@ -10,9 +10,9 @@ class CourseUpdateTest(CourseTestCase): '''Go through each interface and ensure it works.''' # first get the update to force the creation url = reverse('course_info', - kwargs={'org': self.course_location.org, - 'course': self.course_location.course, - 'name': self.course_location.name}) + kwargs={'org': self.course.location.org, + 'course': self.course.location.course, + 'name': self.course.location.name}) self.client.get(url) init_content = '