Merge pull request #1147 from edx/nick/tab-edit
Create management command to edit course's tabs
This commit is contained in:
@@ -9,6 +9,9 @@ LMS: Add PaidCourseRegistration mode, where payment is required before course re
|
||||
|
||||
LMS: Add split testing functionality for internal use.
|
||||
|
||||
CMS: Add edit_course_tabs management command, providing a primitive
|
||||
editing capability for a course's list of tabs.
|
||||
|
||||
Studio and LMS: add ability to lock assets (cannot be viewed unless registered for class).
|
||||
|
||||
LMS: Improved accessibility of parts of forum navigation sidebar.
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
###
|
||||
### Script for editing the course's tabs
|
||||
###
|
||||
|
||||
#
|
||||
# Run it this way:
|
||||
# ./manage.py cms --settings dev edit_course_tabs --course Stanford/CS99/2013_spring
|
||||
# Or via rake:
|
||||
# rake django-admin[edit_course_tabs,cms,dev,"--course Stanford/CS99/2013_spring --delete 4"]
|
||||
#
|
||||
from optparse import make_option
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from .prompt import query_yes_no
|
||||
|
||||
from courseware.courses import get_course_by_id
|
||||
|
||||
from contentstore.views import tabs
|
||||
|
||||
|
||||
def print_course(course):
|
||||
"Prints out the course id and a numbered list of tabs."
|
||||
print course.id
|
||||
for index, item in enumerate(course.tabs):
|
||||
print index + 1, '"' + item.get('type') + '"', '"' + item.get('name', '') + '"'
|
||||
|
||||
|
||||
# course.tabs looks like this
|
||||
# [{u'type': u'courseware'}, {u'type': u'course_info', u'name': u'Course Info'}, {u'type': u'textbooks'},
|
||||
# {u'type': u'discussion', u'name': u'Discussion'}, {u'type': u'wiki', u'name': u'Wiki'},
|
||||
# {u'type': u'progress', u'name': u'Progress'}]
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = """See and edit a course's tabs list.
|
||||
Only supports insertion and deletion. Move and
|
||||
rename etc. can be done with a delete
|
||||
followed by an insert.
|
||||
The tabs are numbered starting with 1.
|
||||
Tabs 1 and 2 cannot be changed, and tabs of type
|
||||
static_tab cannot be edited (use Studio for those).
|
||||
"""
|
||||
# Making these option objects separately, so can refer to their .help below
|
||||
course_option = make_option('--course',
|
||||
action='store',
|
||||
dest='course',
|
||||
default=False,
|
||||
help='--course <id> required, e.g. Stanford/CS99/2013_spring')
|
||||
delete_option = make_option('--delete',
|
||||
action='store_true',
|
||||
dest='delete',
|
||||
default=False,
|
||||
help='--delete <tab-number>')
|
||||
insert_option = make_option('--insert',
|
||||
action='store_true',
|
||||
dest='insert',
|
||||
default=False,
|
||||
help='--insert <tab-number> <type> <name>, e.g. 2 "course_info" "Course Info"')
|
||||
|
||||
option_list = BaseCommand.option_list + (course_option, delete_option, insert_option)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if not options['course']:
|
||||
raise CommandError(Command.course_option.help)
|
||||
|
||||
course = get_course_by_id(options['course'])
|
||||
|
||||
print 'Warning: this command directly edits the list of course tabs in mongo.'
|
||||
print 'Tabs before any changes:'
|
||||
print_course(course)
|
||||
|
||||
try:
|
||||
if options['delete']:
|
||||
if len(args) != 1:
|
||||
raise CommandError(Command.delete_option.help)
|
||||
num = int(args[0])
|
||||
if query_yes_no('Deleting tab {0} Confirm?'.format(num), default='no'):
|
||||
tabs.primitive_delete(course, num - 1) # -1 for 0-based indexing
|
||||
elif options['insert']:
|
||||
if len(args) != 3:
|
||||
raise CommandError(Command.insert_option.help)
|
||||
num = int(args[0])
|
||||
tab_type = args[1]
|
||||
name = args[2]
|
||||
if query_yes_no('Inserting tab {0} "{1}" "{2}" Confirm?'.format(num, tab_type, name), default='no'):
|
||||
tabs.primitive_insert(course, num - 1, tab_type, name) # -1 as above
|
||||
except ValueError as e:
|
||||
# Cute: translate to CommandError so the CLI error prints nicely.
|
||||
raise CommandError(e)
|
||||
41
cms/djangoapps/contentstore/tests/test_tabs.py
Normal file
41
cms/djangoapps/contentstore/tests/test_tabs.py
Normal file
@@ -0,0 +1,41 @@
|
||||
""" Tests for tab functions (just primitive). """
|
||||
|
||||
from contentstore.views import tabs
|
||||
from django.test import TestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from courseware.courses import get_course_by_id
|
||||
|
||||
|
||||
class PrimitiveTabEdit(TestCase):
|
||||
"""Tests for the primitive tab edit data manipulations"""
|
||||
|
||||
def test_delete(self):
|
||||
"""Test primitive tab deletion."""
|
||||
course = CourseFactory.create(org='edX', course='999')
|
||||
with self.assertRaises(ValueError):
|
||||
tabs.primitive_delete(course, 0)
|
||||
with self.assertRaises(ValueError):
|
||||
tabs.primitive_delete(course, 1)
|
||||
with self.assertRaises(IndexError):
|
||||
tabs.primitive_delete(course, 6)
|
||||
tabs.primitive_delete(course, 2)
|
||||
self.assertFalse({u'type': u'textbooks'} in course.tabs)
|
||||
# Check that discussion has shifted down
|
||||
self.assertEquals(course.tabs[2], {'type': 'discussion', 'name': 'Discussion'})
|
||||
|
||||
def test_insert(self):
|
||||
"""Test primitive tab insertion."""
|
||||
course = CourseFactory.create(org='edX', course='999')
|
||||
tabs.primitive_insert(course, 2, 'atype', 'aname')
|
||||
self.assertEquals(course.tabs[2], {'type': 'atype', 'name': 'aname'})
|
||||
with self.assertRaises(ValueError):
|
||||
tabs.primitive_insert(course, 0, 'atype', 'aname')
|
||||
with self.assertRaises(ValueError):
|
||||
tabs.primitive_insert(course, 3, 'static_tab', 'aname')
|
||||
|
||||
def test_save(self):
|
||||
"""Test course saving."""
|
||||
course = CourseFactory.create(org='edX', course='999')
|
||||
tabs.primitive_insert(course, 3, 'atype', 'aname')
|
||||
course2 = get_course_by_id(course.id)
|
||||
self.assertEquals(course2.tabs[3], {'type': 'atype', 'name': 'aname'})
|
||||
@@ -9,13 +9,14 @@ from django.contrib.auth.decorators import login_required
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
from ..utils import get_course_for_item, get_modulestore
|
||||
from .access import get_location_and_verify_access
|
||||
|
||||
|
||||
__all__ = ['edit_tabs', 'reorder_static_tabs', 'static_pages']
|
||||
|
||||
|
||||
@@ -84,6 +85,7 @@ def reorder_static_tabs(request):
|
||||
# MongoKeyValueStore before we update the mongo datastore.
|
||||
course.save()
|
||||
modulestore('direct').update_metadata(course.location, own_metadata(course))
|
||||
# TODO: above two lines are used for the primitive-save case. Maybe factor them out?
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
@@ -136,3 +138,43 @@ def static_pages(request, org, course, coursename):
|
||||
return render_to_response('static-pages.html', {
|
||||
'context_course': course,
|
||||
})
|
||||
|
||||
|
||||
# "primitive" tab edit functions driven by the command line.
|
||||
# These should be replaced/deleted by a more capable GUI someday.
|
||||
# Note that the command line UI identifies the tabs with 1-based
|
||||
# indexing, but this implementation code is standard 0-based.
|
||||
|
||||
def validate_args(num, tab_type):
|
||||
"Throws for the disallowed cases."
|
||||
if num <= 1:
|
||||
raise ValueError('Tabs 1 and 2 cannot be edited')
|
||||
if tab_type == 'static_tab':
|
||||
raise ValueError('Tabs of type static_tab cannot be edited here (use Studio)')
|
||||
|
||||
|
||||
def primitive_delete(course, num):
|
||||
"Deletes the given tab number (0 based)."
|
||||
tabs = course.tabs
|
||||
validate_args(num, tabs[num].get('type', ''))
|
||||
del tabs[num]
|
||||
# Note for future implementations: if you delete a static_tab, then Chris Dodge
|
||||
# points out that there's other stuff to delete beyond this element.
|
||||
# This code happens to not delete static_tab so it doesn't come up.
|
||||
primitive_save(course)
|
||||
|
||||
|
||||
def primitive_insert(course, num, tab_type, name):
|
||||
"Inserts a new tab at the given number (0 based)."
|
||||
validate_args(num, tab_type)
|
||||
new_tab = {u'type': unicode(tab_type), u'name': unicode(name)}
|
||||
tabs = course.tabs
|
||||
tabs.insert(num, new_tab)
|
||||
primitive_save(course)
|
||||
|
||||
|
||||
def primitive_save(course):
|
||||
"Saves the course back to modulestore."
|
||||
# This code copied from reorder_static_tabs above
|
||||
course.save()
|
||||
modulestore('direct').update_metadata(course.location, own_metadata(course))
|
||||
|
||||
Reference in New Issue
Block a user