diff --git a/cms/djangoapps/contentstore/features/checklists.feature b/cms/djangoapps/contentstore/features/checklists.feature
new file mode 100644
index 0000000000..bccb80b8d7
--- /dev/null
+++ b/cms/djangoapps/contentstore/features/checklists.feature
@@ -0,0 +1,24 @@
+Feature: Course checklists
+
+ Scenario: A course author sees checklists defined by edX
+ Given I have opened a new course in Studio
+ When I select Checklists from the Tools menu
+ Then I see the four default edX checklists
+
+ Scenario: A course author can mark tasks as complete
+ Given I have opened Checklists
+ Then I can check and uncheck tasks in a checklist
+ And They are correctly selected after I reload the page
+
+ Scenario: A task can link to a location within Studio
+ Given I have opened Checklists
+ When I select a link to the course outline
+ Then I am brought to the course outline page
+ And I press the browser back button
+ Then I am brought back to the course outline in the correct state
+
+ Scenario: A task can link to a location outside Studio
+ Given I have opened Checklists
+ When I select a link to help page
+ Then I am brought to the help page in a new window
+
diff --git a/cms/djangoapps/contentstore/features/checklists.py b/cms/djangoapps/contentstore/features/checklists.py
new file mode 100644
index 0000000000..9ef66c8096
--- /dev/null
+++ b/cms/djangoapps/contentstore/features/checklists.py
@@ -0,0 +1,119 @@
+from lettuce import world, step
+from common import *
+from terrain.steps import reload_the_page
+
+############### ACTIONS ####################
+@step('I select Checklists from the Tools menu$')
+def i_select_checklists(step):
+ expand_icon_css = 'li.nav-course-tools i.icon-expand'
+ if world.browser.is_element_present_by_css(expand_icon_css):
+ css_click(expand_icon_css)
+ link_css = 'li.nav-course-tools-checklists a'
+ css_click(link_css)
+
+
+@step('I have opened Checklists$')
+def i_have_opened_checklists(step):
+ step.given('I have opened a new course in Studio')
+ step.given('I select Checklists from the Tools menu')
+
+
+@step('I see the four default edX checklists$')
+def i_see_default_checklists(step):
+ checklists = css_find('.checklist-title')
+ assert_equal(4, len(checklists))
+ assert_true(checklists[0].text.endswith('Getting Started With Studio'))
+ assert_true(checklists[1].text.endswith('Draft a Rough Course Outline'))
+ assert_true(checklists[2].text.endswith("Explore edX\'s Support Tools"))
+ assert_true(checklists[3].text.endswith('Draft Your Course About Page'))
+
+
+@step('I can check and uncheck tasks in a checklist$')
+def i_can_check_and_uncheck_tasks(step):
+ # Use the 2nd checklist as a reference
+ verifyChecklist2Status(0, 7, 0)
+ toggleTask(1, 0)
+ verifyChecklist2Status(1, 7, 14)
+ toggleTask(1, 3)
+ verifyChecklist2Status(2, 7, 29)
+ toggleTask(1, 6)
+ verifyChecklist2Status(3, 7, 43)
+ toggleTask(1, 3)
+ verifyChecklist2Status(2, 7, 29)
+
+
+@step('They are correctly selected after I reload the page$')
+def tasks_correctly_selected_after_reload(step):
+ reload_the_page(step)
+ verifyChecklist2Status(2, 7, 29)
+ # verify that task 7 is still selected by toggling its checkbox state and making sure that it deselects
+ toggleTask(1, 6)
+ verifyChecklist2Status(1, 7, 14)
+
+
+@step('I select a link to the course outline$')
+def i_select_a_link_to_the_course_outline(step):
+ clickActionLink(1, 0, 'Edit Course Outline')
+
+
+@step('I am brought to the course outline page$')
+def i_am_brought_to_course_outline(step):
+ assert_equal('Course Outline', css_find('.outline .title-1')[0].text)
+ assert_equal(1, len(world.browser.windows))
+
+
+@step('I am brought back to the course outline in the correct state$')
+def i_am_brought_back_to_course_outline(step):
+ step.given('I see the four default edX checklists')
+ # In a previous step, we selected (1, 0) in order to click the 'Edit Course Outline' link.
+ # Make sure the task is still showing as selected (there was a caching bug with the collection).
+ verifyChecklist2Status(1, 7, 14)
+
+
+@step('I select a link to help page$')
+def i_select_a_link_to_the_help_page(step):
+ clickActionLink(2, 0, 'Visit Studio Help')
+
+
+@step('I am brought to the help page in a new window$')
+def i_am_brought_to_help_page_in_new_window(step):
+ step.given('I see the four default edX checklists')
+ windows = world.browser.windows
+ assert_equal(2, len(windows))
+ world.browser.switch_to_window(windows[1])
+ assert_equal('http://help.edge.edx.org/', world.browser.url)
+
+
+
+
+############### HELPER METHODS ####################
+def verifyChecklist2Status(completed, total, percentage):
+ def verify_count(driver):
+ try:
+ statusCount = css_find('#course-checklist1 .status-count').first
+ return statusCount.text == str(completed)
+ except StaleElementReferenceException:
+ return False
+
+ wait_for(verify_count)
+ assert_equal(str(total), css_find('#course-checklist1 .status-amount').first.text)
+ # Would like to check the CSS width, but not sure how to do that.
+ assert_equal(str(percentage), css_find('#course-checklist1 .viz-checklist-status-value .int').first.text)
+
+
+def toggleTask(checklist, task):
+ css_click('#course-checklist' + str(checklist) +'-task' + str(task))
+
+
+def clickActionLink(checklist, task, actionText):
+ # toggle checklist item to make sure that the link button is showing
+ toggleTask(checklist, task)
+ action_link = css_find('#course-checklist' + str(checklist) + ' a')[task]
+
+ # text will be empty initially, wait for it to populate
+ def verify_action_link_text(driver):
+ return action_link.text == actionText
+
+ wait_for(verify_action_link_text)
+ action_link.click()
+
diff --git a/cms/djangoapps/contentstore/tests/test_checklists.py b/cms/djangoapps/contentstore/tests/test_checklists.py
new file mode 100644
index 0000000000..f0889b0861
--- /dev/null
+++ b/cms/djangoapps/contentstore/tests/test_checklists.py
@@ -0,0 +1,96 @@
+""" 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
+
+
+class ChecklistTestCase(CourseTestCase):
+ """ Test for checklist get and put methods. """
+ def setUp(self):
+ """ Creates the test course. """
+ super(ChecklistTestCase, self).setUp()
+ self.course = CourseFactory.create(org='mitX', number='333', display_name='Checklists Course')
+
+ def get_persisted_checklists(self):
+ """ Returns the checklists as persisted in the modulestore. """
+ modulestore = get_modulestore(self.course.location)
+ return modulestore.get_item(self.course.location).checklists
+
+ def test_get_checklists(self):
+ """ Tests the get checklists method. """
+ checklists_url = get_url_reverse('Checklists', self.course)
+ response = self.client.get(checklists_url)
+ self.assertContains(response, "Getting Started With Studio")
+ payload = response.content
+
+ # Now delete the checklists from the course and verify they get repopulated (for courses
+ # created before checklists were introduced).
+ self.course.checklists = None
+ modulestore = get_modulestore(self.course.location)
+ modulestore.update_metadata(self.course.location, own_metadata(self.course))
+ self.assertEquals(self.get_persisted_checklists(), None)
+ response = self.client.get(checklists_url)
+ self.assertEquals(payload, response.content)
+
+ def test_update_checklists_no_index(self):
+ """ No checklist index, should return all of them. """
+ update_url = reverse('checklists_updates', kwargs={
+ 'org': self.course.location.org,
+ 'course': self.course.location.course,
+ 'name': self.course.location.name})
+
+ returned_checklists = json.loads(self.client.get(update_url).content)
+ self.assertListEqual(self.get_persisted_checklists(), returned_checklists)
+
+ def test_update_checklists_index_ignored_on_get(self):
+ """ Checklist index ignored on get. """
+ update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
+ 'course': self.course.location.course,
+ 'name': self.course.location.name,
+ 'checklist_index': 1})
+
+ returned_checklists = json.loads(self.client.get(update_url).content)
+ self.assertListEqual(self.get_persisted_checklists(), returned_checklists)
+
+ def test_update_checklists_post_no_index(self):
+ """ No checklist index, will error on post. """
+ update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
+ 'course': self.course.location.course,
+ 'name': self.course.location.name})
+ response = self.client.post(update_url)
+ self.assertContains(response, 'Could not save checklist', status_code=400)
+
+ def test_update_checklists_index_out_of_range(self):
+ """ Checklist index out of range, will error on post. """
+ update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
+ 'course': self.course.location.course,
+ 'name': self.course.location.name,
+ 'checklist_index': 100})
+ response = self.client.post(update_url)
+ self.assertContains(response, 'Could not save checklist', status_code=400)
+
+ def test_update_checklists_index(self):
+ """ Check that an update of a particular checklist works. """
+ update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
+ 'course': self.course.location.course,
+ 'name': self.course.location.name,
+ 'checklist_index': 2})
+ payload = self.course.checklists[2]
+ self.assertFalse(payload.get('is_checked'))
+ payload['is_checked'] = True
+
+ returned_checklist = json.loads(self.client.post(update_url, json.dumps(payload), "application/json").content)
+ self.assertTrue(returned_checklist.get('is_checked'))
+ self.assertEqual(self.get_persisted_checklists()[2], returned_checklist)
+
+ def test_update_checklists_delete_unsupported(self):
+ """ Delete operation is not supported. """
+ update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
+ 'course': self.course.location.course,
+ 'name': self.course.location.name,
+ 'checklist_index': 100})
+ response = self.client.delete(update_url)
+ self.assertContains(response, 'Unsupported request', status_code=400)
\ No newline at end of file
diff --git a/cms/djangoapps/contentstore/tests/test_utils.py b/cms/djangoapps/contentstore/tests/test_utils.py
index 4ab40d17a8..bbaebfb687 100644
--- a/cms/djangoapps/contentstore/tests/test_utils.py
+++ b/cms/djangoapps/contentstore/tests/test_utils.py
@@ -1,19 +1,72 @@
-from contentstore import utils
+""" Tests for utils. """
+from contentstore import utils
import mock
from django.test import TestCase
+from xmodule.modulestore.tests.factories import CourseFactory
+from .utils import ModuleStoreTestCase
class LMSLinksTestCase(TestCase):
+ """ Tests for LMS links. """
def about_page_test(self):
+ """ Get URL for about page. """
location = 'i4x', 'mitX', '101', 'course', 'test'
utils.get_course_id = mock.Mock(return_value="mitX/101/test")
link = utils.get_lms_link_for_about_page(location)
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/about")
- def ls_link_test(self):
+ def lms_link_test(self):
+ """ Tests get_lms_link_for_item. """
location = 'i4x', 'mitX', '101', 'vertical', 'contacting_us'
utils.get_course_id = mock.Mock(return_value="mitX/101/test")
link = utils.get_lms_link_for_item(location, False)
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us")
link = utils.get_lms_link_for_item(location, True)
- self.assertEquals(link, "//preview.localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us")
+ self.assertEquals(
+ link,
+ "//preview.localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us"
+ )
+
+
+class UrlReverseTestCase(ModuleStoreTestCase):
+ """ Tests for get_url_reverse """
+ def test_CoursePageNames(self):
+ """ Test the defined course pages. """
+ course = CourseFactory.create(org='mitX', number='666', display_name='URL Reverse Course')
+
+ self.assertEquals(
+ '/manage_users/i4x://mitX/666/course/URL_Reverse_Course',
+ utils.get_url_reverse('ManageUsers', course)
+ )
+
+ self.assertEquals(
+ '/mitX/666/settings-details/URL_Reverse_Course',
+ utils.get_url_reverse('SettingsDetails', course)
+ )
+
+ self.assertEquals(
+ '/mitX/666/settings-grading/URL_Reverse_Course',
+ utils.get_url_reverse('SettingsGrading', course)
+ )
+
+ self.assertEquals(
+ '/mitX/666/course/URL_Reverse_Course',
+ utils.get_url_reverse('CourseOutline', course)
+ )
+
+ self.assertEquals(
+ '/mitX/666/checklists/URL_Reverse_Course',
+ utils.get_url_reverse('Checklists', course)
+ )
+
+ def test_unknown_passes_through(self):
+ """ Test that unknown values pass through. """
+ course = CourseFactory.create(org='mitX', number='666', display_name='URL Reverse Course')
+ self.assertEquals(
+ 'foobar',
+ utils.get_url_reverse('foobar', course)
+ )
+ self.assertEquals(
+ 'https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about',
+ utils.get_url_reverse('https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', course)
+ )
\ No newline at end of file
diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py
index 0a99441fe9..63dfe5bf5f 100644
--- a/cms/djangoapps/contentstore/utils.py
+++ b/cms/djangoapps/contentstore/utils.py
@@ -2,6 +2,7 @@ from django.conf import settings
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
+from django.core.urlresolvers import reverse
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info']
@@ -158,3 +159,35 @@ def update_item(location, value):
get_modulestore(location).delete_item(location)
else:
get_modulestore(location).update_item(location, value)
+
+
+def get_url_reverse(course_page_name, course_module):
+ """
+ Returns the course URL link to the specified location. This value is suitable to use as an href link.
+
+ course_page_name should correspond to an attribute in CoursePageNames (for example, 'ManageUsers'
+ or 'SettingsDetails'), or else it will simply be returned. This method passes back unknown values of
+ course_page_names so that it can also be used for absolute (known) URLs.
+
+ course_module is used to obtain the location, org, course, and name properties for a course, if
+ course_page_name corresponds to an attribute in CoursePageNames.
+ """
+ url_name = getattr(CoursePageNames, course_page_name, None)
+ ctx_loc = course_module.location
+
+ if CoursePageNames.ManageUsers == url_name:
+ return reverse(url_name, kwargs={"location": ctx_loc})
+ elif url_name in [CoursePageNames.SettingsDetails, CoursePageNames.SettingsGrading,
+ CoursePageNames.CourseOutline, CoursePageNames.Checklists]:
+ return reverse(url_name, kwargs={'org': ctx_loc.org, 'course': ctx_loc.course, 'name': ctx_loc.name})
+ else:
+ return course_page_name
+
+
+class CoursePageNames:
+ """ Constants for pages that are recognized by get_url_reverse method. """
+ ManageUsers = "manage_users"
+ SettingsDetails = "settings_details"
+ SettingsGrading = "settings_grading"
+ CourseOutline = "course_index"
+ Checklists = "checklists"
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index ae590f69ed..561708c833 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -51,13 +51,13 @@ from xmodule.contentstore.content import StaticContent
from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups
-from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, get_date_display, UnitState, get_course_for_item
+from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, \
+ get_date_display, UnitState, get_course_for_item, get_url_reverse
from xmodule.modulestore.xml_importer import import_from_xml
from contentstore.course_info_model import get_course_updates, \
update_course_updates, delete_course_update
from cache_toolbox.core import del_cached_content
-from xmodule.timeparse import stringify_time
from contentstore.module_info_model import get_module_info, set_module_info
from models.settings.course_details import CourseDetails, \
CourseSettingsEncoder
@@ -141,10 +141,7 @@ def index(request):
return render_to_response('index.html', {
'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
'courses': [(course.display_name,
- reverse('course_index', args=[
- course.location.org,
- course.location.course,
- course.location.name]),
+ get_url_reverse('CourseOutline', course),
get_lms_link_for_item(course.location, course_id=course.location.course_id))
for course in courses],
'user': request.user,
@@ -181,19 +178,15 @@ def course_index(request, org, course, name):
org, course, name: Attributes of the Location for the item to edit
"""
- location = ['i4x', org, course, 'course', name]
-
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
+ location = get_location_and_verify_access(request, org, course, name)
lms_link = get_lms_link_for_item(location)
upload_asset_callback_url = reverse('upload_asset', kwargs={
- 'org': org,
- 'course': course,
- 'coursename': name
- })
+ 'org': org,
+ 'course': course,
+ 'coursename': name
+ })
course = modulestore().get_item(location)
sections = course.get_children()
@@ -249,7 +242,7 @@ def edit_subsection(request, location):
for field
in item.fields
if field.name not in ['display_name', 'start', 'due', 'format'] and
- field.scope == Scope.settings
+ field.scope == Scope.settings
)
can_view_live = False
@@ -261,18 +254,18 @@ def edit_subsection(request, location):
break
return render_to_response('edit_subsection.html',
- {'subsection': item,
- 'context_course': course,
- 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
- 'lms_link': lms_link,
- 'preview_link': preview_link,
- 'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
- 'parent_location': course.location,
- 'parent_item': parent,
- 'policy_metadata': policy_metadata,
- 'subsection_units': subsection_units,
- 'can_view_live': can_view_live
- })
+ {'subsection': item,
+ 'context_course': course,
+ 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
+ 'lms_link': lms_link,
+ 'preview_link': preview_link,
+ 'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
+ 'parent_location': course.location,
+ 'parent_item': parent,
+ 'policy_metadata': policy_metadata,
+ 'subsection_units': subsection_units,
+ 'can_view_live': can_view_live
+ })
@login_required
@@ -798,9 +791,7 @@ def upload_asset(request, org, course, coursename):
return HttpResponseBadRequest()
# construct a location from the passed in path
- location = ['i4x', org, course, 'course', coursename]
- if not has_access(request.user, location):
- return HttpResponseForbidden()
+ location = get_location_and_verify_access(request, org, course, coursename)
# Does the course actually exist?!? Get anything from it to prove its existance
@@ -952,11 +943,7 @@ def landing(request, org, course, coursename):
@ensure_csrf_cookie
def static_pages(request, org, course, coursename):
- location = ['i4x', org, course, 'course', coursename]
-
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
+ location = get_location_and_verify_access(request, org, course, coursename)
course = modulestore().get_item(location)
@@ -1068,11 +1055,7 @@ def course_info(request, org, course, name, provided_id=None):
org, course, name: Attributes of the Location for the item to edit
"""
- location = ['i4x', org, course, 'course', name]
-
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
+ location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
@@ -1111,11 +1094,7 @@ def course_info_updates(request, org, course, provided_id=None):
if not has_access(request.user, location):
raise PermissionDenied()
- # NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
- if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
- real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
- else:
- real_method = request.method
+ real_method = get_request_method(request)
if request.method == 'GET':
return HttpResponse(json.dumps(get_course_updates(location)),
@@ -1146,11 +1125,7 @@ def module_info(request, module_location):
if not has_access(request.user, location):
raise PermissionDenied()
- # NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
- if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
- real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
- else:
- real_method = request.method
+ real_method = get_request_method(request)
rewrite_static_links = request.GET.get('rewrite_url_links', 'True') in ['True', 'true']
logging.debug('rewrite_static_links = {0} {1}'.format(request.GET.get('rewrite_url_links', 'False'), rewrite_static_links))
@@ -1175,11 +1150,7 @@ def get_course_settings(request, org, course, name):
org, course, name: Attributes of the Location for the item to edit
"""
- location = ['i4x', org, course, 'course', name]
-
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
+ location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
@@ -1202,11 +1173,7 @@ def course_config_graders_page(request, org, course, name):
org, course, name: Attributes of the Location for the item to edit
"""
- location = ['i4x', org, course, 'course', name]
-
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
+ location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
course_details = CourseGradingModel.fetch(location)
@@ -1226,11 +1193,7 @@ def course_config_advanced_page(request, org, course, name):
org, course, name: Attributes of the Location for the item to edit
"""
- location = ['i4x', org, course, 'course', name]
-
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
+ location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
@@ -1252,11 +1215,7 @@ def course_settings_updates(request, org, course, name, section):
org, course: Attributes of the Location for the item to edit
section: one of details, faculty, grading, problems, discussions
"""
- location = ['i4x', org, course, 'course', name]
-
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
+ get_location_and_verify_access(request, org, course, name)
if section == 'details':
manager = CourseDetails
@@ -1284,27 +1243,20 @@ def course_grader_updates(request, org, course, name, grader_index=None):
org, course: Attributes of the Location for the item to edit
"""
- location = ['i4x', org, course, 'course', name]
+ location = get_location_and_verify_access(request, org, course, name)
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
-
- if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
- real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
- else:
- real_method = request.method
+ real_method = get_request_method(request)
if real_method == 'GET':
# Cannot just do a get w/o knowing the course name :-(
- return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(['i4x', org, course, 'course', name]), grader_index)),
+ return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(location), grader_index)),
mimetype="application/json")
elif real_method == "DELETE":
- # ??? Shoudl this return anything? Perhaps success fail?
- CourseGradingModel.delete_grader(Location(['i4x', org, course, 'course', name]), grader_index)
+ # ??? Should this return anything? Perhaps success fail?
+ CourseGradingModel.delete_grader(Location(location), grader_index)
return HttpResponse()
- elif request.method == 'POST': # post or put, doesn't matter.
- return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(['i4x', org, course, 'course', name]), request.POST)),
+ elif request.method == 'POST': # post or put, doesn't matter.
+ return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(location), request.POST)),
mimetype="application/json")
@@ -1318,18 +1270,10 @@ def course_advanced_updates(request, org, course, name):
org, course: Attributes of the Location for the item to edit
"""
- location = ['i4x', org, course, 'course', name]
-
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
-
- # NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
- if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
- real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
- else:
- real_method = request.method
+ location = get_location_and_verify_access(request, org, course, name)
+ real_method = get_request_method(request)
+
if real_method == 'GET':
return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json")
elif real_method == 'DELETE':
@@ -1339,6 +1283,95 @@ def course_advanced_updates(request, org, course, name):
return HttpResponse(json.dumps(CourseMetadata.update_from_json(location, json.loads(request.body))), mimetype="application/json")
+@ensure_csrf_cookie
+@login_required
+def get_checklists(request, org, course, name):
+ """
+ Send models, views, and html for displaying the course checklists.
+
+ org, course, name: Attributes of the Location for the item to edit
+ """
+ location = get_location_and_verify_access(request, org, course, name)
+
+ modulestore = get_modulestore(location)
+ course_module = modulestore.get_item(location)
+ new_course_template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
+ template_module = modulestore.get_item(new_course_template)
+
+ # If course was created before checklists were introduced, copy them over from the template.
+ copied = False
+ if not course_module.checklists:
+ course_module.checklists = template_module.checklists
+ copied = True
+
+ checklists, modified = expand_checklist_action_urls(course_module)
+ if copied or modified:
+ modulestore.update_metadata(location, own_metadata(course_module))
+ return render_to_response('checklists.html',
+ {
+ 'context_course': course_module,
+ 'checklists': checklists
+ })
+
+
+@ensure_csrf_cookie
+@login_required
+def update_checklist(request, org, course, name, checklist_index=None):
+ """
+ restful CRUD operations on course checklists. The payload is a json rep of
+ the modified checklist. For PUT or POST requests, the index of the
+ checklist being modified must be included; the returned payload will
+ be just that one checklist. For GET requests, the returned payload
+ is a json representation of the list of all checklists.
+
+ org, course, name: Attributes of the Location for the item to edit
+ """
+ location = get_location_and_verify_access(request, org, course, name)
+ modulestore = get_modulestore(location)
+ course_module = modulestore.get_item(location)
+
+ real_method = get_request_method(request)
+ if real_method == 'POST' or real_method == 'PUT':
+ if checklist_index is not None and 0 <= int(checklist_index) < len(course_module.checklists):
+ index = int(checklist_index)
+ course_module.checklists[index] = json.loads(request.body)
+ checklists, modified = expand_checklist_action_urls(course_module)
+ modulestore.update_metadata(location, own_metadata(course_module))
+ return HttpResponse(json.dumps(checklists[index]), mimetype="application/json")
+ else:
+ return HttpResponseBadRequest(
+ "Could not save checklist state because the checklist index was out of range or unspecified.",
+ content_type="text/plain")
+ elif request.method == 'GET':
+ # In the JavaScript view initialize method, we do a fetch to get all the checklists.
+ checklists, modified = expand_checklist_action_urls(course_module)
+ if modified:
+ modulestore.update_metadata(location, own_metadata(course_module))
+ return HttpResponse(json.dumps(checklists), mimetype="application/json")
+ else:
+ return HttpResponseBadRequest("Unsupported request.", content_type="text/plain")
+
+
+def expand_checklist_action_urls(course_module):
+ """
+ Gets the checklists out of the course module and expands their action urls
+ if they have not yet been expanded.
+
+ Returns the checklists with modified urls, as well as a boolean
+ indicating whether or not the checklists were modified.
+ """
+ checklists = course_module.checklists
+ modified = False
+ for checklist in checklists:
+ if not checklist.get('action_urls_expanded', False):
+ for item in checklist.get('items'):
+ item['action_url'] = get_url_reverse(item.get('action_url'), course_module)
+ checklist['action_urls_expanded'] = True
+ modified = True
+
+ return checklists, modified
+
+
@login_required
@ensure_csrf_cookie
def asset_index(request, org, course, name):
@@ -1347,18 +1380,13 @@ def asset_index(request, org, course, name):
org, course, name: Attributes of the Location for the item to edit
"""
- location = ['i4x', org, course, 'course', name]
-
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
-
+ location = get_location_and_verify_access(request, org, course, name)
upload_asset_callback_url = reverse('upload_asset', kwargs={
- 'org': org,
- 'course': course,
- 'coursename': name
- })
+ 'org': org,
+ 'course': course,
+ 'coursename': name
+ })
course_module = modulestore().get_item(location)
@@ -1474,11 +1502,7 @@ def initialize_course_tabs(course):
@login_required
def import_course(request, org, course, name):
- location = ['i4x', org, course, 'course', name]
-
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
+ location = get_location_and_verify_access(request, org, course, name)
if request.method == 'POST':
filename = request.FILES['course-data'].name
@@ -1541,20 +1565,14 @@ def import_course(request, org, course, name):
return render_to_response('import.html', {
'context_course': course_module,
'active_tab': 'import',
- 'successful_import_redirect_url': reverse('course_index', args=[
- course_module.location.org,
- course_module.location.course,
- course_module.location.name])
+ 'successful_import_redirect_url': get_url_reverse('CourseOutline', course_module)
})
@ensure_csrf_cookie
@login_required
def generate_export_course(request, org, course, name):
- location = ['i4x', org, course, 'course', name]
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
+ location = get_location_and_verify_access(request, org, course, name)
loc = Location(location)
export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
@@ -1587,11 +1605,9 @@ def generate_export_course(request, org, course, name):
@login_required
def export_course(request, org, course, name):
- location = ['i4x', org, course, 'course', name]
+ location = get_location_and_verify_access(request, org, course, name)
+
course_module = modulestore().get_item(location)
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
return render_to_response('export.html', {
'context_course': course_module,
@@ -1614,3 +1630,31 @@ def render_404(request):
def render_500(request):
return HttpResponseServerError(render_to_string('500.html', {}))
+
+
+def get_location_and_verify_access(request, org, course, name):
+ """
+ Create the location tuple verify that the user has permissions
+ to view the location. Returns the location.
+ """
+ location = ['i4x', org, course, 'course', name]
+
+ # check that logged in user has permissions to this item
+ if not has_access(request.user, location):
+ raise PermissionDenied()
+
+ return location
+
+
+def get_request_method(request):
+ """
+ Using HTTP_X_HTTP_METHOD_OVERRIDE, in the request metadata, determine
+ what type of request came from the client, and return it.
+ """
+ # NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
+ if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
+ real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
+ else:
+ real_method = request.method
+
+ return real_method
diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py
index ed11a6d7a4..63025d8abe 100644
--- a/cms/djangoapps/models/settings/course_metadata.py
+++ b/cms/djangoapps/models/settings/course_metadata.py
@@ -10,7 +10,7 @@ class CourseMetadata(object):
For CRUD operations on metadata fields which do not have specific editors on the other pages including any user generated ones.
The objects have no predefined attrs but instead are obj encodings of the editable metadata.
'''
- FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod']
+ FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod', 'checklists']
@classmethod
def fetch(cls, course_location):
diff --git a/cms/static/client_templates/checklist.html b/cms/static/client_templates/checklist.html
new file mode 100644
index 0000000000..ec6ff4e892
--- /dev/null
+++ b/cms/static/client_templates/checklist.html
@@ -0,0 +1,61 @@
+<% var allChecked = itemsChecked == items.length; %>
+
+ ▾
+ <%= checklistShortDescription %>
+
+ Tasks Completed: <%= itemsChecked %>/<%= items.length %>
+ ✓
+
+
+ <% var taskIndex = 0; %>
+ <% _.each(items, function(item) { %>
+ <% var checked = item['is_checked']; %>
+
+