Merge pull request #2905 from edx/opaque-keys
(WIP) Make course ids and Locations/Locators opaque to LMS/Studio
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
|
||||
import ConfigParser
|
||||
from django.conf import settings
|
||||
import logging
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
# pylint: disable=C0111
|
||||
# pylint: disable=W0621
|
||||
|
||||
import time
|
||||
import os
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_true, assert_in # pylint: disable=no-name-in-module
|
||||
from django.conf import settings
|
||||
|
||||
from student.roles import CourseRole, CourseStaffRole, CourseInstructorRole
|
||||
from student.roles import CourseStaffRole, CourseInstructorRole, GlobalStaff
|
||||
from student.models import get_user
|
||||
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
@@ -162,7 +161,7 @@ def add_course_author(user, course):
|
||||
"""
|
||||
global_admin = AdminFactory()
|
||||
for role in (CourseStaffRole, CourseInstructorRole):
|
||||
auth.add_users(global_admin, role(course.location), user)
|
||||
auth.add_users(global_admin, role(course.id), user)
|
||||
|
||||
|
||||
def create_a_course():
|
||||
@@ -380,18 +379,17 @@ def create_other_user(_step, name, has_extra_perms, role_name):
|
||||
user = create_studio_user(uname=name, password="test", email=email)
|
||||
if has_extra_perms:
|
||||
if role_name == "is_staff":
|
||||
user.is_staff = True
|
||||
user.save()
|
||||
GlobalStaff().add_users(user)
|
||||
else:
|
||||
if role_name == "admin":
|
||||
# admins get staff privileges, as well
|
||||
roles = (CourseStaffRole, CourseInstructorRole)
|
||||
else:
|
||||
roles = (CourseStaffRole,)
|
||||
location = world.scenario_dict["COURSE"].location
|
||||
course_key = world.scenario_dict["COURSE"].id
|
||||
global_admin = AdminFactory()
|
||||
for role in roles:
|
||||
auth.add_users(global_admin, role(location), user)
|
||||
auth.add_users(global_admin, role(course_key), user)
|
||||
|
||||
|
||||
@step('I log out')
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
from lettuce import world, step
|
||||
from component_settings_editor_helpers import enter_xml_in_advanced_problem
|
||||
from nose.tools import assert_true, assert_equal
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
from contentstore.utils import reverse_usage_url
|
||||
|
||||
|
||||
@step('I go to the export page$')
|
||||
@@ -49,4 +51,9 @@ def get_an_error_dialog(step):
|
||||
def i_click_on_error_dialog(step):
|
||||
world.click_link_by_text('Correct failed component')
|
||||
assert_true(world.css_html("span.inline-error").startswith("Problem i4x://MITx/999/problem"))
|
||||
assert_equal(1, world.browser.url.count("unit/MITx.999.Robot_Super_Course/branch/draft/block/vertical"))
|
||||
course_key = SlashSeparatedCourseKey("MITx", "999", "Robot_Super_Course")
|
||||
# we don't know the actual ID of the vertical. So just check that we did go to a
|
||||
# vertical page in the course (there should only be one).
|
||||
vertical_usage_key = course_key.make_usage_key("vertical", "")
|
||||
vertical_url = reverse_usage_url('unit_handler', vertical_usage_key)
|
||||
assert_equal(1, world.browser.url.count(vertical_url))
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
from terrain.steps import reload_the_page
|
||||
from selenium.common.exceptions import (
|
||||
InvalidElementStateException, WebDriverException)
|
||||
from selenium.common.exceptions import InvalidElementStateException
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
from contentstore.utils import reverse_course_url
|
||||
from nose.tools import assert_in, assert_not_in, assert_equal, assert_not_equal # pylint: disable=E0611
|
||||
|
||||
|
||||
@@ -68,11 +69,12 @@ def change_assignment_name(step, old_name, new_name):
|
||||
@step(u'I go back to the main course page')
|
||||
def main_course_page(step):
|
||||
course_name = world.scenario_dict['COURSE'].display_name.replace(' ', '_')
|
||||
main_page_link = '/course/{org}.{number}.{name}/branch/draft/block/{name}'.format(
|
||||
org=world.scenario_dict['COURSE'].org,
|
||||
number=world.scenario_dict['COURSE'].number,
|
||||
name=course_name
|
||||
course_key = SlashSeparatedCourseKey(
|
||||
world.scenario_dict['COURSE'].org,
|
||||
world.scenario_dict['COURSE'].number,
|
||||
course_name
|
||||
)
|
||||
main_page_link = reverse_course_url('course_handler', course_key)
|
||||
|
||||
world.visit(main_page_link)
|
||||
assert_in('Course Outline', world.css_text('h1.page-header'))
|
||||
|
||||
@@ -14,11 +14,11 @@ Feature: CMS.Sign in
|
||||
Scenario: Login with a valid redirect
|
||||
Given I have opened a new course in Studio
|
||||
And I am not logged in
|
||||
And I visit the url "/course/MITx.999.Robot_Super_Course/branch/draft/block/Robot_Super_Course"
|
||||
And I should see that the path is "/signin?next=/course/MITx.999.Robot_Super_Course/branch/draft/block/Robot_Super_Course"
|
||||
And I visit the url "/course/slashes:MITx+999+Robot_Super_Course"
|
||||
And I should see that the path is "/signin?next=/course/slashes%3AMITx%2B999%2BRobot_Super_Course"
|
||||
When I fill in and submit the signin form
|
||||
And I wait for "2" seconds
|
||||
Then I should see that the path is "/course/MITx.999.Robot_Super_Course/branch/draft/block/Robot_Super_Course"
|
||||
Then I should see that the path is "/course/slashes:MITx+999+Robot_Super_Course"
|
||||
|
||||
Scenario: Login with an invalid redirect
|
||||
Given I have opened a new course in Studio
|
||||
@@ -26,4 +26,4 @@ Feature: CMS.Sign in
|
||||
And I visit the url "/signin?next=http://www.google.com/"
|
||||
When I fill in and submit the signin form
|
||||
And I wait for "2" seconds
|
||||
Then I should see that the path is "/course"
|
||||
Then I should see that the path is "/course/"
|
||||
|
||||
@@ -166,8 +166,7 @@ def remove_transcripts_from_store(_step, subs_id):
|
||||
"""Remove from store, if transcripts content exists."""
|
||||
filename = 'subs_{0}.srt.sjson'.format(subs_id.strip())
|
||||
content_location = StaticContent.compute_location(
|
||||
world.scenario_dict['COURSE'].org,
|
||||
world.scenario_dict['COURSE'].number,
|
||||
world.scenario_dict['COURSE'].id,
|
||||
filename
|
||||
)
|
||||
try:
|
||||
|
||||
@@ -154,7 +154,7 @@ def user_foo_is_enrolled_in_the_course(step, name):
|
||||
world.create_user(name, 'test')
|
||||
user = User.objects.get(username=name)
|
||||
|
||||
course_id = world.scenario_dict['COURSE'].location.course_id
|
||||
course_id = world.scenario_dict['COURSE'].id
|
||||
CourseEnrollment.enroll(user, course_id)
|
||||
|
||||
|
||||
|
||||
@@ -140,10 +140,10 @@ def xml_only_video(step):
|
||||
# Wait for the new unit to be created and to load the page
|
||||
world.wait(1)
|
||||
|
||||
location = world.scenario_dict['COURSE'].location
|
||||
store = get_modulestore(location)
|
||||
course = world.scenario_dict['COURSE']
|
||||
store = get_modulestore(course.location)
|
||||
|
||||
parent_location = store.get_items(Location(category='vertical', revision='draft'))[0].location
|
||||
parent_location = store.get_items(course.id, category='vertical', revision='draft')[0].location
|
||||
|
||||
youtube_id = 'ABCDEFG'
|
||||
world.scenario_dict['YOUTUBE_ID'] = youtube_id
|
||||
|
||||
@@ -14,7 +14,6 @@ from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.xml_exporter import export_to_xml
|
||||
|
||||
@@ -64,13 +63,10 @@ def cmd_log(cmd, cwd):
|
||||
return output
|
||||
|
||||
|
||||
def export_to_git(course_loc, repo, user='', rdir=None):
|
||||
def export_to_git(course_id, repo, user='', rdir=None):
|
||||
"""Export a course to git."""
|
||||
# pylint: disable=R0915
|
||||
|
||||
if course_loc.startswith('i4x://'):
|
||||
course_loc = course_loc[6:]
|
||||
|
||||
if not GIT_REPO_EXPORT_DIR:
|
||||
raise GitExportError(GitExportError.NO_EXPORT_DIR)
|
||||
|
||||
@@ -129,15 +125,10 @@ def export_to_git(course_loc, repo, user='', rdir=None):
|
||||
raise GitExportError(GitExportError.CANNOT_PULL)
|
||||
|
||||
# export course as xml before commiting and pushing
|
||||
try:
|
||||
location = CourseDescriptor.id_to_location(course_loc)
|
||||
except ValueError:
|
||||
raise GitExportError(GitExportError.BAD_COURSE)
|
||||
|
||||
root_dir = os.path.dirname(rdirp)
|
||||
course_dir = os.path.splitext(os.path.basename(rdirp))[0]
|
||||
try:
|
||||
export_to_xml(modulestore('direct'), contentstore(), location,
|
||||
export_to_xml(modulestore('direct'), contentstore(), course_id,
|
||||
root_dir, course_dir, modulestore())
|
||||
except (EnvironmentError, AttributeError):
|
||||
log.exception('Failed export to xml')
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.xml_importer import check_module_metadata_editability
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.keys import CourseKey
|
||||
from opaque_keys import InvalidKeyError
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -10,14 +11,16 @@ class Command(BaseCommand):
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if len(args) != 1:
|
||||
raise CommandError("check_course requires one argument: <location>")
|
||||
raise CommandError("check_course requires one argument: <course_id>")
|
||||
|
||||
loc_str = args[0]
|
||||
try:
|
||||
course_key = CourseKey.from_string(args[0])
|
||||
except InvalidKeyError:
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(args[0])
|
||||
|
||||
loc = CourseDescriptor.id_to_location(loc_str)
|
||||
store = modulestore()
|
||||
|
||||
course = store.get_item(loc, depth=3)
|
||||
course = store.get_course(course_key, depth=3)
|
||||
|
||||
err_cnt = 0
|
||||
|
||||
@@ -33,7 +36,7 @@ class Command(BaseCommand):
|
||||
def _check_xml_attributes_field(module):
|
||||
err_cnt = 0
|
||||
if hasattr(module, 'xml_attributes') and isinstance(module.xml_attributes, basestring):
|
||||
print 'module = {0} has xml_attributes as a string. It should be a dict'.format(module.location.url())
|
||||
print 'module = {0} has xml_attributes as a string. It should be a dict'.format(module.location)
|
||||
err_cnt = err_cnt + 1
|
||||
for child in module.get_children():
|
||||
err_cnt = err_cnt + _check_xml_attributes_field(child)
|
||||
@@ -45,7 +48,7 @@ class Command(BaseCommand):
|
||||
def _get_discussion_items(module):
|
||||
discussion_items = []
|
||||
if module.location.category == 'discussion':
|
||||
discussion_items = discussion_items + [module.location.url()]
|
||||
discussion_items = discussion_items + [module.location]
|
||||
|
||||
for child in module.get_children():
|
||||
discussion_items = discussion_items + _get_discussion_items(child)
|
||||
@@ -55,17 +58,8 @@ class Command(BaseCommand):
|
||||
discussion_items = _get_discussion_items(course)
|
||||
|
||||
# now query all discussion items via get_items() and compare with the tree-traversal
|
||||
queried_discussion_items = store.get_items(
|
||||
Location(
|
||||
'i4x',
|
||||
course.location.org,
|
||||
course.location.course,
|
||||
'discussion',
|
||||
None,
|
||||
None
|
||||
)
|
||||
)
|
||||
queried_discussion_items = store.get_items(course_key=course_key, category='discussion',)
|
||||
|
||||
for item in queried_discussion_items:
|
||||
if item.location.url() not in discussion_items:
|
||||
print 'Found dangling discussion module = {0}'.format(item.location.url())
|
||||
if item.location not in discussion_items:
|
||||
print 'Found dangling discussion module = {0}'.format(item.location)
|
||||
|
||||
@@ -5,9 +5,10 @@ from django.core.management.base import BaseCommand, CommandError
|
||||
from xmodule.modulestore.store_utilities import clone_course
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from student.roles import CourseInstructorRole, CourseStaffRole
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.keys import CourseKey
|
||||
from opaque_keys import InvalidKeyError
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
|
||||
#
|
||||
@@ -17,34 +18,36 @@ class Command(BaseCommand):
|
||||
"""Clone a MongoDB-backed course to another location"""
|
||||
help = 'Clone a MongoDB backed course to another location'
|
||||
|
||||
def course_key_from_arg(self, arg):
|
||||
"""
|
||||
Convert the command line arg into a course key
|
||||
"""
|
||||
try:
|
||||
return CourseKey.from_string(arg)
|
||||
except InvalidKeyError:
|
||||
return SlashSeparatedCourseKey.from_deprecated_string(arg)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"Execute the command"
|
||||
if len(args) != 2:
|
||||
raise CommandError("clone requires 2 arguments: <source-course_id> <dest-course_id>")
|
||||
|
||||
source_course_id = args[0]
|
||||
dest_course_id = args[1]
|
||||
source_course_id = self.course_key_from_arg(args[0])
|
||||
dest_course_id = self.course_key_from_arg(args[1])
|
||||
|
||||
mstore = modulestore('direct')
|
||||
cstore = contentstore()
|
||||
|
||||
course_id_dict = Location.parse_course_id(dest_course_id)
|
||||
mstore.ignore_write_events_on_courses.append('{org}/{course}'.format(**course_id_dict))
|
||||
mstore.ignore_write_events_on_courses.add(dest_course_id)
|
||||
|
||||
print("Cloning course {0} to {1}".format(source_course_id, dest_course_id))
|
||||
|
||||
source_location = CourseDescriptor.id_to_location(source_course_id)
|
||||
dest_location = CourseDescriptor.id_to_location(dest_course_id)
|
||||
|
||||
if clone_course(mstore, cstore, source_location, dest_location):
|
||||
# be sure to recompute metadata inheritance after all those updates
|
||||
mstore.refresh_cached_metadata_inheritance_tree(dest_location)
|
||||
|
||||
if clone_course(mstore, cstore, source_course_id, dest_course_id):
|
||||
print("copying User permissions...")
|
||||
# purposely avoids auth.add_user b/c it doesn't have a caller to authorize
|
||||
CourseInstructorRole(dest_location).add_users(
|
||||
*CourseInstructorRole(source_location).users_with_role()
|
||||
CourseInstructorRole(dest_course_id).add_users(
|
||||
*CourseInstructorRole(source_course_id).users_with_role()
|
||||
)
|
||||
CourseStaffRole(dest_location).add_users(
|
||||
*CourseStaffRole(source_location).users_with_role()
|
||||
CourseStaffRole(dest_course_id).add_users(
|
||||
*CourseStaffRole(source_course_id).users_with_role()
|
||||
)
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from .prompt import query_yes_no
|
||||
from contentstore.utils import delete_course_and_groups
|
||||
from xmodule.modulestore.keys import CourseKey
|
||||
from opaque_keys import InvalidKeyError
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -11,9 +14,12 @@ class Command(BaseCommand):
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if len(args) != 1 and len(args) != 2:
|
||||
raise CommandError("delete_course requires one or more arguments: <location> |commit|")
|
||||
raise CommandError("delete_course requires one or more arguments: <course_id> |commit|")
|
||||
|
||||
course_id = args[0]
|
||||
try:
|
||||
course_key = CourseKey.from_string(args[0])
|
||||
except InvalidKeyError:
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(args[0])
|
||||
|
||||
commit = False
|
||||
if len(args) == 2:
|
||||
@@ -22,6 +28,6 @@ class Command(BaseCommand):
|
||||
if commit:
|
||||
print('Actually going to delete the course from DB....')
|
||||
|
||||
if query_yes_no("Deleting course {0}. Confirm?".format(course_id), default="no"):
|
||||
if query_yes_no("Deleting course {0}. Confirm?".format(course_key), default="no"):
|
||||
if query_yes_no("Are you sure. This action cannot be undone!", default="no"):
|
||||
delete_course_and_groups(course_id, commit)
|
||||
delete_course_and_groups(course_key, commit)
|
||||
|
||||
@@ -13,6 +13,9 @@ from .prompt import query_yes_no
|
||||
from courseware.courses import get_course_by_id
|
||||
|
||||
from contentstore.views import tabs
|
||||
from opaque_keys import InvalidKeyError
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
from xmodule.modulestore.keys import CourseKey
|
||||
|
||||
|
||||
def print_course(course):
|
||||
@@ -64,7 +67,12 @@ command again, adding --insert or --delete to edit the list.
|
||||
if not options['course']:
|
||||
raise CommandError(Command.course_option.help)
|
||||
|
||||
course = get_course_by_id(options['course'])
|
||||
try:
|
||||
course_key = CourseKey.from_string(options['course'])
|
||||
except InvalidKeyError:
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(options['course'])
|
||||
|
||||
course = get_course_by_id(course_key)
|
||||
|
||||
print 'Warning: this command directly edits the list of course tabs in mongo.'
|
||||
print 'Tabs before any changes:'
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.contentstore.utils import empty_asset_trashcan
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.keys import CourseKey
|
||||
from .prompt import query_yes_no
|
||||
from opaque_keys import InvalidKeyError
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -10,16 +12,17 @@ class Command(BaseCommand):
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if len(args) != 1 and len(args) != 0:
|
||||
raise CommandError("empty_asset_trashcan requires one or no arguments: |<location>|")
|
||||
|
||||
locs = []
|
||||
raise CommandError("empty_asset_trashcan requires one or no arguments: |<course_id>|")
|
||||
|
||||
if len(args) == 1:
|
||||
locs.append(CourseDescriptor.id_to_location(args[0]))
|
||||
try:
|
||||
course_key = CourseKey.from_string(args[0])
|
||||
except InvalidKeyError:
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(args[0])
|
||||
|
||||
course_ids = [course_key]
|
||||
else:
|
||||
courses = modulestore('direct').get_courses()
|
||||
for course in courses:
|
||||
locs.append(course.location)
|
||||
course_ids = [course.id for course in modulestore('direct').get_courses()]
|
||||
|
||||
if query_yes_no("Emptying trashcan. Confirm?", default="no"):
|
||||
empty_asset_trashcan(locs)
|
||||
empty_asset_trashcan(course_ids)
|
||||
|
||||
@@ -6,8 +6,10 @@ import os
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from xmodule.modulestore.xml_exporter import export_to_xml
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.keys import CourseKey
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from opaque_keys import InvalidKeyError
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -19,16 +21,18 @@ class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
"Execute the command"
|
||||
if len(args) != 2:
|
||||
raise CommandError("export requires two arguments: <course location> <output path>")
|
||||
raise CommandError("export requires two arguments: <course id> <output path>")
|
||||
|
||||
try:
|
||||
course_key = CourseKey.from_string(args[0])
|
||||
except InvalidKeyError:
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(args[0])
|
||||
|
||||
course_id = args[0]
|
||||
output_path = args[1]
|
||||
|
||||
print("Exporting course id = {0} to {1}".format(course_id, output_path))
|
||||
|
||||
location = CourseDescriptor.id_to_location(course_id)
|
||||
print("Exporting course id = {0} to {1}".format(course_key, output_path))
|
||||
|
||||
root_dir = os.path.dirname(output_path)
|
||||
course_dir = os.path.splitext(os.path.basename(output_path))[0]
|
||||
|
||||
export_to_xml(modulestore('direct'), contentstore(), location, root_dir, course_dir, modulestore())
|
||||
export_to_xml(modulestore('direct'), contentstore(), course_key, root_dir, course_dir, modulestore())
|
||||
|
||||
@@ -5,7 +5,6 @@ from django.core.management.base import BaseCommand, CommandError
|
||||
from xmodule.modulestore.xml_exporter import export_to_xml
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -35,9 +34,8 @@ class Command(BaseCommand):
|
||||
|
||||
if 1:
|
||||
try:
|
||||
location = CourseDescriptor.id_to_location(course_id)
|
||||
course_dir = course_id.replace('/', '...')
|
||||
export_to_xml(ms, cs, location, root_dir, course_dir, modulestore())
|
||||
export_to_xml(ms, cs, course_id, root_dir, course_dir, modulestore())
|
||||
except Exception as err:
|
||||
print("="*30 + "> Oops, failed to export %s" % course_id)
|
||||
print("Error:")
|
||||
|
||||
@@ -20,6 +20,10 @@ from django.core.management.base import BaseCommand, CommandError
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
import contentstore.git_export_utils as git_export_utils
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
from opaque_keys import InvalidKeyError
|
||||
from contentstore.git_export_utils import GitExportError
|
||||
from xmodule.modulestore.keys import CourseKey
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -52,9 +56,17 @@ class Command(BaseCommand):
|
||||
'course_loc and git_url')
|
||||
|
||||
# Rethrow GitExportError as CommandError for SystemExit
|
||||
try:
|
||||
course_key = CourseKey.from_string(args[0])
|
||||
except InvalidKeyError:
|
||||
try:
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(args[0])
|
||||
except InvalidKeyError:
|
||||
raise CommandError(GitExportError.BAD_COURSE)
|
||||
|
||||
try:
|
||||
git_export_utils.export_to_git(
|
||||
args[0],
|
||||
course_key,
|
||||
args[1],
|
||||
options.get('user', ''),
|
||||
options.get('rdir', None)
|
||||
|
||||
@@ -47,11 +47,12 @@ class Command(BaseCommand):
|
||||
_, course_items = import_from_xml(
|
||||
mstore, data_dir, course_dirs, load_error_modules=False,
|
||||
static_content_store=contentstore(), verbose=True,
|
||||
do_import_static=do_import_static
|
||||
do_import_static=do_import_static,
|
||||
create_new_course=True,
|
||||
)
|
||||
|
||||
for module in course_items:
|
||||
course_id = module.location.course_id
|
||||
for course in course_items:
|
||||
course_id = course.id
|
||||
if not are_permissions_roles_seeded(course_id):
|
||||
self.stdout.write('Seeding forum roles for course {0}\n'.format(course_id))
|
||||
seed_permissions_roles(course_id)
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
"""
|
||||
Script for traversing all courses and add/modify mapping with 'lower_id' and 'lower_course_id'
|
||||
"""
|
||||
from django.core.management.base import BaseCommand
|
||||
from xmodule.modulestore.django import modulestore, loc_mapper
|
||||
|
||||
|
||||
#
|
||||
# To run from command line: ./manage.py cms --settings dev map_courses_location_lower
|
||||
#
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
Create or modify map entry for each course in 'loc_mapper' with 'lower_id' and 'lower_course_id'
|
||||
"""
|
||||
help = "Create or modify map entry for each course in 'loc_mapper' with 'lower_id' and 'lower_course_id'"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# get all courses
|
||||
courses = modulestore('direct').get_courses()
|
||||
for course in courses:
|
||||
# create/modify map_entry in 'loc_mapper' with 'lower_id' and 'lower_course_id'
|
||||
loc_mapper().create_map_entry(course.location)
|
||||
@@ -4,11 +4,12 @@ to the new split-Mongo modulestore.
|
||||
"""
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.contrib.auth.models import User
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.split_migrator import SplitMigrator
|
||||
from xmodule.modulestore import InvalidLocationError
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
from xmodule.modulestore.keys import CourseKey
|
||||
from opaque_keys import InvalidKeyError
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
|
||||
def user_from_str(identifier):
|
||||
@@ -30,24 +31,23 @@ class Command(BaseCommand):
|
||||
"Migrate a course from old-Mongo to split-Mongo"
|
||||
|
||||
help = "Migrate a course from old-Mongo to split-Mongo"
|
||||
args = "location email <locator>"
|
||||
args = "course_key email <new org> <new offering>"
|
||||
|
||||
def parse_args(self, *args):
|
||||
"""
|
||||
Return a three-tuple of (location, user, locator_string).
|
||||
If the user didn't specify a locator string, the third return value
|
||||
will be None.
|
||||
Return a 4-tuple of (course_key, user, org, offering).
|
||||
If the user didn't specify an org & offering, those will be None.
|
||||
"""
|
||||
if len(args) < 2:
|
||||
raise CommandError(
|
||||
"migrate_to_split requires at least two arguments: "
|
||||
"a location and a user identifier (email or ID)"
|
||||
"a course_key and a user identifier (email or ID)"
|
||||
)
|
||||
|
||||
try:
|
||||
location = Location(args[0])
|
||||
except InvalidLocationError:
|
||||
raise CommandError("Invalid location string {}".format(args[0]))
|
||||
course_key = CourseKey.from_string(args[0])
|
||||
except InvalidKeyError:
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(args[0])
|
||||
|
||||
try:
|
||||
user = user_from_str(args[1])
|
||||
@@ -55,14 +55,15 @@ class Command(BaseCommand):
|
||||
raise CommandError("No user found identified by {}".format(args[1]))
|
||||
|
||||
try:
|
||||
package_id = args[2]
|
||||
org = args[2]
|
||||
offering = args[3]
|
||||
except IndexError:
|
||||
package_id = None
|
||||
org = offering = None
|
||||
|
||||
return location, user, package_id
|
||||
return course_key, user, org, offering
|
||||
|
||||
def handle(self, *args, **options):
|
||||
location, user, package_id = self.parse_args(*args)
|
||||
course_key, user, org, offering = self.parse_args(*args)
|
||||
|
||||
migrator = SplitMigrator(
|
||||
draft_modulestore=modulestore('default'),
|
||||
@@ -71,4 +72,4 @@ class Command(BaseCommand):
|
||||
loc_mapper=loc_mapper(),
|
||||
)
|
||||
|
||||
migrator.migrate_mongo_course(location, user, package_id)
|
||||
migrator.migrate_mongo_course(course_key, user, org, offering)
|
||||
|
||||
@@ -4,7 +4,7 @@ is to delete the course from the split mongo datastore.
|
||||
"""
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from xmodule.modulestore.django import modulestore, loc_mapper
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, InsufficientSpecificationError
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore.locator import CourseLocator
|
||||
|
||||
|
||||
@@ -12,18 +12,18 @@ class Command(BaseCommand):
|
||||
"Rollback a course that was migrated to the split Mongo datastore"
|
||||
|
||||
help = "Rollback a course that was migrated to the split Mongo datastore"
|
||||
args = "locator"
|
||||
args = "org offering"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if len(args) < 1:
|
||||
if len(args) < 2:
|
||||
raise CommandError(
|
||||
"rollback_split_course requires at least one argument (locator)"
|
||||
"rollback_split_course requires 2 arguments (org offering)"
|
||||
)
|
||||
|
||||
try:
|
||||
locator = CourseLocator(url=args[0])
|
||||
locator = CourseLocator(org=args[0], offering=args[1])
|
||||
except ValueError:
|
||||
raise CommandError("Invalid locator string {}".format(args[0]))
|
||||
raise CommandError("Invalid org or offering string {}, {}".format(*args))
|
||||
|
||||
location = loc_mapper().translate_locator_to_location(locator, get_course=True)
|
||||
if not location:
|
||||
@@ -41,7 +41,7 @@ class Command(BaseCommand):
|
||||
)
|
||||
|
||||
try:
|
||||
modulestore('split').delete_course(locator.package_id)
|
||||
modulestore('split').delete_course(locator)
|
||||
except ItemNotFoundError:
|
||||
raise CommandError("No course found with locator {}".format(locator))
|
||||
|
||||
|
||||
@@ -15,19 +15,19 @@ class ClashIdTestCase(TestCase):
|
||||
expected = []
|
||||
# clashing courses
|
||||
course = CourseFactory.create(org="test", course="courseid", display_name="run1")
|
||||
expected.append(course.location.course_id)
|
||||
expected.append(course.id)
|
||||
course = CourseFactory.create(org="TEST", course="courseid", display_name="RUN12")
|
||||
expected.append(course.location.course_id)
|
||||
expected.append(course.id)
|
||||
course = CourseFactory.create(org="test", course="CourseId", display_name="aRUN123")
|
||||
expected.append(course.location.course_id)
|
||||
expected.append(course.id)
|
||||
# not clashing courses
|
||||
not_expected = []
|
||||
course = CourseFactory.create(org="test", course="course2", display_name="run1")
|
||||
not_expected.append(course.location.course_id)
|
||||
not_expected.append(course.id)
|
||||
course = CourseFactory.create(org="test1", course="courseid", display_name="run1")
|
||||
not_expected.append(course.location.course_id)
|
||||
not_expected.append(course.id)
|
||||
course = CourseFactory.create(org="test", course="courseid0", display_name="run1")
|
||||
not_expected.append(course.location.course_id)
|
||||
not_expected.append(course.id)
|
||||
|
||||
old_stdout = sys.stdout
|
||||
sys.stdout = mystdout = StringIO()
|
||||
@@ -35,6 +35,6 @@ class ClashIdTestCase(TestCase):
|
||||
sys.stdout = old_stdout
|
||||
result = mystdout.getvalue()
|
||||
for courseid in expected:
|
||||
self.assertIn(courseid, result)
|
||||
self.assertIn(courseid.to_deprecated_string(), result)
|
||||
for courseid in not_expected:
|
||||
self.assertNotIn(courseid, result)
|
||||
self.assertNotIn(courseid.to_deprecated_string(), result)
|
||||
|
||||
@@ -18,6 +18,7 @@ from django.test.utils import override_settings
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
import contentstore.git_export_utils as git_export_utils
|
||||
from contentstore.git_export_utils import GitExportError
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
FEATURES_WITH_EXPORT_GIT = settings.FEATURES.copy()
|
||||
FEATURES_WITH_EXPORT_GIT['ENABLE_EXPORT_GIT'] = True
|
||||
@@ -52,7 +53,7 @@ class TestGitExport(CourseTestCase):
|
||||
|
||||
def test_command(self):
|
||||
"""
|
||||
Test that the command interface works. Ignore stderr fo clean
|
||||
Test that the command interface works. Ignore stderr for clean
|
||||
test output.
|
||||
"""
|
||||
with self.assertRaises(SystemExit) as ex:
|
||||
@@ -69,7 +70,13 @@ class TestGitExport(CourseTestCase):
|
||||
# Send bad url to get course not exported
|
||||
with self.assertRaises(SystemExit) as ex:
|
||||
with self.assertRaisesRegexp(CommandError, GitExportError.URL_BAD):
|
||||
call_command('git_export', 'foo', 'silly',
|
||||
call_command('git_export', 'foo/bar/baz', 'silly',
|
||||
stderr=StringIO.StringIO())
|
||||
self.assertEqual(ex.exception.code, 1)
|
||||
# Send bad course_id to get course not exported
|
||||
with self.assertRaises(SystemExit) as ex:
|
||||
with self.assertRaisesRegexp(CommandError, GitExportError.BAD_COURSE):
|
||||
call_command('git_export', 'foo/bar:baz', 'silly',
|
||||
stderr=StringIO.StringIO())
|
||||
self.assertEqual(ex.exception.code, 1)
|
||||
|
||||
@@ -77,15 +84,16 @@ class TestGitExport(CourseTestCase):
|
||||
"""
|
||||
Test several bad URLs for validation
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey('org', 'course', 'run')
|
||||
with self.assertRaisesRegexp(GitExportError, str(GitExportError.URL_BAD)):
|
||||
git_export_utils.export_to_git('', 'Sillyness')
|
||||
git_export_utils.export_to_git(course_key, 'Sillyness')
|
||||
|
||||
with self.assertRaisesRegexp(GitExportError, str(GitExportError.URL_BAD)):
|
||||
git_export_utils.export_to_git('', 'example.com:edx/notreal')
|
||||
git_export_utils.export_to_git(course_key, 'example.com:edx/notreal')
|
||||
|
||||
with self.assertRaisesRegexp(GitExportError,
|
||||
str(GitExportError.URL_NO_AUTH)):
|
||||
git_export_utils.export_to_git('', 'http://blah')
|
||||
git_export_utils.export_to_git(course_key, 'http://blah')
|
||||
|
||||
def test_bad_git_repos(self):
|
||||
"""
|
||||
@@ -93,11 +101,12 @@ class TestGitExport(CourseTestCase):
|
||||
"""
|
||||
test_repo_path = '{}/test_repo'.format(git_export_utils.GIT_REPO_EXPORT_DIR)
|
||||
self.assertFalse(os.path.isdir(test_repo_path))
|
||||
course_key = SlashSeparatedCourseKey('foo', 'blah', '100-')
|
||||
# Test bad clones
|
||||
with self.assertRaisesRegexp(GitExportError,
|
||||
str(GitExportError.CANNOT_PULL)):
|
||||
git_export_utils.export_to_git(
|
||||
'foo/blah/100',
|
||||
course_key,
|
||||
'https://user:blah@example.com/test_repo.git')
|
||||
self.assertFalse(os.path.isdir(test_repo_path))
|
||||
|
||||
@@ -105,24 +114,16 @@ class TestGitExport(CourseTestCase):
|
||||
with self.assertRaisesRegexp(GitExportError,
|
||||
str(GitExportError.XML_EXPORT_FAIL)):
|
||||
git_export_utils.export_to_git(
|
||||
'foo/blah/100',
|
||||
course_key,
|
||||
'file://{0}'.format(self.bare_repo_dir))
|
||||
|
||||
# Test bad git remote after successful clone
|
||||
with self.assertRaisesRegexp(GitExportError,
|
||||
str(GitExportError.CANNOT_PULL)):
|
||||
git_export_utils.export_to_git(
|
||||
'foo/blah/100',
|
||||
course_key,
|
||||
'https://user:blah@example.com/r.git')
|
||||
|
||||
def test_bad_course_id(self):
|
||||
"""
|
||||
Test valid git url, but bad course.
|
||||
"""
|
||||
with self.assertRaisesRegexp(GitExportError, str(GitExportError.BAD_COURSE)):
|
||||
git_export_utils.export_to_git(
|
||||
'', 'file://{0}'.format(self.bare_repo_dir), '', '/blah')
|
||||
|
||||
@unittest.skipIf(os.environ.get('GIT_CONFIG') or
|
||||
os.environ.get('GIT_AUTHOR_EMAIL') or
|
||||
os.environ.get('GIT_AUTHOR_NAME') or
|
||||
@@ -170,7 +171,7 @@ class TestGitExport(CourseTestCase):
|
||||
Test response if there are no changes
|
||||
"""
|
||||
git_export_utils.export_to_git(
|
||||
'i4x://{0}'.format(self.course.id),
|
||||
self.course.id,
|
||||
'file://{0}'.format(self.bare_repo_dir)
|
||||
)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from contentstore.tests.modulestore_config import TEST_MODULESTORE
|
||||
from django_comment_common.utils import are_permissions_roles_seeded
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_MODULESTORE)
|
||||
@@ -22,18 +23,18 @@ class TestImport(ModuleStoreTestCase):
|
||||
Unit tests for importing a course from command line
|
||||
"""
|
||||
|
||||
BASE_COURSE_ID = ['EDx', '0.00x', '2013_Spring', ]
|
||||
DIFF_RUN = ['EDx', '0.00x', '2014_Spring', ]
|
||||
TRUNCATED_COURSE = ['EDx', '0.00', '2014_Spring', ]
|
||||
BASE_COURSE_KEY = SlashSeparatedCourseKey(u'edX', u'test_import_course', u'2013_Spring')
|
||||
DIFF_KEY = SlashSeparatedCourseKey(u'edX', u'test_import_course', u'2014_Spring')
|
||||
TRUNCATED_KEY = SlashSeparatedCourseKey(u'edX', u'test_import', u'2014_Spring')
|
||||
|
||||
def create_course_xml(self, content_dir, course_id):
|
||||
directory = tempfile.mkdtemp(dir=content_dir)
|
||||
os.makedirs(os.path.join(directory, "course"))
|
||||
with open(os.path.join(directory, "course.xml"), "w+") as f:
|
||||
f.write('<course url_name="{0[2]}" org="{0[0]}" '
|
||||
'course="{0[1]}"/>'.format(course_id))
|
||||
f.write('<course url_name="{0.run}" org="{0.org}" '
|
||||
'course="{0.course}"/>'.format(course_id))
|
||||
|
||||
with open(os.path.join(directory, "course", "{0[2]}.xml".format(course_id)), "w+") as f:
|
||||
with open(os.path.join(directory, "course", "{0.run}.xml".format(course_id)), "w+") as f:
|
||||
f.write('<course></course>')
|
||||
|
||||
return directory
|
||||
@@ -47,22 +48,22 @@ class TestImport(ModuleStoreTestCase):
|
||||
self.addCleanup(shutil.rmtree, self.content_dir)
|
||||
|
||||
# Create good course xml
|
||||
self.good_dir = self.create_course_xml(self.content_dir, self.BASE_COURSE_ID)
|
||||
self.good_dir = self.create_course_xml(self.content_dir, self.BASE_COURSE_KEY)
|
||||
|
||||
# Create run changed course xml
|
||||
self.dupe_dir = self.create_course_xml(self.content_dir, self.DIFF_RUN)
|
||||
self.dupe_dir = self.create_course_xml(self.content_dir, self.DIFF_KEY)
|
||||
|
||||
# Create course XML where TRUNCATED_COURSE.org == BASE_COURSE_ID.org
|
||||
# and BASE_COURSE_ID.startswith(TRUNCATED_COURSE.course)
|
||||
self.course_dir = self.create_course_xml(self.content_dir, self.TRUNCATED_COURSE)
|
||||
self.course_dir = self.create_course_xml(self.content_dir, self.TRUNCATED_KEY)
|
||||
|
||||
def test_forum_seed(self):
|
||||
"""
|
||||
Tests that forum roles were created with import.
|
||||
"""
|
||||
self.assertFalse(are_permissions_roles_seeded('/'.join(self.BASE_COURSE_ID)))
|
||||
self.assertFalse(are_permissions_roles_seeded(self.BASE_COURSE_KEY))
|
||||
call_command('import', self.content_dir, self.good_dir)
|
||||
self.assertTrue(are_permissions_roles_seeded('/'.join(self.BASE_COURSE_ID)))
|
||||
self.assertTrue(are_permissions_roles_seeded(self.BASE_COURSE_KEY))
|
||||
|
||||
def test_duplicate_with_url(self):
|
||||
"""
|
||||
@@ -73,11 +74,11 @@ class TestImport(ModuleStoreTestCase):
|
||||
# Load up base course and verify it is available
|
||||
call_command('import', self.content_dir, self.good_dir)
|
||||
store = modulestore()
|
||||
self.assertIsNotNone(store.get_course('/'.join(self.BASE_COURSE_ID)))
|
||||
self.assertIsNotNone(store.get_course(self.BASE_COURSE_KEY))
|
||||
|
||||
# Now load up duped course and verify it doesn't load
|
||||
call_command('import', self.content_dir, self.dupe_dir)
|
||||
self.assertIsNone(store.get_course('/'.join(self.DIFF_RUN)))
|
||||
self.assertIsNone(store.get_course(self.DIFF_KEY))
|
||||
|
||||
def test_truncated_course_with_url(self):
|
||||
"""
|
||||
@@ -89,8 +90,8 @@ class TestImport(ModuleStoreTestCase):
|
||||
# Load up base course and verify it is available
|
||||
call_command('import', self.content_dir, self.good_dir)
|
||||
store = modulestore()
|
||||
self.assertIsNotNone(store.get_course('/'.join(self.BASE_COURSE_ID)))
|
||||
self.assertIsNotNone(store.get_course(self.BASE_COURSE_KEY))
|
||||
|
||||
# Now load up the course with a similar course_id and verify it loads
|
||||
call_command('import', self.content_dir, self.course_dir)
|
||||
self.assertIsNotNone(store.get_course('/'.join(self.TRUNCATED_COURSE)))
|
||||
self.assertIsNotNone(store.get_course(self.TRUNCATED_KEY))
|
||||
|
||||
@@ -10,11 +10,12 @@ from contentstore.management.commands.migrate_to_split import Command
|
||||
from contentstore.tests.modulestore_config import TEST_MODULESTORE
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.django import modulestore, loc_mapper, clear_existing_modulestores
|
||||
from xmodule.modulestore.django import modulestore, clear_existing_modulestores
|
||||
from xmodule.modulestore.locator import CourseLocator
|
||||
# pylint: disable=E1101
|
||||
|
||||
|
||||
@unittest.skip("Not fixing split mongo until we land this long branch")
|
||||
class TestArgParsing(unittest.TestCase):
|
||||
"""
|
||||
Tests for parsing arguments for the `migrate_to_split` management command
|
||||
@@ -43,6 +44,7 @@ class TestArgParsing(unittest.TestCase):
|
||||
self.command.handle("i4x://org/course/category/name", "fake@example.com")
|
||||
|
||||
|
||||
@unittest.skip("Not fixing split mongo until we land this long branch")
|
||||
@override_settings(MODULESTORE=TEST_MODULESTORE)
|
||||
class TestMigrateToSplit(ModuleStoreTestCase):
|
||||
"""
|
||||
@@ -65,8 +67,7 @@ class TestMigrateToSplit(ModuleStoreTestCase):
|
||||
str(self.course.location),
|
||||
str(self.user.email),
|
||||
)
|
||||
locator = loc_mapper().translate_location(self.course.id, self.course.location)
|
||||
course_from_split = modulestore('split').get_course(locator)
|
||||
course_from_split = modulestore('split').get_course(self.course.id)
|
||||
self.assertIsNotNone(course_from_split)
|
||||
|
||||
def test_user_id(self):
|
||||
@@ -75,8 +76,7 @@ class TestMigrateToSplit(ModuleStoreTestCase):
|
||||
str(self.course.location),
|
||||
str(self.user.id),
|
||||
)
|
||||
locator = loc_mapper().translate_location(self.course.id, self.course.location)
|
||||
course_from_split = modulestore('split').get_course(locator)
|
||||
course_from_split = modulestore('split').get_course(self.course.id)
|
||||
self.assertIsNotNone(course_from_split)
|
||||
|
||||
def test_locator_string(self):
|
||||
@@ -84,8 +84,8 @@ class TestMigrateToSplit(ModuleStoreTestCase):
|
||||
"migrate_to_split",
|
||||
str(self.course.location),
|
||||
str(self.user.id),
|
||||
"org.dept.name.run",
|
||||
"org.dept+name.run",
|
||||
)
|
||||
locator = CourseLocator(package_id="org.dept.name.run", branch="published")
|
||||
locator = CourseLocator(org="org.dept", offering="name.run", branch="published")
|
||||
course_from_split = modulestore('split').get_course(locator)
|
||||
self.assertIsNotNone(course_from_split)
|
||||
|
||||
@@ -19,6 +19,7 @@ from xmodule.modulestore.split_migrator import SplitMigrator
|
||||
# pylint: disable=E1101
|
||||
|
||||
|
||||
@unittest.skip("Not fixing split mongo until we land opaque-keys 0.9")
|
||||
class TestArgParsing(unittest.TestCase):
|
||||
"""
|
||||
Tests for parsing arguments for the `rollback_split_course` management command
|
||||
@@ -37,6 +38,7 @@ class TestArgParsing(unittest.TestCase):
|
||||
self.command.handle("!?!")
|
||||
|
||||
|
||||
@unittest.skip("Not fixing split mongo until we land opaque-keys 0.9")
|
||||
@override_settings(MODULESTORE=TEST_MODULESTORE)
|
||||
class TestRollbackSplitCourseNoOldMongo(ModuleStoreTestCase):
|
||||
"""
|
||||
@@ -54,6 +56,8 @@ class TestRollbackSplitCourseNoOldMongo(ModuleStoreTestCase):
|
||||
with self.assertRaisesRegexp(CommandError, errstring):
|
||||
Command().handle(str(locator))
|
||||
|
||||
|
||||
@unittest.skip("Not fixing split mongo until we land opaque-keys 0.9")
|
||||
@override_settings(MODULESTORE=TEST_MODULESTORE)
|
||||
class TestRollbackSplitCourseNoSplitMongo(ModuleStoreTestCase):
|
||||
"""
|
||||
@@ -66,12 +70,13 @@ class TestRollbackSplitCourseNoSplitMongo(ModuleStoreTestCase):
|
||||
self.old_course = CourseFactory()
|
||||
|
||||
def test_nonexistent_locator(self):
|
||||
locator = loc_mapper().translate_location(self.old_course.id, self.old_course.location)
|
||||
locator = loc_mapper().translate_location(self.old_course.location)
|
||||
errstring = "No course found with locator"
|
||||
with self.assertRaisesRegexp(CommandError, errstring):
|
||||
Command().handle(str(locator))
|
||||
|
||||
|
||||
@unittest.skip("Not fixing split mongo until we land opaque-keys 0.9")
|
||||
@override_settings(MODULESTORE=TEST_MODULESTORE)
|
||||
class TestRollbackSplitCourse(ModuleStoreTestCase):
|
||||
"""
|
||||
@@ -93,18 +98,17 @@ class TestRollbackSplitCourse(ModuleStoreTestCase):
|
||||
loc_mapper=loc_mapper(),
|
||||
)
|
||||
migrator.migrate_mongo_course(self.old_course.location, self.user)
|
||||
locator = loc_mapper().translate_location(self.old_course.id, self.old_course.location)
|
||||
self.course = modulestore('split').get_course(locator)
|
||||
self.course = modulestore('split').get_course(self.old_course.id)
|
||||
|
||||
@patch("sys.stdout", new_callable=StringIO)
|
||||
def test_happy_path(self, mock_stdout):
|
||||
locator = self.course.location
|
||||
course_id = self.course.id
|
||||
call_command(
|
||||
"rollback_split_course",
|
||||
str(locator),
|
||||
str(course_id),
|
||||
)
|
||||
with self.assertRaises(ItemNotFoundError):
|
||||
modulestore('split').get_course(locator)
|
||||
modulestore('split').get_course(course_id)
|
||||
|
||||
self.assertIn("Course rolled back successfully", mock_stdout.getvalue())
|
||||
|
||||
|
||||
@@ -16,8 +16,7 @@ from textwrap import dedent
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User, Group
|
||||
from django.dispatch import Signal
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
@@ -30,11 +29,12 @@ from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.contentstore.django import contentstore, _CONTENTSTORE
|
||||
from xmodule.contentstore.utils import restore_asset_from_trashcan, empty_asset_trashcan
|
||||
from xmodule.exceptions import NotFoundError, InvalidVersionError
|
||||
from xmodule.modulestore import Location, mongo
|
||||
from xmodule.modulestore.django import modulestore, loc_mapper
|
||||
from xmodule.modulestore import mongo
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from xmodule.modulestore.locator import BlockUsageLocator
|
||||
from xmodule.modulestore.keys import UsageKey
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey, AssetLocation
|
||||
from xmodule.modulestore.store_utilities import clone_course, delete_course
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
@@ -45,11 +45,14 @@ from xmodule.capa_module import CapaDescriptor
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.seq_module import SequenceDescriptor
|
||||
|
||||
from contentstore.utils import delete_course_and_groups
|
||||
from contentstore.utils import delete_course_and_groups, reverse_url, reverse_course_url
|
||||
from django_comment_common.utils import are_permissions_roles_seeded
|
||||
|
||||
from student import auth
|
||||
from student.models import CourseEnrollment
|
||||
from student.roles import CourseCreatorRole, CourseInstructorRole
|
||||
from opaque_keys import InvalidKeyError
|
||||
|
||||
|
||||
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
|
||||
TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex
|
||||
@@ -65,6 +68,12 @@ class MongoCollectionFindWrapper(object):
|
||||
return self.original(query, *args, **kwargs)
|
||||
|
||||
|
||||
def get_url(handler_name, key_value, key_name='usage_key_string', kwargs=None):
|
||||
# Helper function for getting HTML for a page in Studio and
|
||||
# checking that it does not error.
|
||||
return reverse_url(handler_name, key_name, key_value, kwargs)
|
||||
|
||||
|
||||
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE, MODULESTORE=TEST_MODULESTORE)
|
||||
class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
"""
|
||||
@@ -111,19 +120,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
component_types should cause 'Video' to be present.
|
||||
"""
|
||||
store = modulestore('direct')
|
||||
import_from_xml(store, 'common/test/data/', ['simple'])
|
||||
|
||||
course = store.get_item(Location(['i4x', 'edX', 'simple',
|
||||
'course', '2012_Fall', None]), depth=None)
|
||||
|
||||
_, course_items = import_from_xml(store, 'common/test/data/', ['simple'])
|
||||
course = course_items[0]
|
||||
course.advanced_modules = component_types
|
||||
|
||||
store.update_item(course, self.user.id)
|
||||
|
||||
# just pick one vertical
|
||||
descriptor = store.get_items(Location('i4x', 'edX', 'simple', 'vertical', None, None))[0]
|
||||
locator = loc_mapper().translate_location(course.location.course_id, descriptor.location, True, True)
|
||||
resp = self.client.get_html(locator.url_reverse('unit'))
|
||||
descriptor = store.get_items(course.id, category='vertical',)
|
||||
resp = self.client.get_html(get_url('unit_handler', descriptor[0].location))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
_test_no_locations(self, resp)
|
||||
|
||||
@@ -147,30 +151,29 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
_, course_items = import_from_xml(store, 'common/test/data/', ['simple'])
|
||||
|
||||
# just pick one vertical
|
||||
descriptor = store.get_items(Location('i4x', 'edX', 'simple', 'vertical', None, None))[0]
|
||||
location = descriptor.location.replace(name='.' + descriptor.location.name)
|
||||
locator = loc_mapper().translate_location(
|
||||
course_items[0].location.course_id, location, add_entry_if_missing=True)
|
||||
usage_key = course_items[0].id.make_usage_key('vertical', None)
|
||||
|
||||
resp = self.client.get_html(locator.url_reverse('unit'))
|
||||
resp = self.client.get_html(get_url('unit_handler', usage_key))
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
_test_no_locations(self, resp, status_code=400)
|
||||
|
||||
def check_edit_unit(self, test_course_name):
|
||||
_, course_items = import_from_xml(modulestore('direct'), 'common/test/data/', [test_course_name])
|
||||
|
||||
items = modulestore().get_items(Location('i4x', 'edX', test_course_name, 'vertical', None, None))
|
||||
self._check_verticals(items, course_items[0].location.course_id)
|
||||
items = modulestore().get_items(course_items[0].id, category='vertical')
|
||||
self._check_verticals(items)
|
||||
|
||||
def _lock_an_asset(self, content_store, course_location):
|
||||
def _lock_an_asset(self, content_store, course_id):
|
||||
"""
|
||||
Lock an arbitrary asset in the course
|
||||
:param course_location:
|
||||
"""
|
||||
course_assets, __ = content_store.get_all_content_for_course(course_location)
|
||||
course_assets, __ = content_store.get_all_content_for_course(course_id)
|
||||
self.assertGreater(len(course_assets), 0, "No assets to lock")
|
||||
content_store.set_attr(course_assets[0]['_id'], 'locked', True)
|
||||
return course_assets[0]['_id']
|
||||
asset_id = course_assets[0]['_id']
|
||||
asset_key = StaticContent.compute_location(course_id, asset_id['name'])
|
||||
content_store.set_attr(asset_key, 'locked', True)
|
||||
return asset_key
|
||||
|
||||
def test_edit_unit_toy(self):
|
||||
self.check_edit_unit('toy')
|
||||
@@ -188,26 +191,29 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
Unfortunately, None = published for the revision field, so get_items() would return
|
||||
both draft and non-draft copies.
|
||||
'''
|
||||
store = modulestore('direct')
|
||||
direct_store = modulestore('direct')
|
||||
draft_store = modulestore('draft')
|
||||
import_from_xml(store, 'common/test/data/', ['simple'])
|
||||
_, course_items = import_from_xml(direct_store, 'common/test/data/', ['simple'])
|
||||
course_key = course_items[0].id
|
||||
html_usage_key = course_key.make_usage_key('html', 'test_html')
|
||||
|
||||
html_module = draft_store.get_item(Location('i4x', 'edX', 'simple', 'html', 'test_html', None))
|
||||
html_module_from_draft_store = draft_store.get_item(html_usage_key)
|
||||
draft_store.convert_to_draft(html_module_from_draft_store.location)
|
||||
|
||||
draft_store.convert_to_draft(html_module.location)
|
||||
# Query get_items() and find the html item. This should just return back a single item (not 2).
|
||||
|
||||
# now query get_items() to get this location with revision=None, this should just
|
||||
# return back a single item (not 2)
|
||||
direct_store_items = direct_store.get_items(course_key)
|
||||
html_items_from_direct_store = [item for item in direct_store_items if (item.location == html_usage_key)]
|
||||
self.assertEqual(len(html_items_from_direct_store), 1)
|
||||
self.assertFalse(getattr(html_items_from_direct_store[0], 'is_draft', False))
|
||||
|
||||
items = store.get_items(Location('i4x', 'edX', 'simple', 'html', 'test_html', None))
|
||||
self.assertEqual(len(items), 1)
|
||||
self.assertFalse(getattr(items[0], 'is_draft', False))
|
||||
# Fetch from the draft store. Note that even though we pass
|
||||
# None in the revision field, the draft store will replace that with 'draft'.
|
||||
draft_store_items = draft_store.get_items(course_key)
|
||||
html_items_from_draft_store = [item for item in draft_store_items if (item.location == html_usage_key)]
|
||||
self.assertEqual(len(html_items_from_draft_store), 1)
|
||||
self.assertTrue(getattr(html_items_from_draft_store[0], 'is_draft', False))
|
||||
|
||||
# now refetch from the draft store. Note that even though we pass
|
||||
# None in the revision field, the draft store will replace that with 'draft'
|
||||
items = draft_store.get_items(Location('i4x', 'edX', 'simple', 'html', 'test_html', None))
|
||||
self.assertEqual(len(items), 1)
|
||||
self.assertTrue(getattr(items[0], 'is_draft', False))
|
||||
|
||||
def test_draft_metadata(self):
|
||||
'''
|
||||
@@ -219,9 +225,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
draft_store = modulestore('draft')
|
||||
import_from_xml(store, 'common/test/data/', ['simple'])
|
||||
|
||||
course = draft_store.get_item(Location('i4x', 'edX', 'simple',
|
||||
'course', '2012_Fall', None), depth=None)
|
||||
html_module = draft_store.get_item(Location('i4x', 'edX', 'simple', 'html', 'test_html', None))
|
||||
course_key = SlashSeparatedCourseKey('edX', 'simple', '2012_Fall')
|
||||
html_usage_key = course_key.make_usage_key('html', 'test_html')
|
||||
course = draft_store.get_course(course_key)
|
||||
html_module = draft_store.get_item(html_usage_key)
|
||||
|
||||
self.assertEqual(html_module.graceperiod, course.graceperiod)
|
||||
self.assertNotIn('graceperiod', own_metadata(html_module))
|
||||
@@ -229,7 +236,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
draft_store.convert_to_draft(html_module.location)
|
||||
|
||||
# refetch to check metadata
|
||||
html_module = draft_store.get_item(Location('i4x', 'edX', 'simple', 'html', 'test_html', None))
|
||||
html_module = draft_store.get_item(html_usage_key)
|
||||
|
||||
self.assertEqual(html_module.graceperiod, course.graceperiod)
|
||||
self.assertNotIn('graceperiod', own_metadata(html_module))
|
||||
@@ -238,14 +245,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
draft_store.publish(html_module.location, 0)
|
||||
|
||||
# refetch to check metadata
|
||||
html_module = draft_store.get_item(Location('i4x', 'edX', 'simple', 'html', 'test_html', None))
|
||||
html_module = draft_store.get_item(html_usage_key)
|
||||
|
||||
self.assertEqual(html_module.graceperiod, course.graceperiod)
|
||||
self.assertNotIn('graceperiod', own_metadata(html_module))
|
||||
|
||||
# put back in draft and change metadata and see if it's now marked as 'own_metadata'
|
||||
draft_store.convert_to_draft(html_module.location)
|
||||
html_module = draft_store.get_item(Location('i4x', 'edX', 'simple', 'html', 'test_html', None))
|
||||
html_module = draft_store.get_item(html_usage_key)
|
||||
|
||||
new_graceperiod = timedelta(hours=1)
|
||||
|
||||
@@ -260,7 +267,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
draft_store.update_item(html_module, self.user.id)
|
||||
|
||||
# read back to make sure it reads as 'own-metadata'
|
||||
html_module = draft_store.get_item(Location('i4x', 'edX', 'simple', 'html', 'test_html', None))
|
||||
html_module = draft_store.get_item(html_usage_key)
|
||||
|
||||
self.assertIn('graceperiod', own_metadata(html_module))
|
||||
self.assertEqual(html_module.graceperiod, new_graceperiod)
|
||||
@@ -270,7 +277,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
# and re-read and verify 'own-metadata'
|
||||
draft_store.convert_to_draft(html_module.location)
|
||||
html_module = draft_store.get_item(Location('i4x', 'edX', 'simple', 'html', 'test_html', None))
|
||||
html_module = draft_store.get_item(html_usage_key)
|
||||
|
||||
self.assertIn('graceperiod', own_metadata(html_module))
|
||||
self.assertEqual(html_module.graceperiod, new_graceperiod)
|
||||
@@ -278,33 +285,25 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
def test_get_depth_with_drafts(self):
|
||||
import_from_xml(modulestore('direct'), 'common/test/data/', ['simple'])
|
||||
|
||||
course = modulestore('draft').get_item(
|
||||
Location('i4x', 'edX', 'simple', 'course', '2012_Fall', None),
|
||||
depth=None
|
||||
)
|
||||
course_key = SlashSeparatedCourseKey('edX', 'simple', '2012_Fall')
|
||||
course = modulestore('draft').get_course(course_key)
|
||||
|
||||
# make sure no draft items have been returned
|
||||
num_drafts = self._get_draft_counts(course)
|
||||
self.assertEqual(num_drafts, 0)
|
||||
|
||||
problem = modulestore('draft').get_item(
|
||||
Location('i4x', 'edX', 'simple', 'problem', 'ps01-simple', None)
|
||||
)
|
||||
problem_usage_key = course_key.make_usage_key('problem', 'ps01-simple')
|
||||
problem = modulestore('draft').get_item(problem_usage_key)
|
||||
|
||||
# put into draft
|
||||
modulestore('draft').convert_to_draft(problem.location)
|
||||
|
||||
# make sure we can query that item and verify that it is a draft
|
||||
draft_problem = modulestore('draft').get_item(
|
||||
Location('i4x', 'edX', 'simple', 'problem', 'ps01-simple', None)
|
||||
)
|
||||
draft_problem = modulestore('draft').get_item(problem_usage_key)
|
||||
self.assertTrue(getattr(draft_problem, 'is_draft', False))
|
||||
|
||||
# now requery with depth
|
||||
course = modulestore('draft').get_item(
|
||||
Location('i4x', 'edX', 'simple', 'course', '2012_Fall', None),
|
||||
depth=None
|
||||
)
|
||||
course = modulestore('draft').get_course(course_key)
|
||||
|
||||
# make sure just one draft item have been returned
|
||||
num_drafts = self._get_draft_counts(course)
|
||||
@@ -312,12 +311,15 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
def test_no_static_link_rewrites_on_import(self):
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
_, course_items = import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
course = course_items[0]
|
||||
|
||||
handouts = module_store.get_item(Location('i4x', 'edX', 'toy', 'course_info', 'handouts', None))
|
||||
handouts_usage_key = course.id.make_usage_key('course_info', 'handouts')
|
||||
handouts = module_store.get_item(handouts_usage_key)
|
||||
self.assertIn('/static/', handouts.data)
|
||||
|
||||
handouts = module_store.get_item(Location('i4x', 'edX', 'toy', 'html', 'toyhtml', None))
|
||||
handouts_usage_key = course.id.make_usage_key('html', 'toyhtml')
|
||||
handouts = module_store.get_item(handouts_usage_key)
|
||||
self.assertIn('/static/', handouts.data)
|
||||
|
||||
@mock.patch('xmodule.course_module.requests.get')
|
||||
@@ -330,150 +332,15 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
|
||||
course = module_store.get_item(Location('i4x', 'edX', 'toy', 'course', '2012_Fall', None))
|
||||
|
||||
course = module_store.get_course(SlashSeparatedCourseKey('edX', 'toy', '2012_Fall'))
|
||||
self.assertGreater(len(course.textbooks), 0)
|
||||
|
||||
def test_default_tabs_on_create_course(self):
|
||||
module_store = modulestore('direct')
|
||||
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
|
||||
course_location = Location('i4x', 'edX', '999', 'course', 'Robot_Super_Course', None)
|
||||
|
||||
course = module_store.get_item(course_location)
|
||||
|
||||
expected_tabs = []
|
||||
expected_tabs.append({u'type': u'courseware'})
|
||||
expected_tabs.append({u'type': u'course_info', u'name': u'Course Info'})
|
||||
expected_tabs.append({u'type': u'textbooks'})
|
||||
expected_tabs.append({u'type': u'discussion', u'name': u'Discussion'})
|
||||
expected_tabs.append({u'type': u'wiki', u'name': u'Wiki'})
|
||||
expected_tabs.append({u'type': u'progress', u'name': u'Progress'})
|
||||
|
||||
self.assertEqual(course.tabs, expected_tabs)
|
||||
|
||||
def test_create_static_tab_and_rename(self):
|
||||
module_store = modulestore('direct')
|
||||
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
|
||||
course_location = Location('i4x', 'edX', '999', 'course', 'Robot_Super_Course', None)
|
||||
|
||||
item = ItemFactory.create(parent_location=course_location, category='static_tab', display_name="My Tab")
|
||||
|
||||
course = module_store.get_item(course_location)
|
||||
|
||||
expected_tabs = []
|
||||
expected_tabs.append({u'type': u'courseware'})
|
||||
expected_tabs.append({u'type': u'course_info', u'name': u'Course Info'})
|
||||
expected_tabs.append({u'type': u'textbooks'})
|
||||
expected_tabs.append({u'type': u'discussion', u'name': u'Discussion'})
|
||||
expected_tabs.append({u'type': u'wiki', u'name': u'Wiki'})
|
||||
expected_tabs.append({u'type': u'progress', u'name': u'Progress'})
|
||||
expected_tabs.append({u'type': u'static_tab', u'name': u'My Tab', u'url_slug': u'My_Tab'})
|
||||
|
||||
self.assertEqual(course.tabs, expected_tabs)
|
||||
|
||||
item.display_name = 'Updated'
|
||||
module_store.update_item(item, self.user.id)
|
||||
|
||||
course = module_store.get_item(course_location)
|
||||
|
||||
expected_tabs = []
|
||||
expected_tabs.append({u'type': u'courseware'})
|
||||
expected_tabs.append({u'type': u'course_info', u'name': u'Course Info'})
|
||||
expected_tabs.append({u'type': u'textbooks'})
|
||||
expected_tabs.append({u'type': u'discussion', u'name': u'Discussion'})
|
||||
expected_tabs.append({u'type': u'wiki', u'name': u'Wiki'})
|
||||
expected_tabs.append({u'type': u'progress', u'name': u'Progress'})
|
||||
expected_tabs.append({u'type': u'static_tab', u'name': u'Updated', u'url_slug': u'My_Tab'})
|
||||
|
||||
self.assertEqual(course.tabs, expected_tabs)
|
||||
|
||||
def test_static_tab_reordering(self):
|
||||
module_store, course_location, new_location = self._create_static_tabs()
|
||||
|
||||
course = module_store.get_item(course_location)
|
||||
|
||||
# reverse the ordering of the static tabs
|
||||
reverse_static_tabs = []
|
||||
built_in_tabs = []
|
||||
for tab in course.tabs:
|
||||
if tab['type'] == 'static_tab':
|
||||
reverse_static_tabs.insert(0, tab)
|
||||
else:
|
||||
built_in_tabs.append(tab)
|
||||
|
||||
# create the requested tab_id_locators list
|
||||
tab_id_locators = [
|
||||
{
|
||||
'tab_id': tab.tab_id
|
||||
} for tab in built_in_tabs
|
||||
]
|
||||
tab_id_locators.extend([
|
||||
{
|
||||
'tab_locator': unicode(self._get_tab_locator(course, tab))
|
||||
} for tab in reverse_static_tabs
|
||||
])
|
||||
|
||||
self.client.ajax_post(new_location.url_reverse('tabs'), {'tabs': tab_id_locators})
|
||||
|
||||
course = module_store.get_item(course_location)
|
||||
|
||||
# compare to make sure that the tabs information is in the expected order after the server call
|
||||
new_static_tabs = [tab for tab in course.tabs if (tab['type'] == 'static_tab')]
|
||||
self.assertEqual(reverse_static_tabs, new_static_tabs)
|
||||
|
||||
def test_static_tab_deletion(self):
|
||||
module_store, course_location, _ = self._create_static_tabs()
|
||||
|
||||
course = module_store.get_item(course_location)
|
||||
num_tabs = len(course.tabs)
|
||||
last_tab = course.tabs[-1]
|
||||
url_slug = last_tab['url_slug']
|
||||
delete_url = self._get_tab_locator(course, last_tab).url_reverse('xblock')
|
||||
|
||||
self.client.delete(delete_url)
|
||||
|
||||
course = module_store.get_item(course_location)
|
||||
self.assertEqual(num_tabs - 1, len(course.tabs))
|
||||
|
||||
def tab_matches(tab):
|
||||
""" Checks if the tab matches the one we deleted """
|
||||
return tab['type'] == 'static_tab' and tab['url_slug'] == url_slug
|
||||
|
||||
tab_found = any(tab_matches(tab) for tab in course.tabs)
|
||||
|
||||
self.assertFalse(tab_found, "tab should have been deleted")
|
||||
|
||||
def _get_tab_locator(self, course, tab):
|
||||
""" Returns the locator for a given tab. """
|
||||
tab_location = 'i4x://edX/999/static_tab/{0}'.format(tab['url_slug'])
|
||||
return loc_mapper().translate_location(
|
||||
course.location.course_id, Location(tab_location), True, True
|
||||
)
|
||||
|
||||
def _create_static_tabs(self):
|
||||
""" Creates two static tabs in a dummy course. """
|
||||
module_store = modulestore('direct')
|
||||
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
|
||||
course_location = Location('i4x', 'edX', '999', 'course', 'Robot_Super_Course', None)
|
||||
new_location = loc_mapper().translate_location(course_location.course_id, course_location, True, True)
|
||||
|
||||
ItemFactory.create(
|
||||
parent_location=course_location,
|
||||
category="static_tab",
|
||||
display_name="Static_1")
|
||||
ItemFactory.create(
|
||||
parent_location=course_location,
|
||||
category="static_tab",
|
||||
display_name="Static_2")
|
||||
|
||||
return module_store, course_location, new_location
|
||||
|
||||
def test_import_polls(self):
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
_, course_items = import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
course_key = course_items[0].id
|
||||
|
||||
items = module_store.get_items(Location('i4x', 'edX', 'toy', 'poll_question', None, None))
|
||||
items = module_store.get_items(course_key, category='poll_question')
|
||||
found = len(items) > 0
|
||||
|
||||
self.assertTrue(found)
|
||||
@@ -489,57 +356,54 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests the ajax callback to render an XModule
|
||||
"""
|
||||
resp = self._test_preview(Location('i4x', 'edX', 'toy', 'vertical', 'vertical_test', None), 'container_preview')
|
||||
self.assertContains(resp, '/branch/draft/block/sample_video')
|
||||
self.assertContains(resp, '/branch/draft/block/separate_file_video')
|
||||
self.assertContains(resp, '/branch/draft/block/video_with_end_time')
|
||||
self.assertContains(resp, '/branch/draft/block/T1_changemind_poll_foo_2')
|
||||
|
||||
def _test_preview(self, location, view_name):
|
||||
""" Preview test case. """
|
||||
direct_store = modulestore('direct')
|
||||
_, course_items = import_from_xml(direct_store, 'common/test/data/', ['toy'])
|
||||
usage_key = course_items[0].id.make_usage_key('vertical', 'vertical_test')
|
||||
|
||||
# also try a custom response which will trigger the 'is this course in whitelist' logic
|
||||
locator = loc_mapper().translate_location(
|
||||
course_items[0].location.course_id, location, True, True
|
||||
resp = self.client.get_json(
|
||||
get_url('xblock_view_handler', usage_key, kwargs={'view_name': 'container_preview'})
|
||||
)
|
||||
resp = self.client.get_json(locator.url_reverse('xblock', view_name))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
# TODO: uncomment when preview no longer has locations being returned.
|
||||
# _test_no_locations(self, resp)
|
||||
return resp
|
||||
|
||||
# These are the data-ids of the xblocks contained in the vertical.
|
||||
self.assertContains(resp, 'edX+toy+2012_Fall+video+sample_video')
|
||||
self.assertContains(resp, 'edX+toy+2012_Fall+video+separate_file_video')
|
||||
self.assertContains(resp, 'edX+toy+2012_Fall+video+video_with_end_time')
|
||||
self.assertContains(resp, 'edX+toy+2012_Fall+poll_question+T1_changemind_poll_foo_2')
|
||||
|
||||
def test_delete(self):
|
||||
direct_store = modulestore('direct')
|
||||
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
|
||||
course_location = Location('i4x', 'edX', '999', 'course', 'Robot_Super_Course', None)
|
||||
course = CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
|
||||
|
||||
chapterloc = ItemFactory.create(parent_location=course_location, display_name="Chapter").location
|
||||
chapterloc = ItemFactory.create(parent_location=course.location, display_name="Chapter").location
|
||||
ItemFactory.create(parent_location=chapterloc, category='sequential', display_name="Sequential")
|
||||
|
||||
sequential = direct_store.get_item(Location('i4x', 'edX', '999', 'sequential', 'Sequential', None))
|
||||
chapter = direct_store.get_item(Location('i4x', 'edX', '999', 'chapter', 'Chapter', None))
|
||||
sequential_key = course.id.make_usage_key('sequential', 'Sequential')
|
||||
sequential = direct_store.get_item(sequential_key)
|
||||
chapter_key = course.id.make_usage_key('chapter', 'Chapter')
|
||||
chapter = direct_store.get_item(chapter_key)
|
||||
|
||||
# make sure the parent points to the child object which is to be deleted
|
||||
self.assertTrue(sequential.location.url() in chapter.children)
|
||||
self.assertTrue(sequential.location in chapter.children)
|
||||
|
||||
location = loc_mapper().translate_location(course_location.course_id, sequential.location, True, True)
|
||||
self.client.delete(location.url_reverse('xblock'), {'recurse': True, 'all_versions': True})
|
||||
self.client.delete(get_url('xblock_handler', sequential_key), {'recurse': True, 'all_versions': True})
|
||||
|
||||
found = False
|
||||
try:
|
||||
direct_store.get_item(Location(['i4x', 'edX', '999', 'sequential', 'Sequential', None]))
|
||||
direct_store.get_item(sequential_key)
|
||||
found = True
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
|
||||
self.assertFalse(found)
|
||||
|
||||
chapter = direct_store.get_item(Location(['i4x', 'edX', '999', 'chapter', 'Chapter', None]))
|
||||
chapter = direct_store.get_item(chapter_key)
|
||||
|
||||
# make sure the parent no longer points to the child object which was deleted
|
||||
self.assertFalse(sequential.location.url() in chapter.children)
|
||||
self.assertFalse(sequential.location in chapter.children)
|
||||
|
||||
def test_about_overrides(self):
|
||||
'''
|
||||
@@ -547,21 +411,15 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
while there is a base definition in /about/effort.html
|
||||
'''
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
effort = module_store.get_item(Location(['i4x', 'edX', 'toy', 'about', 'effort', None]))
|
||||
_, course_items = import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
course_key = course_items[0].id
|
||||
effort = module_store.get_item(course_key.make_usage_key('about', 'effort'))
|
||||
self.assertEqual(effort.data, '6 hours')
|
||||
|
||||
# this one should be in a non-override folder
|
||||
effort = module_store.get_item(Location(['i4x', 'edX', 'toy', 'about', 'end_date', None]))
|
||||
effort = module_store.get_item(course_key.make_usage_key('about', 'end_date'))
|
||||
self.assertEqual(effort.data, 'TBD')
|
||||
|
||||
def test_remove_hide_progress_tab(self):
|
||||
module_store = modulestore('direct')
|
||||
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
|
||||
course_location = Location(['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None])
|
||||
course = module_store.get_item(course_location)
|
||||
self.assertFalse(course.hide_progress_tab)
|
||||
|
||||
def test_asset_import(self):
|
||||
'''
|
||||
This test validates that an image asset is imported and a thumbnail was generated for a .gif
|
||||
@@ -571,17 +429,16 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store, verbose=True)
|
||||
|
||||
course_location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
|
||||
course = module_store.get_item(course_location)
|
||||
course = module_store.get_course(SlashSeparatedCourseKey('edX', 'toy', '2012_Fall'))
|
||||
|
||||
self.assertIsNotNone(course)
|
||||
|
||||
# make sure we have some assets in our contentstore
|
||||
all_assets, __ = content_store.get_all_content_for_course(course_location)
|
||||
all_assets, __ = content_store.get_all_content_for_course(course.id)
|
||||
self.assertGreater(len(all_assets), 0)
|
||||
|
||||
# make sure we have some thumbnails in our contentstore
|
||||
content_store.get_all_content_thumbnails_for_course(course_location)
|
||||
content_store.get_all_content_thumbnails_for_course(course.id)
|
||||
|
||||
#
|
||||
# cdodge: temporarily comment out assertion on thumbnails because many environments
|
||||
@@ -592,7 +449,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
content = None
|
||||
try:
|
||||
location = StaticContent.get_location_from_path('/c4x/edX/toy/asset/sample_static.txt')
|
||||
location = AssetLocation.from_deprecated_string('/c4x/edX/toy/asset/sample_static.txt')
|
||||
content = content_store.find(location)
|
||||
except NotFoundError:
|
||||
pass
|
||||
@@ -617,8 +474,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
'''
|
||||
This test will exercise the soft delete/restore functionality of the assets
|
||||
'''
|
||||
content_store, trash_store, thumbnail_location = self._delete_asset_in_course()
|
||||
asset_location = StaticContent.get_location_from_path('/c4x/edX/toy/asset/sample_static.txt')
|
||||
content_store, trash_store, thumbnail_location, _location = self._delete_asset_in_course()
|
||||
asset_location = AssetLocation.from_deprecated_string('/c4x/edX/toy/asset/sample_static.txt')
|
||||
|
||||
# now try to find it in store, but they should not be there any longer
|
||||
content = content_store.find(asset_location, throw_on_not_found=False)
|
||||
@@ -662,7 +519,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
_, course_items = import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store)
|
||||
|
||||
# look up original (and thumbnail) in content store, should be there after import
|
||||
location = StaticContent.get_location_from_path('/c4x/edX/toy/asset/sample_static.txt')
|
||||
location = AssetLocation.from_deprecated_string('/c4x/edX/toy/asset/sample_static.txt')
|
||||
content = content_store.find(location, throw_on_not_found=False)
|
||||
thumbnail_location = content.thumbnail_location
|
||||
self.assertIsNotNone(content)
|
||||
@@ -675,12 +532,15 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
# go through the website to do the delete, since the soft-delete logic is in the view
|
||||
course = course_items[0]
|
||||
location = loc_mapper().translate_location(course.location.course_id, course.location, True, True)
|
||||
url = location.url_reverse('assets/', '/c4x/edX/toy/asset/sample_static.txt')
|
||||
url = reverse_course_url(
|
||||
'assets_handler',
|
||||
course.id,
|
||||
kwargs={'asset_key_string': course.id.make_asset_key('asset', 'sample_static.txt')}
|
||||
)
|
||||
resp = self.client.delete(url)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
|
||||
return content_store, trash_store, thumbnail_location
|
||||
return content_store, trash_store, thumbnail_location, location
|
||||
|
||||
def test_course_info_updates_import_export(self):
|
||||
"""
|
||||
@@ -692,17 +552,16 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
import_from_xml(module_store, data_dir, ['course_info_updates'],
|
||||
static_content_store=content_store, verbose=True)
|
||||
|
||||
course_location = CourseDescriptor.id_to_location('edX/course_info_updates/2014_T1')
|
||||
course = module_store.get_item(course_location)
|
||||
course_id = SlashSeparatedCourseKey('edX', 'course_info_updates', '2014_T1')
|
||||
course = module_store.get_course(course_id)
|
||||
|
||||
self.assertIsNotNone(course)
|
||||
|
||||
course_updates = module_store.get_item(
|
||||
Location(['i4x', 'edX', 'course_info_updates', 'course_info', 'updates', None]))
|
||||
course_updates = module_store.get_item(course_id.make_usage_key('course_info', 'updates'))
|
||||
|
||||
self.assertIsNotNone(course_updates)
|
||||
|
||||
# check that course which is imported has files 'updates.html' and 'updates.items.json'
|
||||
# check that course which is imported has files 'updates.html' and 'updates.items.json'
|
||||
filesystem = OSFS(data_dir + 'course_info_updates/info')
|
||||
self.assertTrue(filesystem.exists('updates.html'))
|
||||
self.assertTrue(filesystem.exists('updates.items.json'))
|
||||
@@ -722,7 +581,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
# with same content as in course 'info' directory
|
||||
root_dir = path(mkdtemp_clean())
|
||||
print 'Exporting to tempdir = {0}'.format(root_dir)
|
||||
export_to_xml(module_store, content_store, course_location, root_dir, 'test_export')
|
||||
export_to_xml(module_store, content_store, course_id, root_dir, 'test_export')
|
||||
|
||||
# check that exported course has files 'updates.html' and 'updates.items.json'
|
||||
filesystem = OSFS(root_dir / 'test_export/info')
|
||||
@@ -742,15 +601,15 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
'''
|
||||
This test will exercise the emptying of the asset trashcan
|
||||
'''
|
||||
_, trash_store, _ = self._delete_asset_in_course()
|
||||
__, trash_store, __, _location = self._delete_asset_in_course()
|
||||
|
||||
# make sure there's something in the trashcan
|
||||
course_location = CourseDescriptor.id_to_location('edX/toy/6.002_Spring_2012')
|
||||
all_assets, __ = trash_store.get_all_content_for_course(course_location)
|
||||
course_id = SlashSeparatedCourseKey('edX', 'toy', '6.002_Spring_2012')
|
||||
all_assets, __ = trash_store.get_all_content_for_course(course_id)
|
||||
self.assertGreater(len(all_assets), 0)
|
||||
|
||||
# make sure we have some thumbnails in our trashcan
|
||||
_all_thumbnails = trash_store.get_all_content_thumbnails_for_course(course_location)
|
||||
_all_thumbnails = trash_store.get_all_content_thumbnails_for_course(course_id)
|
||||
#
|
||||
# cdodge: temporarily comment out assertion on thumbnails because many environments
|
||||
# will not have the jpeg converter installed and this test will fail
|
||||
@@ -758,14 +617,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
# self.assertGreater(len(all_thumbnails), 0)
|
||||
|
||||
# empty the trashcan
|
||||
empty_asset_trashcan([course_location])
|
||||
empty_asset_trashcan([course_id])
|
||||
|
||||
# make sure trashcan is empty
|
||||
all_assets, count = trash_store.get_all_content_for_course(course_location)
|
||||
all_assets, count = trash_store.get_all_content_for_course(course_id)
|
||||
self.assertEqual(len(all_assets), 0)
|
||||
self.assertEqual(count, 0)
|
||||
|
||||
all_thumbnails = trash_store.get_all_content_thumbnails_for_course(course_location)
|
||||
all_thumbnails = trash_store.get_all_content_thumbnails_for_course(course_id)
|
||||
self.assertEqual(len(all_thumbnails), 0)
|
||||
|
||||
def test_clone_course(self):
|
||||
@@ -774,63 +633,57 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
'org': 'MITx',
|
||||
'number': '999',
|
||||
'display_name': 'Robot Super Course',
|
||||
'run': '2013_Spring'
|
||||
'run': '2013_Spring',
|
||||
}
|
||||
|
||||
module_store = modulestore('direct')
|
||||
draft_store = modulestore('draft')
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
_, course_items = import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
|
||||
source_course_id = 'edX/toy/2012_Fall'
|
||||
dest_course_id = 'MITx/999/2013_Spring'
|
||||
source_location = CourseDescriptor.id_to_location(source_course_id)
|
||||
dest_location = CourseDescriptor.id_to_location(dest_course_id)
|
||||
source_course_id = course_items[0].id
|
||||
dest_course_id = _get_course_id(course_data)
|
||||
|
||||
# get a vertical (and components in it) to put into 'draft'
|
||||
# this is to assert that draft content is also cloned over
|
||||
vertical = module_store.get_instance(source_course_id, Location([
|
||||
source_location.tag, source_location.org, source_location.course, 'vertical', 'vertical_test', None]), depth=1)
|
||||
vertical = module_store.get_item(
|
||||
source_course_id.make_usage_key('vertical', 'vertical_test'),
|
||||
depth=1
|
||||
)
|
||||
|
||||
draft_store.convert_to_draft(vertical.location)
|
||||
for child in vertical.get_children():
|
||||
draft_store.convert_to_draft(child.location)
|
||||
|
||||
items = module_store.get_items(Location([source_location.tag, source_location.org, source_location.course, None, None, 'draft']))
|
||||
items = module_store.get_items(source_course_id, revision='draft')
|
||||
self.assertGreater(len(items), 0)
|
||||
|
||||
_create_course(self, course_data)
|
||||
_create_course(self, dest_course_id, course_data)
|
||||
|
||||
content_store = contentstore()
|
||||
|
||||
# now do the actual cloning
|
||||
clone_course(module_store, content_store, source_location, dest_location)
|
||||
clone_course(module_store, content_store, source_course_id, dest_course_id)
|
||||
|
||||
# first assert that all draft content got cloned as well
|
||||
items = module_store.get_items(Location([source_location.tag, source_location.org, source_location.course, None, None, 'draft']))
|
||||
items = module_store.get_items(source_course_id, revision='draft')
|
||||
self.assertGreater(len(items), 0)
|
||||
clone_items = module_store.get_items(Location([dest_location.tag, dest_location.org, dest_location.course, None, None, 'draft']))
|
||||
clone_items = module_store.get_items(dest_course_id, revision='draft')
|
||||
self.assertGreater(len(clone_items), 0)
|
||||
self.assertEqual(len(items), len(clone_items))
|
||||
|
||||
# now loop through all the units in the course and verify that the clone can render them, which
|
||||
# means the objects are at least present
|
||||
items = module_store.get_items(Location([source_location.tag, source_location.org, source_location.course, None, None]))
|
||||
items = module_store.get_items(source_course_id, revision=None)
|
||||
self.assertGreater(len(items), 0)
|
||||
clone_items = module_store.get_items(Location([dest_location.tag, dest_location.org, dest_location.course, None, None]))
|
||||
clone_items = module_store.get_items(dest_course_id, revision=None)
|
||||
self.assertGreater(len(clone_items), 0)
|
||||
|
||||
for descriptor in items:
|
||||
source_item = module_store.get_instance(source_course_id, descriptor.location)
|
||||
if descriptor.location.category == 'course':
|
||||
new_loc = descriptor.location.replace(org=dest_location.org, course=dest_location.course, name='2013_Spring')
|
||||
else:
|
||||
new_loc = descriptor.location.replace(org=dest_location.org, course=dest_location.course)
|
||||
print "Checking {0} should now also be at {1}".format(descriptor.location.url(), new_loc.url())
|
||||
source_item = module_store.get_item(descriptor.location)
|
||||
new_loc = descriptor.location.map_into_course(dest_course_id)
|
||||
print "Checking {0} should now also be at {1}".format(descriptor.location, new_loc)
|
||||
lookup_item = module_store.get_item(new_loc)
|
||||
|
||||
# we want to assert equality between the objects, but we know the locations
|
||||
# differ, so just make them equal for testing purposes
|
||||
source_item.location = new_loc
|
||||
if hasattr(source_item, 'data') and hasattr(lookup_item, 'data'):
|
||||
self.assertEqual(source_item.data, lookup_item.data)
|
||||
|
||||
@@ -842,14 +695,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
self.assertEqual(source_item.has_children, lookup_item.has_children)
|
||||
if source_item.has_children:
|
||||
expected_children = []
|
||||
for child_loc_url in source_item.children:
|
||||
child_loc = Location(child_loc_url)
|
||||
child_loc = child_loc.replace(
|
||||
tag=dest_location.tag,
|
||||
org=dest_location.org,
|
||||
course=dest_location.course
|
||||
)
|
||||
expected_children.append(child_loc.url())
|
||||
for child_loc in source_item.children:
|
||||
child_loc = child_loc.map_into_course(dest_course_id)
|
||||
expected_children.append(child_loc)
|
||||
self.assertEqual(expected_children, lookup_item.children)
|
||||
|
||||
def test_portable_link_rewrites_during_clone_course(self):
|
||||
@@ -865,37 +713,31 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
|
||||
source_course_id = 'edX/toy/2012_Fall'
|
||||
dest_course_id = 'MITx/999/2013_Spring'
|
||||
source_location = CourseDescriptor.id_to_location(source_course_id)
|
||||
dest_location = CourseDescriptor.id_to_location(dest_course_id)
|
||||
source_course_id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
|
||||
dest_course_id = _get_course_id(course_data)
|
||||
|
||||
# let's force a non-portable link in the clone source
|
||||
# as a final check, make sure that any non-portable links are rewritten during cloning
|
||||
html_module_location = Location([
|
||||
source_location.tag, source_location.org, source_location.course, 'html', 'nonportable'])
|
||||
html_module = module_store.get_instance(source_location.course_id, html_module_location)
|
||||
html_module = module_store.get_item(source_course_id.make_usage_key('html', 'nonportable'))
|
||||
|
||||
self.assertIsInstance(html_module.data, basestring)
|
||||
new_data = html_module.data = html_module.data.replace('/static/', '/c4x/{0}/{1}/asset/'.format(
|
||||
source_location.org, source_location.course))
|
||||
source_course_id.org, source_course_id.run))
|
||||
module_store.update_item(html_module, self.user.id)
|
||||
|
||||
html_module = module_store.get_instance(source_location.course_id, html_module_location)
|
||||
html_module = module_store.get_item(html_module.location)
|
||||
self.assertEqual(new_data, html_module.data)
|
||||
|
||||
# create the destination course
|
||||
_create_course(self, course_data)
|
||||
_create_course(self, dest_course_id, course_data)
|
||||
|
||||
# do the actual cloning
|
||||
clone_course(module_store, content_store, source_location, dest_location)
|
||||
clone_course(module_store, content_store, source_course_id, dest_course_id)
|
||||
|
||||
# make sure that any non-portable links are rewritten during cloning
|
||||
html_module_location = Location([
|
||||
dest_location.tag, dest_location.org, dest_location.course, 'html', 'nonportable'])
|
||||
html_module = module_store.get_instance(dest_location.course_id, html_module_location)
|
||||
html_module = module_store.get_item(dest_course_id.make_usage_key('html', 'nonportable'))
|
||||
|
||||
self.assertIn('/static/foo.jpg', html_module.data)
|
||||
self.assertIn('/asset/foo.jpg', html_module.data)
|
||||
|
||||
def test_illegal_draft_crud_ops(self):
|
||||
draft_store = modulestore('draft')
|
||||
@@ -903,18 +745,21 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
course = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
|
||||
location = Location('i4x://MITx/999/chapter/neuvo')
|
||||
location = course.id.make_usage_key('chapter', 'neuvo')
|
||||
# Ensure draft mongo store does not allow us to create chapters either directly or via convert to draft
|
||||
self.assertRaises(InvalidVersionError, draft_store.create_and_save_xmodule, location)
|
||||
with self.assertRaises(InvalidVersionError):
|
||||
draft_store.create_and_save_xmodule(location)
|
||||
direct_store.create_and_save_xmodule(location)
|
||||
self.assertRaises(InvalidVersionError, draft_store.convert_to_draft, location)
|
||||
chapter = draft_store.get_instance(course.id, location)
|
||||
with self.assertRaises(InvalidVersionError):
|
||||
draft_store.convert_to_draft(location)
|
||||
chapter = draft_store.get_item(location)
|
||||
chapter.data = 'chapter data'
|
||||
|
||||
with self.assertRaises(InvalidVersionError):
|
||||
draft_store.update_item(chapter, self.user.id)
|
||||
|
||||
self.assertRaises(InvalidVersionError, draft_store.unpublish, location)
|
||||
with self.assertRaises(InvalidVersionError):
|
||||
draft_store.unpublish(location)
|
||||
|
||||
def test_bad_contentstore_request(self):
|
||||
resp = self.client.get_html('http://localhost:8001/c4x/CDX/123123/asset/&images_circuits_Lab7Solution2.png')
|
||||
@@ -928,13 +773,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store)
|
||||
|
||||
# first check a static asset link
|
||||
html_module_location = Location(['i4x', 'edX', 'toy', 'html', 'nonportable'])
|
||||
html_module = module_store.get_instance('edX/toy/2012_Fall', html_module_location)
|
||||
course_key = SlashSeparatedCourseKey('edX', 'toy', 'run')
|
||||
html_module_location = course_key.make_usage_key('html', 'nonportable')
|
||||
html_module = module_store.get_item(html_module_location)
|
||||
self.assertIn('/static/foo.jpg', html_module.data)
|
||||
|
||||
# then check a intra courseware link
|
||||
html_module_location = Location(['i4x', 'edX', 'toy', 'html', 'nonportable_link'])
|
||||
html_module = module_store.get_instance('edX/toy/2012_Fall', html_module_location)
|
||||
html_module_location = course_key.make_usage_key('html', 'nonportable_link')
|
||||
html_module = module_store.get_item(html_module_location)
|
||||
self.assertIn('/jump_to_id/nonportable_link', html_module.data)
|
||||
|
||||
def test_delete_course(self):
|
||||
@@ -947,37 +793,35 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
content_store = contentstore()
|
||||
draft_store = modulestore('draft')
|
||||
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store)
|
||||
_, course_items = import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store)
|
||||
|
||||
location = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course').location
|
||||
course_id = course_items[0].id
|
||||
|
||||
# get a vertical (and components in it) to put into 'draft'
|
||||
vertical = module_store.get_item(Location(['i4x', 'edX', 'toy',
|
||||
'vertical', 'vertical_test', None]), depth=1)
|
||||
vertical = module_store.get_item(course_id.make_usage_key('vertical', 'vertical_test'), depth=1)
|
||||
|
||||
draft_store.convert_to_draft(vertical.location)
|
||||
for child in vertical.get_children():
|
||||
draft_store.convert_to_draft(child.location)
|
||||
|
||||
# delete the course
|
||||
delete_course(module_store, content_store, location, commit=True)
|
||||
delete_course(module_store, content_store, course_id, commit=True)
|
||||
|
||||
# assert that there's absolutely no non-draft modules in the course
|
||||
# this should also include all draft items
|
||||
items = module_store.get_items(Location(['i4x', 'edX', '999', 'course', None]))
|
||||
items = module_store.get_items(course_id)
|
||||
self.assertEqual(len(items), 0)
|
||||
|
||||
# assert that all content in the asset library is also deleted
|
||||
assets, count = content_store.get_all_content_for_course(location)
|
||||
assets, count = content_store.get_all_content_for_course(course_id)
|
||||
self.assertEqual(len(assets), 0)
|
||||
self.assertEqual(count, 0)
|
||||
|
||||
def verify_content_existence(self, store, root_dir, location, dirname, category_name, filename_suffix=''):
|
||||
def verify_content_existence(self, store, root_dir, course_id, dirname, category_name, filename_suffix=''):
|
||||
filesystem = OSFS(root_dir / 'test_export')
|
||||
self.assertTrue(filesystem.exists(dirname))
|
||||
|
||||
query_loc = Location('i4x', location.org, location.course, category_name, None)
|
||||
items = store.get_items(query_loc)
|
||||
items = store.get_items(course_id, category=category_name)
|
||||
|
||||
for item in items:
|
||||
filesystem = OSFS(root_dir / ('test_export/' + dirname))
|
||||
@@ -996,13 +840,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
content_store = contentstore()
|
||||
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store)
|
||||
location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
|
||||
course_id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
|
||||
|
||||
# get a vertical (and components in it) to copy into an orphan sub dag
|
||||
vertical = module_store.get_item(
|
||||
Location(['i4x', 'edX', 'toy', 'vertical', 'vertical_test', None]),
|
||||
depth=1
|
||||
)
|
||||
vertical = module_store.get_item(course_id.make_usage_key('vertical', 'vertical_test'), depth=1)
|
||||
# We had a bug where orphaned draft nodes caused export to fail. This is here to cover that case.
|
||||
vertical.location = mongo.draft.as_draft(vertical.location.replace(name='no_references'))
|
||||
|
||||
@@ -1011,9 +852,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
self.assertEqual(orphan_vertical.location.name, 'no_references')
|
||||
|
||||
# get the original vertical (and components in it) to put into 'draft'
|
||||
vertical = module_store.get_item(
|
||||
Location(['i4x', 'edX', 'toy', 'vertical', 'vertical_test', None]),
|
||||
depth=1)
|
||||
vertical = module_store.get_item(course_id.make_usage_key('vertical', 'vertical_test'), depth=1)
|
||||
self.assertEqual(len(orphan_vertical.children), len(vertical.children))
|
||||
draft_store.convert_to_draft(vertical.location)
|
||||
for child in vertical.get_children():
|
||||
@@ -1022,46 +861,43 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
root_dir = path(mkdtemp_clean())
|
||||
|
||||
# now create a new/different private (draft only) vertical
|
||||
vertical.location = mongo.draft.as_draft(Location(['i4x', 'edX', 'toy', 'vertical', 'a_private_vertical', None]))
|
||||
vertical.location = mongo.draft.as_draft(course_id.make_usage_key('vertical', 'a_private_vertical'))
|
||||
draft_store.update_item(vertical, allow_not_found=True)
|
||||
private_vertical = draft_store.get_item(vertical.location)
|
||||
vertical = None # blank out b/c i destructively manipulated its location 2 lines above
|
||||
|
||||
# add the new private to list of children
|
||||
sequential = module_store.get_item(
|
||||
Location('i4x', 'edX', 'toy', 'sequential', 'vertical_sequential', None)
|
||||
)
|
||||
sequential = module_store.get_item(course_id.make_usage_key('sequential', 'vertical_sequential'))
|
||||
private_location_no_draft = private_vertical.location.replace(revision=None)
|
||||
sequential.children.append(private_location_no_draft.url())
|
||||
sequential.children.append(private_location_no_draft)
|
||||
module_store.update_item(sequential, self.user.id)
|
||||
|
||||
# read back the sequential, to make sure we have a pointer to
|
||||
sequential = module_store.get_item(Location(['i4x', 'edX', 'toy',
|
||||
'sequential', 'vertical_sequential', None]))
|
||||
sequential = module_store.get_item(course_id.make_usage_key('sequential', 'vertical_sequential'))
|
||||
|
||||
self.assertIn(private_location_no_draft.url(), sequential.children)
|
||||
self.assertIn(private_location_no_draft, sequential.children)
|
||||
|
||||
locked_asset = self._lock_an_asset(content_store, location)
|
||||
locked_asset_attrs = content_store.get_attrs(locked_asset)
|
||||
locked_asset_key = self._lock_an_asset(content_store, course_id)
|
||||
locked_asset_attrs = content_store.get_attrs(locked_asset_key)
|
||||
# the later import will reupload
|
||||
del locked_asset_attrs['uploadDate']
|
||||
|
||||
print 'Exporting to tempdir = {0}'.format(root_dir)
|
||||
|
||||
# export out to a tempdir
|
||||
export_to_xml(module_store, content_store, location, root_dir, 'test_export', draft_modulestore=draft_store)
|
||||
export_to_xml(module_store, content_store, course_id, root_dir, 'test_export', draft_modulestore=draft_store)
|
||||
|
||||
# check for static tabs
|
||||
self.verify_content_existence(module_store, root_dir, location, 'tabs', 'static_tab', '.html')
|
||||
self.verify_content_existence(module_store, root_dir, course_id, 'tabs', 'static_tab', '.html')
|
||||
|
||||
# check for about content
|
||||
self.verify_content_existence(module_store, root_dir, location, 'about', 'about', '.html')
|
||||
self.verify_content_existence(module_store, root_dir, course_id, 'about', 'about', '.html')
|
||||
|
||||
# check for graiding_policy.json
|
||||
# check for grading_policy.json
|
||||
filesystem = OSFS(root_dir / 'test_export/policies/2012_Fall')
|
||||
self.assertTrue(filesystem.exists('grading_policy.json'))
|
||||
|
||||
course = module_store.get_item(location)
|
||||
course = module_store.get_course(course_id)
|
||||
# compare what's on disk compared to what we have in our course
|
||||
with filesystem.open('grading_policy.json', 'r') as grading_policy:
|
||||
on_disk = loads(grading_policy.read())
|
||||
@@ -1077,42 +913,38 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
self.assertEqual(on_disk['course/2012_Fall'], own_metadata(course))
|
||||
|
||||
# remove old course
|
||||
delete_course(module_store, content_store, location, commit=True)
|
||||
delete_course(module_store, content_store, course_id, commit=True)
|
||||
# reimport over old course
|
||||
stub_location = Location(['i4x', 'edX', 'toy', None, None])
|
||||
course_location = course.location
|
||||
self.check_import(
|
||||
module_store, root_dir, draft_store, content_store, stub_location, course_location,
|
||||
locked_asset, locked_asset_attrs
|
||||
module_store, root_dir, draft_store, content_store, course_id,
|
||||
locked_asset_key, locked_asset_attrs
|
||||
)
|
||||
# import to different course id
|
||||
stub_location = Location(['i4x', 'anotherX', 'anotherToy', None, None])
|
||||
course_location = stub_location.replace(category='course', name='Someday')
|
||||
self.check_import(
|
||||
module_store, root_dir, draft_store, content_store, stub_location, course_location,
|
||||
locked_asset, locked_asset_attrs
|
||||
module_store, root_dir, draft_store, content_store, SlashSeparatedCourseKey('anotherX', 'anotherToy', 'Someday'),
|
||||
locked_asset_key, locked_asset_attrs
|
||||
)
|
||||
|
||||
shutil.rmtree(root_dir)
|
||||
|
||||
def check_import(self, module_store, root_dir, draft_store, content_store, stub_location, course_location,
|
||||
locked_asset, locked_asset_attrs):
|
||||
def check_import(self, module_store, root_dir, draft_store, content_store, course_id,
|
||||
locked_asset_key, locked_asset_attrs):
|
||||
# reimport
|
||||
import_from_xml(
|
||||
module_store, root_dir, ['test_export'], draft_store=draft_store,
|
||||
module_store,
|
||||
root_dir,
|
||||
['test_export'],
|
||||
draft_store=draft_store,
|
||||
static_content_store=content_store,
|
||||
target_location_namespace=course_location
|
||||
target_course_id=course_id,
|
||||
)
|
||||
|
||||
# Unit test fails in Jenkins without this.
|
||||
loc_mapper().translate_location(course_location.course_id, course_location, True, True)
|
||||
|
||||
items = module_store.get_items(stub_location.replace(category='vertical', name=None))
|
||||
self._check_verticals(items, course_location.course_id)
|
||||
items = module_store.get_items(course_id, category='vertical')
|
||||
self._check_verticals(items)
|
||||
|
||||
# verify that we have the content in the draft store as well
|
||||
vertical = draft_store.get_item(
|
||||
stub_location.replace(category='vertical', name='vertical_test', revision=None),
|
||||
course_id.make_usage_key('vertical', 'vertical_test'),
|
||||
depth=1
|
||||
)
|
||||
|
||||
@@ -1131,26 +963,25 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
# make sure that we don't have a sequential that is in draft mode
|
||||
sequential = draft_store.get_item(
|
||||
stub_location.replace(category='sequential', name='vertical_sequential', revision=None)
|
||||
course_id.make_usage_key('sequential', 'vertical_sequential')
|
||||
)
|
||||
|
||||
self.assertFalse(getattr(sequential, 'is_draft', False))
|
||||
|
||||
# verify that we have the private vertical
|
||||
test_private_vertical = draft_store.get_item(
|
||||
stub_location.replace(category='vertical', name='a_private_vertical', revision=None)
|
||||
course_id.make_usage_key('vertical', 'a_private_vertical')
|
||||
)
|
||||
|
||||
self.assertTrue(getattr(test_private_vertical, 'is_draft', False))
|
||||
|
||||
# make sure the textbook survived the export/import
|
||||
course = module_store.get_item(course_location)
|
||||
course = module_store.get_course(course_id)
|
||||
|
||||
self.assertGreater(len(course.textbooks), 0)
|
||||
|
||||
locked_asset['course'] = stub_location.course
|
||||
locked_asset['org'] = stub_location.org
|
||||
new_attrs = content_store.get_attrs(locked_asset)
|
||||
locked_asset_key = locked_asset_key.map_into_course(course_id)
|
||||
new_attrs = content_store.get_attrs(locked_asset_key)
|
||||
for key, value in locked_asset_attrs.iteritems():
|
||||
if key == '_id':
|
||||
self.assertEqual(value['name'], new_attrs[key]['name'])
|
||||
@@ -1165,12 +996,12 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
content_store = contentstore()
|
||||
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
|
||||
course_id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
|
||||
|
||||
# create a new video module and add it as a child to a vertical
|
||||
# this re-creates a bug whereby since the video template doesn't have
|
||||
# anything in 'data' field, the export was blowing up
|
||||
verticals = module_store.get_items(Location('i4x', 'edX', 'toy', 'vertical', None, None))
|
||||
verticals = module_store.get_items(course_id, category='vertical')
|
||||
|
||||
self.assertGreater(len(verticals), 0)
|
||||
|
||||
@@ -1183,7 +1014,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
print 'Exporting to tempdir = {0}'.format(root_dir)
|
||||
|
||||
# export out to a tempdir
|
||||
export_to_xml(module_store, content_store, location, root_dir, 'test_export', draft_modulestore=draft_store)
|
||||
export_to_xml(module_store, content_store, course_id, root_dir, 'test_export', draft_modulestore=draft_store)
|
||||
|
||||
shutil.rmtree(root_dir)
|
||||
|
||||
@@ -1196,9 +1027,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
content_store = contentstore()
|
||||
|
||||
import_from_xml(module_store, 'common/test/data/', ['word_cloud'])
|
||||
location = CourseDescriptor.id_to_location('HarvardX/ER22x/2013_Spring')
|
||||
course_id = SlashSeparatedCourseKey('HarvardX', 'ER22x', '2013_Spring')
|
||||
|
||||
verticals = module_store.get_items(Location('i4x', 'HarvardX', 'ER22x', 'vertical', None, None))
|
||||
verticals = module_store.get_items(course_id, category='vertical')
|
||||
|
||||
self.assertGreater(len(verticals), 0)
|
||||
|
||||
@@ -1211,7 +1042,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
print 'Exporting to tempdir = {0}'.format(root_dir)
|
||||
|
||||
# export out to a tempdir
|
||||
export_to_xml(module_store, content_store, location, root_dir, 'test_export', draft_modulestore=draft_store)
|
||||
export_to_xml(module_store, content_store, course_id, root_dir, 'test_export', draft_modulestore=draft_store)
|
||||
|
||||
shutil.rmtree(root_dir)
|
||||
|
||||
@@ -1225,9 +1056,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
content_store = contentstore()
|
||||
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
|
||||
course_id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
|
||||
|
||||
verticals = module_store.get_items(Location('i4x', 'edX', 'toy', 'vertical', None, None))
|
||||
verticals = module_store.get_items(course_id, category='vertical')
|
||||
|
||||
self.assertGreater(len(verticals), 0)
|
||||
|
||||
@@ -1240,11 +1071,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
# Export the course
|
||||
root_dir = path(mkdtemp_clean())
|
||||
export_to_xml(module_store, content_store, location, root_dir, 'test_roundtrip', draft_modulestore=draft_store)
|
||||
export_to_xml(module_store, content_store, course_id, root_dir, 'test_roundtrip', draft_modulestore=draft_store)
|
||||
|
||||
# Reimport and get the video back
|
||||
import_from_xml(module_store, root_dir)
|
||||
imported_word_cloud = module_store.get_item(Location('i4x', 'edX', 'toy', 'word_cloud', 'untitled', None))
|
||||
imported_word_cloud = module_store.get_item(course_id.make_usage_key('word_cloud', 'untitled'))
|
||||
|
||||
# It should now contain empty data
|
||||
self.assertEquals(imported_word_cloud.data, '')
|
||||
@@ -1258,41 +1089,34 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
|
||||
location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
|
||||
course_id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
|
||||
|
||||
# Export the course
|
||||
root_dir = path(mkdtemp_clean())
|
||||
export_to_xml(module_store, content_store, location, root_dir, 'test_roundtrip')
|
||||
export_to_xml(module_store, content_store, course_id, root_dir, 'test_roundtrip')
|
||||
|
||||
# Reimport and get the video back
|
||||
import_from_xml(module_store, root_dir)
|
||||
|
||||
# get the sample HTML with styling information
|
||||
html_module = module_store.get_instance(
|
||||
'edX/toy/2012_Fall',
|
||||
Location('i4x', 'edX', 'toy', 'html', 'with_styling')
|
||||
)
|
||||
html_module = module_store.get_item(course_id.make_usage_key('html', 'with_styling'))
|
||||
self.assertIn('<p style="font:italic bold 72px/30px Georgia, serif; color: red; ">', html_module.data)
|
||||
|
||||
# get the sample HTML with just a simple <img> tag information
|
||||
html_module = module_store.get_instance(
|
||||
'edX/toy/2012_Fall',
|
||||
Location('i4x', 'edX', 'toy', 'html', 'just_img')
|
||||
)
|
||||
html_module = module_store.get_item(course_id.make_usage_key('html', 'just_img'))
|
||||
self.assertIn('<img src="/static/foo_bar.jpg" />', html_module.data)
|
||||
|
||||
def test_course_handouts_rewrites(self):
|
||||
module_store = modulestore('direct')
|
||||
|
||||
# import a test course
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
_, course_items = import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
course_id = course_items[0].id
|
||||
|
||||
handout_location = Location(['i4x', 'edX', 'toy', 'course_info', 'handouts'])
|
||||
# get the translation
|
||||
handouts_locator = loc_mapper().translate_location('edX/toy/2012_Fall', handout_location)
|
||||
handouts_location = course_id.make_usage_key('course_info', 'handouts')
|
||||
|
||||
# get module info (json)
|
||||
resp = self.client.get(handouts_locator.url_reverse('/xblock'))
|
||||
resp = self.client.get(get_url('xblock_handler', handouts_location))
|
||||
|
||||
# make sure we got a successful response
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -1303,13 +1127,13 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
def test_prefetch_children(self):
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
|
||||
course_id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
|
||||
|
||||
wrapper = MongoCollectionFindWrapper(module_store.collection.find)
|
||||
module_store.collection.find = wrapper.find
|
||||
print module_store.metadata_inheritance_cache_subsystem
|
||||
print module_store.request_cache
|
||||
course = module_store.get_item(location, depth=2)
|
||||
course = module_store.get_course(course_id, depth=2)
|
||||
|
||||
# make sure we haven't done too many round trips to DB
|
||||
# note we say 3 round trips here for 1) the course, and 2 & 3) for the chapters and sequentials
|
||||
@@ -1318,12 +1142,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
self.assertEqual(wrapper.counter, 3)
|
||||
|
||||
# make sure we pre-fetched a known sequential which should be at depth=2
|
||||
self.assertTrue(Location(['i4x', 'edX', 'toy', 'sequential',
|
||||
'vertical_sequential', None]) in course.system.module_data)
|
||||
self.assertTrue(course_id.make_usage_key('sequential', 'vertical_sequential') in course.system.module_data)
|
||||
|
||||
# make sure we don't have a specific vertical which should be at depth=3
|
||||
self.assertFalse(Location(['i4x', 'edX', 'toy', 'vertical', 'vertical_test', None])
|
||||
in course.system.module_data)
|
||||
self.assertFalse(course_id.make_usage_key('vertical', 'vertical_test') in course.system.module_data)
|
||||
|
||||
def test_export_course_without_content_store(self):
|
||||
module_store = modulestore('direct')
|
||||
@@ -1331,39 +1153,40 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
# Create toy course
|
||||
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
|
||||
|
||||
stub_location = Location(['i4x', 'edX', 'toy', 'sequential', 'vertical_sequential'])
|
||||
_, course_items = import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
course_id = course_items[0].id
|
||||
|
||||
root_dir = path(mkdtemp_clean())
|
||||
|
||||
print 'Exporting to tempdir = {0}'.format(root_dir)
|
||||
export_to_xml(module_store, None, location, root_dir, 'test_export_no_content_store')
|
||||
export_to_xml(module_store, None, course_id, root_dir, 'test_export_no_content_store')
|
||||
|
||||
# Delete the course from module store and reimport it
|
||||
|
||||
delete_course(module_store, content_store, location, commit=True)
|
||||
delete_course(module_store, content_store, course_id, commit=True)
|
||||
|
||||
import_from_xml(
|
||||
module_store, root_dir, ['test_export_no_content_store'],
|
||||
draft_store=None,
|
||||
static_content_store=None,
|
||||
target_location_namespace=location
|
||||
target_course_id=course_id
|
||||
)
|
||||
|
||||
# Verify reimported course
|
||||
|
||||
items = module_store.get_items(stub_location)
|
||||
items = module_store.get_items(
|
||||
course_id,
|
||||
category='sequential',
|
||||
name='vertical_sequential'
|
||||
)
|
||||
self.assertEqual(len(items), 1)
|
||||
|
||||
def _check_verticals(self, items, course_id):
|
||||
def _check_verticals(self, items):
|
||||
""" Test getting the editing HTML for each vertical. """
|
||||
# Assert is here to make sure that the course being tested actually has verticals (units) to check.
|
||||
self.assertGreater(len(items), 0)
|
||||
for descriptor in items:
|
||||
unit_locator = loc_mapper().translate_location(course_id, descriptor.location, True, True)
|
||||
resp = self.client.get_html(unit_locator.url_reverse('unit'))
|
||||
resp = self.client.get_html(get_url('unit_handler', descriptor.location))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
_test_no_locations(self, resp)
|
||||
|
||||
@@ -1409,10 +1232,6 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
MongoClient().drop_database(TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'])
|
||||
_CONTENTSTORE.clear()
|
||||
|
||||
def test_create_course(self):
|
||||
"""Test new course creation - happy path"""
|
||||
self.assert_created_course()
|
||||
|
||||
def assert_created_course(self, number_suffix=None):
|
||||
"""
|
||||
Checks that the course was created properly.
|
||||
@@ -1421,20 +1240,32 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
test_course_data.update(self.course_data)
|
||||
if number_suffix:
|
||||
test_course_data['number'] = '{0}_{1}'.format(test_course_data['number'], number_suffix)
|
||||
_create_course(self, test_course_data)
|
||||
course_key = _get_course_id(test_course_data)
|
||||
_create_course(self, course_key, test_course_data)
|
||||
# Verify that the creator is now registered in the course.
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(self.user, _get_course_id(test_course_data)))
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(self.user, course_key))
|
||||
return test_course_data
|
||||
|
||||
def assert_create_course_failed(self, error_message):
|
||||
"""
|
||||
Checks that the course not created.
|
||||
"""
|
||||
resp = self.client.ajax_post('/course', self.course_data)
|
||||
resp = self.client.ajax_post('/course/', self.course_data)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
data = parse_json(resp)
|
||||
self.assertEqual(data['error'], error_message)
|
||||
|
||||
def test_create_course(self):
|
||||
"""Test new course creation - happy path"""
|
||||
self.assert_created_course()
|
||||
|
||||
def test_create_course_with_dots(self):
|
||||
"""Test new course creation with dots in the name"""
|
||||
self.course_data['org'] = 'org.foo.bar'
|
||||
self.course_data['number'] = 'course.number'
|
||||
self.course_data['run'] = 'run.name'
|
||||
self.assert_created_course()
|
||||
|
||||
def test_create_course_check_forum_seeding(self):
|
||||
"""Test new course creation and verify forum seeding """
|
||||
test_course_data = self.assert_created_course(number_suffix=uuid4().hex)
|
||||
@@ -1490,83 +1321,90 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
"""
|
||||
test_course_data = self.assert_created_course(number_suffix=uuid4().hex)
|
||||
course_id = _get_course_id(test_course_data)
|
||||
course_location = CourseDescriptor.id_to_location(course_id)
|
||||
|
||||
# Add user in possible groups and check that user in instructor groups of this course
|
||||
instructor_role = CourseInstructorRole(course_location)
|
||||
groupnames = instructor_role._group_names # pylint: disable=protected-access
|
||||
groups = Group.objects.filter(name__in=groupnames)
|
||||
for group in groups:
|
||||
group.user_set.add(self.user)
|
||||
instructor_role = CourseInstructorRole(course_id)
|
||||
|
||||
auth.add_users(self.user, instructor_role, self.user)
|
||||
|
||||
self.assertTrue(len(instructor_role.users_with_role()) > 0)
|
||||
|
||||
# Now delete course and check that user not in instructor groups of this course
|
||||
delete_course_and_groups(course_location.course_id, commit=True)
|
||||
delete_course_and_groups(course_id, commit=True)
|
||||
|
||||
# Update our cached user since its roles have changed
|
||||
self.user = User.objects.get_by_natural_key(self.user.natural_key()[0])
|
||||
|
||||
self.assertFalse(instructor_role.has_user(self.user))
|
||||
self.assertEqual(len(instructor_role.users_with_role()), 0)
|
||||
|
||||
def test_create_course_duplicate_course(self):
|
||||
"""Test new course creation - error path"""
|
||||
self.client.ajax_post('/course', self.course_data)
|
||||
self.client.ajax_post('/course/', self.course_data)
|
||||
self.assert_course_creation_failed('There is already a course defined with the same organization, course number, and course run. Please change either organization or course number to be unique.')
|
||||
|
||||
def assert_course_creation_failed(self, error_message):
|
||||
"""
|
||||
Checks that the course did not get created
|
||||
"""
|
||||
course_id = _get_course_id(self.course_data)
|
||||
initially_enrolled = CourseEnrollment.is_enrolled(self.user, course_id)
|
||||
resp = self.client.ajax_post('/course', self.course_data)
|
||||
test_enrollment = False
|
||||
try:
|
||||
course_id = _get_course_id(self.course_data)
|
||||
initially_enrolled = CourseEnrollment.is_enrolled(self.user, course_id)
|
||||
test_enrollment = True
|
||||
except InvalidKeyError:
|
||||
# b/c the intent of the test with bad chars isn't to test auth but to test the handler, ignore
|
||||
pass
|
||||
resp = self.client.ajax_post('/course/', self.course_data)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
data = parse_json(resp)
|
||||
self.assertEqual(data['ErrMsg'], error_message)
|
||||
# One test case involves trying to create the same course twice. Hence for that course,
|
||||
# the user will be enrolled. In the other cases, initially_enrolled will be False.
|
||||
self.assertEqual(initially_enrolled, CourseEnrollment.is_enrolled(self.user, course_id))
|
||||
self.assertRegexpMatches(data['ErrMsg'], error_message)
|
||||
if test_enrollment:
|
||||
# One test case involves trying to create the same course twice. Hence for that course,
|
||||
# the user will be enrolled. In the other cases, initially_enrolled will be False.
|
||||
self.assertEqual(initially_enrolled, CourseEnrollment.is_enrolled(self.user, course_id))
|
||||
|
||||
def test_create_course_duplicate_number(self):
|
||||
"""Test new course creation - error path"""
|
||||
self.client.ajax_post('/course', self.course_data)
|
||||
self.client.ajax_post('/course/', self.course_data)
|
||||
self.course_data['display_name'] = 'Robot Super Course Two'
|
||||
self.course_data['run'] = '2013_Summer'
|
||||
|
||||
self.assert_course_creation_failed('There is already a course defined with the same organization and course number. Please change at least one field to be unique.')
|
||||
self.assert_course_creation_failed('There is already a course defined with the same organization, course number, and course run. Please change either organization or course number to be unique.')
|
||||
|
||||
def test_create_course_case_change(self):
|
||||
"""Test new course creation - error path due to case insensitive name equality"""
|
||||
self.course_data['number'] = 'capital'
|
||||
self.client.ajax_post('/course', self.course_data)
|
||||
self.client.ajax_post('/course/', self.course_data)
|
||||
cache_current = self.course_data['org']
|
||||
self.course_data['org'] = self.course_data['org'].lower()
|
||||
self.assert_course_creation_failed('There is already a course defined with the same organization and course number. Please change at least one field to be unique.')
|
||||
self.assert_course_creation_failed('There is already a course defined with the same organization, course number, and course run. Please change either organization or course number to be unique.')
|
||||
self.course_data['org'] = cache_current
|
||||
|
||||
self.client.ajax_post('/course', self.course_data)
|
||||
self.client.ajax_post('/course/', self.course_data)
|
||||
cache_current = self.course_data['number']
|
||||
self.course_data['number'] = self.course_data['number'].upper()
|
||||
self.assert_course_creation_failed('There is already a course defined with the same organization and course number. Please change at least one field to be unique.')
|
||||
self.assert_course_creation_failed('There is already a course defined with the same organization, course number, and course run. Please change either organization or course number to be unique.')
|
||||
|
||||
def test_course_substring(self):
|
||||
"""
|
||||
Test that a new course can be created whose name is a substring of an existing course
|
||||
"""
|
||||
self.client.ajax_post('/course', self.course_data)
|
||||
self.client.ajax_post('/course/', self.course_data)
|
||||
cache_current = self.course_data['number']
|
||||
self.course_data['number'] = '{}a'.format(self.course_data['number'])
|
||||
resp = self.client.ajax_post('/course', self.course_data)
|
||||
resp = self.client.ajax_post('/course/', self.course_data)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.course_data['number'] = cache_current
|
||||
self.course_data['org'] = 'a{}'.format(self.course_data['org'])
|
||||
resp = self.client.ajax_post('/course', self.course_data)
|
||||
resp = self.client.ajax_post('/course/', self.course_data)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_create_course_with_bad_organization(self):
|
||||
"""Test new course creation - error path for bad organization name"""
|
||||
self.course_data['org'] = 'University of California, Berkeley'
|
||||
self.assert_course_creation_failed(
|
||||
"Unable to create course 'Robot Super Course'.\n\nInvalid characters in u'University of California, Berkeley'.")
|
||||
r"(?s)Unable to create course 'Robot Super Course'.*: Invalid characters in u'University of California, Berkeley'")
|
||||
|
||||
def test_create_course_with_course_creation_disabled_staff(self):
|
||||
"""Test new course creation -- course creation disabled, but staff access."""
|
||||
@@ -1604,26 +1442,26 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
"""
|
||||
with mock.patch.dict('django.conf.settings.FEATURES', {'ALLOW_UNICODE_COURSE_ID': False}):
|
||||
error_message = "Special characters not allowed in organization, course number, and course run."
|
||||
self.course_data['org'] = u'Юникода'
|
||||
self.course_data['org'] = u'<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>'
|
||||
self.assert_create_course_failed(error_message)
|
||||
|
||||
self.course_data['number'] = u'échantillon'
|
||||
self.course_data['number'] = u'<EFBFBD><EFBFBD>chantillon'
|
||||
self.assert_create_course_failed(error_message)
|
||||
|
||||
self.course_data['run'] = u'όνομα'
|
||||
self.course_data['run'] = u'<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>'
|
||||
self.assert_create_course_failed(error_message)
|
||||
|
||||
def assert_course_permission_denied(self):
|
||||
"""
|
||||
Checks that the course did not get created due to a PermissionError.
|
||||
"""
|
||||
resp = self.client.ajax_post('/course', self.course_data)
|
||||
resp = self.client.ajax_post('/course/', self.course_data)
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
|
||||
def test_course_index_view_with_no_courses(self):
|
||||
"""Test viewing the index page with no courses"""
|
||||
# Create a course so there is something to view
|
||||
resp = self.client.get_html('/course')
|
||||
resp = self.client.get_html('/course/')
|
||||
self.assertContains(
|
||||
resp,
|
||||
'<h1 class="page-header">My Courses</h1>',
|
||||
@@ -1646,7 +1484,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
def test_course_index_view_with_course(self):
|
||||
"""Test viewing the index page with an existing course"""
|
||||
CourseFactory.create(display_name='Robot Super Educational Course')
|
||||
resp = self.client.get_html('/course')
|
||||
resp = self.client.get_html('/course/')
|
||||
self.assertContains(
|
||||
resp,
|
||||
'<h3 class="course-title">Robot Super Educational Course</h3>',
|
||||
@@ -1657,51 +1495,48 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
|
||||
def test_course_overview_view_with_course(self):
|
||||
"""Test viewing the course overview page with an existing course"""
|
||||
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
|
||||
loc = Location(['i4x', 'MITx', '999', 'course', Location.clean('Robot Super Course'), None])
|
||||
resp = self._show_course_overview(loc)
|
||||
course = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
resp = self._show_course_overview(course.id)
|
||||
self.assertContains(
|
||||
resp,
|
||||
'<article class="courseware-overview" data-locator="MITx.999.Robot_Super_Course/branch/draft/block/Robot_Super_Course">',
|
||||
'<article class="courseware-overview" data-locator="location:MITx+999+Robot_Super_Course+course+Robot_Super_Course" data-course-key="slashes:MITx+999+Robot_Super_Course">',
|
||||
status_code=200,
|
||||
html=True
|
||||
)
|
||||
|
||||
def test_create_item(self):
|
||||
"""Test creating a new xblock instance."""
|
||||
locator = _course_factory_create_course()
|
||||
course = _course_factory_create_course()
|
||||
|
||||
section_data = {
|
||||
'parent_locator': unicode(locator),
|
||||
'parent_locator': unicode(course.location),
|
||||
'category': 'chapter',
|
||||
'display_name': 'Section One',
|
||||
}
|
||||
|
||||
resp = self.client.ajax_post('/xblock', section_data)
|
||||
resp = self.client.ajax_post(reverse_url('xblock_handler'), section_data)
|
||||
_test_no_locations(self, resp, html=False)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
data = parse_json(resp)
|
||||
self.assertRegexpMatches(
|
||||
data['locator'],
|
||||
r"^MITx.999.Robot_Super_Course/branch/draft/block/chapter([0-9]|[a-f]){3,}$"
|
||||
r"location:MITx\+999\+Robot_Super_Course\+chapter\+([0-9]|[a-f]){3,}$"
|
||||
)
|
||||
|
||||
def test_capa_module(self):
|
||||
"""Test that a problem treats markdown specially."""
|
||||
locator = _course_factory_create_course()
|
||||
course = _course_factory_create_course()
|
||||
|
||||
problem_data = {
|
||||
'parent_locator': unicode(locator),
|
||||
'parent_locator': unicode(course.location),
|
||||
'category': 'problem'
|
||||
}
|
||||
|
||||
resp = self.client.ajax_post('/xblock', problem_data)
|
||||
|
||||
resp = self.client.ajax_post(reverse_url('xblock_handler'), problem_data)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
payload = parse_json(resp)
|
||||
problem_loc = loc_mapper().translate_locator_to_location(BlockUsageLocator(payload['locator']))
|
||||
problem_loc = UsageKey.from_string(payload['locator'])
|
||||
problem = get_modulestore(problem_loc).get_item(problem_loc)
|
||||
# should be a CapaDescriptor
|
||||
self.assertIsInstance(problem, CapaDescriptor, "New problem is not a CapaDescriptor")
|
||||
@@ -1714,53 +1549,51 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
Import and walk through some common URL endpoints. This just verifies non-500 and no other
|
||||
correct behavior, so it is not a deep test
|
||||
"""
|
||||
def test_get_html(page):
|
||||
def test_get_html(handler):
|
||||
# Helper function for getting HTML for a page in Studio and
|
||||
# checking that it does not error.
|
||||
resp = self.client.get_html(new_location.url_reverse(page))
|
||||
resp = self.client.get_html(
|
||||
get_url(handler, course_key, 'course_key_string')
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
_test_no_locations(self, resp)
|
||||
|
||||
import_from_xml(modulestore('direct'), 'common/test/data/', ['simple'])
|
||||
loc = Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None])
|
||||
new_location = loc_mapper().translate_location(loc.course_id, loc, True, True)
|
||||
_, course_items = import_from_xml(modulestore('direct'), 'common/test/data/', ['simple'])
|
||||
course_key = course_items[0].id
|
||||
|
||||
resp = self._show_course_overview(loc)
|
||||
resp = self._show_course_overview(course_key)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertContains(resp, 'Chapter 2')
|
||||
|
||||
# go to various pages
|
||||
test_get_html('import')
|
||||
test_get_html('export')
|
||||
test_get_html('course_team')
|
||||
test_get_html('course_info')
|
||||
test_get_html('checklists')
|
||||
test_get_html('assets')
|
||||
test_get_html('tabs')
|
||||
test_get_html('settings/details')
|
||||
test_get_html('settings/grading')
|
||||
test_get_html('settings/advanced')
|
||||
test_get_html('textbooks')
|
||||
test_get_html('import_handler')
|
||||
test_get_html('export_handler')
|
||||
test_get_html('course_team_handler')
|
||||
test_get_html('course_info_handler')
|
||||
test_get_html('checklists_handler')
|
||||
test_get_html('assets_handler')
|
||||
test_get_html('tabs_handler')
|
||||
test_get_html('settings_handler')
|
||||
test_get_html('grading_handler')
|
||||
test_get_html('advanced_settings_handler')
|
||||
test_get_html('textbooks_list_handler')
|
||||
|
||||
# go look at a subsection page
|
||||
subsection_location = loc.replace(category='sequential', name='test_sequence')
|
||||
subsection_locator = loc_mapper().translate_location(loc.course_id, subsection_location, True, True)
|
||||
resp = self.client.get_html(subsection_locator.url_reverse('subsection'))
|
||||
subsection_key = course_key.make_usage_key('sequential', 'test_sequence')
|
||||
resp = self.client.get_html(get_url('subsection_handler', subsection_key))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
_test_no_locations(self, resp)
|
||||
|
||||
# go look at the Edit page
|
||||
unit_location = loc.replace(category='vertical', name='test_vertical')
|
||||
unit_locator = loc_mapper().translate_location(loc.course_id, unit_location, True, True)
|
||||
resp = self.client.get_html(unit_locator.url_reverse('unit'))
|
||||
unit_key = course_key.make_usage_key('vertical', 'test_vertical')
|
||||
resp = self.client.get_html(get_url('unit_handler', unit_key))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
_test_no_locations(self, resp)
|
||||
|
||||
def delete_item(category, name):
|
||||
""" Helper method for testing the deletion of an xblock item. """
|
||||
del_loc = loc.replace(category=category, name=name)
|
||||
del_location = loc_mapper().translate_location(loc.course_id, del_loc, True, True)
|
||||
resp = self.client.delete(del_location.url_reverse('xblock'))
|
||||
item_key = course_key.make_usage_key(category, name)
|
||||
resp = self.client.delete(get_url('xblock_handler', item_key))
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
_test_no_locations(self, resp, status_code=204, html=False)
|
||||
|
||||
@@ -1778,23 +1611,12 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
|
||||
def test_import_into_new_course_id(self):
|
||||
module_store = modulestore('direct')
|
||||
target_location = Location(['i4x', 'MITx', '999', 'course', '2013_Spring'])
|
||||
target_course_id = _get_course_id(self.course_data)
|
||||
_create_course(self, target_course_id, self.course_data)
|
||||
|
||||
course_data = {
|
||||
'org': target_location.org,
|
||||
'number': target_location.course,
|
||||
'display_name': 'Robot Super Course',
|
||||
'run': target_location.name
|
||||
}
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'], target_course_id=target_course_id)
|
||||
|
||||
target_course_id = '{0}/{1}/{2}'.format(target_location.org, target_location.course, target_location.name)
|
||||
|
||||
_create_course(self, course_data)
|
||||
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'], target_location_namespace=target_location)
|
||||
|
||||
modules = module_store.get_items(Location([
|
||||
target_location.tag, target_location.org, target_location.course, None, None, None]))
|
||||
modules = module_store.get_items(target_course_id)
|
||||
|
||||
# we should have a number of modules in there
|
||||
# we can't specify an exact number since it'll always be changing
|
||||
@@ -1805,7 +1627,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
#
|
||||
|
||||
# first check PDF textbooks, to make sure the url paths got updated
|
||||
course_module = module_store.get_instance(target_course_id, target_location)
|
||||
course_module = module_store.get_course(target_course_id)
|
||||
|
||||
self.assertEqual(len(course_module.pdf_textbooks), 1)
|
||||
self.assertEqual(len(course_module.pdf_textbooks[0]["chapters"]), 2)
|
||||
@@ -1816,41 +1638,41 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
module_store = modulestore('direct')
|
||||
|
||||
# If reimporting into the same course do not change the wiki_slug.
|
||||
target_location = Location('i4x', 'edX', 'toy', 'course', '2012_Fall')
|
||||
target_course_id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
|
||||
course_data = {
|
||||
'org': target_location.org,
|
||||
'number': target_location.course,
|
||||
'org': target_course_id.org,
|
||||
'number': target_course_id.course,
|
||||
'display_name': 'Robot Super Course',
|
||||
'run': target_location.name
|
||||
'run': target_course_id.run
|
||||
}
|
||||
_create_course(self, course_data)
|
||||
course_module = module_store.get_instance(target_location.course_id, target_location)
|
||||
_create_course(self, target_course_id, course_data)
|
||||
course_module = module_store.get_course(target_course_id)
|
||||
course_module.wiki_slug = 'toy'
|
||||
course_module.save()
|
||||
|
||||
# Import a course with wiki_slug == location.course
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'], target_location_namespace=target_location)
|
||||
course_module = module_store.get_instance(target_location.course_id, target_location)
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'], target_course_id=target_course_id)
|
||||
course_module = module_store.get_course(target_course_id)
|
||||
self.assertEquals(course_module.wiki_slug, 'toy')
|
||||
|
||||
# But change the wiki_slug if it is a different course.
|
||||
target_location = Location('i4x', 'MITx', '999', 'course', '2013_Spring')
|
||||
target_course_id = SlashSeparatedCourseKey('MITx', '999', '2013_Spring')
|
||||
course_data = {
|
||||
'org': target_location.org,
|
||||
'number': target_location.course,
|
||||
'org': target_course_id.org,
|
||||
'number': target_course_id.course,
|
||||
'display_name': 'Robot Super Course',
|
||||
'run': target_location.name
|
||||
'run': target_course_id.run
|
||||
}
|
||||
_create_course(self, course_data)
|
||||
_create_course(self, target_course_id, course_data)
|
||||
|
||||
# Import a course with wiki_slug == location.course
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'], target_location_namespace=target_location)
|
||||
course_module = module_store.get_instance(target_location.course_id, target_location)
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'], target_course_id=target_course_id)
|
||||
course_module = module_store.get_course(target_course_id)
|
||||
self.assertEquals(course_module.wiki_slug, 'MITx.999.2013_Spring')
|
||||
|
||||
# Now try importing a course with wiki_slug == '{0}.{1}.{2}'.format(location.org, location.course, location.name)
|
||||
import_from_xml(module_store, 'common/test/data/', ['two_toys'], target_location_namespace=target_location)
|
||||
course_module = module_store.get_instance(target_location.course_id, target_location)
|
||||
# Now try importing a course with wiki_slug == '{0}.{1}.{2}'.format(location.org, location.course, location.run)
|
||||
import_from_xml(module_store, 'common/test/data/', ['two_toys'], target_course_id=target_course_id)
|
||||
course_module = module_store.get_course(target_course_id)
|
||||
self.assertEquals(course_module.wiki_slug, 'MITx.999.2013_Spring')
|
||||
|
||||
def test_import_metadata_with_attempts_empty_string(self):
|
||||
@@ -1858,7 +1680,9 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
import_from_xml(module_store, 'common/test/data/', ['simple'])
|
||||
did_load_item = False
|
||||
try:
|
||||
module_store.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]))
|
||||
course_key = SlashSeparatedCourseKey('edX', 'simple', 'problem')
|
||||
usage_key = course_key.make_usage_key('problem', 'ps01-simple')
|
||||
module_store.get_item(usage_key)
|
||||
did_load_item = True
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
@@ -1868,9 +1692,8 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
|
||||
def test_forum_id_generation(self):
|
||||
module_store = modulestore('direct')
|
||||
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
|
||||
|
||||
new_component_location = Location('i4x', 'edX', '999', 'discussion', 'new_component')
|
||||
course = CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
|
||||
new_component_location = course.id.make_usage_key('discussion', 'new_component')
|
||||
|
||||
# crate a new module and add it as a child to a vertical
|
||||
module_store.create_and_save_xmodule(new_component_location)
|
||||
@@ -1879,37 +1702,12 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
|
||||
self.assertNotEquals(new_discussion_item.discussion_id, '$$GUID$$')
|
||||
|
||||
def test_update_modulestore_signal_did_fire(self):
|
||||
module_store = modulestore('direct')
|
||||
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
|
||||
|
||||
try:
|
||||
module_store.modulestore_update_signal = Signal(providing_args=['modulestore', 'course_id', 'location'])
|
||||
|
||||
self.got_signal = False
|
||||
|
||||
def _signal_hander(modulestore=None, course_id=None, location=None, **kwargs):
|
||||
self.got_signal = True
|
||||
|
||||
module_store.modulestore_update_signal.connect(_signal_hander)
|
||||
|
||||
new_component_location = Location('i4x', 'edX', '999', 'html', 'new_component')
|
||||
|
||||
# crate a new module
|
||||
module_store.create_and_save_xmodule(new_component_location)
|
||||
|
||||
finally:
|
||||
module_store.modulestore_update_signal = None
|
||||
|
||||
self.assertTrue(self.got_signal)
|
||||
|
||||
def test_metadata_inheritance(self):
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
_, course_items = import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
|
||||
course = module_store.get_item(Location(['i4x', 'edX', 'toy', 'course', '2012_Fall', None]))
|
||||
|
||||
verticals = module_store.get_items(Location('i4x', 'edX', 'toy', 'vertical', None, None))
|
||||
course = course_items[0]
|
||||
verticals = module_store.get_items(course.id, category='vertical')
|
||||
|
||||
# let's assert on the metadata_inheritance on an existing vertical
|
||||
for vertical in verticals:
|
||||
@@ -1918,16 +1716,16 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
|
||||
self.assertGreater(len(verticals), 0)
|
||||
|
||||
new_component_location = Location('i4x', 'edX', 'toy', 'html', 'new_component')
|
||||
new_component_location = course.id.make_usage_key('html', 'new_component')
|
||||
|
||||
# crate a new module and add it as a child to a vertical
|
||||
module_store.create_and_save_xmodule(new_component_location)
|
||||
parent = verticals[0]
|
||||
parent.children.append(new_component_location.url())
|
||||
parent.children.append(new_component_location)
|
||||
module_store.update_item(parent, self.user.id)
|
||||
|
||||
# flush the cache
|
||||
module_store.refresh_cached_metadata_inheritance_tree(new_component_location)
|
||||
module_store.refresh_cached_metadata_inheritance_tree(new_component_location.course_key)
|
||||
new_module = module_store.get_item(new_component_location)
|
||||
|
||||
# check for grace period definition which should be defined at the course level
|
||||
@@ -1944,7 +1742,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
module_store.update_item(new_module, self.user.id)
|
||||
|
||||
# flush the cache and refetch
|
||||
module_store.refresh_cached_metadata_inheritance_tree(new_component_location)
|
||||
module_store.refresh_cached_metadata_inheritance_tree(new_component_location.course_key)
|
||||
new_module = module_store.get_item(new_component_location)
|
||||
|
||||
self.assertEqual(timedelta(1), new_module.graceperiod)
|
||||
@@ -1992,24 +1790,23 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
self.assertEqual(course.course_image, 'images_course_image.jpg')
|
||||
|
||||
# Ensure that the imported course image is present -- this shouldn't raise an exception
|
||||
location = course.location._replace(tag='c4x', category='asset', name=course.course_image)
|
||||
content_store.find(location)
|
||||
asset_key = course.id.make_asset_key('asset', course.course_image)
|
||||
content_store.find(asset_key)
|
||||
|
||||
def _show_course_overview(self, location):
|
||||
def _show_course_overview(self, course_key):
|
||||
"""
|
||||
Show the course overview page.
|
||||
"""
|
||||
new_location = loc_mapper().translate_location(location.course_id, location, True, True)
|
||||
resp = self.client.get_html(new_location.url_reverse('course/', ''))
|
||||
resp = self.client.get_html(get_url('course_handler', course_key, 'course_key_string'))
|
||||
_test_no_locations(self, resp)
|
||||
return resp
|
||||
|
||||
def test_wiki_slug(self):
|
||||
"""When creating a course a unique wiki_slug should be set."""
|
||||
|
||||
course_location = Location(['i4x', 'MITx', '999', 'course', '2013_Spring'])
|
||||
_create_course(self, self.course_data)
|
||||
course_module = modulestore('direct').get_item(course_location)
|
||||
course_key = _get_course_id(self.course_data)
|
||||
_create_course(self, course_key, self.course_data)
|
||||
course_module = modulestore('direct').get_course(course_key)
|
||||
self.assertEquals(course_module.wiki_slug, 'MITx.999.2013_Spring')
|
||||
|
||||
|
||||
@@ -2018,10 +1815,8 @@ class MetadataSaveTestCase(ModuleStoreTestCase):
|
||||
"""Test that metadata is correctly cached and decached."""
|
||||
|
||||
def setUp(self):
|
||||
CourseFactory.create(
|
||||
course = CourseFactory.create(
|
||||
org='edX', course='999', display_name='Robot Super Course')
|
||||
course_location = Location(
|
||||
['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None])
|
||||
|
||||
video_sample_xml = '''
|
||||
<video display_name="Test Video"
|
||||
@@ -2034,7 +1829,7 @@ class MetadataSaveTestCase(ModuleStoreTestCase):
|
||||
</video>
|
||||
'''
|
||||
self.video_descriptor = ItemFactory.create(
|
||||
parent_location=course_location, category='video',
|
||||
parent_location=course.location, category='video',
|
||||
data={'data': video_sample_xml}
|
||||
)
|
||||
|
||||
@@ -2100,31 +1895,28 @@ class EntryPageTestCase(TestCase):
|
||||
self._test_page("/logout", 302)
|
||||
|
||||
|
||||
def _create_course(test, course_data):
|
||||
def _create_course(test, course_key, course_data):
|
||||
"""
|
||||
Creates a course via an AJAX request and verifies the URL returned in the response.
|
||||
"""
|
||||
course_id = _get_course_id(course_data)
|
||||
new_location = loc_mapper().translate_location(course_id, CourseDescriptor.id_to_location(course_id), False, True)
|
||||
|
||||
response = test.client.ajax_post('/course', course_data)
|
||||
course_url = get_url('course_handler', course_key, 'course_key_string')
|
||||
response = test.client.ajax_post(course_url, course_data)
|
||||
test.assertEqual(response.status_code, 200)
|
||||
data = parse_json(response)
|
||||
test.assertNotIn('ErrMsg', data)
|
||||
test.assertEqual(data['url'], new_location.url_reverse("course"))
|
||||
test.assertEqual(data['url'], course_url)
|
||||
|
||||
|
||||
def _course_factory_create_course():
|
||||
"""
|
||||
Creates a course via the CourseFactory and returns the locator for it.
|
||||
"""
|
||||
course = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
return loc_mapper().translate_location(course.id, course.location, False, True)
|
||||
return CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
|
||||
|
||||
def _get_course_id(test_course_data):
|
||||
def _get_course_id(course_data):
|
||||
"""Returns the course ID (org/number/run)."""
|
||||
return u"{org}/{number}/{run}".format(**test_course_data)
|
||||
return SlashSeparatedCourseKey(course_data['org'], course_data['number'], course_data['run'])
|
||||
|
||||
|
||||
def _test_no_locations(test, resp, status_code=200, html=True):
|
||||
|
||||
@@ -10,14 +10,14 @@ class Content:
|
||||
self.content = content
|
||||
|
||||
def get_id(self):
|
||||
return StaticContent.get_id_from_location(self.location)
|
||||
return self.location.to_deprecated_son()
|
||||
|
||||
|
||||
class CachingTestCase(TestCase):
|
||||
# Tests for https://edx.lighthouseapp.com/projects/102637/tickets/112-updating-asset-does-not-refresh-the-cached-copy
|
||||
unicodeLocation = Location(u'c4x', u'mitX', u'800', u'thumbnail', u'monsters.jpg')
|
||||
unicodeLocation = Location(u'c4x', u'mitX', u'800', u'run', u'thumbnail', u'monsters.jpg')
|
||||
# Note that some of the parts are strings instead of unicode strings
|
||||
nonUnicodeLocation = Location('c4x', u'mitX', u'800', 'thumbnail', 'monsters.jpg')
|
||||
nonUnicodeLocation = Location('c4x', u'mitX', u'800', u'run', 'thumbnail', 'monsters.jpg')
|
||||
mockAsset = Content(unicodeLocation, 'my content')
|
||||
|
||||
def test_put_and_get(self):
|
||||
|
||||
@@ -4,21 +4,19 @@ by reversing group name formats.
|
||||
"""
|
||||
import random
|
||||
from chrono import Timer
|
||||
from unittest import skip
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
from django.test import RequestFactory
|
||||
|
||||
from contentstore.views.course import _accessible_courses_list, _accessible_courses_list_from_groups
|
||||
from contentstore.utils import delete_course_and_groups
|
||||
from contentstore.utils import delete_course_and_groups, reverse_course_url
|
||||
from contentstore.tests.utils import AjaxEnabledTestClient
|
||||
from student.tests.factories import UserFactory
|
||||
from student.roles import CourseInstructorRole, CourseStaffRole
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
TOTAL_COURSES_COUNT = 500
|
||||
USER_COURSES_COUNT = 50
|
||||
@@ -39,39 +37,20 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
self.client = AjaxEnabledTestClient()
|
||||
self.client.login(username=self.user.username, password='test')
|
||||
|
||||
def _create_course_with_access_groups(self, course_location, group_name_format='group_name_with_dots', user=None):
|
||||
def _create_course_with_access_groups(self, course_location, user=None):
|
||||
"""
|
||||
Create dummy course with 'CourseFactory' and role (instructor/staff) groups with provided group_name_format
|
||||
Create dummy course with 'CourseFactory' and role (instructor/staff) groups
|
||||
"""
|
||||
course_locator = loc_mapper().translate_location(
|
||||
course_location.course_id, course_location, False, True
|
||||
)
|
||||
course = CourseFactory.create(
|
||||
org=course_location.org,
|
||||
number=course_location.course,
|
||||
display_name=course_location.name
|
||||
run=course_location.run
|
||||
)
|
||||
|
||||
for role in [CourseInstructorRole, CourseStaffRole]:
|
||||
# pylint: disable=protected-access
|
||||
groupnames = role(course_locator)._group_names
|
||||
if group_name_format == 'group_name_with_course_name_only':
|
||||
# Create role (instructor/staff) groups with course_name only: 'instructor_run'
|
||||
group, __ = Group.objects.get_or_create(name=groupnames[2])
|
||||
elif group_name_format == 'group_name_with_slashes':
|
||||
# Create role (instructor/staff) groups with format: 'instructor_edX/Course/Run'
|
||||
# Since "Group.objects.get_or_create(name=groupnames[1])" would have made group with lowercase name
|
||||
# so manually create group name of old type
|
||||
if role == CourseInstructorRole:
|
||||
group, __ = Group.objects.get_or_create(name=u'{}_{}'.format('instructor', course_location.course_id))
|
||||
else:
|
||||
group, __ = Group.objects.get_or_create(name=u'{}_{}'.format('staff', course_location.course_id))
|
||||
else:
|
||||
# Create role (instructor/staff) groups with format: 'instructor_edx.course.run'
|
||||
group, __ = Group.objects.get_or_create(name=groupnames[0])
|
||||
if user is not None:
|
||||
for role in [CourseInstructorRole, CourseStaffRole]:
|
||||
role(course.id).add_users(user)
|
||||
|
||||
if user is not None:
|
||||
user.groups.add(group)
|
||||
return course
|
||||
|
||||
def tearDown(self):
|
||||
@@ -85,11 +64,11 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
"""
|
||||
Test getting courses with new access group format e.g. 'instructor_edx.course.run'
|
||||
"""
|
||||
request = self.factory.get('/course')
|
||||
request = self.factory.get('/course/')
|
||||
request.user = self.user
|
||||
|
||||
course_location = Location(['i4x', 'Org1', 'Course1', 'course', 'Run1'])
|
||||
self._create_course_with_access_groups(course_location, 'group_name_with_dots', self.user)
|
||||
course_location = SlashSeparatedCourseKey('Org1', 'Course1', 'Run1')
|
||||
self._create_course_with_access_groups(course_location, self.user)
|
||||
|
||||
# get courses through iterating all courses
|
||||
courses_list = _accessible_courses_list(request)
|
||||
@@ -101,61 +80,15 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
# check both course lists have same courses
|
||||
self.assertEqual(courses_list, courses_list_by_groups)
|
||||
|
||||
def test_get_course_list_with_old_group_formats(self):
|
||||
"""
|
||||
Test getting all courses with old course role (instructor/staff) groups
|
||||
"""
|
||||
request = self.factory.get('/course')
|
||||
request.user = self.user
|
||||
|
||||
# create a course with new groups name format e.g. 'instructor_edx.course.run'
|
||||
course_location = Location(['i4x', 'Org_1', 'Course_1', 'course', 'Run_1'])
|
||||
self._create_course_with_access_groups(course_location, 'group_name_with_dots', self.user)
|
||||
|
||||
# create a course with old groups name format e.g. 'instructor_edX/Course/Run'
|
||||
old_course_location = Location(['i4x', 'Org_2', 'Course_2', 'course', 'Run_2'])
|
||||
self._create_course_with_access_groups(old_course_location, 'group_name_with_slashes', self.user)
|
||||
|
||||
# get courses through iterating all courses
|
||||
courses_list = _accessible_courses_list(request)
|
||||
self.assertEqual(len(courses_list), 2)
|
||||
|
||||
# get courses by reversing groups name
|
||||
courses_list_by_groups = _accessible_courses_list_from_groups(request)
|
||||
self.assertEqual(len(courses_list_by_groups), 2)
|
||||
|
||||
# create a new course with older group name format (with dots in names) e.g. 'instructor_edX/Course.name/Run.1'
|
||||
old_course_location = Location(['i4x', 'Org.Foo.Bar', 'Course.number', 'course', 'Run.name'])
|
||||
self._create_course_with_access_groups(old_course_location, 'group_name_with_slashes', self.user)
|
||||
# get courses through iterating all courses
|
||||
courses_list = _accessible_courses_list(request)
|
||||
self.assertEqual(len(courses_list), 3)
|
||||
# get courses by reversing group name formats
|
||||
courses_list_by_groups = _accessible_courses_list_from_groups(request)
|
||||
self.assertEqual(len(courses_list_by_groups), 3)
|
||||
|
||||
# create a new course with older group name format e.g. 'instructor_Run'
|
||||
old_course_location = Location(['i4x', 'Org_3', 'Course_3', 'course', 'Run_3'])
|
||||
self._create_course_with_access_groups(old_course_location, 'group_name_with_course_name_only', self.user)
|
||||
|
||||
# get courses through iterating all courses
|
||||
courses_list = _accessible_courses_list(request)
|
||||
self.assertEqual(len(courses_list), 4)
|
||||
|
||||
# should raise an exception for getting courses with older format of access group by reversing django groups
|
||||
with self.assertRaises(ItemNotFoundError):
|
||||
courses_list_by_groups = _accessible_courses_list_from_groups(request)
|
||||
|
||||
def test_get_course_list_with_invalid_course_location(self):
|
||||
"""
|
||||
Test getting courses with invalid course location (course deleted from modulestore but
|
||||
location exists in loc_mapper).
|
||||
Test getting courses with invalid course location (course deleted from modulestore).
|
||||
"""
|
||||
request = self.factory.get('/course')
|
||||
request.user = self.user
|
||||
|
||||
course_location = Location('i4x', 'Org', 'Course', 'course', 'Run')
|
||||
self._create_course_with_access_groups(course_location, 'group_name_with_dots', self.user)
|
||||
course_key = SlashSeparatedCourseKey('Org', 'Course', 'Run')
|
||||
self._create_course_with_access_groups(course_key, self.user)
|
||||
|
||||
# get courses through iterating all courses
|
||||
courses_list = _accessible_courses_list(request)
|
||||
@@ -168,12 +101,9 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
self.assertEqual(courses_list, courses_list_by_groups)
|
||||
|
||||
# now delete this course and re-add user to instructor group of this course
|
||||
delete_course_and_groups(course_location.course_id, commit=True)
|
||||
delete_course_and_groups(course_key, commit=True)
|
||||
|
||||
course_locator = loc_mapper().translate_location(course_location.course_id, course_location)
|
||||
instructor_group_name = CourseInstructorRole(course_locator)._group_names[0] # pylint: disable=protected-access
|
||||
group, __ = Group.objects.get_or_create(name=instructor_group_name)
|
||||
self.user.groups.add(group)
|
||||
CourseInstructorRole(course_key).add_users(self.user)
|
||||
|
||||
# test that get courses through iterating all courses now returns no course
|
||||
courses_list = _accessible_courses_list(request)
|
||||
@@ -203,11 +133,11 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
org = 'Org{0}'.format(number)
|
||||
course = 'Course{0}'.format(number)
|
||||
run = 'Run{0}'.format(number)
|
||||
course_location = Location(['i4x', org, course, 'course', run])
|
||||
course_location = SlashSeparatedCourseKey(org, course, run)
|
||||
if number in user_course_ids:
|
||||
self._create_course_with_access_groups(course_location, 'group_name_with_dots', self.user)
|
||||
self._create_course_with_access_groups(course_location, self.user)
|
||||
else:
|
||||
self._create_course_with_access_groups(course_location, 'group_name_with_dots')
|
||||
self._create_course_with_access_groups(course_location)
|
||||
|
||||
# time the get courses by iterating through all courses
|
||||
with Timer() as iteration_over_courses_time_1:
|
||||
@@ -245,8 +175,8 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
request.user = self.user
|
||||
self.client.login(username=self.user.username, password='test')
|
||||
|
||||
course_location_caps = Location(['i4x', 'Org', 'COURSE', 'course', 'Run'])
|
||||
self._create_course_with_access_groups(course_location_caps, 'group_name_with_dots', self.user)
|
||||
course_location_caps = SlashSeparatedCourseKey('Org', 'COURSE', 'Run')
|
||||
self._create_course_with_access_groups(course_location_caps, self.user)
|
||||
|
||||
# get courses through iterating all courses
|
||||
courses_list = _accessible_courses_list(request)
|
||||
@@ -259,34 +189,19 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
self.assertEqual(courses_list, courses_list_by_groups)
|
||||
|
||||
# now create another course with same course_id but different name case
|
||||
course_location_camel = Location(['i4x', 'Org', 'Course', 'course', 'Run'])
|
||||
self._create_course_with_access_groups(course_location_camel, 'group_name_with_dots', self.user)
|
||||
course_location_camel = SlashSeparatedCourseKey('Org', 'Course', 'Run')
|
||||
self._create_course_with_access_groups(course_location_camel, self.user)
|
||||
|
||||
# test that get courses through iterating all courses returns both courses
|
||||
courses_list = _accessible_courses_list(request)
|
||||
self.assertEqual(len(courses_list), 2)
|
||||
|
||||
# test that get courses by reversing group name formats returns only one course
|
||||
# test that get courses by reversing group name formats returns both courses
|
||||
courses_list_by_groups = _accessible_courses_list_from_groups(request)
|
||||
self.assertEqual(len(courses_list_by_groups), 1)
|
||||
self.assertEqual(len(courses_list_by_groups), 2)
|
||||
|
||||
course_locator = loc_mapper().translate_location(course_location_caps.course_id, course_location_caps)
|
||||
outline_url = course_locator.url_reverse('course/')
|
||||
# now delete first course (course_location_caps) and check that it is no longer accessible
|
||||
delete_course_and_groups(course_location_caps.course_id, commit=True)
|
||||
# add user to this course instructor group since he was removed from that group on course delete
|
||||
instructor_group_name = CourseInstructorRole(course_locator)._group_names[0] # pylint: disable=protected-access
|
||||
group, __ = Group.objects.get_or_create(name=instructor_group_name)
|
||||
self.user.groups.add(group)
|
||||
|
||||
# test viewing the index page which creates missing courses loc_map entries
|
||||
resp = self.client.get_html('/course')
|
||||
self.assertContains(
|
||||
resp,
|
||||
'<h1 class="page-header">My Courses</h1>',
|
||||
status_code=200,
|
||||
html=True
|
||||
)
|
||||
delete_course_and_groups(course_location_caps, commit=True)
|
||||
|
||||
# test that get courses through iterating all courses now returns one course
|
||||
courses_list = _accessible_courses_list(request)
|
||||
@@ -296,12 +211,12 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
courses_list_by_groups = _accessible_courses_list_from_groups(request)
|
||||
self.assertEqual(len(courses_list_by_groups), 1)
|
||||
|
||||
# now check that deleted course in not accessible
|
||||
# now check that deleted course is not accessible
|
||||
outline_url = reverse_course_url('course_handler', course_location_caps)
|
||||
response = self.client.get(outline_url, HTTP_ACCEPT='application/json')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
# now check that other course in accessible
|
||||
course_locator = loc_mapper().translate_location(course_location_camel.course_id, course_location_camel)
|
||||
outline_url = course_locator.url_reverse('course/')
|
||||
# now check that other course is accessible
|
||||
outline_url = reverse_course_url('course_handler', course_location_camel)
|
||||
response = self.client.get(outline_url, HTTP_ACCEPT='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@@ -11,24 +11,26 @@ from django.test.utils import override_settings
|
||||
|
||||
from models.settings.course_details import (CourseDetails, CourseSettingsEncoder)
|
||||
from models.settings.course_grading import CourseGradingModel
|
||||
from contentstore.utils import get_modulestore, EXTRA_TAB_PANELS
|
||||
from contentstore.utils import get_modulestore, EXTRA_TAB_PANELS, reverse_course_url, reverse_usage_url
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
|
||||
from models.settings.course_metadata import CourseMetadata
|
||||
from xmodule.fields import Date
|
||||
|
||||
from .utils import CourseTestCase
|
||||
from xmodule.modulestore.django import loc_mapper, modulestore
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from contentstore.views.component import ADVANCED_COMPONENT_POLICY_KEY
|
||||
|
||||
|
||||
def get_url(course_id, handler_name='settings_handler'):
|
||||
return reverse_course_url(handler_name, course_id)
|
||||
|
||||
class CourseDetailsTestCase(CourseTestCase):
|
||||
"""
|
||||
Tests the first course settings page (course dates, overview, etc.).
|
||||
"""
|
||||
def test_virgin_fetch(self):
|
||||
details = CourseDetails.fetch(self.course_locator)
|
||||
details = CourseDetails.fetch(self.course.id)
|
||||
self.assertEqual(details.org, self.course.location.org, "Org not copied into")
|
||||
self.assertEqual(details.course_id, self.course.location.course, "Course_id not copied into")
|
||||
self.assertEqual(details.run, self.course.location.name, "Course name not copied into")
|
||||
@@ -42,7 +44,7 @@ class CourseDetailsTestCase(CourseTestCase):
|
||||
self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort))
|
||||
|
||||
def test_encoder(self):
|
||||
details = CourseDetails.fetch(self.course_locator)
|
||||
details = CourseDetails.fetch(self.course.id)
|
||||
jsondetails = json.dumps(details, cls=CourseSettingsEncoder)
|
||||
jsondetails = json.loads(jsondetails)
|
||||
self.assertEqual(jsondetails['course_image_name'], self.course.course_image)
|
||||
@@ -69,47 +71,47 @@ class CourseDetailsTestCase(CourseTestCase):
|
||||
self.assertEqual(jsondetails['string'], 'string')
|
||||
|
||||
def test_update_and_fetch(self):
|
||||
jsondetails = CourseDetails.fetch(self.course_locator)
|
||||
jsondetails = CourseDetails.fetch(self.course.id)
|
||||
jsondetails.syllabus = "<a href='foo'>bar</a>"
|
||||
# encode - decode to convert date fields and other data which changes form
|
||||
self.assertEqual(
|
||||
CourseDetails.update_from_json(self.course_locator, jsondetails.__dict__, self.user).syllabus,
|
||||
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).syllabus,
|
||||
jsondetails.syllabus, "After set syllabus"
|
||||
)
|
||||
jsondetails.short_description = "Short Description"
|
||||
self.assertEqual(
|
||||
CourseDetails.update_from_json(self.course_locator, jsondetails.__dict__, self.user).short_description,
|
||||
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).short_description,
|
||||
jsondetails.short_description, "After set short_description"
|
||||
)
|
||||
jsondetails.overview = "Overview"
|
||||
self.assertEqual(
|
||||
CourseDetails.update_from_json(self.course_locator, jsondetails.__dict__, self.user).overview,
|
||||
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).overview,
|
||||
jsondetails.overview, "After set overview"
|
||||
)
|
||||
jsondetails.intro_video = "intro_video"
|
||||
self.assertEqual(
|
||||
CourseDetails.update_from_json(self.course_locator, jsondetails.__dict__, self.user).intro_video,
|
||||
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).intro_video,
|
||||
jsondetails.intro_video, "After set intro_video"
|
||||
)
|
||||
jsondetails.effort = "effort"
|
||||
self.assertEqual(
|
||||
CourseDetails.update_from_json(self.course_locator, jsondetails.__dict__, self.user).effort,
|
||||
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).effort,
|
||||
jsondetails.effort, "After set effort"
|
||||
)
|
||||
jsondetails.start_date = datetime.datetime(2010, 10, 1, 0, tzinfo=UTC())
|
||||
self.assertEqual(
|
||||
CourseDetails.update_from_json(self.course_locator, jsondetails.__dict__, self.user).start_date,
|
||||
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).start_date,
|
||||
jsondetails.start_date
|
||||
)
|
||||
jsondetails.course_image_name = "an_image.jpg"
|
||||
self.assertEqual(
|
||||
CourseDetails.update_from_json(self.course_locator, jsondetails.__dict__, self.user).course_image_name,
|
||||
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).course_image_name,
|
||||
jsondetails.course_image_name
|
||||
)
|
||||
|
||||
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
|
||||
def test_marketing_site_fetch(self):
|
||||
settings_details_url = self.course_locator.url_reverse('settings/details/')
|
||||
settings_details_url = get_url(self.course.id)
|
||||
|
||||
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
|
||||
response = self.client.get_html(settings_details_url)
|
||||
@@ -131,7 +133,7 @@ class CourseDetailsTestCase(CourseTestCase):
|
||||
self.assertNotContains(response, "Requirements")
|
||||
|
||||
def test_editable_short_description_fetch(self):
|
||||
settings_details_url = self.course_locator.url_reverse('settings/details/')
|
||||
settings_details_url = get_url(self.course.id)
|
||||
|
||||
with mock.patch.dict('django.conf.settings.FEATURES', {'EDITABLE_SHORT_DESCRIPTION': False}):
|
||||
response = self.client.get_html(settings_details_url)
|
||||
@@ -139,7 +141,7 @@ class CourseDetailsTestCase(CourseTestCase):
|
||||
|
||||
|
||||
def test_regular_site_fetch(self):
|
||||
settings_details_url = self.course_locator.url_reverse('settings/details/')
|
||||
settings_details_url = get_url(self.course.id)
|
||||
|
||||
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': False}):
|
||||
response = self.client.get_html(settings_details_url)
|
||||
@@ -187,10 +189,10 @@ class CourseDetailsViewTest(CourseTestCase):
|
||||
return Date().to_json(datetime_obj)
|
||||
|
||||
def test_update_and_fetch(self):
|
||||
details = CourseDetails.fetch(self.course_locator)
|
||||
details = CourseDetails.fetch(self.course.id)
|
||||
|
||||
# resp s/b json from here on
|
||||
url = self.course_locator.url_reverse('settings/details/')
|
||||
url = get_url(self.course.id)
|
||||
resp = self.client.get_json(url)
|
||||
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, "virgin get")
|
||||
|
||||
@@ -248,93 +250,93 @@ class CourseGradingTest(CourseTestCase):
|
||||
self.assertIsNotNone(test_grader.grade_cutoffs)
|
||||
|
||||
def test_fetch_grader(self):
|
||||
test_grader = CourseGradingModel.fetch(self.course_locator)
|
||||
test_grader = CourseGradingModel.fetch(self.course.id)
|
||||
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_locator, i)
|
||||
subgrader = CourseGradingModel.fetch_grader(self.course.id, i)
|
||||
self.assertDictEqual(grader, subgrader, str(i) + "th graders not equal")
|
||||
|
||||
def test_update_from_json(self):
|
||||
test_grader = CourseGradingModel.fetch(self.course_locator)
|
||||
altered_grader = CourseGradingModel.update_from_json(self.course_locator, test_grader.__dict__, self.user)
|
||||
test_grader = CourseGradingModel.fetch(self.course.id)
|
||||
altered_grader = CourseGradingModel.update_from_json(self.course.id, test_grader.__dict__, self.user)
|
||||
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Noop update")
|
||||
|
||||
test_grader.graders[0]['weight'] = test_grader.graders[0].get('weight') * 2
|
||||
altered_grader = CourseGradingModel.update_from_json(self.course_locator, test_grader.__dict__, self.user)
|
||||
altered_grader = CourseGradingModel.update_from_json(self.course.id, test_grader.__dict__, self.user)
|
||||
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Weight[0] * 2")
|
||||
|
||||
test_grader.grade_cutoffs['D'] = 0.3
|
||||
altered_grader = CourseGradingModel.update_from_json(self.course_locator, test_grader.__dict__, self.user)
|
||||
altered_grader = CourseGradingModel.update_from_json(self.course.id, test_grader.__dict__, self.user)
|
||||
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D")
|
||||
|
||||
test_grader.grace_period = {'hours': 4, 'minutes': 5, 'seconds': 0}
|
||||
altered_grader = CourseGradingModel.update_from_json(self.course_locator, test_grader.__dict__, self.user)
|
||||
altered_grader = CourseGradingModel.update_from_json(self.course.id, test_grader.__dict__, self.user)
|
||||
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_locator)
|
||||
test_grader = CourseGradingModel.fetch(self.course.id)
|
||||
altered_grader = CourseGradingModel.update_grader_from_json(
|
||||
self.course_locator, test_grader.graders[1], self.user
|
||||
self.course.id, test_grader.graders[1], self.user
|
||||
)
|
||||
self.assertDictEqual(test_grader.graders[1], altered_grader, "Noop update")
|
||||
|
||||
test_grader.graders[1]['min_count'] = test_grader.graders[1].get('min_count') + 2
|
||||
altered_grader = CourseGradingModel.update_grader_from_json(
|
||||
self.course_locator, test_grader.graders[1], self.user)
|
||||
self.course.id, test_grader.graders[1], self.user)
|
||||
self.assertDictEqual(test_grader.graders[1], altered_grader, "min_count[1] + 2")
|
||||
|
||||
test_grader.graders[1]['drop_count'] = test_grader.graders[1].get('drop_count') + 1
|
||||
altered_grader = CourseGradingModel.update_grader_from_json(
|
||||
self.course_locator, test_grader.graders[1], self.user)
|
||||
self.course.id, test_grader.graders[1], self.user)
|
||||
self.assertDictEqual(test_grader.graders[1], altered_grader, "drop_count[1] + 2")
|
||||
|
||||
def test_update_cutoffs_from_json(self):
|
||||
test_grader = CourseGradingModel.fetch(self.course_locator)
|
||||
CourseGradingModel.update_cutoffs_from_json(self.course_locator, test_grader.grade_cutoffs, self.user)
|
||||
test_grader = CourseGradingModel.fetch(self.course.id)
|
||||
CourseGradingModel.update_cutoffs_from_json(self.course.id, test_grader.grade_cutoffs, self.user)
|
||||
# Unlike other tests, need to actually perform a db fetch for this test since update_cutoffs_from_json
|
||||
# simply returns the cutoffs you send into it, rather than returning the db contents.
|
||||
altered_grader = CourseGradingModel.fetch(self.course_locator)
|
||||
altered_grader = CourseGradingModel.fetch(self.course.id)
|
||||
self.assertDictEqual(test_grader.grade_cutoffs, altered_grader.grade_cutoffs, "Noop update")
|
||||
|
||||
test_grader.grade_cutoffs['D'] = 0.3
|
||||
CourseGradingModel.update_cutoffs_from_json(self.course_locator, test_grader.grade_cutoffs, self.user)
|
||||
altered_grader = CourseGradingModel.fetch(self.course_locator)
|
||||
CourseGradingModel.update_cutoffs_from_json(self.course.id, test_grader.grade_cutoffs, self.user)
|
||||
altered_grader = CourseGradingModel.fetch(self.course.id)
|
||||
self.assertDictEqual(test_grader.grade_cutoffs, altered_grader.grade_cutoffs, "cutoff add D")
|
||||
|
||||
test_grader.grade_cutoffs['Pass'] = 0.75
|
||||
CourseGradingModel.update_cutoffs_from_json(self.course_locator, test_grader.grade_cutoffs, self.user)
|
||||
altered_grader = CourseGradingModel.fetch(self.course_locator)
|
||||
CourseGradingModel.update_cutoffs_from_json(self.course.id, test_grader.grade_cutoffs, self.user)
|
||||
altered_grader = CourseGradingModel.fetch(self.course.id)
|
||||
self.assertDictEqual(test_grader.grade_cutoffs, altered_grader.grade_cutoffs, "cutoff change 'Pass'")
|
||||
|
||||
def test_delete_grace_period(self):
|
||||
test_grader = CourseGradingModel.fetch(self.course_locator)
|
||||
test_grader = CourseGradingModel.fetch(self.course.id)
|
||||
CourseGradingModel.update_grace_period_from_json(
|
||||
self.course_locator, test_grader.grace_period, self.user
|
||||
self.course.id, test_grader.grace_period, self.user
|
||||
)
|
||||
# update_grace_period_from_json doesn't return anything, so query the db for its contents.
|
||||
altered_grader = CourseGradingModel.fetch(self.course_locator)
|
||||
altered_grader = CourseGradingModel.fetch(self.course.id)
|
||||
self.assertEqual(test_grader.grace_period, altered_grader.grace_period, "Noop update")
|
||||
|
||||
test_grader.grace_period = {'hours': 15, 'minutes': 5, 'seconds': 30}
|
||||
CourseGradingModel.update_grace_period_from_json(
|
||||
self.course_locator, test_grader.grace_period, self.user)
|
||||
altered_grader = CourseGradingModel.fetch(self.course_locator)
|
||||
self.course.id, test_grader.grace_period, self.user)
|
||||
altered_grader = CourseGradingModel.fetch(self.course.id)
|
||||
self.assertDictEqual(test_grader.grace_period, altered_grader.grace_period, "Adding in a grace period")
|
||||
|
||||
test_grader.grace_period = {'hours': 1, 'minutes': 10, 'seconds': 0}
|
||||
# Now delete the grace period
|
||||
CourseGradingModel.delete_grace_period(self.course_locator, self.user)
|
||||
CourseGradingModel.delete_grace_period(self.course.id, self.user)
|
||||
# update_grace_period_from_json doesn't return anything, so query the db for its contents.
|
||||
altered_grader = CourseGradingModel.fetch(self.course_locator)
|
||||
altered_grader = CourseGradingModel.fetch(self.course.id)
|
||||
# Once deleted, the grace period should simply be None
|
||||
self.assertEqual(None, altered_grader.grace_period, "Delete grace period")
|
||||
|
||||
def test_update_section_grader_type(self):
|
||||
# Get the descriptor and the section_grader_type and assert they are the default values
|
||||
descriptor = get_modulestore(self.course.location).get_item(self.course.location)
|
||||
section_grader_type = CourseGradingModel.get_section_grader_type(self.course_locator)
|
||||
section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location)
|
||||
|
||||
self.assertEqual('notgraded', section_grader_type['graderType'])
|
||||
self.assertEqual(None, descriptor.format)
|
||||
@@ -343,7 +345,7 @@ class CourseGradingTest(CourseTestCase):
|
||||
# Change the default grader type to Homework, which should also mark the section as graded
|
||||
CourseGradingModel.update_section_grader_type(self.course, 'Homework', self.user)
|
||||
descriptor = get_modulestore(self.course.location).get_item(self.course.location)
|
||||
section_grader_type = CourseGradingModel.get_section_grader_type(self.course_locator)
|
||||
section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location)
|
||||
|
||||
self.assertEqual('Homework', section_grader_type['graderType'])
|
||||
self.assertEqual('Homework', descriptor.format)
|
||||
@@ -352,7 +354,7 @@ class CourseGradingTest(CourseTestCase):
|
||||
# Change the grader type back to notgraded, which should also unmark the section as graded
|
||||
CourseGradingModel.update_section_grader_type(self.course, 'notgraded', self.user)
|
||||
descriptor = get_modulestore(self.course.location).get_item(self.course.location)
|
||||
section_grader_type = CourseGradingModel.get_section_grader_type(self.course_locator)
|
||||
section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location)
|
||||
|
||||
self.assertEqual('notgraded', section_grader_type['graderType'])
|
||||
self.assertEqual(None, descriptor.format)
|
||||
@@ -362,7 +364,7 @@ class CourseGradingTest(CourseTestCase):
|
||||
"""
|
||||
Test configuring the graders via ajax calls
|
||||
"""
|
||||
grader_type_url_base = self.course_locator.url_reverse('settings/grading')
|
||||
grader_type_url_base = get_url(self.course.id, 'grading_handler')
|
||||
# test get whole
|
||||
response = self.client.get_json(grader_type_url_base)
|
||||
whole_model = json.loads(response.content)
|
||||
@@ -411,14 +413,12 @@ class CourseGradingTest(CourseTestCase):
|
||||
Populate the course, grab a section, get the url for the assignment type access
|
||||
"""
|
||||
self.populate_course()
|
||||
sections = get_modulestore(self.course_location).get_items(
|
||||
self.course_location.replace(category="sequential", name=None)
|
||||
)
|
||||
sequential_usage_key = self.course.id.make_usage_key("sequential", None)
|
||||
sections = get_modulestore(self.course.id).get_items(sequential_usage_key)
|
||||
# see if test makes sense
|
||||
self.assertGreater(len(sections), 0, "No sections found")
|
||||
section = sections[0] # just take the first one
|
||||
section_locator = loc_mapper().translate_location(self.course_location.course_id, section.location, False, True)
|
||||
return section_locator.url_reverse('xblock')
|
||||
return reverse_usage_url('xblock_handler', section.location)
|
||||
|
||||
def test_set_get_section_grader_ajax(self):
|
||||
"""
|
||||
@@ -443,11 +443,8 @@ class CourseMetadataEditingTest(CourseTestCase):
|
||||
def setUp(self):
|
||||
CourseTestCase.setUp(self)
|
||||
self.fullcourse = CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
|
||||
self.course_setting_url = self.course_locator.url_reverse('settings/advanced')
|
||||
self.fullcourse_setting_url = loc_mapper().translate_location(
|
||||
self.fullcourse.location.course_id,
|
||||
self.fullcourse.location, False, True
|
||||
).url_reverse('settings/advanced')
|
||||
self.course_setting_url = get_url(self.course.id, 'advanced_settings_handler')
|
||||
self.fullcourse_setting_url = get_url(self.fullcourse.id, 'advanced_settings_handler')
|
||||
|
||||
def test_fetch_initial_fields(self):
|
||||
test_model = CourseMetadata.fetch(self.course)
|
||||
@@ -473,7 +470,7 @@ class CourseMetadataEditingTest(CourseTestCase):
|
||||
)
|
||||
self.update_check(test_model)
|
||||
# try fresh fetch to ensure persistence
|
||||
fresh = modulestore().get_item(self.course_location)
|
||||
fresh = modulestore('direct').get_course(self.course.id)
|
||||
test_model = CourseMetadata.fetch(fresh)
|
||||
self.update_check(test_model)
|
||||
# now change some of the existing metadata
|
||||
@@ -563,13 +560,13 @@ class CourseMetadataEditingTest(CourseTestCase):
|
||||
self.client.ajax_post(self.course_setting_url, {
|
||||
ADVANCED_COMPONENT_POLICY_KEY: ["combinedopenended"]
|
||||
})
|
||||
course = modulestore().get_item(self.course_location)
|
||||
course = modulestore().get_course(self.course.id)
|
||||
self.assertIn(EXTRA_TAB_PANELS.get("open_ended"), course.tabs)
|
||||
self.assertNotIn(EXTRA_TAB_PANELS.get("notes"), course.tabs)
|
||||
self.client.ajax_post(self.course_setting_url, {
|
||||
ADVANCED_COMPONENT_POLICY_KEY: []
|
||||
})
|
||||
course = modulestore().get_item(self.course_location)
|
||||
course = modulestore().get_course(self.course.id)
|
||||
self.assertNotIn(EXTRA_TAB_PANELS.get("open_ended"), course.tabs)
|
||||
|
||||
|
||||
@@ -580,7 +577,7 @@ class CourseGraderUpdatesTest(CourseTestCase):
|
||||
def setUp(self):
|
||||
"""Compute the url to use in tests"""
|
||||
super(CourseGraderUpdatesTest, self).setUp()
|
||||
self.url = self.course_locator.url_reverse('settings/grading')
|
||||
self.url = get_url(self.course.id, 'grading_handler')
|
||||
self.starting_graders = CourseGradingModel(self.course).graders
|
||||
|
||||
def test_get(self):
|
||||
@@ -594,7 +591,7 @@ class CourseGraderUpdatesTest(CourseTestCase):
|
||||
"""Test deleting a specific grading type record."""
|
||||
resp = self.client.delete(self.url + '/0', HTTP_ACCEPT="application/json")
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
current_graders = CourseGradingModel.fetch(self.course_locator).graders
|
||||
current_graders = CourseGradingModel.fetch(self.course.id).graders
|
||||
self.assertNotIn(self.starting_graders[0], current_graders)
|
||||
self.assertEqual(len(self.starting_graders) - 1, len(current_graders))
|
||||
|
||||
@@ -612,7 +609,7 @@ class CourseGraderUpdatesTest(CourseTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
obj = json.loads(resp.content)
|
||||
self.assertEqual(obj, grader)
|
||||
current_graders = CourseGradingModel.fetch(self.course_locator).graders
|
||||
current_graders = CourseGradingModel.fetch(self.course.id).graders
|
||||
self.assertEqual(len(self.starting_graders), len(current_graders))
|
||||
|
||||
def test_add(self):
|
||||
@@ -633,5 +630,5 @@ class CourseGraderUpdatesTest(CourseTestCase):
|
||||
self.assertEqual(obj['id'], len(self.starting_graders))
|
||||
del obj['id']
|
||||
self.assertEqual(obj, grader)
|
||||
current_graders = CourseGradingModel.fetch(self.course_locator).graders
|
||||
current_graders = CourseGradingModel.fetch(self.course.id).graders
|
||||
self.assertEqual(len(self.starting_graders) + 1, len(current_graders))
|
||||
|
||||
@@ -6,7 +6,7 @@ from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.django import modulestore, loc_mapper, clear_existing_modulestores
|
||||
from xmodule.seq_module import SequenceDescriptor
|
||||
from xmodule.capa_module import CapaDescriptor
|
||||
from xmodule.modulestore.locator import CourseLocator, BlockUsageLocator, LocalId
|
||||
from xmodule.modulestore.locator import BlockUsageLocator, LocalId
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError
|
||||
from xmodule.html_module import HtmlDescriptor
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
@@ -54,25 +54,25 @@ class TemplateTests(unittest.TestCase):
|
||||
|
||||
def test_factories(self):
|
||||
test_course = persistent_factories.PersistentCourseFactory.create(
|
||||
course_id='testx.tempcourse', org='testx',
|
||||
offering='tempcourse', org='testx',
|
||||
display_name='fun test course', user_id='testbot'
|
||||
)
|
||||
self.assertIsInstance(test_course, CourseDescriptor)
|
||||
self.assertEqual(test_course.display_name, 'fun test course')
|
||||
index_info = modulestore('split').get_course_index_info(test_course.location)
|
||||
index_info = modulestore('split').get_course_index_info(test_course.id)
|
||||
self.assertEqual(index_info['org'], 'testx')
|
||||
self.assertEqual(index_info['_id'], 'testx.tempcourse')
|
||||
self.assertEqual(index_info['offering'], 'tempcourse')
|
||||
|
||||
test_chapter = persistent_factories.ItemFactory.create(display_name='chapter 1',
|
||||
parent_location=test_course.location)
|
||||
self.assertIsInstance(test_chapter, SequenceDescriptor)
|
||||
# refetch parent which should now point to child
|
||||
test_course = modulestore('split').get_course(test_chapter.location)
|
||||
test_course = modulestore('split').get_course(test_course.id.version_agnostic())
|
||||
self.assertIn(test_chapter.location.block_id, test_course.children)
|
||||
|
||||
with self.assertRaises(DuplicateCourseError):
|
||||
persistent_factories.PersistentCourseFactory.create(
|
||||
course_id='testx.tempcourse', org='testx',
|
||||
offering='tempcourse', org='testx',
|
||||
display_name='fun test course', user_id='testbot'
|
||||
)
|
||||
|
||||
@@ -81,7 +81,7 @@ class TemplateTests(unittest.TestCase):
|
||||
Test create_xblock to create non persisted xblocks
|
||||
"""
|
||||
test_course = persistent_factories.PersistentCourseFactory.create(
|
||||
course_id='testx.tempcourse', org='testx',
|
||||
offering='tempcourse', org='testx',
|
||||
display_name='fun test course', user_id='testbot'
|
||||
)
|
||||
|
||||
@@ -108,7 +108,7 @@ class TemplateTests(unittest.TestCase):
|
||||
try saving temporary xblocks
|
||||
"""
|
||||
test_course = persistent_factories.PersistentCourseFactory.create(
|
||||
course_id='testx.tempcourse', org='testx',
|
||||
offering='tempcourse', org='testx',
|
||||
display_name='fun test course', user_id='testbot'
|
||||
)
|
||||
test_chapter = modulestore('split').create_xblock(
|
||||
@@ -147,30 +147,30 @@ class TemplateTests(unittest.TestCase):
|
||||
|
||||
def test_delete_course(self):
|
||||
test_course = persistent_factories.PersistentCourseFactory.create(
|
||||
course_id='edu.harvard.history.doomed', org='testx',
|
||||
offering='history.doomed', org='edu.harvard',
|
||||
display_name='doomed test course',
|
||||
user_id='testbot')
|
||||
persistent_factories.ItemFactory.create(display_name='chapter 1',
|
||||
parent_location=test_course.location)
|
||||
|
||||
id_locator = CourseLocator(package_id=test_course.location.package_id, branch='draft')
|
||||
guid_locator = CourseLocator(version_guid=test_course.location.version_guid)
|
||||
# verify it can be retireved by id
|
||||
id_locator = test_course.id.for_branch('draft')
|
||||
guid_locator = test_course.location.course_agnostic()
|
||||
# verify it can be retrieved by id
|
||||
self.assertIsInstance(modulestore('split').get_course(id_locator), CourseDescriptor)
|
||||
# and by guid
|
||||
self.assertIsInstance(modulestore('split').get_course(guid_locator), CourseDescriptor)
|
||||
modulestore('split').delete_course(id_locator.package_id)
|
||||
self.assertIsInstance(modulestore('split').get_item(guid_locator), CourseDescriptor)
|
||||
modulestore('split').delete_course(id_locator)
|
||||
# test can no longer retrieve by id
|
||||
self.assertRaises(ItemNotFoundError, modulestore('split').get_course, id_locator)
|
||||
# but can by guid
|
||||
self.assertIsInstance(modulestore('split').get_course(guid_locator), CourseDescriptor)
|
||||
self.assertIsInstance(modulestore('split').get_item(guid_locator), CourseDescriptor)
|
||||
|
||||
def test_block_generations(self):
|
||||
"""
|
||||
Test get_block_generations
|
||||
"""
|
||||
test_course = persistent_factories.PersistentCourseFactory.create(
|
||||
course_id='edu.harvard.history.hist101', org='testx',
|
||||
offering='history.hist101', org='edu.harvard',
|
||||
display_name='history test course',
|
||||
user_id='testbot'
|
||||
)
|
||||
@@ -192,7 +192,9 @@ class TemplateTests(unittest.TestCase):
|
||||
|
||||
second_problem = persistent_factories.ItemFactory.create(
|
||||
display_name='problem 2',
|
||||
parent_location=BlockUsageLocator(updated_loc, block_id=sub.location.block_id),
|
||||
parent_location=BlockUsageLocator.make_relative(
|
||||
updated_loc, block_type='problem', block_id=sub.location.block_id
|
||||
),
|
||||
user_id='testbot', category='problem',
|
||||
data="<problem></problem>"
|
||||
)
|
||||
|
||||
@@ -9,7 +9,6 @@ import subprocess
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test.utils import override_settings
|
||||
from pymongo import MongoClient
|
||||
|
||||
@@ -17,7 +16,7 @@ from .utils import CourseTestCase
|
||||
import contentstore.git_export_utils as git_export_utils
|
||||
from xmodule.contentstore.django import _CONTENTSTORE
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from contentstore.utils import get_modulestore
|
||||
from contentstore.utils import get_modulestore, reverse_course_url
|
||||
|
||||
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
|
||||
TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex
|
||||
@@ -34,12 +33,8 @@ class TestExportGit(CourseTestCase):
|
||||
Setup test course, user, and url.
|
||||
"""
|
||||
super(TestExportGit, self).setUp()
|
||||
self.course_module = modulestore().get_item(self.course.location)
|
||||
self.test_url = reverse('export_git', kwargs={
|
||||
'org': self.course.location.org,
|
||||
'course': self.course.location.course,
|
||||
'name': self.course.location.name,
|
||||
})
|
||||
self.course_module = modulestore().get_course(self.course.id)
|
||||
self.test_url = reverse_course_url('export_git', self.course.id)
|
||||
|
||||
def tearDown(self):
|
||||
MongoClient().drop_database(TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'])
|
||||
|
||||
@@ -47,7 +47,7 @@ class InternationalizationTest(ModuleStoreTestCase):
|
||||
self.client = AjaxEnabledTestClient()
|
||||
self.client.login(username=self.uname, password=self.password)
|
||||
|
||||
resp = self.client.get_html('/course')
|
||||
resp = self.client.get_html('/course/')
|
||||
self.assertContains(resp,
|
||||
'<h1 class="page-header">My Courses</h1>',
|
||||
status_code=200,
|
||||
@@ -58,7 +58,7 @@ class InternationalizationTest(ModuleStoreTestCase):
|
||||
self.client = AjaxEnabledTestClient()
|
||||
self.client.login(username=self.uname, password=self.password)
|
||||
|
||||
resp = self.client.get_html('/course',
|
||||
resp = self.client.get_html('/course/',
|
||||
{},
|
||||
HTTP_ACCEPT_LANGUAGE='en'
|
||||
)
|
||||
@@ -83,7 +83,7 @@ class InternationalizationTest(ModuleStoreTestCase):
|
||||
self.client.login(username=self.uname, password=self.password)
|
||||
|
||||
resp = self.client.get_html(
|
||||
'/course',
|
||||
'/course/',
|
||||
{},
|
||||
HTTP_ACCEPT_LANGUAGE='eo'
|
||||
)
|
||||
|
||||
@@ -15,15 +15,13 @@ from django.contrib.auth.models import User
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from contentstore.tests.modulestore_config import TEST_MODULESTORE
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey, AssetLocation
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.contentstore.django import _CONTENTSTORE
|
||||
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from uuid import uuid4
|
||||
from pymongo import MongoClient
|
||||
@@ -80,24 +78,22 @@ class ContentStoreImportTest(ModuleStoreTestCase):
|
||||
do_import_static=False,
|
||||
verbose=True,
|
||||
)
|
||||
course_location = CourseDescriptor.id_to_location(
|
||||
'edX/test_import_course/2012_Fall'
|
||||
)
|
||||
course = module_store.get_item(course_location)
|
||||
course_id = SlashSeparatedCourseKey('edX', 'test_import_course', '2012_Fall')
|
||||
course = module_store.get_course(course_id)
|
||||
self.assertIsNotNone(course)
|
||||
|
||||
return module_store, content_store, course, course_location
|
||||
return module_store, content_store, course
|
||||
|
||||
def test_import_course_into_similar_namespace(self):
|
||||
# Checks to make sure that a course with an org/course like
|
||||
# edx/course can be imported into a namespace with an org/course
|
||||
# like edx/course_name
|
||||
module_store, __, __, course_location = self.load_test_import_course()
|
||||
module_store, __, course = self.load_test_import_course()
|
||||
__, course_items = import_from_xml(
|
||||
module_store,
|
||||
'common/test/data',
|
||||
['test_import_course_2'],
|
||||
target_location_namespace=course_location,
|
||||
target_course_id=course.id,
|
||||
verbose=True,
|
||||
)
|
||||
self.assertEqual(len(course_items), 1)
|
||||
@@ -107,15 +103,15 @@ class ContentStoreImportTest(ModuleStoreTestCase):
|
||||
# Test that importing course with unicode 'id' and 'display name' doesn't give UnicodeEncodeError
|
||||
"""
|
||||
module_store = modulestore('direct')
|
||||
target_location = Location(['i4x', u'Юникода', 'unicode_course', 'course', u'échantillon'])
|
||||
course_id = SlashSeparatedCourseKey(u'Юникода', u'unicode_course', u'échantillon')
|
||||
import_from_xml(
|
||||
module_store,
|
||||
'common/test/data/',
|
||||
['2014_Uni'],
|
||||
target_location_namespace=target_location
|
||||
target_course_id=course_id
|
||||
)
|
||||
|
||||
course = module_store.get_item(target_location)
|
||||
course = module_store.get_course(course_id)
|
||||
self.assertIsNotNone(course)
|
||||
|
||||
# test that course 'display_name' same as imported course 'display_name'
|
||||
@@ -125,17 +121,19 @@ class ContentStoreImportTest(ModuleStoreTestCase):
|
||||
'''
|
||||
Stuff in static_import should always be imported into contentstore
|
||||
'''
|
||||
_, content_store, course, course_location = self.load_test_import_course()
|
||||
_, content_store, course = self.load_test_import_course()
|
||||
|
||||
# make sure we have ONE asset in our contentstore ("should_be_imported.html")
|
||||
all_assets, count = content_store.get_all_content_for_course(course_location)
|
||||
all_assets, count = content_store.get_all_content_for_course(course.id)
|
||||
print "len(all_assets)=%d" % len(all_assets)
|
||||
self.assertEqual(len(all_assets), 1)
|
||||
self.assertEqual(count, 1)
|
||||
|
||||
content = None
|
||||
try:
|
||||
location = StaticContent.get_location_from_path('/c4x/edX/test_import_course/asset/should_be_imported.html')
|
||||
location = AssetLocation.from_deprecated_string(
|
||||
'/c4x/edX/test_import_course/asset/should_be_imported.html'
|
||||
)
|
||||
content = content_store.find(location)
|
||||
except NotFoundError:
|
||||
pass
|
||||
@@ -155,92 +153,93 @@ class ContentStoreImportTest(ModuleStoreTestCase):
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store, do_import_static=False, verbose=True)
|
||||
|
||||
course_location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
|
||||
module_store.get_item(course_location)
|
||||
course = module_store.get_course(SlashSeparatedCourseKey('edX', 'toy', '2012_Fall'))
|
||||
|
||||
# make sure we have NO assets in our contentstore
|
||||
all_assets, count = content_store.get_all_content_for_course(course_location)
|
||||
all_assets, count = content_store.get_all_content_for_course(course.id)
|
||||
self.assertEqual(len(all_assets), 0)
|
||||
self.assertEqual(count, 0)
|
||||
|
||||
def test_no_static_link_rewrites_on_import(self):
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'], do_import_static=False, verbose=True)
|
||||
_, courses = import_from_xml(module_store, 'common/test/data/', ['toy'], do_import_static=False, verbose=True)
|
||||
course_key = courses[0].id
|
||||
|
||||
handouts = module_store.get_item(Location(['i4x', 'edX', 'toy', 'course_info', 'handouts', None]))
|
||||
handouts = module_store.get_item(course_key.make_usage_key('course_info', 'handouts'))
|
||||
self.assertIn('/static/', handouts.data)
|
||||
|
||||
handouts = module_store.get_item(Location(['i4x', 'edX', 'toy', 'html', 'toyhtml', None]))
|
||||
handouts = module_store.get_item(course_key.make_usage_key('html', 'toyhtml'))
|
||||
self.assertIn('/static/', handouts.data)
|
||||
|
||||
def test_tab_name_imports_correctly(self):
|
||||
_module_store, _content_store, course, _course_location = self.load_test_import_course()
|
||||
_module_store, _content_store, course = self.load_test_import_course()
|
||||
print "course tabs = {0}".format(course.tabs)
|
||||
self.assertEqual(course.tabs[2]['name'], 'Syllabus')
|
||||
|
||||
def test_rewrite_reference_list(self):
|
||||
module_store = modulestore('direct')
|
||||
target_location = Location(['i4x', 'testX', 'conditional_copy', 'course', 'copy_run'])
|
||||
target_course_id = SlashSeparatedCourseKey('testX', 'conditional_copy', 'copy_run')
|
||||
import_from_xml(
|
||||
module_store,
|
||||
'common/test/data/',
|
||||
['conditional'],
|
||||
target_location_namespace=target_location
|
||||
target_course_id=target_course_id
|
||||
)
|
||||
conditional_module = module_store.get_item(
|
||||
Location(['i4x', 'testX', 'conditional_copy', 'conditional', 'condone'])
|
||||
target_course_id.make_usage_key('conditional', 'condone')
|
||||
)
|
||||
self.assertIsNotNone(conditional_module)
|
||||
different_course_id = SlashSeparatedCourseKey('edX', 'different_course', 'copy_run')
|
||||
self.assertListEqual(
|
||||
[
|
||||
u'i4x://testX/conditional_copy/problem/choiceprob',
|
||||
u'i4x://edX/different_course/html/for_testing_import_rewrites'
|
||||
target_course_id.make_usage_key('problem', 'choiceprob'),
|
||||
different_course_id.make_usage_key('html', 'for_testing_import_rewrites')
|
||||
],
|
||||
conditional_module.sources_list
|
||||
)
|
||||
self.assertListEqual(
|
||||
[
|
||||
u'i4x://testX/conditional_copy/html/congrats',
|
||||
u'i4x://testX/conditional_copy/html/secret_page'
|
||||
target_course_id.make_usage_key('html', 'congrats'),
|
||||
target_course_id.make_usage_key('html', 'secret_page')
|
||||
],
|
||||
conditional_module.show_tag_list
|
||||
)
|
||||
|
||||
def test_rewrite_reference(self):
|
||||
module_store = modulestore('direct')
|
||||
target_location = Location(['i4x', 'testX', 'peergrading_copy', 'course', 'copy_run'])
|
||||
target_course_id = SlashSeparatedCourseKey('testX', 'peergrading_copy', 'copy_run')
|
||||
import_from_xml(
|
||||
module_store,
|
||||
'common/test/data/',
|
||||
['open_ended'],
|
||||
target_location_namespace=target_location
|
||||
target_course_id=target_course_id
|
||||
)
|
||||
peergrading_module = module_store.get_item(
|
||||
Location(['i4x', 'testX', 'peergrading_copy', 'peergrading', 'PeerGradingLinked'])
|
||||
target_course_id.make_usage_key('peergrading', 'PeerGradingLinked')
|
||||
)
|
||||
self.assertIsNotNone(peergrading_module)
|
||||
self.assertEqual(
|
||||
u'i4x://testX/peergrading_copy/combinedopenended/SampleQuestion',
|
||||
target_course_id.make_usage_key('combinedopenended', 'SampleQuestion'),
|
||||
peergrading_module.link_to_location
|
||||
)
|
||||
|
||||
def test_rewrite_reference_value_dict(self):
|
||||
module_store = modulestore('direct')
|
||||
target_location = Location(['i4x', 'testX', 'split_test_copy', 'course', 'copy_run'])
|
||||
target_course_id = SlashSeparatedCourseKey('testX', 'split_test_copy', 'copy_run')
|
||||
import_from_xml(
|
||||
module_store,
|
||||
'common/test/data/',
|
||||
['split_test_module'],
|
||||
target_location_namespace=target_location
|
||||
target_course_id=target_course_id
|
||||
)
|
||||
split_test_module = module_store.get_item(
|
||||
Location(['i4x', 'testX', 'split_test_copy', 'split_test', 'split1'])
|
||||
target_course_id.make_usage_key('split_test', 'split1')
|
||||
)
|
||||
self.assertIsNotNone(split_test_module)
|
||||
self.assertEqual(
|
||||
{
|
||||
"0": "i4x://testX/split_test_copy/vertical/sample_0",
|
||||
"2": "i4x://testX/split_test_copy/vertical/sample_2",
|
||||
"0": target_course_id.make_usage_key('vertical', 'sample_0'),
|
||||
"2": target_course_id.make_usage_key('vertical', 'sample_2'),
|
||||
},
|
||||
split_test_module.group_id_to_child,
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ from xmodule.modulestore.xml_importer import import_from_xml
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
from contentstore.tests.modulestore_config import TEST_MODULESTORE
|
||||
|
||||
@@ -17,10 +16,9 @@ class DraftReorderTestCase(ModuleStoreTestCase):
|
||||
def test_order(self):
|
||||
store = modulestore('direct')
|
||||
draft_store = modulestore('default')
|
||||
import_from_xml(store, 'common/test/data/', ['import_draft_order'], draft_store=draft_store)
|
||||
sequential = draft_store.get_item(
|
||||
Location('i4x', 'test_org', 'import_draft_order', 'sequential', '0f4f7649b10141b0bdc9922dcf94515a', None)
|
||||
)
|
||||
_, course_items = import_from_xml(store, 'common/test/data/', ['import_draft_order'], draft_store=draft_store)
|
||||
course_key = course_items[0].id
|
||||
sequential = draft_store.get_item(course_key.make_usage_key('sequential', '0f4f7649b10141b0bdc9922dcf94515a'))
|
||||
verticals = sequential.children
|
||||
|
||||
# The order that files are read in from the file system is not guaranteed (cannot rely on
|
||||
@@ -32,22 +30,20 @@ class DraftReorderTestCase(ModuleStoreTestCase):
|
||||
#
|
||||
# '5a05be9d59fc4bb79282c94c9e6b88c7' and 'second' are public verticals.
|
||||
self.assertEqual(7, len(verticals))
|
||||
self.assertEqual(u'i4x://test_org/import_draft_order/vertical/z', verticals[0])
|
||||
self.assertEqual(u'i4x://test_org/import_draft_order/vertical/5a05be9d59fc4bb79282c94c9e6b88c7', verticals[1])
|
||||
self.assertEqual(u'i4x://test_org/import_draft_order/vertical/a', verticals[2])
|
||||
self.assertEqual(u'i4x://test_org/import_draft_order/vertical/second', verticals[3])
|
||||
self.assertEqual(u'i4x://test_org/import_draft_order/vertical/b', verticals[4])
|
||||
self.assertEqual(u'i4x://test_org/import_draft_order/vertical/d', verticals[5])
|
||||
self.assertEqual(u'i4x://test_org/import_draft_order/vertical/c', verticals[6])
|
||||
self.assertEqual(course_key.make_usage_key('vertical', 'z'), verticals[0])
|
||||
self.assertEqual(course_key.make_usage_key('vertical', '5a05be9d59fc4bb79282c94c9e6b88c7'), verticals[1])
|
||||
self.assertEqual(course_key.make_usage_key('vertical', 'a'), verticals[2])
|
||||
self.assertEqual(course_key.make_usage_key('vertical', 'second'), verticals[3])
|
||||
self.assertEqual(course_key.make_usage_key('vertical', 'b'), verticals[4])
|
||||
self.assertEqual(course_key.make_usage_key('vertical', 'd'), verticals[5])
|
||||
self.assertEqual(course_key.make_usage_key('vertical', 'c'), verticals[6])
|
||||
|
||||
# Now also test that the verticals in a second sequential are correct.
|
||||
sequential = draft_store.get_item(
|
||||
Location('i4x', 'test_org', 'import_draft_order', 'sequential', 'secondseq', None)
|
||||
)
|
||||
sequential = draft_store.get_item(course_key.make_usage_key('sequential', 'secondseq'))
|
||||
verticals = sequential.children
|
||||
# 'asecond' and 'zsecond' are drafts with 'index_in_children_list' 0 and 2, respectively.
|
||||
# 'secondsubsection' is a public vertical.
|
||||
self.assertEqual(3, len(verticals))
|
||||
self.assertEqual(u'i4x://test_org/import_draft_order/vertical/asecond', verticals[0])
|
||||
self.assertEqual(u'i4x://test_org/import_draft_order/vertical/secondsubsection', verticals[1])
|
||||
self.assertEqual(u'i4x://test_org/import_draft_order/vertical/zsecond', verticals[2])
|
||||
self.assertEqual(course_key.make_usage_key('vertical', 'asecond'), verticals[0])
|
||||
self.assertEqual(course_key.make_usage_key('vertical', 'secondsubsection'), verticals[1])
|
||||
self.assertEqual(course_key.make_usage_key('vertical', 'zsecond'), verticals[2])
|
||||
|
||||
@@ -10,6 +10,7 @@ from xblock.fields import String
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.mongo.draft import as_draft
|
||||
from contentstore.tests.modulestore_config import TEST_MODULESTORE
|
||||
|
||||
|
||||
@@ -39,7 +40,6 @@ class XBlockImportTest(ModuleStoreTestCase):
|
||||
def test_import_public(self):
|
||||
self._assert_import(
|
||||
'pure_xblock_public',
|
||||
'i4x://edX/pure_xblock_public/stubxblock/xblock_test',
|
||||
'set by xml'
|
||||
)
|
||||
|
||||
@@ -47,12 +47,11 @@ class XBlockImportTest(ModuleStoreTestCase):
|
||||
def test_import_draft(self):
|
||||
self._assert_import(
|
||||
'pure_xblock_draft',
|
||||
'i4x://edX/pure_xblock_draft/stubxblock/xblock_test@draft',
|
||||
'set by xml',
|
||||
has_draft=True
|
||||
)
|
||||
|
||||
def _assert_import(self, course_dir, expected_xblock_loc, expected_field_val, has_draft=False):
|
||||
def _assert_import(self, course_dir, expected_field_val, has_draft=False):
|
||||
"""
|
||||
Import a course from XML, then verify that the XBlock was loaded
|
||||
with the correct field value.
|
||||
@@ -67,16 +66,21 @@ class XBlockImportTest(ModuleStoreTestCase):
|
||||
the expected field value set.
|
||||
|
||||
"""
|
||||
import_from_xml(
|
||||
_, courses = import_from_xml(
|
||||
self.store, 'common/test/data', [course_dir],
|
||||
draft_store=self.draft_store
|
||||
)
|
||||
|
||||
xblock = self.store.get_item(expected_xblock_loc)
|
||||
xblock_location = courses[0].id.make_usage_key('stubxblock', 'xblock_test')
|
||||
|
||||
if has_draft:
|
||||
xblock_location = as_draft(xblock_location)
|
||||
|
||||
xblock = self.store.get_item(xblock_location)
|
||||
self.assertTrue(isinstance(xblock, StubXBlock))
|
||||
self.assertEqual(xblock.test_field, expected_field_val)
|
||||
|
||||
if has_draft:
|
||||
draft_xblock = self.draft_store.get_item(expected_xblock_loc)
|
||||
draft_xblock = self.draft_store.get_item(xblock_location)
|
||||
self.assertTrue(isinstance(draft_xblock, StubXBlock))
|
||||
self.assertEqual(draft_xblock.test_field, expected_field_val)
|
||||
|
||||
@@ -3,9 +3,10 @@ Test finding orphans via the view and django config
|
||||
"""
|
||||
import json
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
from student.models import CourseEnrollment
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from contentstore.utils import reverse_course_url
|
||||
|
||||
|
||||
class TestOrphan(CourseTestCase):
|
||||
"""
|
||||
@@ -27,6 +28,8 @@ class TestOrphan(CourseTestCase):
|
||||
self._create_item('about', 'overview', "<p>overview</p>", {}, None, None, runtime)
|
||||
self._create_item('course_info', 'updates', "<ol><li><h2>Sep 22</h2><p>test</p></li></ol>", {}, None, None, runtime)
|
||||
|
||||
self.orphan_url = reverse_course_url('orphan_handler', self.course.id)
|
||||
|
||||
def _create_item(self, category, name, data, metadata, parent_category, parent_name, runtime):
|
||||
location = self.course.location.replace(category=category, name=name)
|
||||
store = modulestore('direct')
|
||||
@@ -35,39 +38,34 @@ class TestOrphan(CourseTestCase):
|
||||
# add child to parent in mongo
|
||||
parent_location = self.course.location.replace(category=parent_category, name=parent_name)
|
||||
parent = store.get_item(parent_location)
|
||||
parent.children.append(location.url())
|
||||
parent.children.append(location)
|
||||
store.update_item(parent, self.user.id)
|
||||
|
||||
def test_mongo_orphan(self):
|
||||
"""
|
||||
Test that old mongo finds the orphans
|
||||
"""
|
||||
locator = loc_mapper().translate_location(self.course.location.course_id, self.course.location, False, True)
|
||||
orphan_url = locator.url_reverse('orphan/', '')
|
||||
|
||||
orphans = json.loads(
|
||||
self.client.get(
|
||||
orphan_url,
|
||||
self.orphan_url,
|
||||
HTTP_ACCEPT='application/json'
|
||||
).content
|
||||
)
|
||||
self.assertEqual(len(orphans), 3, "Wrong # {}".format(orphans))
|
||||
location = self.course.location.replace(category='chapter', name='OrphanChapter')
|
||||
self.assertIn(location.url(), orphans)
|
||||
self.assertIn(location.to_deprecated_string(), orphans)
|
||||
location = self.course.location.replace(category='vertical', name='OrphanVert')
|
||||
self.assertIn(location.url(), orphans)
|
||||
self.assertIn(location.to_deprecated_string(), orphans)
|
||||
location = self.course.location.replace(category='html', name='OrphanHtml')
|
||||
self.assertIn(location.url(), orphans)
|
||||
self.assertIn(location.to_deprecated_string(), orphans)
|
||||
|
||||
def test_mongo_orphan_delete(self):
|
||||
"""
|
||||
Test that old mongo deletes the orphans
|
||||
"""
|
||||
locator = loc_mapper().translate_location(self.course.location.course_id, self.course.location, False, True)
|
||||
orphan_url = locator.url_reverse('orphan/', '')
|
||||
self.client.delete(orphan_url)
|
||||
self.client.delete(self.orphan_url)
|
||||
orphans = json.loads(
|
||||
self.client.get(orphan_url, HTTP_ACCEPT='application/json').content
|
||||
self.client.get(self.orphan_url, HTTP_ACCEPT='application/json').content
|
||||
)
|
||||
self.assertEqual(len(orphans), 0, "Orphans not deleted {}".format(orphans))
|
||||
|
||||
@@ -76,10 +74,8 @@ class TestOrphan(CourseTestCase):
|
||||
Test that auth restricts get and delete appropriately
|
||||
"""
|
||||
test_user_client, test_user = self.create_non_staff_authed_user_client()
|
||||
CourseEnrollment.enroll(test_user, self.course.location.course_id)
|
||||
locator = loc_mapper().translate_location(self.course.location.course_id, self.course.location, False, True)
|
||||
orphan_url = locator.url_reverse('orphan/', '')
|
||||
response = test_user_client.get(orphan_url)
|
||||
CourseEnrollment.enroll(test_user, self.course.id)
|
||||
response = test_user_client.get(self.orphan_url)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
response = test_user_client.delete(orphan_url)
|
||||
response = test_user_client.delete(self.orphan_url)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@@ -4,13 +4,13 @@ Test CRUD for authorization.
|
||||
import copy
|
||||
|
||||
from django.test.utils import override_settings
|
||||
from django.contrib.auth.models import User, Group
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from contentstore.tests.modulestore_config import TEST_MODULESTORE
|
||||
from contentstore.tests.utils import AjaxEnabledTestClient
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
from contentstore.utils import reverse_url, reverse_course_url
|
||||
from student.roles import CourseInstructorRole, CourseStaffRole
|
||||
from contentstore.views.access import has_course_access
|
||||
from student import auth
|
||||
@@ -46,17 +46,14 @@ class TestCourseAccess(ModuleStoreTestCase):
|
||||
self.client.login(username=uname, password=password)
|
||||
|
||||
# create a course via the view handler which has a different strategy for permissions than the factory
|
||||
self.course_location = Location(['i4x', 'myu', 'mydept.mycourse', 'course', 'myrun'])
|
||||
self.course_locator = loc_mapper().translate_location(
|
||||
self.course_location.course_id, self.course_location, False, True
|
||||
)
|
||||
self.client.ajax_post(
|
||||
self.course_locator.url_reverse('course'),
|
||||
self.course_key = SlashSeparatedCourseKey('myu', 'mydept.mycourse', 'myrun')
|
||||
course_url = reverse_url('course_handler')
|
||||
self.client.ajax_post(course_url,
|
||||
{
|
||||
'org': self.course_location.org,
|
||||
'number': self.course_location.course,
|
||||
'org': self.course_key.org,
|
||||
'number': self.course_key.course,
|
||||
'display_name': 'My favorite course',
|
||||
'run': self.course_location.name,
|
||||
'run': self.course_key.run,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -91,7 +88,7 @@ class TestCourseAccess(ModuleStoreTestCase):
|
||||
# first check the course creator.has explicit access (don't use has_access as is_staff
|
||||
# will trump the actual test)
|
||||
self.assertTrue(
|
||||
CourseInstructorRole(self.course_locator).has_user(self.user),
|
||||
CourseInstructorRole(self.course_key).has_user(self.user),
|
||||
"Didn't add creator as instructor."
|
||||
)
|
||||
users = copy.copy(self.users)
|
||||
@@ -101,35 +98,28 @@ class TestCourseAccess(ModuleStoreTestCase):
|
||||
for role in [CourseInstructorRole, CourseStaffRole]:
|
||||
user_by_role[role] = []
|
||||
# pylint: disable=protected-access
|
||||
groupnames = role(self.course_locator)._group_names
|
||||
self.assertGreater(len(groupnames), 1, "Only 0 or 1 groupname for {}".format(role.ROLE))
|
||||
group = role(self.course_key)
|
||||
# NOTE: this loop breaks the roles.py abstraction by purposely assigning
|
||||
# users to one of each possible groupname in order to test that has_course_access
|
||||
# and remove_user work
|
||||
for groupname in groupnames:
|
||||
group, _ = Group.objects.get_or_create(name=groupname)
|
||||
user = users.pop()
|
||||
user_by_role[role].append(user)
|
||||
user.groups.add(group)
|
||||
user.save()
|
||||
self.assertTrue(has_course_access(user, self.course_locator), "{} does not have access".format(user))
|
||||
self.assertTrue(has_course_access(user, self.course_location), "{} does not have access".format(user))
|
||||
user = users.pop()
|
||||
group.add_users(user)
|
||||
user_by_role[role].append(user)
|
||||
self.assertTrue(has_course_access(user, self.course_key), "{} does not have access".format(user))
|
||||
|
||||
response = self.client.get_html(self.course_locator.url_reverse('course_team'))
|
||||
course_team_url = reverse_course_url('course_team_handler', self.course_key)
|
||||
response = self.client.get_html(course_team_url)
|
||||
for role in [CourseInstructorRole, CourseStaffRole]:
|
||||
for user in user_by_role[role]:
|
||||
self.assertContains(response, user.email)
|
||||
|
||||
|
||||
# test copying course permissions
|
||||
copy_course_location = Location(['i4x', 'copyu', 'copydept.mycourse', 'course', 'myrun'])
|
||||
copy_course_locator = loc_mapper().translate_location(
|
||||
copy_course_location.course_id, copy_course_location, False, True
|
||||
)
|
||||
copy_course_key = SlashSeparatedCourseKey('copyu', 'copydept.mycourse', 'myrun')
|
||||
for role in [CourseInstructorRole, CourseStaffRole]:
|
||||
auth.add_users(
|
||||
self.user,
|
||||
role(copy_course_locator),
|
||||
*role(self.course_locator).users_with_role()
|
||||
role(copy_course_key),
|
||||
*role(self.course_key).users_with_role()
|
||||
)
|
||||
# verify access in copy course and verify that removal from source course w/ the various
|
||||
# groupnames works
|
||||
@@ -138,10 +128,9 @@ class TestCourseAccess(ModuleStoreTestCase):
|
||||
# forcefully decache the groups: premise is that any real request will not have
|
||||
# multiple objects repr the same user but this test somehow uses different instance
|
||||
# in above add_users call
|
||||
if hasattr(user, '_groups'):
|
||||
del user._groups
|
||||
if hasattr(user, '_roles'):
|
||||
del user._roles
|
||||
|
||||
self.assertTrue(has_course_access(user, copy_course_locator), "{} no copy access".format(user))
|
||||
self.assertTrue(has_course_access(user, copy_course_location), "{} no copy access".format(user))
|
||||
auth.remove_users(self.user, role(self.course_locator), user)
|
||||
self.assertFalse(has_course_access(user, self.course_locator), "{} remove didn't work".format(user))
|
||||
self.assertTrue(has_course_access(user, copy_course_key), "{} no copy access".format(user))
|
||||
auth.remove_users(self.user, role(self.course_key), user)
|
||||
self.assertFalse(has_course_access(user, self.course_key), "{} remove didn't work".format(user))
|
||||
|
||||
@@ -111,18 +111,14 @@ class TestSaveSubsToStore(ModuleStoreTestCase):
|
||||
|
||||
self.subs_id = str(uuid4())
|
||||
filename = 'subs_{0}.srt.sjson'.format(self.subs_id)
|
||||
self.content_location = StaticContent.compute_location(
|
||||
self.org, self.number, filename
|
||||
)
|
||||
self.content_location = StaticContent.compute_location(self.course.id, filename)
|
||||
|
||||
# incorrect subs
|
||||
self.unjsonable_subs = set([1]) # set can't be serialized
|
||||
|
||||
self.unjsonable_subs_id = str(uuid4())
|
||||
filename_unjsonable = 'subs_{0}.srt.sjson'.format(self.unjsonable_subs_id)
|
||||
self.content_location_unjsonable = StaticContent.compute_location(
|
||||
self.org, self.number, filename_unjsonable
|
||||
)
|
||||
self.content_location_unjsonable = StaticContent.compute_location(self.course.id, filename_unjsonable)
|
||||
|
||||
self.clear_subs_content()
|
||||
|
||||
@@ -172,9 +168,7 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
|
||||
"""Remove, if subtitles content exists."""
|
||||
for subs_id in youtube_subs.values():
|
||||
filename = 'subs_{0}.srt.sjson'.format(subs_id)
|
||||
content_location = StaticContent.compute_location(
|
||||
self.org, self.number, filename
|
||||
)
|
||||
content_location = StaticContent.compute_location(self.course.id, filename)
|
||||
try:
|
||||
content = contentstore().find(content_location)
|
||||
contentstore().delete(content.get_id())
|
||||
@@ -218,9 +212,7 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
|
||||
# Check assets status after importing subtitles.
|
||||
for subs_id in good_youtube_subs.values():
|
||||
filename = 'subs_{0}.srt.sjson'.format(subs_id)
|
||||
content_location = StaticContent.compute_location(
|
||||
self.org, self.number, filename
|
||||
)
|
||||
content_location = StaticContent.compute_location(self.course.id, filename)
|
||||
self.assertTrue(contentstore().find(content_location))
|
||||
|
||||
self.clear_subs_content(good_youtube_subs)
|
||||
@@ -256,7 +248,7 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
|
||||
for subs_id in bad_youtube_subs.values():
|
||||
filename = 'subs_{0}.srt.sjson'.format(subs_id)
|
||||
content_location = StaticContent.compute_location(
|
||||
self.org, self.number, filename
|
||||
self.course.id, filename
|
||||
)
|
||||
with self.assertRaises(NotFoundError):
|
||||
contentstore().find(content_location)
|
||||
@@ -282,7 +274,7 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
|
||||
for subs_id in good_youtube_subs.values():
|
||||
filename = 'subs_{0}.srt.sjson'.format(subs_id)
|
||||
content_location = StaticContent.compute_location(
|
||||
self.org, self.number, filename
|
||||
self.course.id, filename
|
||||
)
|
||||
self.assertTrue(contentstore().find(content_location))
|
||||
|
||||
@@ -317,7 +309,7 @@ class TestGenerateSubsFromSource(TestDownloadYoutubeSubs):
|
||||
for subs_id in youtube_subs.values():
|
||||
filename = 'subs_{0}.srt.sjson'.format(subs_id)
|
||||
content_location = StaticContent.compute_location(
|
||||
self.org, self.number, filename
|
||||
self.course.id, filename
|
||||
)
|
||||
self.assertTrue(contentstore().find(content_location))
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ Unit tests for checking default forum role "Student" of a user when he creates a
|
||||
after deleting it creates same course again
|
||||
"""
|
||||
from contentstore.tests.utils import AjaxEnabledTestClient
|
||||
from contentstore.utils import delete_course_and_groups
|
||||
from contentstore.utils import delete_course_and_groups, reverse_url
|
||||
from courseware.tests.factories import UserFactory
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
from student.models import CourseEnrollment
|
||||
|
||||
@@ -27,23 +27,20 @@ class TestUsersDefaultRole(ModuleStoreTestCase):
|
||||
self.client.login(username=self.user.username, password='test')
|
||||
|
||||
# create a course via the view handler to create course
|
||||
self.course_location = Location(['i4x', 'Org_1', 'Course_1', 'course', 'Run_1'])
|
||||
self._create_course_with_given_location(self.course_location)
|
||||
self.course_key = SlashSeparatedCourseKey('Org_1', 'Course_1', 'Run_1')
|
||||
self._create_course_with_given_location(self.course_key)
|
||||
|
||||
def _create_course_with_given_location(self, course_location):
|
||||
def _create_course_with_given_location(self, course_key):
|
||||
"""
|
||||
Create course at provided location
|
||||
"""
|
||||
course_locator = loc_mapper().translate_location(
|
||||
course_location.course_id, course_location, False, True
|
||||
)
|
||||
resp = self.client.ajax_post(
|
||||
course_locator.url_reverse('course'),
|
||||
reverse_url('course_handler'),
|
||||
{
|
||||
'org': course_location.org,
|
||||
'number': course_location.course,
|
||||
'org': course_key.org,
|
||||
'number': course_key.course,
|
||||
'display_name': 'test course',
|
||||
'run': course_location.name,
|
||||
'run': course_key.run,
|
||||
}
|
||||
)
|
||||
return resp
|
||||
@@ -60,66 +57,61 @@ class TestUsersDefaultRole(ModuleStoreTestCase):
|
||||
Test that a user enrolls and gets "Student" forum role for that course which he creates and remains
|
||||
enrolled even the course is deleted and keeps its "Student" forum role for that course
|
||||
"""
|
||||
course_id = self.course_location.course_id
|
||||
# check that user has enrollment for this course
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(self.user, course_id))
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_key))
|
||||
|
||||
# check that user has his default "Student" forum role for this course
|
||||
self.assertTrue(self.user.roles.filter(name="Student", course_id=course_id)) # pylint: disable=no-member
|
||||
self.assertTrue(self.user.roles.filter(name="Student", course_id=self.course_key)) # pylint: disable=no-member
|
||||
|
||||
delete_course_and_groups(course_id, commit=True)
|
||||
delete_course_and_groups(self.course_key, commit=True)
|
||||
|
||||
# check that user's enrollment for this course is not deleted
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(self.user, course_id))
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_key))
|
||||
|
||||
# check that user has forum role for this course even after deleting it
|
||||
self.assertTrue(self.user.roles.filter(name="Student", course_id=course_id)) # pylint: disable=no-member
|
||||
self.assertTrue(self.user.roles.filter(name="Student", course_id=self.course_key)) # pylint: disable=no-member
|
||||
|
||||
def test_user_role_on_course_recreate(self):
|
||||
"""
|
||||
Test that creating same course again after deleting it gives user his default
|
||||
forum role "Student" for that course
|
||||
"""
|
||||
course_id = self.course_location.course_id
|
||||
# check that user has enrollment and his default "Student" forum role for this course
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(self.user, course_id))
|
||||
self.assertTrue(self.user.roles.filter(name="Student", course_id=course_id)) # pylint: disable=no-member
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_key))
|
||||
self.assertTrue(self.user.roles.filter(name="Student", course_id=self.course_key)) # pylint: disable=no-member
|
||||
|
||||
# delete this course and recreate this course with same user
|
||||
delete_course_and_groups(course_id, commit=True)
|
||||
resp = self._create_course_with_given_location(self.course_location)
|
||||
delete_course_and_groups(self.course_key, commit=True)
|
||||
resp = self._create_course_with_given_location(self.course_key)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# check that user has his enrollment for this course
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(self.user, course_id))
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_key))
|
||||
|
||||
# check that user has his default "Student" forum role for this course
|
||||
self.assertTrue(self.user.roles.filter(name="Student", course_id=course_id)) # pylint: disable=no-member
|
||||
self.assertTrue(self.user.roles.filter(name="Student", course_id=self.course_key)) # pylint: disable=no-member
|
||||
|
||||
def test_user_role_on_course_recreate_with_change_name_case(self):
|
||||
"""
|
||||
Test that creating same course again with different name case after deleting it gives user
|
||||
his default forum role "Student" for that course
|
||||
"""
|
||||
course_location = self.course_location
|
||||
# check that user has enrollment and his default "Student" forum role for this course
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(self.user, course_location.course_id))
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_key))
|
||||
# delete this course and recreate this course with same user
|
||||
delete_course_and_groups(course_location.course_id, commit=True)
|
||||
delete_course_and_groups(self.course_key, commit=True)
|
||||
|
||||
# now create same course with different name case ('uppercase')
|
||||
new_course_location = Location(
|
||||
['i4x', course_location.org, course_location.course.upper(), 'course', course_location.name]
|
||||
)
|
||||
resp = self._create_course_with_given_location(new_course_location)
|
||||
new_course_key = self.course_key.replace(course=self.course_key.course.upper())
|
||||
resp = self._create_course_with_given_location(new_course_key)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# check that user has his default "Student" forum role again for this course (with changed name case)
|
||||
self.assertTrue(
|
||||
self.user.roles.filter(name="Student", course_id=new_course_location.course_id) # pylint: disable=no-member
|
||||
self.user.roles.filter(name="Student", course_id=new_course_key) # pylint: disable=no-member
|
||||
)
|
||||
|
||||
# Disabled due to case-sensitive test db (sqlite3)
|
||||
# # check that there user has only one "Student" forum role (with new updated course_id)
|
||||
# self.assertEqual(self.user.roles.filter(name='Student').count(), 1) # pylint: disable=no-member
|
||||
# self.assertEqual(self.user.roles.filter(name='Student')[0].course_id, new_course_location.course_id)
|
||||
# self.assertEqual(self.user.roles.filter(name='Student')[0].course_id, new_course_location.course_key)
|
||||
|
||||
@@ -7,8 +7,8 @@ from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from contentstore import utils
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
|
||||
class LMSLinksTestCase(TestCase):
|
||||
@@ -57,29 +57,27 @@ class LMSLinksTestCase(TestCase):
|
||||
|
||||
def get_about_page_link(self):
|
||||
""" create mock course and return the about page link """
|
||||
location = Location('i4x', 'mitX', '101', 'course', 'test')
|
||||
return utils.get_lms_link_for_about_page(location)
|
||||
course_key = SlashSeparatedCourseKey('mitX', '101', 'test')
|
||||
return utils.get_lms_link_for_about_page(course_key)
|
||||
|
||||
def lms_link_test(self):
|
||||
""" Tests get_lms_link_for_item. """
|
||||
location = Location('i4x', 'mitX', '101', 'vertical', 'contacting_us')
|
||||
link = utils.get_lms_link_for_item(location, False, "mitX/101/test")
|
||||
course_key = SlashSeparatedCourseKey('mitX', '101', 'test')
|
||||
location = course_key.make_usage_key('vertical', 'contacting_us')
|
||||
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, "mitX/101/test")
|
||||
|
||||
# test preview
|
||||
link = utils.get_lms_link_for_item(location, True)
|
||||
self.assertEquals(
|
||||
link,
|
||||
"//preview/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us"
|
||||
)
|
||||
|
||||
# If no course_id is passed in, it is obtained from the location. This is the case for
|
||||
# Studio dashboard.
|
||||
location = Location('i4x', 'mitX', '101', 'course', 'test')
|
||||
# now test with the course' location
|
||||
location = course_key.make_usage_key('course', 'test')
|
||||
link = utils.get_lms_link_for_item(location)
|
||||
self.assertEquals(
|
||||
link,
|
||||
"//localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/course/test"
|
||||
)
|
||||
|
||||
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/course/test")
|
||||
|
||||
class ExtraPanelTabTestCase(TestCase):
|
||||
""" Tests adding and removing extra course tabs. """
|
||||
|
||||
@@ -7,9 +7,9 @@ import unittest
|
||||
|
||||
from django.test.utils import override_settings
|
||||
from django.core.cache import cache
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from contentstore.tests.utils import parse_json, user, registration, AjaxEnabledTestClient
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
@@ -235,13 +235,13 @@ class AuthTestCase(ContentStoreTestCase):
|
||||
def test_private_pages_auth(self):
|
||||
"""Make sure pages that do require login work."""
|
||||
auth_pages = (
|
||||
'/course',
|
||||
'/course/',
|
||||
)
|
||||
|
||||
# These are pages that should just load when the user is logged in
|
||||
# (no data needed)
|
||||
simple_auth_pages = (
|
||||
'/course',
|
||||
'/course/',
|
||||
)
|
||||
|
||||
# need an activated user
|
||||
@@ -267,7 +267,7 @@ class AuthTestCase(ContentStoreTestCase):
|
||||
def test_index_auth(self):
|
||||
|
||||
# not logged in. Should return a redirect.
|
||||
resp = self.client.get_html('/course')
|
||||
resp = self.client.get_html('/course/')
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
|
||||
# Logged in should work.
|
||||
@@ -284,16 +284,17 @@ class AuthTestCase(ContentStoreTestCase):
|
||||
self.login(self.email, self.pw)
|
||||
|
||||
# make sure we can access courseware immediately
|
||||
resp = self.client.get_html('/course')
|
||||
course_url = '/course/'
|
||||
resp = self.client.get_html(course_url)
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
|
||||
# then wait a bit and see if we get timed out
|
||||
time.sleep(2)
|
||||
|
||||
resp = self.client.get_html('/course')
|
||||
resp = self.client.get_html(course_url)
|
||||
|
||||
# re-request, and we should get a redirect to login page
|
||||
self.assertRedirects(resp, settings.LOGIN_REDIRECT_URL + '?next=/course')
|
||||
self.assertRedirects(resp, settings.LOGIN_REDIRECT_URL + '?next=/course/')
|
||||
|
||||
|
||||
class ForumTestCase(CourseTestCase):
|
||||
|
||||
@@ -92,10 +92,6 @@ class CourseTestCase(ModuleStoreTestCase):
|
||||
number='999',
|
||||
display_name='Robot Super Course',
|
||||
)
|
||||
self.course_location = self.course.location
|
||||
self.course_locator = loc_mapper().translate_location(
|
||||
self.course.location.course_id, self.course.location, False, True
|
||||
)
|
||||
self.store = get_modulestore(self.course.location)
|
||||
|
||||
def create_non_staff_authed_user_client(self, authenticate=True):
|
||||
@@ -134,7 +130,7 @@ class CourseTestCase(ModuleStoreTestCase):
|
||||
"""
|
||||
Reloads the course object from the database
|
||||
"""
|
||||
self.course = self.store.get_item(self.course.location)
|
||||
self.course = self.store.get_course(self.course.id)
|
||||
|
||||
def save_course(self):
|
||||
"""
|
||||
|
||||
@@ -6,16 +6,16 @@ import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from student.roles import CourseInstructorRole, CourseStaffRole
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import loc_mapper, modulestore
|
||||
from xmodule.modulestore.draft import DIRECT_ONLY_CATEGORIES
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey, Location
|
||||
from xmodule.modulestore.store_utilities import delete_course
|
||||
from xmodule.modulestore.draft import DIRECT_ONLY_CATEGORIES
|
||||
from student.roles import CourseInstructorRole, CourseStaffRole
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -34,25 +34,20 @@ def delete_course_and_groups(course_id, commit=False):
|
||||
module_store = modulestore('direct')
|
||||
content_store = contentstore()
|
||||
|
||||
course_id_dict = Location.parse_course_id(course_id)
|
||||
module_store.ignore_write_events_on_courses.append('{org}/{course}'.format(**course_id_dict))
|
||||
module_store.ignore_write_events_on_courses.add(course_id)
|
||||
|
||||
loc = CourseDescriptor.id_to_location(course_id)
|
||||
if delete_course(module_store, content_store, loc, commit):
|
||||
if delete_course(module_store, content_store, course_id, commit):
|
||||
|
||||
print 'removing User permissions from course....'
|
||||
# in the django layer, we need to remove all the user permissions groups associated with this course
|
||||
if commit:
|
||||
try:
|
||||
staff_role = CourseStaffRole(loc)
|
||||
staff_role = CourseStaffRole(course_id)
|
||||
staff_role.remove_users(*staff_role.users_with_role())
|
||||
instructor_role = CourseInstructorRole(loc)
|
||||
instructor_role = CourseInstructorRole(course_id)
|
||||
instructor_role.remove_users(*instructor_role.users_with_role())
|
||||
except Exception as err:
|
||||
log.error("Error in deleting course groups for {0}: {1}".format(loc, err))
|
||||
|
||||
# remove location of this course from loc_mapper and cache
|
||||
loc_mapper().delete_course_mapping(loc)
|
||||
log.error("Error in deleting course groups for {0}: {1}".format(course_id, err))
|
||||
|
||||
|
||||
def get_modulestore(category_or_location):
|
||||
@@ -68,131 +63,70 @@ def get_modulestore(category_or_location):
|
||||
return modulestore()
|
||||
|
||||
|
||||
def get_course_location_for_item(location):
|
||||
'''
|
||||
cdodge: for a given Xmodule, return the course that it belongs to
|
||||
NOTE: This makes a lot of assumptions about the format of the course location
|
||||
Also we have to assert that this module maps to only one course item - it'll throw an
|
||||
assert if not
|
||||
'''
|
||||
item_loc = Location(location)
|
||||
|
||||
# check to see if item is already a course, if so we can skip this
|
||||
if item_loc.category != 'course':
|
||||
# @hack! We need to find the course location however, we don't
|
||||
# know the 'name' parameter in this context, so we have
|
||||
# to assume there's only one item in this query even though we are not specifying a name
|
||||
course_search_location = Location('i4x', item_loc.org, item_loc.course, 'course', None)
|
||||
courses = modulestore().get_items(course_search_location)
|
||||
|
||||
# make sure we found exactly one match on this above course search
|
||||
found_cnt = len(courses)
|
||||
if found_cnt == 0:
|
||||
raise Exception('Could not find course at {0}'.format(course_search_location))
|
||||
|
||||
if found_cnt > 1:
|
||||
raise Exception('Found more than one course at {0}. There should only be one!!! Dump = {1}'.format(course_search_location, courses))
|
||||
|
||||
location = courses[0].location
|
||||
|
||||
return location
|
||||
|
||||
|
||||
def get_course_for_item(location):
|
||||
'''
|
||||
cdodge: for a given Xmodule, return the course that it belongs to
|
||||
NOTE: This makes a lot of assumptions about the format of the course location
|
||||
Also we have to assert that this module maps to only one course item - it'll throw an
|
||||
assert if not
|
||||
'''
|
||||
item_loc = Location(location)
|
||||
|
||||
# @hack! We need to find the course location however, we don't
|
||||
# know the 'name' parameter in this context, so we have
|
||||
# to assume there's only one item in this query even though we are not specifying a name
|
||||
course_search_location = Location('i4x', item_loc.org, item_loc.course, 'course', None)
|
||||
courses = modulestore().get_items(course_search_location)
|
||||
|
||||
# make sure we found exactly one match on this above course search
|
||||
found_cnt = len(courses)
|
||||
if found_cnt == 0:
|
||||
raise BaseException('Could not find course at {0}'.format(course_search_location))
|
||||
|
||||
if found_cnt > 1:
|
||||
raise BaseException('Found more than one course at {0}. There should only be one!!! Dump = {1}'.format(course_search_location, courses))
|
||||
|
||||
return courses[0]
|
||||
|
||||
|
||||
def get_lms_link_for_item(location, preview=False, course_id=None):
|
||||
def get_lms_link_for_item(location, preview=False):
|
||||
"""
|
||||
Returns an LMS link to the course with a jump_to to the provided location.
|
||||
|
||||
:param location: the location to jump to
|
||||
:param preview: True if the preview version of LMS should be returned. Default value is false.
|
||||
:param course_id: the course_id within which the location lives. If not specified, the course_id is obtained
|
||||
by calling Location(location).course_id; note that this only works for locations representing courses
|
||||
instead of elements within courses.
|
||||
"""
|
||||
if course_id is None:
|
||||
course_id = Location(location).course_id
|
||||
assert(isinstance(location, Location))
|
||||
|
||||
if settings.LMS_BASE is not None:
|
||||
if preview:
|
||||
lms_base = settings.FEATURES.get('PREVIEW_LMS_BASE')
|
||||
else:
|
||||
lms_base = settings.LMS_BASE
|
||||
if settings.LMS_BASE is None:
|
||||
return None
|
||||
|
||||
lms_link = u"//{lms_base}/courses/{course_id}/jump_to/{location}".format(
|
||||
lms_base=lms_base,
|
||||
course_id=course_id,
|
||||
location=Location(location)
|
||||
)
|
||||
if preview:
|
||||
lms_base = settings.FEATURES.get('PREVIEW_LMS_BASE')
|
||||
else:
|
||||
lms_link = None
|
||||
lms_base = settings.LMS_BASE
|
||||
|
||||
return lms_link
|
||||
return u"//{lms_base}/courses/{course_id}/jump_to/{location}".format(
|
||||
lms_base=lms_base,
|
||||
course_id=location.course_key.to_deprecated_string(),
|
||||
location=location.to_deprecated_string(),
|
||||
)
|
||||
|
||||
|
||||
def get_lms_link_for_about_page(location):
|
||||
def get_lms_link_for_about_page(course_id):
|
||||
"""
|
||||
Returns the url to the course about page from the location tuple.
|
||||
"""
|
||||
|
||||
assert(isinstance(course_id, SlashSeparatedCourseKey))
|
||||
|
||||
if settings.FEATURES.get('ENABLE_MKTG_SITE', False):
|
||||
if not hasattr(settings, 'MKTG_URLS'):
|
||||
log.exception("ENABLE_MKTG_SITE is True, but MKTG_URLS is not defined.")
|
||||
about_base = None
|
||||
else:
|
||||
marketing_urls = settings.MKTG_URLS
|
||||
if marketing_urls.get('ROOT', None) is None:
|
||||
log.exception('There is no ROOT defined in MKTG_URLS')
|
||||
about_base = None
|
||||
else:
|
||||
# Root will be "https://www.edx.org". The complete URL will still not be exactly correct,
|
||||
# but redirects exist from www.edx.org to get to the Drupal course about page URL.
|
||||
about_base = marketing_urls.get('ROOT')
|
||||
# Strip off https:// (or http://) to be consistent with the formatting of LMS_BASE.
|
||||
about_base = re.sub(r"^https?://", "", about_base)
|
||||
return None
|
||||
|
||||
marketing_urls = settings.MKTG_URLS
|
||||
|
||||
# Root will be "https://www.edx.org". The complete URL will still not be exactly correct,
|
||||
# but redirects exist from www.edx.org to get to the Drupal course about page URL.
|
||||
about_base = marketing_urls.get('ROOT', None)
|
||||
|
||||
if about_base is None:
|
||||
log.exception('There is no ROOT defined in MKTG_URLS')
|
||||
return None
|
||||
|
||||
# Strip off https:// (or http://) to be consistent with the formatting of LMS_BASE.
|
||||
about_base = re.sub(r"^https?://", "", about_base)
|
||||
|
||||
elif settings.LMS_BASE is not None:
|
||||
about_base = settings.LMS_BASE
|
||||
else:
|
||||
about_base = None
|
||||
return None
|
||||
|
||||
if about_base is not None:
|
||||
lms_link = u"//{about_base_url}/courses/{course_id}/about".format(
|
||||
about_base_url=about_base,
|
||||
course_id=Location(location).course_id
|
||||
)
|
||||
else:
|
||||
lms_link = None
|
||||
|
||||
return lms_link
|
||||
return u"//{about_base_url}/courses/{course_id}/about".format(
|
||||
about_base_url=about_base,
|
||||
course_id=course_id.to_deprecated_string()
|
||||
)
|
||||
|
||||
|
||||
def course_image_url(course):
|
||||
"""Returns the image url for the course."""
|
||||
loc = StaticContent.compute_location(course.location.org, course.location.course, course.course_image)
|
||||
path = StaticContent.get_url_path_from_location(loc)
|
||||
loc = StaticContent.compute_location(course.location.course_key, course.course_image)
|
||||
path = loc.to_deprecated_string()
|
||||
return path
|
||||
|
||||
|
||||
@@ -265,3 +199,28 @@ def remove_extra_panel_tab(tab_type, course):
|
||||
course_tabs = [ct for ct in course_tabs if ct != tab_panel]
|
||||
changed = True
|
||||
return changed, course_tabs
|
||||
|
||||
|
||||
def reverse_url(handler_name, key_name=None, key_value=None, kwargs=None):
|
||||
"""
|
||||
Creates the URL for the given handler.
|
||||
The optional key_name and key_value are passed in as kwargs to the handler.
|
||||
"""
|
||||
kwargs_for_reverse = {key_name: unicode(key_value)} if key_name else None
|
||||
if kwargs:
|
||||
kwargs_for_reverse.update(kwargs)
|
||||
return reverse('contentstore.views.' + handler_name, kwargs=kwargs_for_reverse)
|
||||
|
||||
|
||||
def reverse_course_url(handler_name, course_key, kwargs=None):
|
||||
"""
|
||||
Creates the URL for handlers that use course_keys as URL parameters.
|
||||
"""
|
||||
return reverse_url(handler_name, 'course_key_string', course_key, kwargs)
|
||||
|
||||
|
||||
def reverse_usage_url(handler_name, usage_key, kwargs=None):
|
||||
"""
|
||||
Creates the URL for handlers that use usage_keys as URL parameters.
|
||||
"""
|
||||
return reverse_url(handler_name, 'usage_key_string', usage_key, kwargs)
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
from ..utils import get_course_location_for_item
|
||||
from xmodule.modulestore.locator import CourseLocator
|
||||
from student.roles import CourseStaffRole, GlobalStaff, CourseInstructorRole
|
||||
from student import auth
|
||||
|
||||
|
||||
def has_course_access(user, location, role=CourseStaffRole):
|
||||
def has_course_access(user, course_key, role=CourseStaffRole):
|
||||
"""
|
||||
Return True if user allowed to access this piece of data
|
||||
Return True if user allowed to access this course_id
|
||||
Note that the CMS permissions model is with respect to courses
|
||||
There is a super-admin permissions if user.is_staff is set
|
||||
Also, since we're unifying the user database between LMS and CAS,
|
||||
@@ -16,21 +14,22 @@ def has_course_access(user, location, role=CourseStaffRole):
|
||||
"""
|
||||
if GlobalStaff().has_user(user):
|
||||
return True
|
||||
if not isinstance(location, CourseLocator):
|
||||
# this can be expensive if location is not category=='course'
|
||||
location = get_course_location_for_item(location)
|
||||
return auth.has_access(user, role(location))
|
||||
return auth.has_access(user, role(course_key))
|
||||
|
||||
|
||||
def get_user_role(user, location, context=None):
|
||||
def get_user_role(user, course_id):
|
||||
"""
|
||||
Return corresponding string if user has staff or instructor role in Studio.
|
||||
What type of access: staff or instructor does this user have in Studio?
|
||||
|
||||
No code should use this for access control, only to quickly serialize the type of access
|
||||
where this code knows that Instructor trumps Staff and assumes the user has one or the other.
|
||||
|
||||
This will not return student role because its purpose for using in Studio.
|
||||
|
||||
:param location: a descriptor.location (which may be a Location or a CourseLocator)
|
||||
:param context: a course_id. This is not used if location is a CourseLocator.
|
||||
:param course_id: the course_id of the course we're interested in
|
||||
"""
|
||||
if auth.has_access(user, CourseInstructorRole(location, context)):
|
||||
# afaik, this is only used in lti
|
||||
if auth.has_access(user, CourseInstructorRole(course_id)):
|
||||
return 'instructor'
|
||||
else:
|
||||
return 'staff'
|
||||
|
||||
@@ -13,15 +13,13 @@ from django.conf import settings
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from cache_toolbox.core import del_cached_content
|
||||
|
||||
from contentstore.utils import reverse_course_url
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.modulestore import InvalidLocationError
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
from xmodule.modulestore.locator import BlockUsageLocator
|
||||
from xmodule.modulestore.keys import CourseKey, AssetKey
|
||||
|
||||
from util.date_utils import get_default_time_display
|
||||
from util.json_request import JsonResponse
|
||||
@@ -29,13 +27,15 @@ from django.http import HttpResponseNotFound
|
||||
from django.utils.translation import ugettext as _
|
||||
from pymongo import ASCENDING, DESCENDING
|
||||
from .access import has_course_access
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
__all__ = ['assets_handler']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def assets_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None, asset_id=None):
|
||||
def assets_handler(request, course_key_string=None, asset_key_string=None):
|
||||
"""
|
||||
The restful handler for assets.
|
||||
It allows retrieval of all the assets (as an HTML page), as well as uploading new assets,
|
||||
@@ -56,38 +56,38 @@ def assets_handler(request, tag=None, package_id=None, branch=None, version_guid
|
||||
DELETE
|
||||
json: delete an asset
|
||||
"""
|
||||
location = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block)
|
||||
if not has_course_access(request.user, location):
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
if not has_course_access(request.user, course_key):
|
||||
raise PermissionDenied()
|
||||
|
||||
response_format = request.REQUEST.get('format', 'html')
|
||||
if response_format == 'json' or 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'):
|
||||
if request.method == 'GET':
|
||||
return _assets_json(request, location)
|
||||
return _assets_json(request, course_key)
|
||||
else:
|
||||
return _update_asset(request, location, asset_id)
|
||||
asset_key = AssetKey.from_string(asset_key_string) if asset_key_string else None
|
||||
return _update_asset(request, course_key, asset_key)
|
||||
elif request.method == 'GET': # assume html
|
||||
return _asset_index(request, location)
|
||||
return _asset_index(request, course_key)
|
||||
else:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
|
||||
def _asset_index(request, location):
|
||||
def _asset_index(request, course_key):
|
||||
"""
|
||||
Display an editable asset library.
|
||||
|
||||
Supports start (0-based index into the list of assets) and max query parameters.
|
||||
"""
|
||||
old_location = loc_mapper().translate_locator_to_location(location)
|
||||
course_module = modulestore().get_item(old_location)
|
||||
course_module = modulestore().get_course(course_key)
|
||||
|
||||
return render_to_response('asset_index.html', {
|
||||
'context_course': course_module,
|
||||
'asset_callback_url': location.url_reverse('assets/', '')
|
||||
'asset_callback_url': reverse_course_url('assets_handler', course_key)
|
||||
})
|
||||
|
||||
|
||||
def _assets_json(request, location):
|
||||
def _assets_json(request, course_key):
|
||||
"""
|
||||
Display an editable asset library.
|
||||
|
||||
@@ -109,23 +109,24 @@ def _assets_json(request, location):
|
||||
|
||||
current_page = max(requested_page, 0)
|
||||
start = current_page * requested_page_size
|
||||
assets, total_count = _get_assets_for_page(request, location, current_page, requested_page_size, sort)
|
||||
assets, total_count = _get_assets_for_page(request, course_key, current_page, requested_page_size, sort)
|
||||
end = start + len(assets)
|
||||
|
||||
# If the query is beyond the final page, then re-query the final page so that at least one asset is returned
|
||||
if requested_page > 0 and start >= total_count:
|
||||
current_page = int(math.floor((total_count - 1) / requested_page_size))
|
||||
start = current_page * requested_page_size
|
||||
assets, total_count = _get_assets_for_page(request, location, current_page, requested_page_size, sort)
|
||||
assets, total_count = _get_assets_for_page(request, course_key, current_page, requested_page_size, sort)
|
||||
end = start + len(assets)
|
||||
|
||||
asset_json = []
|
||||
for asset in assets:
|
||||
asset_id = asset['_id']
|
||||
asset_location = StaticContent.compute_location(asset_id['org'], asset_id['course'], asset_id['name'])
|
||||
asset_location = StaticContent.compute_location(course_key, asset_id['name'])
|
||||
# note, due to the schema change we may not have a 'thumbnail_location' in the result set
|
||||
_thumbnail_location = asset.get('thumbnail_location', None)
|
||||
thumbnail_location = Location(_thumbnail_location) if _thumbnail_location is not None else None
|
||||
thumbnail_location = asset.get('thumbnail_location', None)
|
||||
if thumbnail_location:
|
||||
thumbnail_location = course_key.make_asset_key('thumbnail', thumbnail_location[4])
|
||||
|
||||
asset_locked = asset.get('locked', False)
|
||||
asset_json.append(_get_asset_json(asset['displayname'], asset['uploadDate'], asset_location, thumbnail_location, asset_locked))
|
||||
@@ -141,37 +142,32 @@ def _assets_json(request, location):
|
||||
})
|
||||
|
||||
|
||||
def _get_assets_for_page(request, location, current_page, page_size, sort):
|
||||
def _get_assets_for_page(request, course_key, current_page, page_size, sort):
|
||||
"""
|
||||
Returns the list of assets for the specified page and page size.
|
||||
"""
|
||||
start = current_page * page_size
|
||||
|
||||
old_location = loc_mapper().translate_locator_to_location(location)
|
||||
|
||||
course_reference = StaticContent.compute_location(old_location.org, old_location.course, old_location.name)
|
||||
return contentstore().get_all_content_for_course(
|
||||
course_reference, start=start, maxresults=page_size, sort=sort
|
||||
course_key, start=start, maxresults=page_size, sort=sort
|
||||
)
|
||||
|
||||
|
||||
@require_POST
|
||||
@ensure_csrf_cookie
|
||||
@login_required
|
||||
def _upload_asset(request, location):
|
||||
def _upload_asset(request, course_key):
|
||||
'''
|
||||
This method allows for POST uploading of files into the course asset
|
||||
library, which will be supported by GridFS in MongoDB.
|
||||
'''
|
||||
old_location = loc_mapper().translate_locator_to_location(location)
|
||||
|
||||
# Does the course actually exist?!? Get anything from it to prove its
|
||||
# existence
|
||||
try:
|
||||
modulestore().get_item(old_location)
|
||||
except:
|
||||
modulestore().get_course(course_key)
|
||||
except ItemNotFoundError:
|
||||
# no return it as a Bad Request response
|
||||
logging.error("Could not find course: %s", old_location)
|
||||
logging.error("Could not find course: %s", course_key)
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
# compute a 'filename' which is similar to the location formatting, we're
|
||||
@@ -182,7 +178,7 @@ def _upload_asset(request, location):
|
||||
filename = upload_file.name
|
||||
mime_type = upload_file.content_type
|
||||
|
||||
content_loc = StaticContent.compute_location(old_location.org, old_location.course, filename)
|
||||
content_loc = StaticContent.compute_location(course_key, filename)
|
||||
|
||||
chunked = upload_file.multiple_chunks()
|
||||
sc_partial = partial(StaticContent, content_loc, filename, mime_type)
|
||||
@@ -225,26 +221,17 @@ def _upload_asset(request, location):
|
||||
@require_http_methods(("DELETE", "POST", "PUT"))
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def _update_asset(request, location, asset_id):
|
||||
def _update_asset(request, course_key, asset_key):
|
||||
"""
|
||||
restful CRUD operations for a course asset.
|
||||
Currently only DELETE, POST, and PUT methods are implemented.
|
||||
|
||||
asset_id: the URL of the asset (used by Backbone as the id)
|
||||
asset_path_encoding: the odd /c4x/org/course/category/name repr of the asset (used by Backbone as the id)
|
||||
"""
|
||||
def get_asset_location(asset_id):
|
||||
""" Helper method to get the location (and verify it is valid). """
|
||||
try:
|
||||
return StaticContent.get_location_from_path(asset_id)
|
||||
except InvalidLocationError as err:
|
||||
# return a 'Bad Request' to browser as we have a malformed Location
|
||||
return JsonResponse({"error": err.message}, status=400)
|
||||
|
||||
if request.method == 'DELETE':
|
||||
loc = get_asset_location(asset_id)
|
||||
# Make sure the item to delete actually exists.
|
||||
try:
|
||||
content = contentstore().find(loc)
|
||||
content = contentstore().find(asset_key)
|
||||
except NotFoundError:
|
||||
return JsonResponse(status=404)
|
||||
|
||||
@@ -253,15 +240,18 @@ def _update_asset(request, location, asset_id):
|
||||
|
||||
# see if there is a thumbnail as well, if so move that as well
|
||||
if content.thumbnail_location is not None:
|
||||
# We are ignoring the value of the thumbnail_location-- we only care whether
|
||||
# or not a thumbnail has been stored, and we can now easily create the correct path.
|
||||
thumbnail_location = course_key.make_asset_key('thumbnail', asset_key.name)
|
||||
try:
|
||||
thumbnail_content = contentstore().find(content.thumbnail_location)
|
||||
thumbnail_content = contentstore().find(thumbnail_location)
|
||||
contentstore('trashcan').save(thumbnail_content)
|
||||
# hard delete thumbnail from origin
|
||||
contentstore().delete(thumbnail_content.get_id())
|
||||
# remove from any caching
|
||||
del_cached_content(thumbnail_content.location)
|
||||
del_cached_content(thumbnail_location)
|
||||
except:
|
||||
logging.warning('Could not delete thumbnail: %s', content.thumbnail_location)
|
||||
logging.warning('Could not delete thumbnail: %s', thumbnail_location)
|
||||
|
||||
# delete the original
|
||||
contentstore().delete(content.get_id())
|
||||
@@ -271,18 +261,16 @@ def _update_asset(request, location, asset_id):
|
||||
|
||||
elif request.method in ('PUT', 'POST'):
|
||||
if 'file' in request.FILES:
|
||||
return _upload_asset(request, location)
|
||||
return _upload_asset(request, course_key)
|
||||
else:
|
||||
# Update existing asset
|
||||
try:
|
||||
modified_asset = json.loads(request.body)
|
||||
except ValueError:
|
||||
return HttpResponseBadRequest()
|
||||
asset_id = modified_asset['url']
|
||||
asset_location = get_asset_location(asset_id)
|
||||
contentstore().set_attr(asset_location, 'locked', modified_asset['locked'])
|
||||
contentstore().set_attr(asset_key, 'locked', modified_asset['locked'])
|
||||
# Delete the asset from the cache so we check the lock status the next time it is requested.
|
||||
del_cached_content(asset_location)
|
||||
del_cached_content(asset_key)
|
||||
return JsonResponse(modified_asset, status=201)
|
||||
|
||||
|
||||
@@ -290,7 +278,7 @@ def _get_asset_json(display_name, date, location, thumbnail_location, locked):
|
||||
"""
|
||||
Helper method for formatting the asset information to send to client.
|
||||
"""
|
||||
asset_url = StaticContent.get_url_path_from_location(location)
|
||||
asset_url = location.to_deprecated_string()
|
||||
external_url = settings.LMS_BASE + asset_url
|
||||
return {
|
||||
'display_name': display_name,
|
||||
@@ -298,8 +286,8 @@ def _get_asset_json(display_name, date, location, thumbnail_location, locked):
|
||||
'url': asset_url,
|
||||
'external_url': external_url,
|
||||
'portable_url': StaticContent.get_static_path_from_location(location),
|
||||
'thumbnail': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_location is not None else None,
|
||||
'thumbnail': thumbnail_location.to_deprecated_string() if thumbnail_location is not None else None,
|
||||
'locked': locked,
|
||||
# Needed for Backbone delete/update.
|
||||
'id': asset_url
|
||||
'id': unicode(location)
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@ from django_future.csrf import ensure_csrf_cookie
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from django.http import HttpResponseNotFound
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
from xmodule.modulestore.keys import CourseKey
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from contentstore.utils import get_modulestore, reverse_course_url
|
||||
|
||||
from ..utils import get_modulestore
|
||||
from .access import has_course_access
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.locator import BlockUsageLocator
|
||||
|
||||
from django.utils.translation import ugettext
|
||||
|
||||
@@ -25,7 +25,7 @@ __all__ = ['checklists_handler']
|
||||
@require_http_methods(("GET", "POST", "PUT"))
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def checklists_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None, checklist_index=None):
|
||||
def checklists_handler(request, course_key_string, checklist_index=None):
|
||||
"""
|
||||
The restful handler for checklists.
|
||||
|
||||
@@ -35,14 +35,11 @@ def checklists_handler(request, tag=None, package_id=None, branch=None, version_
|
||||
POST or PUT
|
||||
json: updates the checked state for items within a particular checklist. checklist_index is required.
|
||||
"""
|
||||
location = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block)
|
||||
if not has_course_access(request.user, location):
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
if not has_course_access(request.user, course_key):
|
||||
raise PermissionDenied()
|
||||
|
||||
old_location = loc_mapper().translate_locator_to_location(location)
|
||||
|
||||
modulestore = get_modulestore(old_location)
|
||||
course_module = modulestore.get_item(old_location)
|
||||
course_module = modulestore().get_course(course_key)
|
||||
|
||||
json_request = 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json')
|
||||
if request.method == 'GET':
|
||||
@@ -50,13 +47,13 @@ def checklists_handler(request, tag=None, package_id=None, branch=None, version_
|
||||
# from the template.
|
||||
if not course_module.checklists:
|
||||
course_module.checklists = CourseDescriptor.checklists.default
|
||||
modulestore.update_item(course_module, request.user.id)
|
||||
get_modulestore(course_module.location).update_item(course_module, request.user.id)
|
||||
|
||||
expanded_checklists = expand_all_action_urls(course_module)
|
||||
if json_request:
|
||||
return JsonResponse(expanded_checklists)
|
||||
else:
|
||||
handler_url = location.url_reverse('checklists/', '')
|
||||
handler_url = reverse_course_url('checklists_handler', course_key)
|
||||
return render_to_response('checklists.html',
|
||||
{
|
||||
'handler_url': handler_url,
|
||||
@@ -79,7 +76,7 @@ def checklists_handler(request, tag=None, package_id=None, branch=None, version_
|
||||
# not default
|
||||
course_module.checklists = course_module.checklists
|
||||
course_module.save()
|
||||
modulestore.update_item(course_module, request.user.id)
|
||||
get_modulestore(course_module.location).update_item(course_module, request.user.id)
|
||||
expanded_checklist = expand_checklist_action_url(course_module, persisted_checklist)
|
||||
return JsonResponse(localize_checklist_text(expanded_checklist))
|
||||
else:
|
||||
@@ -114,19 +111,16 @@ def expand_checklist_action_url(course_module, checklist):
|
||||
expanded_checklist = copy.deepcopy(checklist)
|
||||
|
||||
urlconf_map = {
|
||||
"ManageUsers": "course_team",
|
||||
"CourseOutline": "course",
|
||||
"SettingsDetails": "settings/details",
|
||||
"SettingsGrading": "settings/grading",
|
||||
"ManageUsers": "course_team_handler",
|
||||
"CourseOutline": "course_handler",
|
||||
"SettingsDetails": "settings_handler",
|
||||
"SettingsGrading": "grading_handler",
|
||||
}
|
||||
|
||||
for item in expanded_checklist.get('items'):
|
||||
action_url = item.get('action_url')
|
||||
if action_url in urlconf_map:
|
||||
url_prefix = urlconf_map[action_url]
|
||||
ctx_loc = course_module.location
|
||||
location = loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True)
|
||||
item['action_url'] = location.url_reverse(url_prefix, '')
|
||||
item['action_url'] = reverse_course_url(urlconf_map[action_url], course_module.id)
|
||||
|
||||
return expanded_checklist
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@ from edxmako.shortcuts import render_to_response
|
||||
|
||||
from util.date_utils import get_default_time_display
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
from xmodule.modulestore.locator import BlockUsageLocator
|
||||
|
||||
from xblock.core import XBlock
|
||||
from xblock.django.request import webob_to_django_response, django_to_webob_request
|
||||
@@ -24,12 +22,11 @@ from xblock.fields import Scope
|
||||
from xblock.plugin import PluginMissingError
|
||||
from xblock.runtime import Mixologist
|
||||
|
||||
from lms.lib.xblock.runtime import unquote_slashes
|
||||
|
||||
from contentstore.utils import get_lms_link_for_item, compute_publish_state, PublishState, get_modulestore
|
||||
from contentstore.views.helpers import get_parent_xblock
|
||||
|
||||
from models.settings.course_grading import CourseGradingModel
|
||||
from xmodule.modulestore.keys import UsageKey
|
||||
|
||||
from .access import has_course_access
|
||||
|
||||
@@ -70,7 +67,7 @@ ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
|
||||
|
||||
@require_GET
|
||||
@login_required
|
||||
def subsection_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None):
|
||||
def subsection_handler(request, usage_key_string):
|
||||
"""
|
||||
The restful handler for subsection-specific requests.
|
||||
|
||||
@@ -79,13 +76,13 @@ def subsection_handler(request, tag=None, package_id=None, branch=None, version_
|
||||
json: not currently supported
|
||||
"""
|
||||
if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
|
||||
locator = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block)
|
||||
usage_key = UsageKey.from_string(usage_key_string)
|
||||
try:
|
||||
old_location, course, item, lms_link = _get_item_in_course(request, locator)
|
||||
course, item, lms_link = _get_item_in_course(request, usage_key)
|
||||
except ItemNotFoundError:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
preview_link = get_lms_link_for_item(old_location, course_id=course.location.course_id, preview=True)
|
||||
preview_link = get_lms_link_for_item(usage_key, preview=True)
|
||||
|
||||
# make sure that location references a 'sequential', otherwise return
|
||||
# BadRequest
|
||||
@@ -114,10 +111,6 @@ def subsection_handler(request, tag=None, package_id=None, branch=None, version_
|
||||
can_view_live = True
|
||||
break
|
||||
|
||||
course_locator = loc_mapper().translate_location(
|
||||
course.location.course_id, course.location, False, True
|
||||
)
|
||||
|
||||
return render_to_response(
|
||||
'edit_subsection.html',
|
||||
{
|
||||
@@ -126,9 +119,9 @@ def subsection_handler(request, tag=None, package_id=None, branch=None, version_
|
||||
'new_unit_category': 'vertical',
|
||||
'lms_link': lms_link,
|
||||
'preview_link': preview_link,
|
||||
'course_graders': json.dumps(CourseGradingModel.fetch(course_locator).graders),
|
||||
'course_graders': json.dumps(CourseGradingModel.fetch(usage_key.course_key).graders),
|
||||
'parent_item': parent,
|
||||
'locator': locator,
|
||||
'locator': usage_key,
|
||||
'policy_metadata': policy_metadata,
|
||||
'subsection_units': subsection_units,
|
||||
'can_view_live': can_view_live
|
||||
@@ -149,7 +142,7 @@ def _load_mixed_class(category):
|
||||
|
||||
@require_GET
|
||||
@login_required
|
||||
def unit_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None):
|
||||
def unit_handler(request, usage_key_string):
|
||||
"""
|
||||
The restful handler for unit-specific requests.
|
||||
|
||||
@@ -158,21 +151,15 @@ def unit_handler(request, tag=None, package_id=None, branch=None, version_guid=N
|
||||
json: not currently supported
|
||||
"""
|
||||
if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
|
||||
locator = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block)
|
||||
usage_key = UsageKey.from_string(usage_key_string)
|
||||
try:
|
||||
old_location, course, item, lms_link = _get_item_in_course(request, locator)
|
||||
course, item, lms_link = _get_item_in_course(request, usage_key)
|
||||
except ItemNotFoundError:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
component_templates = _get_component_templates(course)
|
||||
|
||||
xblocks = item.get_children()
|
||||
locators = [
|
||||
loc_mapper().translate_location(
|
||||
course.location.course_id, xblock.location, False, True
|
||||
)
|
||||
for xblock in xblocks
|
||||
]
|
||||
|
||||
# TODO (cpennington): If we share units between courses,
|
||||
# this will need to change to check permissions correctly so as
|
||||
@@ -209,8 +196,8 @@ def unit_handler(request, tag=None, package_id=None, branch=None, version_guid=N
|
||||
return render_to_response('unit.html', {
|
||||
'context_course': course,
|
||||
'unit': item,
|
||||
'unit_locator': locator,
|
||||
'locators': locators,
|
||||
'unit_usage_key': usage_key,
|
||||
'child_usage_keys': [block.scope_ids.usage_id for block in xblocks],
|
||||
'component_templates': json.dumps(component_templates),
|
||||
'draft_preview_link': preview_lms_link,
|
||||
'published_preview_link': lms_link,
|
||||
@@ -234,7 +221,7 @@ def unit_handler(request, tag=None, package_id=None, branch=None, version_guid=N
|
||||
# pylint: disable=unused-argument
|
||||
@require_GET
|
||||
@login_required
|
||||
def container_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None):
|
||||
def container_handler(request, usage_key_string):
|
||||
"""
|
||||
The restful handler for container xblock requests.
|
||||
|
||||
@@ -243,9 +230,10 @@ def container_handler(request, tag=None, package_id=None, branch=None, version_g
|
||||
json: not currently supported
|
||||
"""
|
||||
if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
|
||||
locator = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block)
|
||||
|
||||
usage_key = UsageKey.from_string(usage_key_string)
|
||||
try:
|
||||
__, course, xblock, __ = _get_item_in_course(request, locator)
|
||||
course, xblock, __ = _get_item_in_course(request, usage_key)
|
||||
except ItemNotFoundError:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
@@ -261,11 +249,10 @@ def container_handler(request, tag=None, package_id=None, branch=None, version_g
|
||||
unit_publish_state = compute_publish_state(unit) if unit else None
|
||||
|
||||
return render_to_response('container.html', {
|
||||
'context_course': course,
|
||||
'xblock': xblock,
|
||||
'xblock_locator': locator,
|
||||
'unit': unit,
|
||||
'unit_publish_state': unit_publish_state,
|
||||
'xblock_locator': usage_key,
|
||||
'unit': None if not ancestor_xblocks else ancestor_xblocks[0],
|
||||
'ancestor_xblocks': ancestor_xblocks,
|
||||
'component_templates': json.dumps(component_templates),
|
||||
})
|
||||
@@ -368,32 +355,32 @@ def _get_component_templates(course):
|
||||
|
||||
|
||||
@login_required
|
||||
def _get_item_in_course(request, locator):
|
||||
def _get_item_in_course(request, usage_key):
|
||||
"""
|
||||
Helper method for getting the old location, containing course,
|
||||
item, and lms_link for a given locator.
|
||||
|
||||
Verifies that the caller has permission to access this item.
|
||||
"""
|
||||
if not has_course_access(request.user, locator):
|
||||
course_key = usage_key.course_key
|
||||
|
||||
if not has_course_access(request.user, course_key):
|
||||
raise PermissionDenied()
|
||||
|
||||
old_location = loc_mapper().translate_locator_to_location(locator)
|
||||
course_location = loc_mapper().translate_locator_to_location(locator, True)
|
||||
course = modulestore().get_item(course_location)
|
||||
item = modulestore().get_item(old_location, depth=1)
|
||||
lms_link = get_lms_link_for_item(old_location, course_id=course.location.course_id)
|
||||
course = modulestore().get_course(course_key)
|
||||
item = get_modulestore(usage_key).get_item(usage_key, depth=1)
|
||||
lms_link = get_lms_link_for_item(usage_key)
|
||||
|
||||
return old_location, course, item, lms_link
|
||||
return course, item, lms_link
|
||||
|
||||
|
||||
@login_required
|
||||
def component_handler(request, usage_id, handler, suffix=''):
|
||||
def component_handler(request, usage_key_string, handler, suffix=''):
|
||||
"""
|
||||
Dispatch an AJAX action to an xblock
|
||||
|
||||
Args:
|
||||
usage_id: The usage-id of the block to dispatch to, passed through `quote_slashes`
|
||||
usage_id: The usage-id of the block to dispatch to
|
||||
handler (str): The handler to execute
|
||||
suffix (str): The remainder of the url to be passed to the handler
|
||||
|
||||
@@ -402,9 +389,9 @@ def component_handler(request, usage_id, handler, suffix=''):
|
||||
django response
|
||||
"""
|
||||
|
||||
location = unquote_slashes(usage_id)
|
||||
usage_key = UsageKey.from_string(usage_key_string)
|
||||
|
||||
descriptor = get_modulestore(location).get_item(location)
|
||||
descriptor = get_modulestore(usage_key).get_item(usage_key)
|
||||
# Let the module handle the AJAX
|
||||
req = django_to_webob_request(request)
|
||||
|
||||
@@ -417,6 +404,6 @@ def component_handler(request, usage_id, handler, suffix=''):
|
||||
|
||||
# unintentional update to handle any side effects of handle call; so, request user didn't author
|
||||
# the change
|
||||
get_modulestore(location).update_item(descriptor, None)
|
||||
get_modulestore(usage_key).update_item(descriptor, None)
|
||||
|
||||
return webob_to_django_response(resp)
|
||||
|
||||
@@ -4,10 +4,7 @@ Views related to operations on course objects
|
||||
import json
|
||||
import random
|
||||
import string # pylint: disable=W0402
|
||||
import re
|
||||
import bson
|
||||
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
@@ -20,18 +17,22 @@ from util.json_request import JsonResponse
|
||||
from edxmako.shortcuts import render_to_response
|
||||
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from xmodule.modulestore.django import modulestore, loc_mapper
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.tabs import PDFTextbookTabs
|
||||
|
||||
from xmodule.modulestore.exceptions import (
|
||||
ItemNotFoundError, InvalidLocationError)
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError, InsufficientSpecificationError
|
||||
from opaque_keys import InvalidKeyError
|
||||
from xmodule.modulestore.locations import Location, SlashSeparatedCourseKey
|
||||
|
||||
from contentstore.course_info_model import get_course_updates, update_course_updates, delete_course_update
|
||||
from contentstore.utils import (
|
||||
get_lms_link_for_item, add_extra_panel_tab, remove_extra_panel_tab,
|
||||
get_modulestore)
|
||||
get_lms_link_for_item,
|
||||
add_extra_panel_tab,
|
||||
remove_extra_panel_tab,
|
||||
get_modulestore,
|
||||
reverse_course_url
|
||||
)
|
||||
from models.settings.course_details import CourseDetails, CourseSettingsEncoder
|
||||
|
||||
from models.settings.course_grading import CourseGradingModel
|
||||
@@ -41,16 +42,19 @@ from util.string_utils import _has_non_ascii_characters
|
||||
|
||||
from .access import has_course_access
|
||||
from .component import (
|
||||
OPEN_ENDED_COMPONENT_TYPES, NOTE_COMPONENT_TYPES,
|
||||
ADVANCED_COMPONENT_POLICY_KEY)
|
||||
OPEN_ENDED_COMPONENT_TYPES,
|
||||
NOTE_COMPONENT_TYPES,
|
||||
ADVANCED_COMPONENT_POLICY_KEY
|
||||
)
|
||||
|
||||
from django_comment_common.models import assign_default_role
|
||||
from django_comment_common.utils import seed_permissions_roles
|
||||
|
||||
from student.models import CourseEnrollment
|
||||
from student.roles import CourseRole, UserBasedRole
|
||||
|
||||
from xmodule.html_module import AboutDescriptor
|
||||
from xmodule.modulestore.locator import BlockUsageLocator, CourseLocator
|
||||
from xmodule.modulestore.keys import CourseKey
|
||||
from course_creators.views import get_course_creator_status, add_user_with_status_unrequested
|
||||
from contentstore import utils
|
||||
from student.roles import CourseInstructorRole, CourseStaffRole, CourseCreatorRole, GlobalStaff
|
||||
@@ -65,26 +69,20 @@ __all__ = ['course_info_handler', 'course_handler', 'course_info_update_handler'
|
||||
'textbooks_list_handler', 'textbooks_detail_handler']
|
||||
|
||||
|
||||
def _get_locator_and_course(package_id, branch, version_guid, block_id, user, depth=0):
|
||||
def _get_course_module(course_key, user, depth=0):
|
||||
"""
|
||||
Internal method used to calculate and return the locator and course module
|
||||
for the view functions in this file.
|
||||
"""
|
||||
locator = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block_id)
|
||||
if not has_course_access(user, locator):
|
||||
if not has_course_access(user, course_key):
|
||||
raise PermissionDenied()
|
||||
|
||||
course_location = loc_mapper().translate_locator_to_location(locator)
|
||||
if course_location is None:
|
||||
raise PermissionDenied()
|
||||
|
||||
course_module = modulestore().get_item(course_location, depth=depth)
|
||||
return locator, course_module
|
||||
course_module = modulestore().get_course(course_key, depth=depth)
|
||||
return course_module
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@login_required
|
||||
def course_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None):
|
||||
def course_handler(request, course_key_string=None):
|
||||
"""
|
||||
The restful handler for course specific requests.
|
||||
It provides the course tree with the necessary information for identifying and labeling the parts. The root
|
||||
@@ -102,20 +100,17 @@ def course_handler(request, tag=None, package_id=None, branch=None, version_guid
|
||||
index entry.
|
||||
PUT
|
||||
json: update this course (index entry not xblock) such as repointing head, changing display name, org,
|
||||
package_id. Return same json as above.
|
||||
offering. Return same json as above.
|
||||
DELETE
|
||||
json: delete this branch from this course (leaving off /branch/draft would imply delete the course)
|
||||
"""
|
||||
response_format = request.REQUEST.get('format', 'html')
|
||||
if response_format == 'json' or 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'):
|
||||
if request.method == 'GET':
|
||||
return JsonResponse(_course_json(request, package_id, branch, version_guid, block))
|
||||
return JsonResponse(_course_json(request, CourseKey.from_string(course_key_string)))
|
||||
elif request.method == 'POST': # not sure if this is only post. If one will have ids, it goes after access
|
||||
return create_new_course(request)
|
||||
elif not has_course_access(
|
||||
request.user,
|
||||
BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block)
|
||||
):
|
||||
elif not has_course_access(request.user, CourseKey.from_string(course_key_string)):
|
||||
raise PermissionDenied()
|
||||
elif request.method == 'PUT':
|
||||
raise NotImplementedError()
|
||||
@@ -124,36 +119,31 @@ def course_handler(request, tag=None, package_id=None, branch=None, version_guid
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
elif request.method == 'GET': # assume html
|
||||
if package_id is None:
|
||||
if course_key_string is None:
|
||||
return course_listing(request)
|
||||
else:
|
||||
return course_index(request, package_id, branch, version_guid, block)
|
||||
return course_index(request, CourseKey.from_string(course_key_string))
|
||||
else:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
|
||||
@login_required
|
||||
def _course_json(request, package_id, branch, version_guid, block):
|
||||
def _course_json(request, course_key):
|
||||
"""
|
||||
Returns a JSON overview of a course
|
||||
"""
|
||||
__, course = _get_locator_and_course(
|
||||
package_id, branch, version_guid, block, request.user, depth=None
|
||||
)
|
||||
return _xmodule_json(course, course.location.course_id)
|
||||
course_module = _get_course_module(course_key, request.user, depth=None)
|
||||
return _xmodule_json(course_module, course_module.id)
|
||||
|
||||
|
||||
def _xmodule_json(xmodule, course_id):
|
||||
"""
|
||||
Returns a JSON overview of an XModule
|
||||
"""
|
||||
locator = loc_mapper().translate_location(
|
||||
course_id, xmodule.location, published=False, add_entry_if_missing=True
|
||||
)
|
||||
is_container = xmodule.has_children
|
||||
result = {
|
||||
'display_name': xmodule.display_name,
|
||||
'id': unicode(locator),
|
||||
'id': unicode(xmodule.location),
|
||||
'category': xmodule.category,
|
||||
'is_draft': getattr(xmodule, 'is_draft', False),
|
||||
'is_container': is_container,
|
||||
@@ -169,7 +159,7 @@ def _accessible_courses_list(request):
|
||||
"""
|
||||
courses = modulestore('direct').get_courses()
|
||||
|
||||
# filter out courses that we don't have access too
|
||||
# filter out courses that we don't have access to
|
||||
def course_filter(course):
|
||||
"""
|
||||
Get courses to which this user has access
|
||||
@@ -177,7 +167,7 @@ def _accessible_courses_list(request):
|
||||
if GlobalStaff().has_user(request.user):
|
||||
return course.location.course != 'templates'
|
||||
|
||||
return (has_course_access(request.user, course.location)
|
||||
return (has_course_access(request.user, course.id)
|
||||
# pylint: disable=fixme
|
||||
# TODO remove this condition when templates purged from db
|
||||
and course.location.course != 'templates'
|
||||
@@ -186,46 +176,25 @@ def _accessible_courses_list(request):
|
||||
return courses
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def _accessible_courses_list_from_groups(request):
|
||||
"""
|
||||
List all courses available to the logged in user by reversing access group names
|
||||
"""
|
||||
courses_list = []
|
||||
course_ids = set()
|
||||
courses_list = {}
|
||||
|
||||
user_staff_group_names = request.user.groups.filter(
|
||||
Q(name__startswith='instructor_') | Q(name__startswith='staff_')
|
||||
).values_list('name', flat=True)
|
||||
instructor_courses = UserBasedRole(request.user, CourseInstructorRole.ROLE).courses_with_role()
|
||||
staff_courses = UserBasedRole(request.user, CourseStaffRole.ROLE).courses_with_role()
|
||||
all_courses = instructor_courses | staff_courses
|
||||
|
||||
# we can only get course_ids from role names with the new format (instructor_org/number/run or
|
||||
# instructor_org.number.run but not instructor_number).
|
||||
for user_staff_group_name in user_staff_group_names:
|
||||
# to avoid duplication try to convert all course_id's to format with dots e.g. "edx.course.run"
|
||||
if user_staff_group_name.startswith("instructor_"):
|
||||
# strip starting text "instructor_"
|
||||
course_id = user_staff_group_name[11:]
|
||||
else:
|
||||
# strip starting text "staff_"
|
||||
course_id = user_staff_group_name[6:]
|
||||
for course_access in all_courses:
|
||||
course_key = course_access.course_id
|
||||
if course_key not in courses_list:
|
||||
course = modulestore('direct').get_course(course_key)
|
||||
if course is None:
|
||||
raise ItemNotFoundError(course_key)
|
||||
courses_list[course_key] = course
|
||||
|
||||
course_ids.add(course_id.replace('/', '.').lower())
|
||||
|
||||
for course_id in course_ids:
|
||||
# get course_location with lowercase id
|
||||
course_location = loc_mapper().translate_locator_to_location(
|
||||
CourseLocator(package_id=course_id), get_course=True, lower_only=True
|
||||
)
|
||||
if course_location is None:
|
||||
raise ItemNotFoundError(course_id)
|
||||
|
||||
course = modulestore('direct').get_course(course_location.course_id)
|
||||
if course is None:
|
||||
raise ItemNotFoundError(course_id)
|
||||
|
||||
courses_list.append(course)
|
||||
|
||||
return courses_list
|
||||
return courses_list.values()
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -247,22 +216,13 @@ def course_listing(request):
|
||||
# so fallback to iterating through all courses
|
||||
courses = _accessible_courses_list(request)
|
||||
|
||||
# update location entry in "loc_mapper" for user courses (add keys 'lower_id' and 'lower_course_id')
|
||||
for course in courses:
|
||||
loc_mapper().create_map_entry(course.location)
|
||||
|
||||
def format_course_for_view(course):
|
||||
"""
|
||||
return tuple of the data which the view requires for each course
|
||||
"""
|
||||
# published = false b/c studio manipulates draft versions not b/c the course isn't pub'd
|
||||
course_loc = loc_mapper().translate_location(
|
||||
course.location.course_id, course.location, published=False, add_entry_if_missing=True
|
||||
)
|
||||
return (
|
||||
course.display_name,
|
||||
# note, couldn't get django reverse to work; so, wrote workaround
|
||||
course_loc.url_reverse('course/', ''),
|
||||
reverse_course_url('course_handler', course.id),
|
||||
get_lms_link_for_item(course.location),
|
||||
course.display_org_with_default,
|
||||
course.display_number_with_default,
|
||||
@@ -280,26 +240,24 @@ def course_listing(request):
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def course_index(request, package_id, branch, version_guid, block):
|
||||
def course_index(request, course_key):
|
||||
"""
|
||||
Display an editable course overview.
|
||||
|
||||
org, course, name: Attributes of the Location for the item to edit
|
||||
"""
|
||||
locator, course = _get_locator_and_course(
|
||||
package_id, branch, version_guid, block, request.user, depth=3
|
||||
)
|
||||
lms_link = get_lms_link_for_item(course.location)
|
||||
sections = course.get_children()
|
||||
course_module = _get_course_module(course_key, request.user, depth=3)
|
||||
lms_link = get_lms_link_for_item(course_module.location)
|
||||
sections = course_module.get_children()
|
||||
|
||||
|
||||
return render_to_response('overview.html', {
|
||||
'context_course': course,
|
||||
'context_course': course_module,
|
||||
'lms_link': lms_link,
|
||||
'sections': sections,
|
||||
'course_graders': json.dumps(
|
||||
CourseGradingModel.fetch(locator).graders
|
||||
CourseGradingModel.fetch(course_key).graders
|
||||
),
|
||||
'parent_locator': locator,
|
||||
'new_section_category': 'chapter',
|
||||
'new_subsection_category': 'sequential',
|
||||
'new_unit_category': 'vertical',
|
||||
@@ -331,149 +289,105 @@ def create_new_course(request):
|
||||
)
|
||||
|
||||
try:
|
||||
dest_location = Location(u'i4x', org, number, u'course', run)
|
||||
except InvalidLocationError as error:
|
||||
return JsonResponse({
|
||||
"ErrMsg": _("Unable to create course '{name}'.\n\n{err}").format(
|
||||
name=display_name, err=error.message)})
|
||||
course_key = SlashSeparatedCourseKey(org, number, run)
|
||||
|
||||
# see if the course already exists
|
||||
existing_course = None
|
||||
try:
|
||||
existing_course = modulestore('direct').get_item(dest_location)
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
if existing_course is not None:
|
||||
# instantiate the CourseDescriptor and then persist it
|
||||
# note: no system to pass
|
||||
if display_name is None:
|
||||
metadata = {}
|
||||
else:
|
||||
metadata = {'display_name': display_name}
|
||||
|
||||
# Set a unique wiki_slug for newly created courses. To maintain active wiki_slugs for
|
||||
# existing xml courses this cannot be changed in CourseDescriptor.
|
||||
# # TODO get rid of defining wiki slug in this org/course/run specific way and reconcile
|
||||
# w/ xmodule.course_module.CourseDescriptor.__init__
|
||||
wiki_slug = u"{0}.{1}.{2}".format(course_key.org, course_key.course, course_key.run)
|
||||
definition_data = {'wiki_slug': wiki_slug}
|
||||
|
||||
# Create the course then fetch it from the modulestore
|
||||
# Check if role permissions group for a course named like this already exists
|
||||
# Important because role groups are case insensitive
|
||||
if CourseRole.course_group_already_exists(course_key):
|
||||
raise InvalidLocationError()
|
||||
|
||||
fields = {}
|
||||
fields.update(definition_data)
|
||||
fields.update(metadata)
|
||||
|
||||
# Creating the course raises InvalidLocationError if an existing course with this org/name is found
|
||||
new_course = modulestore('direct').create_course(
|
||||
course_key.org,
|
||||
course_key.offering,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
# can't use auth.add_users here b/c it requires request.user to already have Instructor perms in this course
|
||||
# however, we can assume that b/c this user had authority to create the course, the user can add themselves
|
||||
CourseInstructorRole(new_course.id).add_users(request.user)
|
||||
auth.add_users(request.user, CourseStaffRole(new_course.id), request.user)
|
||||
|
||||
# seed the forums
|
||||
seed_permissions_roles(new_course.id)
|
||||
|
||||
# auto-enroll the course creator in the course so that "View Live" will
|
||||
# work.
|
||||
CourseEnrollment.enroll(request.user, new_course.id)
|
||||
_users_assign_default_role(new_course.id)
|
||||
|
||||
return JsonResponse({
|
||||
'url': reverse_course_url('course_handler', new_course.id)
|
||||
})
|
||||
|
||||
except InvalidLocationError:
|
||||
return JsonResponse({
|
||||
'ErrMsg': _(
|
||||
'There is already a course defined with the same '
|
||||
'organization, course number, and course run. Please '
|
||||
'change either organization or course number to be '
|
||||
'unique.'
|
||||
'change either organization or course number to be unique.'
|
||||
),
|
||||
'OrgErrMsg': _(
|
||||
'Please change either the organization or '
|
||||
'course number so that it is unique.'
|
||||
),
|
||||
'course number so that it is unique.'),
|
||||
'CourseErrMsg': _(
|
||||
'Please change either the organization or '
|
||||
'course number so that it is unique.'
|
||||
),
|
||||
'course number so that it is unique.'),
|
||||
})
|
||||
|
||||
# dhm: this query breaks the abstraction, but I'll fix it when I do my suspended refactoring of this
|
||||
# file for new locators. get_items should accept a query rather than requiring it be a legal location
|
||||
course_search_location = bson.son.SON({
|
||||
'_id.tag': 'i4x',
|
||||
# cannot pass regex to Location constructor; thus this hack
|
||||
# pylint: disable=E1101
|
||||
'_id.org': re.compile(u'^{}$'.format(dest_location.org), re.IGNORECASE | re.UNICODE),
|
||||
# pylint: disable=E1101
|
||||
'_id.course': re.compile(u'^{}$'.format(dest_location.course), re.IGNORECASE | re.UNICODE),
|
||||
'_id.category': 'course',
|
||||
})
|
||||
courses = modulestore().collection.find(course_search_location, fields=('_id'))
|
||||
if courses.count() > 0:
|
||||
except InvalidKeyError as error:
|
||||
return JsonResponse({
|
||||
'ErrMsg': _(
|
||||
'There is already a course defined with the same '
|
||||
'organization and course number. Please '
|
||||
'change at least one field to be unique.'),
|
||||
'OrgErrMsg': _(
|
||||
'Please change either the organization or '
|
||||
'course number so that it is unique.'),
|
||||
'CourseErrMsg': _(
|
||||
'Please change either the organization or '
|
||||
'course number so that it is unique.'),
|
||||
})
|
||||
|
||||
# instantiate the CourseDescriptor and then persist it
|
||||
# note: no system to pass
|
||||
if display_name is None:
|
||||
metadata = {}
|
||||
else:
|
||||
metadata = {'display_name': display_name}
|
||||
|
||||
# Set a unique wiki_slug for newly created courses. To maintain active wiki_slugs for existing xml courses this
|
||||
# cannot be changed in CourseDescriptor.
|
||||
wiki_slug = u"{0}.{1}.{2}".format(dest_location.org, dest_location.course, dest_location.name)
|
||||
definition_data = {'wiki_slug': wiki_slug}
|
||||
|
||||
modulestore('direct').create_and_save_xmodule(
|
||||
dest_location,
|
||||
definition_data=definition_data,
|
||||
metadata=metadata
|
||||
)
|
||||
new_course = modulestore('direct').get_item(dest_location)
|
||||
|
||||
# clone a default 'about' overview module as well
|
||||
dest_about_location = dest_location.replace(
|
||||
category='about',
|
||||
name='overview'
|
||||
)
|
||||
overview_template = AboutDescriptor.get_template('overview.yaml')
|
||||
modulestore('direct').create_and_save_xmodule(
|
||||
dest_about_location,
|
||||
system=new_course.system,
|
||||
definition_data=overview_template.get('data')
|
||||
)
|
||||
|
||||
new_location = loc_mapper().translate_location(new_course.location.course_id, new_course.location, False, True)
|
||||
# can't use auth.add_users here b/c it requires request.user to already have Instructor perms in this course
|
||||
# however, we can assume that b/c this user had authority to create the course, the user can add themselves
|
||||
CourseInstructorRole(new_location).add_users(request.user)
|
||||
auth.add_users(request.user, CourseStaffRole(new_location), request.user)
|
||||
|
||||
# seed the forums
|
||||
seed_permissions_roles(new_course.location.course_id)
|
||||
|
||||
# auto-enroll the course creator in the course so that "View Live" will
|
||||
# work.
|
||||
CourseEnrollment.enroll(request.user, new_course.location.course_id)
|
||||
_users_assign_default_role(new_course.location)
|
||||
|
||||
return JsonResponse({'url': new_location.url_reverse("course/", "")})
|
||||
"ErrMsg": _("Unable to create course '{name}'.\n\n{err}").format(name=display_name, err=error.message)}
|
||||
)
|
||||
|
||||
|
||||
def _users_assign_default_role(course_location):
|
||||
def _users_assign_default_role(course_id):
|
||||
"""
|
||||
Assign 'Student' role to all previous users (if any) for this course
|
||||
"""
|
||||
enrollments = CourseEnrollment.objects.filter(course_id=course_location.course_id)
|
||||
enrollments = CourseEnrollment.objects.filter(course_id=course_id)
|
||||
for enrollment in enrollments:
|
||||
assign_default_role(course_location.course_id, enrollment.user)
|
||||
assign_default_role(course_id, enrollment.user)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(["GET"])
|
||||
def course_info_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None):
|
||||
def course_info_handler(request, course_key_string):
|
||||
"""
|
||||
GET
|
||||
html: return html for editing the course info handouts and updates.
|
||||
"""
|
||||
__, course_module = _get_locator_and_course(
|
||||
package_id, branch, version_guid, block, request.user
|
||||
)
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
course_module = _get_course_module(course_key, request.user)
|
||||
if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
|
||||
handouts_old_location = course_module.location.replace(category='course_info', name='handouts')
|
||||
handouts_locator = loc_mapper().translate_location(
|
||||
course_module.location.course_id, handouts_old_location, False, True
|
||||
)
|
||||
|
||||
update_location = course_module.location.replace(category='course_info', name='updates')
|
||||
update_locator = loc_mapper().translate_location(
|
||||
course_module.location.course_id, update_location, False, True
|
||||
)
|
||||
|
||||
return render_to_response(
|
||||
'course_info.html',
|
||||
{
|
||||
'context_course': course_module,
|
||||
'updates_url': update_locator.url_reverse('course_info_update/'),
|
||||
'handouts_locator': handouts_locator,
|
||||
'base_asset_url': StaticContent.get_base_url_path_for_course_assets(course_module.location) + '/'
|
||||
'updates_url': reverse_course_url('course_info_update_handler', course_key),
|
||||
'handouts_locator': course_key.make_usage_key('course_info', 'handouts'),
|
||||
'base_asset_url': StaticContent.get_base_url_path_for_course_assets(course_module.id)
|
||||
}
|
||||
)
|
||||
else:
|
||||
@@ -485,8 +399,7 @@ def course_info_handler(request, tag=None, package_id=None, branch=None, version
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(("GET", "POST", "PUT", "DELETE"))
|
||||
@expect_json
|
||||
def course_info_update_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None,
|
||||
provided_id=None):
|
||||
def course_info_update_handler(request, course_key_string, provided_id=None):
|
||||
"""
|
||||
restful CRUD operations on course_info updates.
|
||||
provided_id should be none if it's new (create) and index otherwise.
|
||||
@@ -500,26 +413,24 @@ def course_info_update_handler(request, tag=None, package_id=None, branch=None,
|
||||
if 'application/json' not in request.META.get('HTTP_ACCEPT', 'application/json'):
|
||||
return HttpResponseBadRequest("Only supports json requests")
|
||||
|
||||
course_location = loc_mapper().translate_locator_to_location(
|
||||
CourseLocator(package_id=package_id), get_course=True
|
||||
)
|
||||
updates_location = course_location.replace(category='course_info', name=block)
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
usage_key = course_key.make_usage_key('course_info', 'updates')
|
||||
if provided_id == '':
|
||||
provided_id = None
|
||||
|
||||
# check that logged in user has permissions to this item (GET shouldn't require this level?)
|
||||
if not has_course_access(request.user, updates_location):
|
||||
if not has_course_access(request.user, usage_key.course_key):
|
||||
raise PermissionDenied()
|
||||
|
||||
if request.method == 'GET':
|
||||
course_updates = get_course_updates(updates_location, provided_id)
|
||||
course_updates = get_course_updates(usage_key, provided_id)
|
||||
if isinstance(course_updates, dict) and course_updates.get('error'):
|
||||
return JsonResponse(get_course_updates(updates_location, provided_id), course_updates.get('status', 400))
|
||||
return JsonResponse(course_updates, course_updates.get('status', 400))
|
||||
else:
|
||||
return JsonResponse(get_course_updates(updates_location, provided_id))
|
||||
return JsonResponse(course_updates)
|
||||
elif request.method == 'DELETE':
|
||||
try:
|
||||
return JsonResponse(delete_course_update(updates_location, request.json, provided_id, request.user))
|
||||
return JsonResponse(delete_course_update(usage_key, request.json, provided_id, request.user))
|
||||
except:
|
||||
return HttpResponseBadRequest(
|
||||
"Failed to delete",
|
||||
@@ -528,7 +439,7 @@ def course_info_update_handler(request, tag=None, package_id=None, branch=None,
|
||||
# can be either and sometimes django is rewriting one to the other:
|
||||
elif request.method in ('POST', 'PUT'):
|
||||
try:
|
||||
return JsonResponse(update_course_updates(updates_location, request.json, provided_id, request.user))
|
||||
return JsonResponse(update_course_updates(usage_key, request.json, provided_id, request.user))
|
||||
except:
|
||||
return HttpResponseBadRequest(
|
||||
"Failed to save",
|
||||
@@ -540,7 +451,7 @@ def course_info_update_handler(request, tag=None, package_id=None, branch=None,
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(("GET", "PUT", "POST"))
|
||||
@expect_json
|
||||
def settings_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None):
|
||||
def settings_handler(request, course_key_string):
|
||||
"""
|
||||
Course settings for dates and about pages
|
||||
GET
|
||||
@@ -549,11 +460,10 @@ def settings_handler(request, tag=None, package_id=None, branch=None, version_gu
|
||||
PUT
|
||||
json: update the Course and About xblocks through the CourseDetails model
|
||||
"""
|
||||
locator, course_module = _get_locator_and_course(
|
||||
package_id, branch, version_guid, block, request.user
|
||||
)
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
course_module = _get_course_module(course_key, request.user)
|
||||
if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET':
|
||||
upload_asset_url = locator.url_reverse('assets/')
|
||||
upload_asset_url = reverse_course_url('assets_handler', course_key)
|
||||
|
||||
# see if the ORG of this course can be attributed to a 'Microsite'. In that case, the
|
||||
# course about page should be editable in Studio
|
||||
@@ -567,10 +477,10 @@ def settings_handler(request, tag=None, package_id=None, branch=None, version_gu
|
||||
|
||||
return render_to_response('settings.html', {
|
||||
'context_course': course_module,
|
||||
'course_locator': locator,
|
||||
'lms_link_for_about_page': utils.get_lms_link_for_about_page(course_module.location),
|
||||
'course_locator': course_key,
|
||||
'lms_link_for_about_page': utils.get_lms_link_for_about_page(course_key),
|
||||
'course_image_url': utils.course_image_url(course_module),
|
||||
'details_url': locator.url_reverse('/settings/details/'),
|
||||
'details_url': reverse_course_url('settings_handler', course_key),
|
||||
'about_page_editable': about_page_editable,
|
||||
'short_description_editable': short_description_editable,
|
||||
'upload_asset_url': upload_asset_url
|
||||
@@ -578,13 +488,13 @@ def settings_handler(request, tag=None, package_id=None, branch=None, version_gu
|
||||
elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
|
||||
if request.method == 'GET':
|
||||
return JsonResponse(
|
||||
CourseDetails.fetch(locator),
|
||||
CourseDetails.fetch(course_key),
|
||||
# encoder serializes dates, old locations, and instances
|
||||
encoder=CourseSettingsEncoder
|
||||
)
|
||||
else: # post or put, doesn't matter.
|
||||
return JsonResponse(
|
||||
CourseDetails.update_from_json(locator, request.json, request.user),
|
||||
CourseDetails.update_from_json(course_key, request.json, request.user),
|
||||
encoder=CourseSettingsEncoder
|
||||
)
|
||||
|
||||
@@ -593,7 +503,7 @@ def settings_handler(request, tag=None, package_id=None, branch=None, version_gu
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(("GET", "POST", "PUT", "DELETE"))
|
||||
@expect_json
|
||||
def grading_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None, grader_index=None):
|
||||
def grading_handler(request, course_key_string, grader_index=None):
|
||||
"""
|
||||
Course Grading policy configuration
|
||||
GET
|
||||
@@ -604,42 +514,41 @@ def grading_handler(request, tag=None, package_id=None, branch=None, version_gui
|
||||
json no grader_index: update the Course through the CourseGrading model
|
||||
json w/ grader_index: create or update the specific grader (create if index out of range)
|
||||
"""
|
||||
locator, course_module = _get_locator_and_course(
|
||||
package_id, branch, version_guid, block, request.user
|
||||
)
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
course_module = _get_course_module(course_key, request.user)
|
||||
|
||||
if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET':
|
||||
course_details = CourseGradingModel.fetch(locator)
|
||||
course_details = CourseGradingModel.fetch(course_key)
|
||||
|
||||
return render_to_response('settings_graders.html', {
|
||||
'context_course': course_module,
|
||||
'course_locator': locator,
|
||||
'course_locator': course_key,
|
||||
'course_details': json.dumps(course_details, cls=CourseSettingsEncoder),
|
||||
'grading_url': locator.url_reverse('/settings/grading/'),
|
||||
'grading_url': reverse_course_url('grading_handler', course_key),
|
||||
})
|
||||
elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
|
||||
if request.method == 'GET':
|
||||
if grader_index is None:
|
||||
return JsonResponse(
|
||||
CourseGradingModel.fetch(locator),
|
||||
CourseGradingModel.fetch(course_key),
|
||||
# encoder serializes dates, old locations, and instances
|
||||
encoder=CourseSettingsEncoder
|
||||
)
|
||||
else:
|
||||
return JsonResponse(CourseGradingModel.fetch_grader(locator, grader_index))
|
||||
return JsonResponse(CourseGradingModel.fetch_grader(course_key, grader_index))
|
||||
elif request.method in ('POST', 'PUT'): # post or put, doesn't matter.
|
||||
# None implies update the whole model (cutoffs, graceperiod, and graders) not a specific grader
|
||||
if grader_index is None:
|
||||
return JsonResponse(
|
||||
CourseGradingModel.update_from_json(locator, request.json, request.user),
|
||||
CourseGradingModel.update_from_json(course_key, request.json, request.user),
|
||||
encoder=CourseSettingsEncoder
|
||||
)
|
||||
else:
|
||||
return JsonResponse(
|
||||
CourseGradingModel.update_grader_from_json(locator, request.json, request.user)
|
||||
CourseGradingModel.update_grader_from_json(course_key, request.json, request.user)
|
||||
)
|
||||
elif request.method == "DELETE" and grader_index is not None:
|
||||
CourseGradingModel.delete_grader(locator, grader_index, request.user)
|
||||
CourseGradingModel.delete_grader(course_key, grader_index, request.user)
|
||||
return JsonResponse()
|
||||
|
||||
|
||||
@@ -698,7 +607,7 @@ def _config_course_advanced_components(request, course_module):
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(("GET", "POST", "PUT"))
|
||||
@expect_json
|
||||
def advanced_settings_handler(request, package_id=None, branch=None, version_guid=None, block=None, tag=None):
|
||||
def advanced_settings_handler(request, course_key_string):
|
||||
"""
|
||||
Course settings configuration
|
||||
GET
|
||||
@@ -709,15 +618,14 @@ def advanced_settings_handler(request, package_id=None, branch=None, version_gui
|
||||
metadata dicts. The dict can include a "unsetKeys" entry which is a list
|
||||
of keys whose values to unset: i.e., revert to default
|
||||
"""
|
||||
locator, course_module = _get_locator_and_course(
|
||||
package_id, branch, version_guid, block, request.user
|
||||
)
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
course_module = _get_course_module(course_key, request.user)
|
||||
if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET':
|
||||
|
||||
return render_to_response('settings_advanced.html', {
|
||||
'context_course': course_module,
|
||||
'advanced_dict': json.dumps(CourseMetadata.fetch(course_module)),
|
||||
'advanced_settings_url': locator.url_reverse('settings/advanced')
|
||||
'advanced_settings_url': reverse_course_url('advanced_settings_handler', course_key)
|
||||
})
|
||||
elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
|
||||
if request.method == 'GET':
|
||||
@@ -801,7 +709,7 @@ def assign_textbook_id(textbook, used_ids=()):
|
||||
@require_http_methods(("GET", "POST", "PUT"))
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def textbooks_list_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None):
|
||||
def textbooks_list_handler(request, course_key_string):
|
||||
"""
|
||||
A RESTful handler for textbook collections.
|
||||
|
||||
@@ -813,15 +721,14 @@ def textbooks_list_handler(request, tag=None, package_id=None, branch=None, vers
|
||||
PUT
|
||||
json: overwrite all textbooks in the course with the given list
|
||||
"""
|
||||
locator, course = _get_locator_and_course(
|
||||
package_id, branch, version_guid, block, request.user
|
||||
)
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
course = _get_course_module(course_key, request.user)
|
||||
store = get_modulestore(course.location)
|
||||
|
||||
if not "application/json" in request.META.get('HTTP_ACCEPT', 'text/html'):
|
||||
# return HTML page
|
||||
upload_asset_url = locator.url_reverse('assets/', '')
|
||||
textbook_url = locator.url_reverse('/textbooks')
|
||||
upload_asset_url = reverse_course_url('assets_handler', course_key)
|
||||
textbook_url = reverse_course_url('textbooks_list_handler', course_key)
|
||||
return render_to_response('textbooks.html', {
|
||||
'context_course': course,
|
||||
'textbooks': course.pdf_textbooks,
|
||||
@@ -866,14 +773,18 @@ def textbooks_list_handler(request, tag=None, package_id=None, branch=None, vers
|
||||
course.tabs.append(PDFTextbookTabs())
|
||||
store.update_item(course, request.user.id)
|
||||
resp = JsonResponse(textbook, status=201)
|
||||
resp["Location"] = locator.url_reverse('textbooks', textbook["id"])
|
||||
resp["Location"] = reverse_course_url(
|
||||
'textbooks_detail_handler',
|
||||
course.id,
|
||||
kwargs={'textbook_id': textbook["id"]}
|
||||
)
|
||||
return resp
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(("GET", "POST", "PUT", "DELETE"))
|
||||
def textbooks_detail_handler(request, tid, tag=None, package_id=None, branch=None, version_guid=None, block=None):
|
||||
def textbooks_detail_handler(request, course_key_string, textbook_id):
|
||||
"""
|
||||
JSON API endpoint for manipulating a textbook via its internal ID.
|
||||
Used by the Backbone application.
|
||||
@@ -885,12 +796,11 @@ def textbooks_detail_handler(request, tid, tag=None, package_id=None, branch=Non
|
||||
DELETE
|
||||
json: remove textbook
|
||||
"""
|
||||
__, course = _get_locator_and_course(
|
||||
package_id, branch, version_guid, block, request.user
|
||||
)
|
||||
store = get_modulestore(course.location)
|
||||
matching_id = [tb for tb in course.pdf_textbooks
|
||||
if unicode(tb.get("id")) == unicode(tid)]
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
course_module = _get_course_module(course_key, request.user)
|
||||
store = get_modulestore(course_module.location)
|
||||
matching_id = [tb for tb in course_module.pdf_textbooks
|
||||
if unicode(tb.get("id")) == unicode(textbook_id)]
|
||||
if matching_id:
|
||||
textbook = matching_id[0]
|
||||
else:
|
||||
@@ -906,25 +816,25 @@ def textbooks_detail_handler(request, tid, tag=None, package_id=None, branch=Non
|
||||
new_textbook = validate_textbook_json(request.body)
|
||||
except TextbookValidationError as err:
|
||||
return JsonResponse({"error": err.message}, status=400)
|
||||
new_textbook["id"] = tid
|
||||
new_textbook["id"] = textbook_id
|
||||
if textbook:
|
||||
i = course.pdf_textbooks.index(textbook)
|
||||
new_textbooks = course.pdf_textbooks[0:i]
|
||||
i = course_module.pdf_textbooks.index(textbook)
|
||||
new_textbooks = course_module.pdf_textbooks[0:i]
|
||||
new_textbooks.append(new_textbook)
|
||||
new_textbooks.extend(course.pdf_textbooks[i + 1:])
|
||||
course.pdf_textbooks = new_textbooks
|
||||
new_textbooks.extend(course_module.pdf_textbooks[i + 1:])
|
||||
course_module.pdf_textbooks = new_textbooks
|
||||
else:
|
||||
course.pdf_textbooks.append(new_textbook)
|
||||
store.update_item(course, request.user.id)
|
||||
course_module.pdf_textbooks.append(new_textbook)
|
||||
store.update_item(course_module, request.user.id)
|
||||
return JsonResponse(new_textbook, status=201)
|
||||
elif request.method == 'DELETE':
|
||||
if not textbook:
|
||||
return JsonResponse(status=404)
|
||||
i = course.pdf_textbooks.index(textbook)
|
||||
remaining_textbooks = course.pdf_textbooks[0:i]
|
||||
remaining_textbooks.extend(course.pdf_textbooks[i + 1:])
|
||||
course.pdf_textbooks = remaining_textbooks
|
||||
store.update_item(course, request.user.id)
|
||||
i = course_module.pdf_textbooks.index(textbook)
|
||||
remaining_textbooks = course_module.pdf_textbooks[0:i]
|
||||
remaining_textbooks.extend(course_module.pdf_textbooks[i + 1:])
|
||||
course_module.pdf_textbooks = remaining_textbooks
|
||||
store.update_item(course_module, request.user.id)
|
||||
return JsonResponse()
|
||||
|
||||
|
||||
|
||||
@@ -13,22 +13,23 @@ from django.utils.translation import ugettext as _
|
||||
from .access import has_course_access
|
||||
import contentstore.git_export_utils as git_export_utils
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.keys import CourseKey
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@login_required
|
||||
def export_git(request, org, course, name):
|
||||
def export_git(request, course_key_string):
|
||||
"""
|
||||
This method serves up the 'Export to Git' page
|
||||
"""
|
||||
location = Location('i4x', org, course, 'course', name)
|
||||
if not has_course_access(request.user, location):
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
if not has_course_access(request.user, course_key):
|
||||
raise PermissionDenied()
|
||||
course_module = modulestore().get_item(location)
|
||||
|
||||
course_module = modulestore().get_course(course_key)
|
||||
failed = False
|
||||
|
||||
log.debug('export_git course_module=%s', course_module)
|
||||
|
||||
@@ -3,7 +3,8 @@ import logging
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from edxmako.shortcuts import render_to_string, render_to_response
|
||||
from xmodule.modulestore.django import loc_mapper, modulestore
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from contentstore.utils import reverse_course_url, reverse_usage_url
|
||||
|
||||
__all__ = ['edge', 'event', 'landing']
|
||||
|
||||
@@ -59,7 +60,7 @@ def get_parent_xblock(xblock):
|
||||
Returns the xblock that is the parent of the specified xblock, or None if it has no parent.
|
||||
"""
|
||||
locator = xblock.location
|
||||
parent_locations = modulestore().get_parent_locations(locator, None)
|
||||
parent_locations = modulestore().get_parent_locations(locator,)
|
||||
|
||||
if len(parent_locations) == 0:
|
||||
return None
|
||||
@@ -107,7 +108,7 @@ def xblock_has_own_studio_page(xblock):
|
||||
return xblock.has_children
|
||||
|
||||
|
||||
def xblock_studio_url(xblock, course=None):
|
||||
def xblock_studio_url(xblock):
|
||||
"""
|
||||
Returns the Studio editing URL for the specified xblock.
|
||||
"""
|
||||
@@ -117,13 +118,9 @@ def xblock_studio_url(xblock, course=None):
|
||||
parent_xblock = get_parent_xblock(xblock)
|
||||
parent_category = parent_xblock.category if parent_xblock else None
|
||||
if category == 'course':
|
||||
prefix = 'course'
|
||||
return reverse_course_url('course_handler', xblock.location.course_key)
|
||||
elif category == 'vertical' and parent_category == 'sequential':
|
||||
prefix = 'unit' # only show the unit page for verticals directly beneath a subsection
|
||||
# only show the unit page for verticals directly beneath a subsection
|
||||
return reverse_usage_url('unit_handler', xblock.location)
|
||||
else:
|
||||
prefix = 'container'
|
||||
course_id = None
|
||||
if course:
|
||||
course_id = course.location.course_id
|
||||
locator = loc_mapper().translate_location(course_id, xblock.location, published=False)
|
||||
return locator.url_reverse(prefix)
|
||||
return reverse_usage_url('container_handler', xblock.location)
|
||||
|
||||
@@ -23,17 +23,21 @@ from django_future.csrf import ensure_csrf_cookie
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.exceptions import SerializationError
|
||||
from xmodule.modulestore.django import modulestore, loc_mapper
|
||||
from xmodule.modulestore.locator import BlockUsageLocator
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.keys import CourseKey
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.modulestore.xml_exporter import export_to_xml
|
||||
|
||||
from .access import has_course_access
|
||||
|
||||
from .access import has_course_access
|
||||
from extract_tar import safetar_extractall
|
||||
from student import auth
|
||||
from student.roles import CourseInstructorRole, CourseStaffRole, GlobalStaff
|
||||
from util.json_request import JsonResponse
|
||||
|
||||
from contentstore.utils import reverse_course_url, reverse_usage_url
|
||||
|
||||
|
||||
__all__ = ['import_handler', 'import_status_handler', 'export_handler']
|
||||
|
||||
@@ -45,10 +49,11 @@ log = logging.getLogger(__name__)
|
||||
CONTENT_RE = re.compile(r"(?P<start>\d{1,11})-(?P<stop>\d{1,11})/(?P<end>\d{1,11})")
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(("GET", "POST", "PUT"))
|
||||
def import_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None):
|
||||
def import_handler(request, course_key_string):
|
||||
"""
|
||||
The restful handler for importing a course.
|
||||
|
||||
@@ -58,18 +63,17 @@ def import_handler(request, tag=None, package_id=None, branch=None, version_guid
|
||||
POST or PUT
|
||||
json: import a course via the .tar.gz file specified in request.FILES
|
||||
"""
|
||||
location = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block)
|
||||
if not has_course_access(request.user, location):
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
if not has_course_access(request.user, course_key):
|
||||
raise PermissionDenied()
|
||||
|
||||
old_location = loc_mapper().translate_locator_to_location(location)
|
||||
|
||||
if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'):
|
||||
if request.method == 'GET':
|
||||
raise NotImplementedError('coming soon')
|
||||
else:
|
||||
data_root = path(settings.GITHUB_REPO_ROOT)
|
||||
course_subdir = "{0}-{1}-{2}".format(old_location.org, old_location.course, old_location.name)
|
||||
course_subdir = "{0}-{1}-{2}".format(course_key.org, course_key.course, course_key.run)
|
||||
course_dir = data_root / course_subdir
|
||||
|
||||
filename = request.FILES['course-data'].name
|
||||
@@ -137,7 +141,7 @@ def import_handler(request, tag=None, package_id=None, branch=None, version_guid
|
||||
"size": size,
|
||||
"deleteUrl": "",
|
||||
"deleteType": "",
|
||||
"url": location.url_reverse('import'),
|
||||
"url": reverse_course_url('import_handler', course_key),
|
||||
"thumbnailUrl": ""
|
||||
}]
|
||||
})
|
||||
@@ -146,7 +150,7 @@ def import_handler(request, tag=None, package_id=None, branch=None, version_guid
|
||||
|
||||
# Use sessions to keep info about import progress
|
||||
session_status = request.session.setdefault("import_status", {})
|
||||
key = location.package_id + filename
|
||||
key = unicode(course_key) + filename
|
||||
session_status[key] = 1
|
||||
request.session.modified = True
|
||||
|
||||
@@ -219,7 +223,7 @@ def import_handler(request, tag=None, package_id=None, branch=None, version_guid
|
||||
[course_subdir],
|
||||
load_error_modules=False,
|
||||
static_content_store=contentstore(),
|
||||
target_location_namespace=old_location,
|
||||
target_course_id=course_key,
|
||||
draft_store=modulestore()
|
||||
)
|
||||
|
||||
@@ -247,20 +251,21 @@ def import_handler(request, tag=None, package_id=None, branch=None, version_guid
|
||||
|
||||
return JsonResponse({'Status': 'OK'})
|
||||
elif request.method == 'GET': # assume html
|
||||
course_module = modulestore().get_item(old_location)
|
||||
course_module = modulestore().get_course(course_key)
|
||||
return render_to_response('import.html', {
|
||||
'context_course': course_module,
|
||||
'successful_import_redirect_url': location.url_reverse("course"),
|
||||
'import_status_url': location.url_reverse("import_status", "fillerName"),
|
||||
'successful_import_redirect_url': reverse_course_url('course_handler', course_key),
|
||||
'import_status_url': reverse_course_url("import_status_handler", course_key, kwargs={'filename': "fillerName"}),
|
||||
})
|
||||
else:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@require_GET
|
||||
@ensure_csrf_cookie
|
||||
@login_required
|
||||
def import_status_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None, filename=None):
|
||||
def import_status_handler(request, course_key_string, filename=None):
|
||||
"""
|
||||
Returns an integer corresponding to the status of a file import. These are:
|
||||
|
||||
@@ -270,23 +275,24 @@ def import_status_handler(request, tag=None, package_id=None, branch=None, versi
|
||||
3 : Importing to mongo
|
||||
|
||||
"""
|
||||
location = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block)
|
||||
if not has_course_access(request.user, location):
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
if not has_course_access(request.user, course_key):
|
||||
raise PermissionDenied()
|
||||
|
||||
try:
|
||||
session_status = request.session["import_status"]
|
||||
status = session_status[location.package_id + filename]
|
||||
status = session_status[course_key_string + filename]
|
||||
except KeyError:
|
||||
status = 0
|
||||
|
||||
return JsonResponse({"ImportStatus": status})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@ensure_csrf_cookie
|
||||
@login_required
|
||||
@require_http_methods(("GET",))
|
||||
def export_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None):
|
||||
def export_handler(request, course_key_string):
|
||||
"""
|
||||
The restful handler for exporting a course.
|
||||
|
||||
@@ -301,65 +307,62 @@ def export_handler(request, tag=None, package_id=None, branch=None, version_guid
|
||||
If the tar.gz file has been requested but the export operation fails, an HTML page will be returned
|
||||
which describes the error.
|
||||
"""
|
||||
location = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block)
|
||||
if not has_course_access(request.user, location):
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
if not has_course_access(request.user, course_key):
|
||||
raise PermissionDenied()
|
||||
|
||||
old_location = loc_mapper().translate_locator_to_location(location)
|
||||
course_module = modulestore().get_item(old_location)
|
||||
course_module = modulestore().get_course(course_key)
|
||||
|
||||
# an _accept URL parameter will be preferred over HTTP_ACCEPT in the header.
|
||||
requested_format = request.REQUEST.get('_accept', request.META.get('HTTP_ACCEPT', 'text/html'))
|
||||
|
||||
export_url = location.url_reverse('export') + '?_accept=application/x-tgz'
|
||||
export_url = reverse_course_url('export_handler', course_key) + '?_accept=application/x-tgz'
|
||||
if 'application/x-tgz' in requested_format:
|
||||
name = old_location.name
|
||||
name = course_module.url_name
|
||||
export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
|
||||
root_dir = path(mkdtemp())
|
||||
|
||||
try:
|
||||
export_to_xml(modulestore('direct'), contentstore(), old_location, root_dir, name, modulestore())
|
||||
export_to_xml(modulestore('direct'), contentstore(), course_module.id, root_dir, name, modulestore())
|
||||
|
||||
logging.debug('tar file being generated at {0}'.format(export_file.name))
|
||||
with tarfile.open(name=export_file.name, mode='w:gz') as tar_file:
|
||||
tar_file.add(root_dir / name, arcname=name)
|
||||
except SerializationError, e:
|
||||
logging.exception('There was an error exporting course {0}. {1}'.format(course_module.location, unicode(e)))
|
||||
except SerializationError as exc:
|
||||
log.exception('There was an error exporting course %s', course_module.id)
|
||||
unit = None
|
||||
failed_item = None
|
||||
parent = None
|
||||
try:
|
||||
failed_item = modulestore().get_instance(course_module.location.course_id, e.location)
|
||||
parent_locs = modulestore().get_parent_locations(failed_item.location, course_module.location.course_id)
|
||||
failed_item = modulestore().get_item(exc.location)
|
||||
parent_locs = modulestore().get_parent_locations(failed_item.location)
|
||||
|
||||
if len(parent_locs) > 0:
|
||||
parent = modulestore().get_item(parent_locs[0])
|
||||
if parent.location.category == 'vertical':
|
||||
unit = parent
|
||||
except:
|
||||
except: # pylint: disable=bare-except
|
||||
# if we have a nested exception, then we'll show the more generic error message
|
||||
pass
|
||||
|
||||
unit_locator = loc_mapper().translate_location(old_location.course_id, parent.location, False, True)
|
||||
|
||||
return render_to_response('export.html', {
|
||||
'context_course': course_module,
|
||||
'in_err': True,
|
||||
'raw_err_msg': str(e),
|
||||
'raw_err_msg': str(exc),
|
||||
'failed_module': failed_item,
|
||||
'unit': unit,
|
||||
'edit_unit_url': unit_locator.url_reverse("unit") if parent else "",
|
||||
'course_home_url': location.url_reverse("course"),
|
||||
'edit_unit_url': reverse_usage_url("unit_handler", parent.location) if parent else "",
|
||||
'course_home_url': reverse_course_url("course_handler", course_key),
|
||||
'export_url': export_url
|
||||
})
|
||||
except Exception, e:
|
||||
logging.exception('There was an error exporting course {0}. {1}'.format(course_module.location, unicode(e)))
|
||||
except Exception as exc:
|
||||
log.exception('There was an error exporting course %s', course_module.id)
|
||||
return render_to_response('export.html', {
|
||||
'context_course': course_module,
|
||||
'in_err': True,
|
||||
'unit': None,
|
||||
'raw_err_msg': str(e),
|
||||
'course_home_url': location.url_reverse("course"),
|
||||
'raw_err_msg': str(exc),
|
||||
'course_home_url': reverse_course_url("course_handler", course_key),
|
||||
'export_url': export_url
|
||||
})
|
||||
finally:
|
||||
|
||||
@@ -20,11 +20,9 @@ from xblock.fields import Scope
|
||||
from xblock.fragment import Fragment
|
||||
|
||||
import xmodule
|
||||
from xmodule.modulestore.django import modulestore, loc_mapper
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError, DuplicateItemError
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from xmodule.modulestore.locator import BlockUsageLocator
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.video_module import manage_video_subtitles_save
|
||||
|
||||
from util.json_request import expect_json, JsonResponse
|
||||
@@ -40,6 +38,7 @@ from contentstore.views.preview import get_preview_fragment
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from models.settings.course_grading import CourseGradingModel
|
||||
from cms.lib.xblock.runtime import handler_url, local_resource_url
|
||||
from xmodule.modulestore.keys import UsageKey, CourseKey
|
||||
|
||||
__all__ = ['orphan_handler', 'xblock_handler', 'xblock_view_handler']
|
||||
|
||||
@@ -68,7 +67,7 @@ def hash_resource(resource):
|
||||
@require_http_methods(("DELETE", "GET", "PUT", "POST"))
|
||||
@login_required
|
||||
@expect_json
|
||||
def xblock_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None):
|
||||
def xblock_handler(request, usage_key_string):
|
||||
"""
|
||||
The restful handler for xblock requests.
|
||||
|
||||
@@ -83,7 +82,7 @@ def xblock_handler(request, tag=None, package_id=None, branch=None, version_guid
|
||||
json: if xblock locator is specified, update the xblock instance. The json payload can contain
|
||||
these fields, all optional:
|
||||
:data: the new value for the data.
|
||||
:children: the locator ids of children for this xblock.
|
||||
:children: the unicode representation of the UsageKeys of children for this xblock.
|
||||
:metadata: new values for the metadata fields. Any whose values are None will be deleted not set
|
||||
to None! Absent ones will be left alone.
|
||||
:nullout: which metadata fields to set to None
|
||||
@@ -91,7 +90,7 @@ def xblock_handler(request, tag=None, package_id=None, branch=None, version_guid
|
||||
:publish: can be one of three values, 'make_public, 'make_private', or 'create_draft'
|
||||
The JSON representation on the updated xblock (minus children) is returned.
|
||||
|
||||
if xblock locator is not specified, create a new xblock instance, either by duplicating
|
||||
if usage_key_string is not specified, create a new xblock instance, either by duplicating
|
||||
an existing xblock, or creating an entirely new one. The json playload can contain
|
||||
these fields:
|
||||
:parent_locator: parent for new xblock, required for both duplicate and create new instance
|
||||
@@ -100,13 +99,12 @@ def xblock_handler(request, tag=None, package_id=None, branch=None, version_guid
|
||||
:display_name: name for new xblock, optional
|
||||
:boilerplate: template name for populating fields, optional and only used
|
||||
if duplicate_source_locator is not present
|
||||
The locator (and old-style id) for the created xblock (minus children) is returned.
|
||||
The locator (unicode representation of a UsageKey) for the created xblock (minus children) is returned.
|
||||
"""
|
||||
if package_id is not None:
|
||||
locator = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block)
|
||||
if not has_course_access(request.user, locator):
|
||||
if usage_key_string:
|
||||
usage_key = UsageKey.from_string(usage_key_string)
|
||||
if not has_course_access(request.user, usage_key.course_key):
|
||||
raise PermissionDenied()
|
||||
old_location = loc_mapper().translate_locator_to_location(locator)
|
||||
|
||||
if request.method == 'GET':
|
||||
accept_header = request.META.get('HTTP_ACCEPT', 'application/json')
|
||||
@@ -115,9 +113,9 @@ def xblock_handler(request, tag=None, package_id=None, branch=None, version_guid
|
||||
fields = request.REQUEST.get('fields', '').split(',')
|
||||
if 'graderType' in fields:
|
||||
# right now can't combine output of this w/ output of _get_module_info, but worthy goal
|
||||
return JsonResponse(CourseGradingModel.get_section_grader_type(locator))
|
||||
return JsonResponse(CourseGradingModel.get_section_grader_type(usage_key))
|
||||
# TODO: pass fields to _get_module_info and only return those
|
||||
rsp = _get_module_info(locator)
|
||||
rsp = _get_module_info(usage_key)
|
||||
return JsonResponse(rsp)
|
||||
else:
|
||||
return HttpResponse(status=406)
|
||||
@@ -126,18 +124,11 @@ def xblock_handler(request, tag=None, package_id=None, branch=None, version_guid
|
||||
delete_children = str_to_bool(request.REQUEST.get('recurse', 'False'))
|
||||
delete_all_versions = str_to_bool(request.REQUEST.get('all_versions', 'False'))
|
||||
|
||||
return _delete_item_at_location(old_location, delete_children, delete_all_versions, request.user)
|
||||
else: # Since we have a package_id, we are updating an existing xblock.
|
||||
if block == 'handouts' and old_location is None:
|
||||
# update handouts location in loc_mapper
|
||||
course_location = loc_mapper().translate_locator_to_location(locator, get_course=True)
|
||||
old_location = course_location.replace(category='course_info', name=block)
|
||||
locator = loc_mapper().translate_location(course_location.course_id, old_location)
|
||||
|
||||
return _delete_item_at_location(usage_key, delete_children, delete_all_versions, request.user)
|
||||
else: # Since we have a usage_key, we are updating an existing xblock.
|
||||
return _save_item(
|
||||
request,
|
||||
locator,
|
||||
old_location,
|
||||
usage_key,
|
||||
data=request.json.get('data'),
|
||||
children=request.json.get('children'),
|
||||
metadata=request.json.get('metadata'),
|
||||
@@ -147,27 +138,22 @@ def xblock_handler(request, tag=None, package_id=None, branch=None, version_guid
|
||||
)
|
||||
elif request.method in ('PUT', 'POST'):
|
||||
if 'duplicate_source_locator' in request.json:
|
||||
parent_locator = BlockUsageLocator(request.json['parent_locator'])
|
||||
duplicate_source_locator = BlockUsageLocator(request.json['duplicate_source_locator'])
|
||||
parent_usage_key = UsageKey.from_string(request.json['parent_locator'])
|
||||
duplicate_source_usage_key = UsageKey.from_string(request.json['duplicate_source_locator'])
|
||||
|
||||
# _duplicate_item is dealing with locations to facilitate the recursive call for
|
||||
# duplicating children.
|
||||
parent_location = loc_mapper().translate_locator_to_location(parent_locator)
|
||||
duplicate_source_location = loc_mapper().translate_locator_to_location(duplicate_source_locator)
|
||||
dest_location = _duplicate_item(
|
||||
parent_location,
|
||||
duplicate_source_location,
|
||||
dest_usage_key = _duplicate_item(
|
||||
parent_usage_key,
|
||||
duplicate_source_usage_key,
|
||||
request.json.get('display_name'),
|
||||
request.user,
|
||||
)
|
||||
course_location = loc_mapper().translate_locator_to_location(BlockUsageLocator(parent_locator), get_course=True)
|
||||
dest_locator = loc_mapper().translate_location(course_location.course_id, dest_location, False, True)
|
||||
return JsonResponse({"locator": unicode(dest_locator)})
|
||||
|
||||
return JsonResponse({"locator": unicode(dest_usage_key)})
|
||||
else:
|
||||
return _create_item(request)
|
||||
else:
|
||||
return HttpResponseBadRequest(
|
||||
"Only instance creation is supported without a package_id.",
|
||||
"Only instance creation is supported without a usage key.",
|
||||
content_type="text/plain"
|
||||
)
|
||||
|
||||
@@ -175,7 +161,7 @@ def xblock_handler(request, tag=None, package_id=None, branch=None, version_guid
|
||||
@require_http_methods(("GET"))
|
||||
@login_required
|
||||
@expect_json
|
||||
def xblock_view_handler(request, package_id, view_name, tag=None, branch=None, version_guid=None, block=None):
|
||||
def xblock_view_handler(request, usage_key_string, view_name):
|
||||
"""
|
||||
The restful handler for requests for rendered xblock views.
|
||||
|
||||
@@ -184,23 +170,22 @@ def xblock_view_handler(request, package_id, view_name, tag=None, branch=None, v
|
||||
resources: A list of tuples where the first element is the resource hash, and
|
||||
the second is the resource description
|
||||
"""
|
||||
locator = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block)
|
||||
if not has_course_access(request.user, locator):
|
||||
usage_key = UsageKey.from_string(usage_key_string)
|
||||
if not has_course_access(request.user, usage_key.course_key):
|
||||
raise PermissionDenied()
|
||||
old_location = loc_mapper().translate_locator_to_location(locator)
|
||||
|
||||
accept_header = request.META.get('HTTP_ACCEPT', 'application/json')
|
||||
|
||||
if 'application/json' in accept_header:
|
||||
store = get_modulestore(old_location)
|
||||
xblock = store.get_item(old_location)
|
||||
store = get_modulestore(usage_key)
|
||||
xblock = store.get_item(usage_key)
|
||||
is_read_only = _is_xblock_read_only(xblock)
|
||||
container_views = ['container_preview', 'reorderable_container_child_preview']
|
||||
unit_views = ['student_view']
|
||||
|
||||
# wrap the generated fragment in the xmodule_editor div so that the javascript
|
||||
# can bind to it correctly
|
||||
xblock.runtime.wrappers.append(partial(wrap_xblock, 'StudioRuntime'))
|
||||
xblock.runtime.wrappers.append(partial(wrap_xblock, 'StudioRuntime', usage_id_serializer=unicode))
|
||||
|
||||
if view_name == 'studio_view':
|
||||
try:
|
||||
@@ -227,7 +212,7 @@ def xblock_view_handler(request, package_id, view_name, tag=None, branch=None, v
|
||||
html = render_to_string('container_xblock_component.html', {
|
||||
'xblock_context': context,
|
||||
'xblock': xblock,
|
||||
'locator': locator,
|
||||
'locator': usage_key,
|
||||
})
|
||||
return JsonResponse({
|
||||
'html': html,
|
||||
@@ -292,30 +277,28 @@ def _is_xblock_read_only(xblock):
|
||||
return component_publish_state == PublishState.public
|
||||
|
||||
|
||||
def _save_item(request, usage_loc, item_location, data=None, children=None, metadata=None, nullout=None,
|
||||
def _save_item(request, usage_key, data=None, children=None, metadata=None, nullout=None,
|
||||
grader_type=None, publish=None):
|
||||
"""
|
||||
Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata.
|
||||
nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert
|
||||
to default).
|
||||
|
||||
The item_location is still the old-style location whereas usage_loc is a BlockUsageLocator
|
||||
"""
|
||||
store = get_modulestore(item_location)
|
||||
store = get_modulestore(usage_key)
|
||||
|
||||
try:
|
||||
existing_item = store.get_item(item_location)
|
||||
existing_item = store.get_item(usage_key)
|
||||
except ItemNotFoundError:
|
||||
if item_location.category in CREATE_IF_NOT_FOUND:
|
||||
if usage_key.category in CREATE_IF_NOT_FOUND:
|
||||
# New module at this location, for pages that are not pre-created.
|
||||
# Used for course info handouts.
|
||||
store.create_and_save_xmodule(item_location)
|
||||
existing_item = store.get_item(item_location)
|
||||
store.create_and_save_xmodule(usage_key)
|
||||
existing_item = store.get_item(usage_key)
|
||||
else:
|
||||
raise
|
||||
except InvalidLocationError:
|
||||
log.error("Can't find item by location.")
|
||||
return JsonResponse({"error": "Can't find item by location: " + str(item_location)}, 404)
|
||||
return JsonResponse({"error": "Can't find item by location: " + unicode(usage_key)}, 404)
|
||||
|
||||
old_metadata = own_metadata(existing_item)
|
||||
|
||||
@@ -342,12 +325,12 @@ def _save_item(request, usage_loc, item_location, data=None, children=None, meta
|
||||
data = existing_item.get_explicitly_set_fields_by_scope(Scope.content)
|
||||
|
||||
if children is not None:
|
||||
children_ids = [
|
||||
loc_mapper().translate_locator_to_location(BlockUsageLocator(child_locator)).url()
|
||||
for child_locator
|
||||
children_usage_keys = [
|
||||
UsageKey.from_string(child)
|
||||
for child
|
||||
in children
|
||||
]
|
||||
existing_item.children = children_ids
|
||||
existing_item.children = children_usage_keys
|
||||
|
||||
# also commit any metadata which might have been passed along
|
||||
if nullout is not None or metadata is not None:
|
||||
@@ -381,7 +364,7 @@ def _save_item(request, usage_loc, item_location, data=None, children=None, meta
|
||||
store.update_item(existing_item, request.user.id)
|
||||
|
||||
result = {
|
||||
'id': unicode(usage_loc),
|
||||
'id': unicode(usage_key),
|
||||
'data': data,
|
||||
'metadata': own_metadata(existing_item)
|
||||
}
|
||||
@@ -414,17 +397,16 @@ def _save_item(request, usage_loc, item_location, data=None, children=None, meta
|
||||
@expect_json
|
||||
def _create_item(request):
|
||||
"""View for create items."""
|
||||
parent_locator = BlockUsageLocator(request.json['parent_locator'])
|
||||
parent_location = loc_mapper().translate_locator_to_location(parent_locator)
|
||||
usage_key = UsageKey.from_string(request.json['parent_locator'])
|
||||
category = request.json['category']
|
||||
|
||||
display_name = request.json.get('display_name')
|
||||
|
||||
if not has_course_access(request.user, parent_location):
|
||||
if not has_course_access(request.user, usage_key.course_key):
|
||||
raise PermissionDenied()
|
||||
|
||||
parent = get_modulestore(category).get_item(parent_location)
|
||||
dest_location = parent_location.replace(category=category, name=uuid4().hex)
|
||||
parent = get_modulestore(category).get_item(usage_key)
|
||||
dest_usage_key = usage_key.replace(category=category, name=uuid4().hex)
|
||||
|
||||
# get the metadata, display_name, and definition from the request
|
||||
metadata = {}
|
||||
@@ -442,7 +424,7 @@ def _create_item(request):
|
||||
metadata['display_name'] = display_name
|
||||
|
||||
get_modulestore(category).create_and_save_xmodule(
|
||||
dest_location,
|
||||
dest_usage_key,
|
||||
definition_data=data,
|
||||
metadata=metadata,
|
||||
system=parent.runtime,
|
||||
@@ -450,23 +432,21 @@ def _create_item(request):
|
||||
|
||||
# TODO replace w/ nicer accessor
|
||||
if not 'detached' in parent.runtime.load_block_type(category)._class_tags:
|
||||
parent.children.append(dest_location.url())
|
||||
parent.children.append(dest_usage_key)
|
||||
get_modulestore(parent.location).update_item(parent, request.user.id)
|
||||
|
||||
course_location = loc_mapper().translate_locator_to_location(parent_locator, get_course=True)
|
||||
locator = loc_mapper().translate_location(course_location.course_id, dest_location, False, True)
|
||||
return JsonResponse({"locator": unicode(locator)})
|
||||
return JsonResponse({"locator": unicode(dest_usage_key), "courseKey": unicode(dest_usage_key.course_key)})
|
||||
|
||||
|
||||
def _duplicate_item(parent_location, duplicate_source_location, display_name=None, user=None):
|
||||
def _duplicate_item(parent_usage_key, duplicate_source_usage_key, display_name=None, user=None):
|
||||
"""
|
||||
Duplicate an existing xblock as a child of the supplied parent_location.
|
||||
Duplicate an existing xblock as a child of the supplied parent_usage_key.
|
||||
"""
|
||||
store = get_modulestore(duplicate_source_location)
|
||||
source_item = store.get_item(duplicate_source_location)
|
||||
store = get_modulestore(duplicate_source_usage_key)
|
||||
source_item = store.get_item(duplicate_source_usage_key)
|
||||
# Change the blockID to be unique.
|
||||
dest_location = duplicate_source_location.replace(name=uuid4().hex)
|
||||
category = dest_location.category
|
||||
dest_usage_key = duplicate_source_usage_key.replace(name=uuid4().hex)
|
||||
category = dest_usage_key.category
|
||||
|
||||
# Update the display name to indicate this is a duplicate (unless display name provided).
|
||||
duplicate_metadata = own_metadata(source_item)
|
||||
@@ -479,45 +459,45 @@ def _duplicate_item(parent_location, duplicate_source_location, display_name=Non
|
||||
duplicate_metadata['display_name'] = _("Duplicate of '{0}'").format(source_item.display_name)
|
||||
|
||||
get_modulestore(category).create_and_save_xmodule(
|
||||
dest_location,
|
||||
dest_usage_key,
|
||||
definition_data=source_item.data if hasattr(source_item, 'data') else None,
|
||||
metadata=duplicate_metadata,
|
||||
system=source_item.runtime,
|
||||
)
|
||||
|
||||
dest_module = get_modulestore(category).get_item(dest_location)
|
||||
dest_module = get_modulestore(category).get_item(dest_usage_key)
|
||||
# Children are not automatically copied over (and not all xblocks have a 'children' attribute).
|
||||
# Because DAGs are not fully supported, we need to actually duplicate each child as well.
|
||||
if source_item.has_children:
|
||||
dest_module.children = []
|
||||
for child in source_item.children:
|
||||
dupe = _duplicate_item(dest_location, Location(child), user=user)
|
||||
dest_module.children.append(dupe.url())
|
||||
get_modulestore(dest_location).update_item(dest_module, user.id if user else None)
|
||||
dupe = _duplicate_item(dest_usage_key, child, user=user)
|
||||
dest_module.children.append(dupe)
|
||||
get_modulestore(dest_usage_key).update_item(dest_module, user.id if user else None)
|
||||
|
||||
if not 'detached' in source_item.runtime.load_block_type(category)._class_tags:
|
||||
parent = get_modulestore(parent_location).get_item(parent_location)
|
||||
parent = get_modulestore(parent_usage_key).get_item(parent_usage_key)
|
||||
# If source was already a child of the parent, add duplicate immediately afterward.
|
||||
# Otherwise, add child to end.
|
||||
if duplicate_source_location.url() in parent.children:
|
||||
source_index = parent.children.index(duplicate_source_location.url())
|
||||
parent.children.insert(source_index + 1, dest_location.url())
|
||||
if duplicate_source_usage_key in parent.children:
|
||||
source_index = parent.children.index(duplicate_source_usage_key)
|
||||
parent.children.insert(source_index + 1, dest_usage_key)
|
||||
else:
|
||||
parent.children.append(dest_location.url())
|
||||
get_modulestore(parent_location).update_item(parent, user.id if user else None)
|
||||
parent.children.append(dest_usage_key)
|
||||
get_modulestore(parent_usage_key).update_item(parent, user.id if user else None)
|
||||
|
||||
return dest_location
|
||||
return dest_usage_key
|
||||
|
||||
|
||||
def _delete_item_at_location(item_location, delete_children=False, delete_all_versions=False, user=None):
|
||||
def _delete_item_at_location(item_usage_key, delete_children=False, delete_all_versions=False, user=None):
|
||||
"""
|
||||
Deletes the item at with the given Location.
|
||||
|
||||
It is assumed that course permissions have already been checked.
|
||||
"""
|
||||
store = get_modulestore(item_location)
|
||||
store = get_modulestore(item_usage_key)
|
||||
|
||||
item = store.get_item(item_location)
|
||||
item = store.get_item(item_usage_key)
|
||||
|
||||
if delete_children:
|
||||
_xmodule_recurse(item, lambda i: store.delete_item(i.location, delete_all_versions=delete_all_versions))
|
||||
@@ -526,12 +506,11 @@ def _delete_item_at_location(item_location, delete_children=False, delete_all_ve
|
||||
|
||||
# cdodge: we need to remove our parent's pointer to us so that it is no longer dangling
|
||||
if delete_all_versions:
|
||||
parent_locs = modulestore('direct').get_parent_locations(item_location, None)
|
||||
parent_locs = modulestore('direct').get_parent_locations(item_usage_key)
|
||||
|
||||
item_url = item_location.url()
|
||||
for parent_loc in parent_locs:
|
||||
parent = modulestore('direct').get_item(parent_loc)
|
||||
parent.children.remove(item_url)
|
||||
parent.children.remove(item_usage_key)
|
||||
modulestore('direct').update_item(parent, user.id if user else None)
|
||||
|
||||
return JsonResponse()
|
||||
@@ -540,65 +519,59 @@ def _delete_item_at_location(item_location, delete_children=False, delete_all_ve
|
||||
# pylint: disable=W0613
|
||||
@login_required
|
||||
@require_http_methods(("GET", "DELETE"))
|
||||
def orphan_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None):
|
||||
def orphan_handler(request, course_key_string):
|
||||
"""
|
||||
View for handling orphan related requests. GET gets all of the current orphans.
|
||||
DELETE removes all orphans (requires is_staff access)
|
||||
|
||||
An orphan is a block whose category is not in the DETACHED_CATEGORY list, is not the root, and is not reachable
|
||||
from the root via children
|
||||
|
||||
:param request:
|
||||
:param package_id: Locator syntax package_id
|
||||
"""
|
||||
location = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block)
|
||||
# DHM: when split becomes back-end, move or conditionalize this conversion
|
||||
old_location = loc_mapper().translate_locator_to_location(location)
|
||||
course_usage_key = CourseKey.from_string(course_key_string)
|
||||
if request.method == 'GET':
|
||||
if has_course_access(request.user, old_location):
|
||||
return JsonResponse(modulestore().get_orphans(old_location, 'draft'))
|
||||
if has_course_access(request.user, course_usage_key):
|
||||
return JsonResponse(modulestore().get_orphans(course_usage_key))
|
||||
else:
|
||||
raise PermissionDenied()
|
||||
if request.method == 'DELETE':
|
||||
if request.user.is_staff:
|
||||
items = modulestore().get_orphans(old_location, 'draft')
|
||||
items = modulestore().get_orphans(course_usage_key)
|
||||
for itemloc in items:
|
||||
modulestore('draft').delete_item(itemloc, delete_all_versions=True)
|
||||
# get_orphans returns the deprecated string format
|
||||
usage_key = course_usage_key.make_usage_key_from_deprecated_string(itemloc)
|
||||
modulestore('draft').delete_item(usage_key, delete_all_versions=True)
|
||||
return JsonResponse({'deleted': items})
|
||||
else:
|
||||
raise PermissionDenied()
|
||||
|
||||
|
||||
def _get_module_info(usage_loc, rewrite_static_links=True):
|
||||
def _get_module_info(usage_key, rewrite_static_links=True):
|
||||
"""
|
||||
metadata, data, id representation of a leaf module fetcher.
|
||||
:param usage_loc: A BlockUsageLocator
|
||||
:param usage_key: A UsageKey
|
||||
"""
|
||||
old_location = loc_mapper().translate_locator_to_location(usage_loc)
|
||||
store = get_modulestore(old_location)
|
||||
store = get_modulestore(usage_key)
|
||||
try:
|
||||
module = store.get_item(old_location)
|
||||
module = store.get_item(usage_key)
|
||||
except ItemNotFoundError:
|
||||
if old_location.category in CREATE_IF_NOT_FOUND:
|
||||
if usage_key.category in CREATE_IF_NOT_FOUND:
|
||||
# Create a new one for certain categories only. Used for course info handouts.
|
||||
store.create_and_save_xmodule(old_location)
|
||||
module = store.get_item(old_location)
|
||||
store.create_and_save_xmodule(usage_key)
|
||||
module = store.get_item(usage_key)
|
||||
else:
|
||||
raise
|
||||
|
||||
data = getattr(module, 'data', '')
|
||||
if rewrite_static_links:
|
||||
# we pass a partially bogus course_id as we don't have the RUN information passed yet
|
||||
# through the CMS. Also the contentstore is also not RUN-aware at this point in time.
|
||||
data = replace_static_urls(
|
||||
data,
|
||||
None,
|
||||
course_id=module.location.org + '/' + module.location.course + '/BOGUS_RUN_REPLACE_WHEN_AVAILABLE'
|
||||
course_id=usage_key.course_key
|
||||
)
|
||||
|
||||
# Note that children aren't being returned until we have a use case.
|
||||
return {
|
||||
'id': unicode(usage_loc),
|
||||
'id': unicode(usage_key),
|
||||
'data': data,
|
||||
'metadata': own_metadata(module)
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ from edxmako.shortcuts import render_to_string
|
||||
from xmodule_modifiers import replace_static_urls, wrap_xblock, wrap_fragment
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from xmodule.exceptions import NotFoundError, ProcessingError
|
||||
from xmodule.modulestore.django import modulestore, loc_mapper, ModuleI18nService
|
||||
from xmodule.modulestore.locator import Locator
|
||||
from xmodule.modulestore.django import modulestore, ModuleI18nService
|
||||
from xmodule.modulestore.keys import UsageKey
|
||||
from xmodule.x_module import ModuleSystem
|
||||
from xblock.runtime import KvsFieldData
|
||||
from xblock.django.request import webob_to_django_response, django_to_webob_request
|
||||
@@ -21,7 +21,6 @@ from xblock.exceptions import NoSuchHandlerError
|
||||
from xblock.fragment import Fragment
|
||||
|
||||
from lms.lib.xblock.field_data import LmsFieldData
|
||||
from lms.lib.xblock.runtime import quote_slashes, unquote_slashes
|
||||
from cms.lib.xblock.runtime import local_resource_url
|
||||
|
||||
from util.sandboxing import can_execute_unsafe_code
|
||||
@@ -29,7 +28,6 @@ from util.sandboxing import can_execute_unsafe_code
|
||||
import static_replace
|
||||
from .session_kv_store import SessionKeyValueStore
|
||||
from .helpers import render_from_lms, xblock_has_own_studio_page
|
||||
from ..utils import get_course_for_item
|
||||
|
||||
from contentstore.views.access import get_user_role
|
||||
|
||||
@@ -39,19 +37,17 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@login_required
|
||||
def preview_handler(request, usage_id, handler, suffix=''):
|
||||
def preview_handler(request, usage_key_string, handler, suffix=''):
|
||||
"""
|
||||
Dispatch an AJAX action to an xblock
|
||||
|
||||
usage_id: The usage-id of the block to dispatch to, passed through `quote_slashes`
|
||||
usage_key_string: The usage_key_string-id of the block to dispatch to, passed through `quote_slashes`
|
||||
handler: The handler to execute
|
||||
suffix: The remainder of the url to be passed to the handler
|
||||
"""
|
||||
# Note: usage_id is currently the string form of a Location, but in the
|
||||
# future it will be the string representation of a Locator.
|
||||
location = unquote_slashes(usage_id)
|
||||
usage_key = UsageKey.from_string(usage_key_string)
|
||||
|
||||
descriptor = modulestore().get_item(location)
|
||||
descriptor = modulestore().get_item(usage_key)
|
||||
instance = _load_preview_module(request, descriptor)
|
||||
# Let the module handle the AJAX
|
||||
req = django_to_webob_request(request)
|
||||
@@ -88,7 +84,7 @@ class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method
|
||||
|
||||
def handler_url(self, block, handler_name, suffix='', query='', thirdparty=False):
|
||||
return reverse('preview_handler', kwargs={
|
||||
'usage_id': quote_slashes(unicode(block.scope_ids.usage_id).encode('utf-8')),
|
||||
'usage_key_string': unicode(block.location),
|
||||
'handler': handler_name,
|
||||
'suffix': suffix,
|
||||
}) + '?' + query
|
||||
@@ -106,16 +102,12 @@ def _preview_module_system(request, descriptor):
|
||||
descriptor: An XModuleDescriptor
|
||||
"""
|
||||
|
||||
if isinstance(descriptor.location, Locator):
|
||||
course_location = loc_mapper().translate_locator_to_location(descriptor.location, get_course=True)
|
||||
course_id = course_location.course_id
|
||||
else:
|
||||
course_id = get_course_for_item(descriptor.location).location.course_id
|
||||
course_id = descriptor.location.course_key
|
||||
display_name_only = (descriptor.category == 'static_tab')
|
||||
|
||||
wrappers = [
|
||||
# This wrapper wraps the module in the template specified above
|
||||
partial(wrap_xblock, 'PreviewRuntime', display_name_only=display_name_only),
|
||||
partial(wrap_xblock, 'PreviewRuntime', display_name_only=display_name_only, usage_id_serializer=unicode),
|
||||
|
||||
# This wrapper replaces urls in the output that start with /static
|
||||
# with the correct course-specific url for the static content
|
||||
@@ -141,9 +133,7 @@ def _preview_module_system(request, descriptor):
|
||||
# Set up functions to modify the fragment produced by student_view
|
||||
wrappers=wrappers,
|
||||
error_descriptor_class=ErrorDescriptor,
|
||||
# get_user_role accepts a location or a CourseLocator.
|
||||
# If descriptor.location is a CourseLocator, course_id is unused.
|
||||
get_user_role=lambda: get_user_role(request.user, descriptor.location, course_id),
|
||||
get_user_role=lambda: get_user_role(request.user, course_id),
|
||||
descriptor_runtime=descriptor.runtime,
|
||||
services={
|
||||
"i18n": ModuleI18nService(),
|
||||
@@ -182,12 +172,10 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
|
||||
if context.get('container_view', None) and view == 'student_view':
|
||||
root_xblock = context.get('root_xblock')
|
||||
is_root = root_xblock and xblock.location == root_xblock.location
|
||||
locator = loc_mapper().translate_location(xblock.course_id, xblock.location, published=False)
|
||||
is_reorderable = _is_xblock_reorderable(xblock, context)
|
||||
template_context = {
|
||||
'xblock_context': context,
|
||||
'xblock': xblock,
|
||||
'locator': locator,
|
||||
'content': frag.content,
|
||||
'is_root': is_root,
|
||||
'is_reorderable': is_reorderable,
|
||||
|
||||
@@ -23,7 +23,7 @@ def signup(request):
|
||||
"""
|
||||
csrf_token = csrf(request)['csrf_token']
|
||||
if request.user.is_authenticated():
|
||||
return redirect('/course')
|
||||
return redirect('/course/')
|
||||
if settings.FEATURES.get('AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP'):
|
||||
# Redirect to course to login to process their certificate if SSL is enabled
|
||||
# and registration is disabled.
|
||||
@@ -48,7 +48,7 @@ def login_page(request):
|
||||
if next_url:
|
||||
return redirect(next_url)
|
||||
else:
|
||||
return redirect('/course')
|
||||
return redirect('/course/')
|
||||
if settings.FEATURES.get('AUTH_USE_CAS'):
|
||||
# If CAS is enabled, redirect auth handling to there
|
||||
return redirect(reverse('cas-login'))
|
||||
@@ -66,6 +66,6 @@ def login_page(request):
|
||||
def howitworks(request):
|
||||
"Proxy view"
|
||||
if request.user.is_authenticated():
|
||||
return redirect('/course')
|
||||
return redirect('/course/')
|
||||
else:
|
||||
return render_to_response('howitworks.html', {})
|
||||
|
||||
@@ -12,11 +12,10 @@ from django_future.csrf import ensure_csrf_cookie
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
from xmodule.modulestore.locator import BlockUsageLocator
|
||||
from xmodule.tabs import CourseTabList, StaticTab, CourseTab, InvalidTabsException
|
||||
from xmodule.modulestore.keys import CourseKey, UsageKey
|
||||
|
||||
from ..utils import get_modulestore, get_lms_link_for_item
|
||||
from ..utils import get_lms_link_for_item
|
||||
|
||||
__all__ = ['tabs_handler']
|
||||
|
||||
@@ -24,7 +23,7 @@ __all__ = ['tabs_handler']
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(("GET", "POST", "PUT"))
|
||||
def tabs_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None):
|
||||
def tabs_handler(request, course_key_string):
|
||||
"""
|
||||
The restful handler for static tabs.
|
||||
|
||||
@@ -38,13 +37,11 @@ def tabs_handler(request, tag=None, package_id=None, branch=None, version_guid=N
|
||||
Creating a tab, deleting a tab, or changing its contents is not supported through this method.
|
||||
Instead use the general xblock URL (see item.xblock_handler).
|
||||
"""
|
||||
locator = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block)
|
||||
if not has_course_access(request.user, locator):
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
if not has_course_access(request.user, course_key):
|
||||
raise PermissionDenied()
|
||||
|
||||
old_location = loc_mapper().translate_locator_to_location(locator)
|
||||
store = get_modulestore(old_location)
|
||||
course_item = store.get_item(old_location)
|
||||
course_item = modulestore().get_course(course_key)
|
||||
|
||||
if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'):
|
||||
if request.method == 'GET':
|
||||
@@ -68,16 +65,13 @@ def tabs_handler(request, tag=None, package_id=None, branch=None, version_guid=N
|
||||
):
|
||||
if isinstance(tab, StaticTab):
|
||||
# static tab needs its locator information to render itself as an xmodule
|
||||
static_tab_loc = old_location.replace(category='static_tab', name=tab.url_slug)
|
||||
tab.locator = loc_mapper().translate_location(
|
||||
course_item.location.course_id, static_tab_loc, False, True
|
||||
)
|
||||
static_tab_loc = course_key.make_usage_key('static_tab', tab.url_slug)
|
||||
tab.locator = static_tab_loc
|
||||
tabs_to_render.append(tab)
|
||||
|
||||
return render_to_response('edit-tabs.html', {
|
||||
'context_course': course_item,
|
||||
'tabs_to_render': tabs_to_render,
|
||||
'course_locator': locator,
|
||||
'lms_link': get_lms_link_for_item(course_item.location),
|
||||
})
|
||||
else:
|
||||
@@ -164,11 +158,11 @@ def get_tab_by_tab_id_locator(tab_list, tab_id_locator):
|
||||
return tab
|
||||
|
||||
|
||||
def get_tab_by_locator(tab_list, tab_locator):
|
||||
def get_tab_by_locator(tab_list, usage_key_string):
|
||||
"""
|
||||
Look for a tab with the specified locator. Returns the first matching tab.
|
||||
"""
|
||||
tab_location = loc_mapper().translate_locator_to_location(BlockUsageLocator(tab_locator))
|
||||
tab_location = UsageKey.from_string(usage_key_string)
|
||||
item = modulestore('direct').get_item(tab_location)
|
||||
static_tab = StaticTab(
|
||||
name=item.display_name,
|
||||
|
||||
@@ -3,63 +3,46 @@ Tests access.py
|
||||
"""
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth.models import User
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.locator import CourseLocator
|
||||
|
||||
from student.roles import CourseInstructorRole, CourseStaffRole
|
||||
from student.tests.factories import AdminFactory
|
||||
from student.auth import add_users
|
||||
from contentstore.views.access import get_user_role
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
|
||||
class RolesTest(TestCase):
|
||||
"""
|
||||
Tests for user roles.
|
||||
Tests for lti user role serialization.
|
||||
"""
|
||||
def setUp(self):
|
||||
""" Test case setup """
|
||||
self.global_admin = AdminFactory()
|
||||
self.instructor = User.objects.create_user('testinstructor', 'testinstructor+courses@edx.org', 'foo')
|
||||
self.staff = User.objects.create_user('teststaff', 'teststaff+courses@edx.org', 'foo')
|
||||
self.location = Location('i4x', 'mitX', '101', 'course', 'test')
|
||||
self.locator = CourseLocator(url='edx://mitX.101.test')
|
||||
self.course_key = SlashSeparatedCourseKey('mitX', '101', 'test')
|
||||
|
||||
def test_get_user_role_instructor(self):
|
||||
"""
|
||||
Verifies if user is instructor.
|
||||
"""
|
||||
add_users(self.global_admin, CourseInstructorRole(self.location), self.instructor)
|
||||
add_users(self.global_admin, CourseInstructorRole(self.course_key), self.instructor)
|
||||
self.assertEqual(
|
||||
'instructor',
|
||||
get_user_role(self.instructor, self.location, self.location.course_id)
|
||||
get_user_role(self.instructor, self.course_key)
|
||||
)
|
||||
|
||||
def test_get_user_role_instructor_locator(self):
|
||||
"""
|
||||
Verifies if user is instructor, using a CourseLocator.
|
||||
"""
|
||||
add_users(self.global_admin, CourseInstructorRole(self.locator), self.instructor)
|
||||
add_users(self.global_admin, CourseStaffRole(self.course_key), self.staff)
|
||||
self.assertEqual(
|
||||
'instructor',
|
||||
get_user_role(self.instructor, self.locator)
|
||||
get_user_role(self.instructor, self.course_key)
|
||||
)
|
||||
|
||||
def test_get_user_role_staff(self):
|
||||
"""
|
||||
Verifies if user is staff.
|
||||
"""
|
||||
add_users(self.global_admin, CourseStaffRole(self.location), self.staff)
|
||||
add_users(self.global_admin, CourseStaffRole(self.course_key), self.staff)
|
||||
self.assertEqual(
|
||||
'staff',
|
||||
get_user_role(self.staff, self.location, self.location.course_id)
|
||||
)
|
||||
|
||||
def test_get_user_role_staff_locator(self):
|
||||
"""
|
||||
Verifies if user is staff, using a CourseLocator.
|
||||
"""
|
||||
add_users(self.global_admin, CourseStaffRole(self.locator), self.staff)
|
||||
self.assertEqual(
|
||||
'staff',
|
||||
get_user_role(self.staff, self.locator)
|
||||
get_user_role(self.staff, self.course_key)
|
||||
)
|
||||
|
||||
@@ -12,13 +12,13 @@ from pytz import UTC
|
||||
import json
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from contentstore.views import assets
|
||||
from contentstore.utils import reverse_course_url
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
from django.test.utils import override_settings
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey, AssetLocation
|
||||
|
||||
|
||||
class AssetsTestCase(CourseTestCase):
|
||||
@@ -27,8 +27,7 @@ class AssetsTestCase(CourseTestCase):
|
||||
"""
|
||||
def setUp(self):
|
||||
super(AssetsTestCase, self).setUp()
|
||||
location = loc_mapper().translate_location(self.course.location.course_id, self.course.location, False, True)
|
||||
self.url = location.url_reverse('assets/', '')
|
||||
self.url = reverse_course_url('assets_handler', self.course.id)
|
||||
|
||||
def upload_asset(self, name="asset-1"):
|
||||
f = BytesIO(name)
|
||||
@@ -42,7 +41,9 @@ class BasicAssetsTestCase(AssetsTestCase):
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
|
||||
def test_static_url_generation(self):
|
||||
location = Location(['i4x', 'foo', 'bar', 'asset', 'my_file_name.jpg'])
|
||||
|
||||
course_key = SlashSeparatedCourseKey('org', 'class', 'run')
|
||||
location = course_key.make_asset_key('asset', 'my_file_name.jpg')
|
||||
path = StaticContent.get_static_path_from_location(location)
|
||||
self.assertEquals(path, '/static/my_file_name.jpg')
|
||||
|
||||
@@ -56,13 +57,12 @@ class BasicAssetsTestCase(AssetsTestCase):
|
||||
verbose=True
|
||||
)
|
||||
course = course_items[0]
|
||||
location = loc_mapper().translate_location(course.location.course_id, course.location, False, True)
|
||||
url = location.url_reverse('assets/', '')
|
||||
url = reverse_course_url('assets_handler', course.id)
|
||||
|
||||
# Test valid contentType for pdf asset (textbook.pdf)
|
||||
resp = self.client.get(url, HTTP_ACCEPT='application/json')
|
||||
self.assertContains(resp, "/c4x/edX/toy/asset/textbook.pdf")
|
||||
asset_location = StaticContent.get_location_from_path('/c4x/edX/toy/asset/textbook.pdf')
|
||||
asset_location = AssetLocation.from_deprecated_string('/c4x/edX/toy/asset/textbook.pdf')
|
||||
content = contentstore().find(asset_location)
|
||||
# Check after import textbook.pdf has valid contentType ('application/pdf')
|
||||
|
||||
@@ -122,8 +122,7 @@ class UploadTestCase(AssetsTestCase):
|
||||
"""
|
||||
def setUp(self):
|
||||
super(UploadTestCase, self).setUp()
|
||||
location = loc_mapper().translate_location(self.course.location.course_id, self.course.location, False, True)
|
||||
self.url = location.url_reverse('assets/', '')
|
||||
self.url = reverse_course_url('assets_handler', self.course.id)
|
||||
|
||||
def test_happy_path(self):
|
||||
resp = self.upload_asset()
|
||||
@@ -143,18 +142,19 @@ class AssetToJsonTestCase(AssetsTestCase):
|
||||
def test_basic(self):
|
||||
upload_date = datetime(2013, 6, 1, 10, 30, tzinfo=UTC)
|
||||
|
||||
location = Location(['i4x', 'foo', 'bar', 'asset', 'my_file_name.jpg'])
|
||||
thumbnail_location = Location(['i4x', 'foo', 'bar', 'asset', 'my_file_name_thumb.jpg'])
|
||||
course_key = SlashSeparatedCourseKey('org', 'class', 'run')
|
||||
location = course_key.make_asset_key('asset', 'my_file_name.jpg')
|
||||
thumbnail_location = course_key.make_asset_key('thumbnail', 'my_file_name_thumb.jpg')
|
||||
|
||||
output = assets._get_asset_json("my_file", upload_date, location, thumbnail_location, True)
|
||||
|
||||
self.assertEquals(output["display_name"], "my_file")
|
||||
self.assertEquals(output["date_added"], "Jun 01, 2013 at 10:30 UTC")
|
||||
self.assertEquals(output["url"], "/i4x/foo/bar/asset/my_file_name.jpg")
|
||||
self.assertEquals(output["external_url"], "lms_base_url/i4x/foo/bar/asset/my_file_name.jpg")
|
||||
self.assertEquals(output["url"], "/c4x/org/class/asset/my_file_name.jpg")
|
||||
self.assertEquals(output["external_url"], "lms_base_url/c4x/org/class/asset/my_file_name.jpg")
|
||||
self.assertEquals(output["portable_url"], "/static/my_file_name.jpg")
|
||||
self.assertEquals(output["thumbnail"], "/i4x/foo/bar/asset/my_file_name_thumb.jpg")
|
||||
self.assertEquals(output["id"], output["url"])
|
||||
self.assertEquals(output["thumbnail"], "/c4x/org/class/thumbnail/my_file_name_thumb.jpg")
|
||||
self.assertEquals(output["id"], unicode(location))
|
||||
self.assertEquals(output['locked'], True)
|
||||
|
||||
output = assets._get_asset_json("name", upload_date, location, None, False)
|
||||
@@ -176,12 +176,11 @@ class LockAssetTestCase(AssetsTestCase):
|
||||
content = contentstore().find(asset_location)
|
||||
self.assertEqual(content.locked, locked)
|
||||
|
||||
def post_asset_update(lock):
|
||||
def post_asset_update(lock, course):
|
||||
""" Helper method for posting asset update. """
|
||||
upload_date = datetime(2013, 6, 1, 10, 30, tzinfo=UTC)
|
||||
asset_location = Location(['c4x', 'edX', 'toy', 'asset', 'sample_static.txt'])
|
||||
location = loc_mapper().translate_location(course.location.course_id, course.location, False, True)
|
||||
url = location.url_reverse('assets/', '')
|
||||
asset_location = course.id.make_asset_key('asset', 'sample_static.txt')
|
||||
url = reverse_course_url('assets_handler', course.id, kwargs={'asset_key_string': unicode(asset_location)})
|
||||
|
||||
resp = self.client.post(
|
||||
url,
|
||||
@@ -204,11 +203,11 @@ class LockAssetTestCase(AssetsTestCase):
|
||||
verify_asset_locked_state(False)
|
||||
|
||||
# Lock the asset
|
||||
resp_asset = post_asset_update(True)
|
||||
resp_asset = post_asset_update(True, course)
|
||||
self.assertTrue(resp_asset['locked'])
|
||||
verify_asset_locked_state(True)
|
||||
|
||||
# Unlock the asset
|
||||
resp_asset = post_asset_update(False)
|
||||
resp_asset = post_asset_update(False, course)
|
||||
self.assertFalse(resp_asset['locked'])
|
||||
verify_asset_locked_state(False)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
""" Unit tests for checklist methods in views.py. """
|
||||
from contentstore.utils import get_modulestore
|
||||
from contentstore.utils import get_modulestore, reverse_course_url
|
||||
from contentstore.views.checklist import expand_checklist_action_url
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
|
||||
import json
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
@@ -14,8 +13,11 @@ class ChecklistTestCase(CourseTestCase):
|
||||
""" Creates the test course. """
|
||||
super(ChecklistTestCase, self).setUp()
|
||||
self.course = CourseFactory.create(org='mitX', number='333', display_name='Checklists Course')
|
||||
self.location = loc_mapper().translate_location(self.course.location.course_id, self.course.location, False, True)
|
||||
self.checklists_url = self.location.url_reverse('checklists/', '')
|
||||
self.checklists_url = self.get_url()
|
||||
|
||||
def get_url(self, checklist_index=None):
|
||||
url_args = {'checklist_index': checklist_index} if checklist_index else None
|
||||
return reverse_course_url('checklists_handler', self.course.id, kwargs=url_args)
|
||||
|
||||
def get_persisted_checklists(self):
|
||||
""" Returns the checklists as persisted in the modulestore. """
|
||||
@@ -41,7 +43,7 @@ class ChecklistTestCase(CourseTestCase):
|
||||
response = self.client.get(self.checklists_url)
|
||||
self.assertContains(response, "Getting Started With Studio")
|
||||
# Verify expansion of action URL happened.
|
||||
self.assertContains(response, 'course_team/mitX.333.Checklists_Course')
|
||||
self.assertContains(response, 'course_team/slashes:mitX+333+Checklists_Course')
|
||||
# Verify persisted checklist does NOT have expanded URL.
|
||||
checklist_0 = self.get_persisted_checklists()[0]
|
||||
self.assertEqual('ManageUsers', get_action_url(checklist_0, 0))
|
||||
@@ -77,7 +79,7 @@ class ChecklistTestCase(CourseTestCase):
|
||||
|
||||
def test_update_checklists_index_ignored_on_get(self):
|
||||
""" Checklist index ignored on get. """
|
||||
update_url = self.location.url_reverse('checklists/', '1')
|
||||
update_url = self.get_url(1)
|
||||
|
||||
returned_checklists = json.loads(self.client.get(update_url).content)
|
||||
for pay, resp in zip(self.get_persisted_checklists(), returned_checklists):
|
||||
@@ -90,14 +92,14 @@ class ChecklistTestCase(CourseTestCase):
|
||||
|
||||
def test_update_checklists_index_out_of_range(self):
|
||||
""" Checklist index out of range, will error on post. """
|
||||
update_url = self.location.url_reverse('checklists/', '100')
|
||||
update_url = self.get_url(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 = self.location.url_reverse('checklists/', '1')
|
||||
update_url = self.get_url(1)
|
||||
|
||||
payload = self.course.checklists[1]
|
||||
self.assertFalse(get_first_item(payload).get('is_checked'))
|
||||
@@ -114,7 +116,7 @@ class ChecklistTestCase(CourseTestCase):
|
||||
|
||||
def test_update_checklists_delete_unsupported(self):
|
||||
""" Delete operation is not supported. """
|
||||
update_url = self.location.url_reverse('checklists/', '100')
|
||||
update_url = self.get_url(100)
|
||||
response = self.client.delete(update_url)
|
||||
self.assertEqual(response.status_code, 405)
|
||||
|
||||
@@ -135,8 +137,8 @@ class ChecklistTestCase(CourseTestCase):
|
||||
# Verify no side effect in the original list.
|
||||
self.assertEqual(get_action_url(checklist, index), stored)
|
||||
|
||||
test_expansion(self.course.checklists[0], 0, 'ManageUsers', '/course_team/mitX.333.Checklists_Course/branch/draft/block/Checklists_Course')
|
||||
test_expansion(self.course.checklists[1], 1, 'CourseOutline', '/course/mitX.333.Checklists_Course/branch/draft/block/Checklists_Course')
|
||||
test_expansion(self.course.checklists[0], 0, 'ManageUsers', '/course_team/slashes:mitX+333+Checklists_Course/')
|
||||
test_expansion(self.course.checklists[1], 1, 'CourseOutline', '/course/slashes:mitX+333+Checklists_Course')
|
||||
test_expansion(self.course.checklists[2], 0, 'http://help.edge.edx.org/', 'http://help.edge.edx.org/')
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Unit tests for the container page.
|
||||
"""
|
||||
|
||||
import re
|
||||
from contentstore.utils import compute_publish_state, PublishState
|
||||
from contentstore.views.tests.utils import StudioPageTestCase
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -30,19 +31,17 @@ class ContainerPageTestCase(StudioPageTestCase):
|
||||
category="video", display_name="My Video")
|
||||
|
||||
def test_container_html(self):
|
||||
branch_name = "MITx.999.Robot_Super_Course/branch/draft/block"
|
||||
self._test_html_content(
|
||||
self.child_container,
|
||||
branch_name=branch_name,
|
||||
expected_section_tag=(
|
||||
'<section class="wrapper-xblock level-page is-hidden studio-xblock-wrapper" '
|
||||
'data-locator="{branch_name}/Split_Test">'.format(branch_name=branch_name)
|
||||
'data-locator="{0}" data-course-key="{0.course_key}">'.format(self.child_container.location)
|
||||
),
|
||||
expected_breadcrumbs=(
|
||||
r'<a href="/unit/{branch_name}/Unit"\s*'
|
||||
r'<a href="/unit/{}"\s*'
|
||||
r'class="navigation-link navigation-parent">Unit</a>\s*'
|
||||
r'<a href="#" class="navigation-link navigation-current">Split Test</a>'
|
||||
).format(branch_name=branch_name)
|
||||
).format(re.escape(unicode(self.vertical.location)))
|
||||
)
|
||||
|
||||
def test_container_on_container_html(self):
|
||||
@@ -60,21 +59,22 @@ class ContainerPageTestCase(StudioPageTestCase):
|
||||
)
|
||||
|
||||
def test_container_html(xblock):
|
||||
branch_name = "MITx.999.Robot_Super_Course/branch/draft/block"
|
||||
self._test_html_content(
|
||||
xblock,
|
||||
branch_name=branch_name,
|
||||
expected_section_tag=(
|
||||
'<section class="wrapper-xblock level-page is-hidden studio-xblock-wrapper" '
|
||||
'data-locator="{branch_name}/Wrapper">'.format(branch_name=branch_name)
|
||||
'data-locator="{0}" data-course-key="{0.course_key}">'.format(published_container.location)
|
||||
),
|
||||
expected_breadcrumbs=(
|
||||
r'<a href="/unit/{branch_name}/Unit"\s*'
|
||||
r'<a href="/unit/{unit}"\s*'
|
||||
r'class="navigation-link navigation-parent">Unit</a>\s*'
|
||||
r'<a href="/container/{branch_name}/Split_Test"\s*'
|
||||
r'<a href="/container/{split_test}"\s*'
|
||||
r'class="navigation-link navigation-parent">Split Test</a>\s*'
|
||||
r'<a href="#" class="navigation-link navigation-current">Wrapper</a>'
|
||||
).format(branch_name=branch_name)
|
||||
).format(
|
||||
unit=re.escape(unicode(self.vertical.location)),
|
||||
split_test=re.escape(unicode(self.child_container.location))
|
||||
)
|
||||
)
|
||||
|
||||
# Test the published version of the container
|
||||
@@ -86,7 +86,7 @@ class ContainerPageTestCase(StudioPageTestCase):
|
||||
draft_container = modulestore('draft').convert_to_draft(published_container.location)
|
||||
test_container_html(draft_container)
|
||||
|
||||
def _test_html_content(self, xblock, branch_name, expected_section_tag, expected_breadcrumbs):
|
||||
def _test_html_content(self, xblock, expected_section_tag, expected_breadcrumbs):
|
||||
"""
|
||||
Get the HTML for a container page and verify the section tag is correct
|
||||
and the breadcrumbs trail is correct.
|
||||
@@ -100,12 +100,10 @@ class ContainerPageTestCase(StudioPageTestCase):
|
||||
# Verify the link that allows users to change publish status.
|
||||
expected_message = None
|
||||
if publish_state == PublishState.public:
|
||||
expected_message = 'you need to edit unit <a href="/unit/{branch_name}/Unit">Unit</a> as a draft.'
|
||||
expected_message = 'you need to edit unit <a href="/unit/{}">Unit</a> as a draft.'
|
||||
else:
|
||||
expected_message = 'your changes will be published with unit <a href="/unit/{branch_name}/Unit">Unit</a>.'
|
||||
expected_unit_link = expected_message.format(
|
||||
branch_name=branch_name
|
||||
)
|
||||
expected_message = 'your changes will be published with unit <a href="/unit/{}">Unit</a>.'
|
||||
expected_unit_link = expected_message.format(self.vertical.location)
|
||||
self.assertIn(expected_unit_link, html)
|
||||
|
||||
def test_public_container_preview_html(self):
|
||||
|
||||
@@ -5,9 +5,9 @@ import json
|
||||
import lxml
|
||||
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
from contentstore.utils import reverse_course_url
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from xmodule.modulestore import parsers
|
||||
from xmodule.modulestore.locator import Locator
|
||||
|
||||
|
||||
class TestCourseIndex(CourseTestCase):
|
||||
@@ -30,7 +30,7 @@ class TestCourseIndex(CourseTestCase):
|
||||
"""
|
||||
Test getting the list of courses and then pulling up their outlines
|
||||
"""
|
||||
index_url = '/course'
|
||||
index_url = '/course/'
|
||||
index_response = authed_client.get(index_url, {}, HTTP_ACCEPT='text/html')
|
||||
parsed_html = lxml.html.fromstring(index_response.content)
|
||||
course_link_eles = parsed_html.find_class('course-link')
|
||||
@@ -38,7 +38,7 @@ class TestCourseIndex(CourseTestCase):
|
||||
for link in course_link_eles:
|
||||
self.assertRegexpMatches(
|
||||
link.get("href"),
|
||||
r'course/{0}+/branch/{0}+/block/{0}+'.format(parsers.ALLOWED_ID_CHARS)
|
||||
'course/slashes:{0}'.format(Locator.ALLOWED_ID_CHARS)
|
||||
)
|
||||
# now test that url
|
||||
outline_response = authed_client.get(link.get("href"), {}, HTTP_ACCEPT='text/html')
|
||||
@@ -59,7 +59,7 @@ class TestCourseIndex(CourseTestCase):
|
||||
"""
|
||||
Test the error conditions for the access
|
||||
"""
|
||||
outline_url = self.course_locator.url_reverse('course/', '')
|
||||
outline_url = reverse_course_url('course_handler', self.course.id)
|
||||
# register a non-staff member and try to delete the course branch
|
||||
non_staff_client, _ = self.create_non_staff_authed_user_client()
|
||||
response = non_staff_client.delete(outline_url, {}, HTTP_ACCEPT='application/json')
|
||||
@@ -67,12 +67,11 @@ class TestCourseIndex(CourseTestCase):
|
||||
|
||||
def test_course_staff_access(self):
|
||||
"""
|
||||
Make and register an course_staff and ensure they can access the courses
|
||||
Make and register course_staff and ensure they can access the courses
|
||||
"""
|
||||
course_staff_client, course_staff = self.create_non_staff_authed_user_client()
|
||||
for course in [self.course, self.odd_course]:
|
||||
new_location = loc_mapper().translate_location(course.location.course_id, course.location, False, True)
|
||||
permission_url = new_location.url_reverse("course_team/", course_staff.email)
|
||||
permission_url = reverse_course_url('course_team_handler', course.id, kwargs={'email': course_staff.email})
|
||||
|
||||
self.client.post(
|
||||
permission_url,
|
||||
@@ -85,7 +84,7 @@ class TestCourseIndex(CourseTestCase):
|
||||
self.check_index_and_outline(course_staff_client)
|
||||
|
||||
def test_json_responses(self):
|
||||
outline_url = self.course_locator.url_reverse('course/')
|
||||
outline_url = reverse_course_url('course_handler', self.course.id)
|
||||
chapter = ItemFactory.create(parent_location=self.course.location, category='chapter', display_name="Week 1")
|
||||
lesson = ItemFactory.create(parent_location=chapter.location, category='sequential', display_name="Lesson 1")
|
||||
subsection = ItemFactory.create(parent_location=lesson.location, category='vertical', display_name='Subsection 1')
|
||||
@@ -96,17 +95,17 @@ class TestCourseIndex(CourseTestCase):
|
||||
|
||||
# First spot check some values in the root response
|
||||
self.assertEqual(json_response['category'], 'course')
|
||||
self.assertEqual(json_response['id'], 'MITx.999.Robot_Super_Course/branch/draft/block/Robot_Super_Course')
|
||||
self.assertEqual(json_response['id'], 'location:MITx+999+Robot_Super_Course+course+Robot_Super_Course')
|
||||
self.assertEqual(json_response['display_name'], 'Robot Super Course')
|
||||
self.assertTrue(json_response['is_container'])
|
||||
self.assertFalse(json_response['is_draft'])
|
||||
|
||||
# Now verify that the first child
|
||||
# Now verify the first child
|
||||
children = json_response['children']
|
||||
self.assertTrue(len(children) > 0)
|
||||
first_child_response = children[0]
|
||||
self.assertEqual(first_child_response['category'], 'chapter')
|
||||
self.assertEqual(first_child_response['id'], 'MITx.999.Robot_Super_Course/branch/draft/block/Week_1')
|
||||
self.assertEqual(first_child_response['id'], 'location:MITx+999+Robot_Super_Course+chapter+Week_1')
|
||||
self.assertEqual(first_child_response['display_name'], 'Week 1')
|
||||
self.assertTrue(first_child_response['is_container'])
|
||||
self.assertFalse(first_child_response['is_draft'])
|
||||
|
||||
@@ -4,12 +4,19 @@ unit tests for course_info views and models.
|
||||
import json
|
||||
|
||||
from contentstore.tests.test_course_settings import CourseTestCase
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore, loc_mapper
|
||||
from xmodule.modulestore.locator import BlockUsageLocator
|
||||
from contentstore.utils import reverse_course_url, reverse_usage_url
|
||||
from xmodule.modulestore.locations import Location, SlashSeparatedCourseKey
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
class CourseUpdateTest(CourseTestCase):
|
||||
|
||||
def create_update_url(self, provided_id=None, course_key=None):
|
||||
if course_key is None:
|
||||
course_key = self.course.id
|
||||
kwargs = {'provided_id': str(provided_id)} if provided_id else None
|
||||
return reverse_course_url('course_info_update_handler', course_key, kwargs=kwargs)
|
||||
|
||||
'''The do all and end all of unit test cases.'''
|
||||
def test_course_update(self):
|
||||
'''Go through each interface and ensure it works.'''
|
||||
@@ -20,29 +27,24 @@ class CourseUpdateTest(CourseTestCase):
|
||||
Does not supply a provided_id.
|
||||
"""
|
||||
payload = {'content': content, 'date': date}
|
||||
url = update_locator.url_reverse('course_info_update/')
|
||||
url = self.create_update_url()
|
||||
|
||||
resp = self.client.ajax_post(url, payload)
|
||||
self.assertContains(resp, '', status_code=200)
|
||||
|
||||
return json.loads(resp.content)
|
||||
|
||||
course_locator = loc_mapper().translate_location(
|
||||
self.course.location.course_id, self.course.location, False, True
|
||||
resp = self.client.get_html(
|
||||
reverse_course_url('course_info_handler', self.course.id)
|
||||
)
|
||||
resp = self.client.get_html(course_locator.url_reverse('course_info/'))
|
||||
self.assertContains(resp, 'Course Updates', status_code=200)
|
||||
update_locator = loc_mapper().translate_location(
|
||||
self.course.location.course_id, self.course.location.replace(category='course_info', name='updates'),
|
||||
False, True
|
||||
)
|
||||
|
||||
init_content = '<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0">'
|
||||
content = init_content + '</iframe>'
|
||||
payload = get_response(content, 'January 8, 2013')
|
||||
self.assertHTMLEqual(payload['content'], content)
|
||||
|
||||
first_update_url = update_locator.url_reverse('course_info_update', str(payload['id']))
|
||||
first_update_url = self.create_update_url(provided_id=payload['id'])
|
||||
content += '<div>div <p>p<br/></p></div>'
|
||||
payload['content'] = content
|
||||
# POST requests were coming in w/ these header values causing an error; so, repro error here
|
||||
@@ -63,7 +65,7 @@ class CourseUpdateTest(CourseTestCase):
|
||||
payload = get_response(content, 'January 11, 2013')
|
||||
self.assertHTMLEqual(content, payload['content'], "self closing ol")
|
||||
|
||||
course_update_url = update_locator.url_reverse('course_info_update/')
|
||||
course_update_url = self.create_update_url()
|
||||
resp = self.client.get_json(course_update_url)
|
||||
payload = json.loads(resp.content)
|
||||
self.assertTrue(len(payload) == 2)
|
||||
@@ -83,7 +85,7 @@ class CourseUpdateTest(CourseTestCase):
|
||||
content = 'blah blah'
|
||||
payload = {'content': content, 'date': 'January 21, 2013'}
|
||||
self.assertContains(
|
||||
self.client.ajax_post(course_update_url + '/9', payload),
|
||||
self.client.ajax_post(course_update_url + '9', payload),
|
||||
'Failed to save', status_code=400
|
||||
)
|
||||
|
||||
@@ -103,7 +105,7 @@ class CourseUpdateTest(CourseTestCase):
|
||||
self.assertHTMLEqual(content, payload['content'])
|
||||
|
||||
# now try to delete a non-existent update
|
||||
self.assertContains(self.client.delete(course_update_url + '/19'), "delete", status_code=400)
|
||||
self.assertContains(self.client.delete(course_update_url + '19'), "delete", status_code=400)
|
||||
|
||||
# now delete a real update
|
||||
content = 'blah blah'
|
||||
@@ -115,7 +117,7 @@ class CourseUpdateTest(CourseTestCase):
|
||||
payload = json.loads(resp.content)
|
||||
before_delete = len(payload)
|
||||
|
||||
url = update_locator.url_reverse('course_info_update/', str(this_id))
|
||||
url = self.create_update_url(provided_id=this_id)
|
||||
resp = self.client.delete(url)
|
||||
payload = json.loads(resp.content)
|
||||
self.assertTrue(len(payload) == before_delete - 1)
|
||||
@@ -126,7 +128,7 @@ class CourseUpdateTest(CourseTestCase):
|
||||
Note: new data will save as list in 'items' field.
|
||||
'''
|
||||
# get the updates and populate 'data' field with some data.
|
||||
location = self.course.location.replace(category='course_info', name='updates')
|
||||
location = self.course.id.make_usage_key('course_info', 'updates')
|
||||
modulestore('direct').create_and_save_xmodule(location)
|
||||
course_updates = modulestore('direct').get_item(location)
|
||||
update_date = u"January 23, 2014"
|
||||
@@ -135,18 +137,16 @@ class CourseUpdateTest(CourseTestCase):
|
||||
course_updates.data = update_data
|
||||
modulestore('direct').update_item(course_updates, self.user)
|
||||
|
||||
update_locator = loc_mapper().translate_location(
|
||||
self.course.location.course_id, location, False, True
|
||||
)
|
||||
# test getting all updates list
|
||||
course_update_url = update_locator.url_reverse('course_info_update/')
|
||||
course_update_url = self.create_update_url()
|
||||
resp = self.client.get_json(course_update_url)
|
||||
payload = json.loads(resp.content)
|
||||
self.assertEqual(payload, [{u'date': update_date, u'content': update_content, u'id': 1}])
|
||||
self.assertTrue(len(payload) == 1)
|
||||
|
||||
# test getting single update item
|
||||
first_update_url = update_locator.url_reverse('course_info_update', str(payload[0]['id']))
|
||||
|
||||
first_update_url = self.create_update_url(provided_id=payload[0]['id'])
|
||||
resp = self.client.get_json(first_update_url)
|
||||
payload = json.loads(resp.content)
|
||||
self.assertEqual(payload, {u'date': u'January 23, 2014', u'content': u'Hello world!', u'id': 1})
|
||||
@@ -161,7 +161,7 @@ class CourseUpdateTest(CourseTestCase):
|
||||
update_content = 'Testing'
|
||||
payload = {'content': update_content, 'date': update_date}
|
||||
resp = self.client.ajax_post(
|
||||
course_update_url + '/1', payload, HTTP_X_HTTP_METHOD_OVERRIDE="PUT", REQUEST_METHOD="POST"
|
||||
course_update_url + '1', payload, HTTP_X_HTTP_METHOD_OVERRIDE="PUT", REQUEST_METHOD="POST"
|
||||
)
|
||||
self.assertHTMLEqual(update_content, json.loads(resp.content)['content'])
|
||||
course_updates = modulestore('direct').get_item(location)
|
||||
@@ -174,7 +174,7 @@ class CourseUpdateTest(CourseTestCase):
|
||||
course_updates = modulestore('direct').get_item(location)
|
||||
self.assertEqual(course_updates.items, [{u'date': update_date, u'content': update_content, u'id': 1}])
|
||||
# now try to delete first update item
|
||||
resp = self.client.delete(course_update_url + '/1')
|
||||
resp = self.client.delete(course_update_url + '1')
|
||||
self.assertEqual(json.loads(resp.content), [])
|
||||
# confirm that course update is soft deleted ('status' flag set to 'deleted') in db
|
||||
course_updates = modulestore('direct').get_item(location)
|
||||
@@ -182,7 +182,7 @@ class CourseUpdateTest(CourseTestCase):
|
||||
[{u'date': update_date, u'content': update_content, u'id': 1, u'status': 'deleted'}])
|
||||
|
||||
# now try to get deleted update
|
||||
resp = self.client.get_json(course_update_url + '/1')
|
||||
resp = self.client.get_json(course_update_url + '1')
|
||||
payload = json.loads(resp.content)
|
||||
self.assertEqual(payload.get('error'), u"Course update not found.")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
@@ -203,7 +203,7 @@ class CourseUpdateTest(CourseTestCase):
|
||||
def test_no_ol_course_update(self):
|
||||
'''Test trying to add to a saved course_update which is not an ol.'''
|
||||
# get the updates and set to something wrong
|
||||
location = self.course.location.replace(category='course_info', name='updates')
|
||||
location = self.course.id.make_usage_key('course_info', 'updates')
|
||||
modulestore('direct').create_and_save_xmodule(location)
|
||||
course_updates = modulestore('direct').get_item(location)
|
||||
course_updates.data = 'bad news'
|
||||
@@ -213,10 +213,7 @@ class CourseUpdateTest(CourseTestCase):
|
||||
content = init_content + '</iframe>'
|
||||
payload = {'content': content, 'date': 'January 8, 2013'}
|
||||
|
||||
update_locator = loc_mapper().translate_location(
|
||||
self.course.location.course_id, location, False, True
|
||||
)
|
||||
course_update_url = update_locator.url_reverse('course_info_update/')
|
||||
course_update_url = self.create_update_url()
|
||||
resp = self.client.ajax_post(course_update_url, payload)
|
||||
|
||||
payload = json.loads(resp.content)
|
||||
@@ -231,33 +228,16 @@ class CourseUpdateTest(CourseTestCase):
|
||||
def test_post_course_update(self):
|
||||
"""
|
||||
Test that a user can successfully post on course updates and handouts of a course
|
||||
whose location in not in loc_mapper
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey('Org1', 'Course_1', 'Run_1')
|
||||
course_update_url = self.create_update_url(course_key=course_key)
|
||||
|
||||
# create a course via the view handler
|
||||
course_location = Location(['i4x', 'Org_1', 'Course_1', 'course', 'Run_1'])
|
||||
course_locator = loc_mapper().translate_location(
|
||||
course_location.course_id, course_location, False, True
|
||||
)
|
||||
self.client.ajax_post(
|
||||
course_locator.url_reverse('course'),
|
||||
{
|
||||
'org': course_location.org,
|
||||
'number': course_location.course,
|
||||
'display_name': 'test course',
|
||||
'run': course_location.name,
|
||||
}
|
||||
)
|
||||
self.client.ajax_post(course_update_url)
|
||||
|
||||
branch = u'draft'
|
||||
version = None
|
||||
block = u'updates'
|
||||
updates_locator = BlockUsageLocator(
|
||||
package_id=course_location.course_id.replace('/', '.'), branch=branch, version_guid=version, block_id=block
|
||||
)
|
||||
|
||||
content = u"Sample update"
|
||||
payload = {'content': content, 'date': 'January 8, 2013'}
|
||||
course_update_url = updates_locator.url_reverse('course_info_update')
|
||||
resp = self.client.ajax_post(course_update_url, payload)
|
||||
|
||||
# check that response status is 200 not 400
|
||||
@@ -266,22 +246,17 @@ class CourseUpdateTest(CourseTestCase):
|
||||
payload = json.loads(resp.content)
|
||||
self.assertHTMLEqual(payload['content'], content)
|
||||
|
||||
# now test that calling translate_location returns a locator whose block_id is 'updates'
|
||||
updates_location = course_location.replace(category='course_info', name=block)
|
||||
updates_locator = loc_mapper().translate_location(course_location.course_id, updates_location)
|
||||
self.assertTrue(isinstance(updates_locator, BlockUsageLocator))
|
||||
self.assertEqual(updates_locator.block_id, block)
|
||||
updates_location = self.course.id.make_usage_key('course_info', 'updates')
|
||||
self.assertTrue(isinstance(updates_location, Location))
|
||||
self.assertEqual(updates_location.name, block)
|
||||
|
||||
# check posting on handouts
|
||||
block = u'handouts'
|
||||
handouts_locator = BlockUsageLocator(
|
||||
package_id=updates_locator.package_id, branch=updates_locator.branch, version_guid=version, block_id=block
|
||||
)
|
||||
course_handouts_url = handouts_locator.url_reverse('xblock')
|
||||
content = u"Sample handout"
|
||||
payload = {"data": content}
|
||||
resp = self.client.ajax_post(course_handouts_url, payload)
|
||||
handouts_location = self.course.id.make_usage_key('course_info', 'handouts')
|
||||
course_handouts_url = reverse_usage_url('xblock_handler', handouts_location)
|
||||
|
||||
content = u"Sample handout"
|
||||
payload = {'data': content}
|
||||
resp = self.client.ajax_post(course_handouts_url, payload)
|
||||
# check that response status is 200 not 500
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
|
||||
@@ -12,42 +12,34 @@ class HelpersTestCase(CourseTestCase):
|
||||
Unit tests for helpers.py.
|
||||
"""
|
||||
def test_xblock_studio_url(self):
|
||||
course = self.course
|
||||
|
||||
# Verify course URL
|
||||
self.assertEqual(xblock_studio_url(course),
|
||||
u'/course/MITx.999.Robot_Super_Course/branch/draft/block/Robot_Super_Course')
|
||||
self.assertEqual(xblock_studio_url(self.course),
|
||||
u'/course/slashes:MITx+999+Robot_Super_Course')
|
||||
|
||||
# Verify chapter URL
|
||||
chapter = ItemFactory.create(parent_location=self.course.location, category='chapter',
|
||||
display_name="Week 1")
|
||||
self.assertIsNone(xblock_studio_url(chapter))
|
||||
self.assertIsNone(xblock_studio_url(chapter, course))
|
||||
|
||||
# Verify lesson URL
|
||||
sequential = ItemFactory.create(parent_location=chapter.location, category='sequential',
|
||||
display_name="Lesson 1")
|
||||
self.assertIsNone(xblock_studio_url(sequential))
|
||||
self.assertIsNone(xblock_studio_url(sequential, course))
|
||||
|
||||
# Verify vertical URL
|
||||
vertical = ItemFactory.create(parent_location=sequential.location, category='vertical',
|
||||
display_name='Unit')
|
||||
self.assertEqual(xblock_studio_url(vertical),
|
||||
u'/unit/MITx.999.Robot_Super_Course/branch/draft/block/Unit')
|
||||
self.assertEqual(xblock_studio_url(vertical, course),
|
||||
u'/unit/MITx.999.Robot_Super_Course/branch/draft/block/Unit')
|
||||
u'/unit/location:MITx+999+Robot_Super_Course+vertical+Unit')
|
||||
|
||||
# Verify child vertical URL
|
||||
child_vertical = ItemFactory.create(parent_location=vertical.location, category='vertical',
|
||||
display_name='Child Vertical')
|
||||
self.assertEqual(xblock_studio_url(child_vertical),
|
||||
u'/container/MITx.999.Robot_Super_Course/branch/draft/block/Child_Vertical')
|
||||
self.assertEqual(xblock_studio_url(child_vertical, course),
|
||||
u'/container/MITx.999.Robot_Super_Course/branch/draft/block/Child_Vertical')
|
||||
u'/container/location:MITx+999+Robot_Super_Course+vertical+Child_Vertical')
|
||||
|
||||
# Verify video URL
|
||||
video = ItemFactory.create(parent_location=child_vertical.location, category="video",
|
||||
display_name="My Video")
|
||||
self.assertIsNone(xblock_studio_url(video))
|
||||
self.assertIsNone(xblock_studio_url(video, course))
|
||||
|
||||
@@ -14,6 +14,7 @@ from uuid import uuid4
|
||||
|
||||
from django.test.utils import override_settings
|
||||
from django.conf import settings
|
||||
from contentstore.utils import reverse_course_url
|
||||
|
||||
from xmodule.contentstore.django import _CONTENTSTORE
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
@@ -36,10 +37,7 @@ class ImportTestCase(CourseTestCase):
|
||||
"""
|
||||
def setUp(self):
|
||||
super(ImportTestCase, self).setUp()
|
||||
self.new_location = loc_mapper().translate_location(
|
||||
self.course.location.course_id, self.course.location, False, True
|
||||
)
|
||||
self.url = self.new_location.url_reverse('import/', '')
|
||||
self.url = reverse_course_url('import_handler', self.course.id)
|
||||
self.content_dir = path(tempfile.mkdtemp())
|
||||
|
||||
def touch(name):
|
||||
@@ -91,9 +89,10 @@ class ImportTestCase(CourseTestCase):
|
||||
# Check that `import_status` returns the appropriate stage (i.e., the
|
||||
# stage at which import failed).
|
||||
resp_status = self.client.get(
|
||||
self.new_location.url_reverse(
|
||||
'import_status',
|
||||
os.path.split(self.bad_tar)[1]
|
||||
reverse_course_url(
|
||||
'import_status_handler',
|
||||
self.course.id,
|
||||
kwargs={'filename': os.path.split(self.bad_tar)[1]}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -116,9 +115,9 @@ class ImportTestCase(CourseTestCase):
|
||||
"""
|
||||
# Create a non_staff user and add it to course staff only
|
||||
__, nonstaff_user = self.create_non_staff_authed_user_client(authenticate=False)
|
||||
auth.add_users(self.user, CourseStaffRole(self.course.location), nonstaff_user)
|
||||
auth.add_users(self.user, CourseStaffRole(self.course.id), nonstaff_user)
|
||||
|
||||
course = self.store.get_item(self.course_location)
|
||||
course = self.store.get_course(self.course.id)
|
||||
self.assertIsNotNone(course)
|
||||
display_name_before_import = course.display_name
|
||||
|
||||
@@ -128,7 +127,7 @@ class ImportTestCase(CourseTestCase):
|
||||
resp = self.client.post(self.url, args)
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
|
||||
course = self.store.get_item(self.course_location)
|
||||
course = self.store.get_course(self.course.id)
|
||||
self.assertIsNotNone(course)
|
||||
display_name_after_import = course.display_name
|
||||
|
||||
@@ -136,8 +135,8 @@ class ImportTestCase(CourseTestCase):
|
||||
self.assertNotEqual(display_name_before_import, display_name_after_import)
|
||||
|
||||
# Now check that non_staff user has his same role
|
||||
self.assertFalse(CourseInstructorRole(self.course_location).has_user(nonstaff_user))
|
||||
self.assertTrue(CourseStaffRole(self.course_location).has_user(nonstaff_user))
|
||||
self.assertFalse(CourseInstructorRole(self.course.id).has_user(nonstaff_user))
|
||||
self.assertTrue(CourseStaffRole(self.course.id).has_user(nonstaff_user))
|
||||
|
||||
# Now course staff user can also successfully import course
|
||||
self.client.login(username=nonstaff_user.username, password='foo')
|
||||
@@ -147,8 +146,8 @@ class ImportTestCase(CourseTestCase):
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
|
||||
# Now check that non_staff user has his same role
|
||||
self.assertFalse(CourseInstructorRole(self.course_location).has_user(nonstaff_user))
|
||||
self.assertTrue(CourseStaffRole(self.course_location).has_user(nonstaff_user))
|
||||
self.assertFalse(CourseInstructorRole(self.course.id).has_user(nonstaff_user))
|
||||
self.assertTrue(CourseStaffRole(self.course.id).has_user(nonstaff_user))
|
||||
|
||||
## Unsafe tar methods #####################################################
|
||||
# Each of these methods creates a tarfile with a single type of unsafe
|
||||
@@ -235,9 +234,10 @@ class ImportTestCase(CourseTestCase):
|
||||
# either 3, indicating all previous steps are completed, or 0,
|
||||
# indicating no upload in progress)
|
||||
resp_status = self.client.get(
|
||||
self.new_location.url_reverse(
|
||||
'import_status',
|
||||
os.path.split(self.good_tar)[1]
|
||||
reverse_course_url(
|
||||
'import_status_handler',
|
||||
self.course.id,
|
||||
kwargs={'filename': os.path.split(self.good_tar)[1]}
|
||||
)
|
||||
)
|
||||
import_status = json.loads(resp_status.content)["ImportStatus"]
|
||||
@@ -254,8 +254,7 @@ class ExportTestCase(CourseTestCase):
|
||||
Sets up the test course.
|
||||
"""
|
||||
super(ExportTestCase, self).setUp()
|
||||
location = loc_mapper().translate_location(self.course.location.course_id, self.course.location, False, True)
|
||||
self.url = location.url_reverse('export/', '')
|
||||
self.url = reverse_course_url('export_handler', self.course.id)
|
||||
|
||||
def test_export_html(self):
|
||||
"""
|
||||
@@ -296,7 +295,7 @@ class ExportTestCase(CourseTestCase):
|
||||
Export failure.
|
||||
"""
|
||||
ItemFactory.create(parent_location=self.course.location, category='aawefawef')
|
||||
self._verify_export_failure('/course/MITx.999.Robot_Super_Course/branch/draft/block/Robot_Super_Course')
|
||||
self._verify_export_failure(u'/unit/location:MITx+999+Robot_Super_Course+course+Robot_Super_Course')
|
||||
|
||||
def test_export_failure_subsection_level(self):
|
||||
"""
|
||||
@@ -307,7 +306,8 @@ class ExportTestCase(CourseTestCase):
|
||||
parent_location=vertical.location,
|
||||
category='aawefawef'
|
||||
)
|
||||
self._verify_export_failure(u'/unit/MITx.999.Robot_Super_Course/branch/draft/block/foo')
|
||||
|
||||
self._verify_export_failure(u'/unit/location:MITx+999+Robot_Super_Course+vertical+foo')
|
||||
|
||||
def _verify_export_failure(self, expectedText):
|
||||
""" Export failure helper method. """
|
||||
|
||||
@@ -11,6 +11,8 @@ from webob import Response
|
||||
from django.http import Http404
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.core.urlresolvers import reverse
|
||||
from contentstore.utils import reverse_usage_url
|
||||
|
||||
from contentstore.views.component import component_handler
|
||||
|
||||
@@ -19,10 +21,9 @@ from contentstore.utils import compute_publish_state, PublishState
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.capa_module import CapaDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
from xmodule.modulestore.locator import BlockUsageLocator
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.keys import UsageKey
|
||||
from xmodule.modulestore.locations import Location
|
||||
|
||||
|
||||
class ItemTest(CourseTestCase):
|
||||
@@ -30,60 +31,54 @@ class ItemTest(CourseTestCase):
|
||||
def setUp(self):
|
||||
super(ItemTest, self).setUp()
|
||||
|
||||
self.course_locator = loc_mapper().translate_location(
|
||||
self.course.location.course_id, self.course.location, False, True
|
||||
)
|
||||
self.unicode_locator = unicode(self.course_locator)
|
||||
self.course_key = self.course.id
|
||||
self.usage_key = self.course.location
|
||||
|
||||
def get_old_id(self, locator):
|
||||
@staticmethod
|
||||
def get_item_from_modulestore(usage_key, draft=False):
|
||||
"""
|
||||
Converts new locator to old id format (forcing to non-draft).
|
||||
"""
|
||||
return loc_mapper().translate_locator_to_location(BlockUsageLocator(locator)).replace(revision=None)
|
||||
|
||||
def get_item_from_modulestore(self, locator, draft=False):
|
||||
"""
|
||||
Get the item referenced by the locator from the modulestore
|
||||
Get the item referenced by the UsageKey from the modulestore
|
||||
"""
|
||||
store = modulestore('draft') if draft else modulestore('direct')
|
||||
return store.get_item(self.get_old_id(locator))
|
||||
return store.get_item(usage_key)
|
||||
|
||||
def response_locator(self, response):
|
||||
def response_usage_key(self, response):
|
||||
"""
|
||||
Get the locator (unicode representation) from the response payload
|
||||
Get the UsageKey from the response payload and verify that the status_code was 200.
|
||||
:param response:
|
||||
"""
|
||||
parsed = json.loads(response.content)
|
||||
return parsed['locator']
|
||||
self.assertEqual(response.status_code, 200)
|
||||
return UsageKey.from_string(parsed['locator'])
|
||||
|
||||
def create_xblock(self, parent_locator=None, display_name=None, category=None, boilerplate=None):
|
||||
def create_xblock(self, parent_usage_key=None, display_name=None, category=None, boilerplate=None):
|
||||
data = {
|
||||
'parent_locator': self.unicode_locator if parent_locator is None else parent_locator,
|
||||
'parent_locator': unicode(self.usage_key) if parent_usage_key is None else unicode(parent_usage_key),
|
||||
'category': category
|
||||
}
|
||||
if display_name is not None:
|
||||
data['display_name'] = display_name
|
||||
if boilerplate is not None:
|
||||
data['boilerplate'] = boilerplate
|
||||
return self.client.ajax_post('/xblock', json.dumps(data))
|
||||
return self.client.ajax_post(reverse('contentstore.views.xblock_handler'), json.dumps(data))
|
||||
|
||||
|
||||
class GetItem(ItemTest):
|
||||
"""Tests for '/xblock' GET url."""
|
||||
|
||||
def _create_vertical(self, parent_locator=None):
|
||||
def _create_vertical(self, parent_usage_key=None):
|
||||
"""
|
||||
Creates a vertical, returning its locator.
|
||||
Creates a vertical, returning its UsageKey.
|
||||
"""
|
||||
resp = self.create_xblock(category='vertical', parent_locator=parent_locator)
|
||||
resp = self.create_xblock(category='vertical', parent_usage_key=parent_usage_key)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
return self.response_locator(resp)
|
||||
return self.response_usage_key(resp)
|
||||
|
||||
def _get_container_preview(self, locator):
|
||||
def _get_container_preview(self, usage_key):
|
||||
"""
|
||||
Returns the HTML and resources required for the xblock at the specified locator
|
||||
Returns the HTML and resources required for the xblock at the specified UsageKey
|
||||
"""
|
||||
preview_url = '/xblock/{locator}/container_preview'.format(locator=locator)
|
||||
preview_url = reverse_usage_url("xblock_view_handler", usage_key, {'view_name': 'container_preview'})
|
||||
resp = self.client.get(preview_url, HTTP_ACCEPT='application/json')
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
resp_content = json.loads(resp.content)
|
||||
@@ -96,16 +91,15 @@ class GetItem(ItemTest):
|
||||
def test_get_vertical(self):
|
||||
# Add a vertical
|
||||
resp = self.create_xblock(category='vertical')
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
usage_key = self.response_usage_key(resp)
|
||||
|
||||
# Retrieve it
|
||||
resp_content = json.loads(resp.content)
|
||||
resp = self.client.get('/xblock/' + resp_content['locator'])
|
||||
resp = self.client.get(reverse_usage_url('xblock_handler', usage_key))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_get_empty_container_fragment(self):
|
||||
root_locator = self._create_vertical()
|
||||
html, __ = self._get_container_preview(root_locator)
|
||||
root_usage_key = self._create_vertical()
|
||||
html, __ = self._get_container_preview(root_usage_key)
|
||||
|
||||
# Verify that the Studio wrapper is not added
|
||||
self.assertNotIn('wrapper-xblock', html)
|
||||
@@ -115,15 +109,15 @@ class GetItem(ItemTest):
|
||||
self.assertIn('<article class="xblock-render">', html)
|
||||
|
||||
def test_get_container_fragment(self):
|
||||
root_locator = self._create_vertical()
|
||||
root_usage_key = self._create_vertical()
|
||||
|
||||
# Add a problem beneath a child vertical
|
||||
child_vertical_locator = self._create_vertical(parent_locator=root_locator)
|
||||
resp = self.create_xblock(parent_locator=child_vertical_locator, category='problem', boilerplate='multiplechoice.yaml')
|
||||
child_vertical_usage_key = self._create_vertical(parent_usage_key=root_usage_key)
|
||||
resp = self.create_xblock(parent_usage_key=child_vertical_usage_key, category='problem', boilerplate='multiplechoice.yaml')
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Get the preview HTML
|
||||
html, __ = self._get_container_preview(root_locator)
|
||||
html, __ = self._get_container_preview(root_usage_key)
|
||||
|
||||
# Verify that the Studio nesting wrapper has been added
|
||||
self.assertIn('level-nesting', html)
|
||||
@@ -138,23 +132,23 @@ class GetItem(ItemTest):
|
||||
Test the case of the container page containing a link to another container page.
|
||||
"""
|
||||
# Add a wrapper with child beneath a child vertical
|
||||
root_locator = self._create_vertical()
|
||||
root_usage_key = self._create_vertical()
|
||||
|
||||
resp = self.create_xblock(parent_locator=root_locator, category="wrapper")
|
||||
resp = self.create_xblock(parent_usage_key=root_usage_key, category="wrapper")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
wrapper_locator = self.response_locator(resp)
|
||||
wrapper_usage_key = self.response_usage_key(resp)
|
||||
|
||||
resp = self.create_xblock(parent_locator=wrapper_locator, category='problem', boilerplate='multiplechoice.yaml')
|
||||
resp = self.create_xblock(parent_usage_key=wrapper_usage_key, category='problem', boilerplate='multiplechoice.yaml')
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Get the preview HTML and verify the View -> link is present.
|
||||
html, __ = self._get_container_preview(root_locator)
|
||||
html, __ = self._get_container_preview(root_usage_key)
|
||||
self.assertIn('wrapper-xblock', html)
|
||||
self.assertRegexpMatches(
|
||||
html,
|
||||
# The instance of the wrapper class will have an auto-generated ID (wrapperxxx). Allow anything
|
||||
# for the 3 characters after wrapper.
|
||||
(r'"/container/MITx.999.Robot_Super_Course/branch/draft/block/wrapper.{3}" class="action-button">\s*'
|
||||
# The instance of the wrapper class will have an auto-generated ID. Allow any
|
||||
# characters after wrapper.
|
||||
(r'"/container/location:MITx\+999\+Robot_Super_Course\+wrapper\+\w+" class="action-button">\s*'
|
||||
'<span class="action-button-text">View</span>')
|
||||
)
|
||||
|
||||
@@ -162,15 +156,14 @@ class GetItem(ItemTest):
|
||||
"""
|
||||
Test that a split_test module renders all of its children in Studio.
|
||||
"""
|
||||
root_locator = self._create_vertical()
|
||||
resp = self.create_xblock(category='split_test', parent_locator=root_locator)
|
||||
root_usage_key = self._create_vertical()
|
||||
resp = self.create_xblock(category='split_test', parent_usage_key=root_usage_key)
|
||||
split_test_usage_key = self.response_usage_key(resp)
|
||||
resp = self.create_xblock(parent_usage_key=split_test_usage_key, category='html', boilerplate='announcement.yaml')
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
split_test_locator = self.response_locator(resp)
|
||||
resp = self.create_xblock(parent_locator=split_test_locator, category='html', boilerplate='announcement.yaml')
|
||||
resp = self.create_xblock(parent_usage_key=split_test_usage_key, category='html', boilerplate='zooming_image.yaml')
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
resp = self.create_xblock(parent_locator=split_test_locator, category='html', boilerplate='zooming_image.yaml')
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
html, __ = self._get_container_preview(split_test_locator)
|
||||
html, __ = self._get_container_preview(split_test_usage_key)
|
||||
self.assertIn('Announcement', html)
|
||||
self.assertIn('Zooming', html)
|
||||
|
||||
@@ -180,11 +173,10 @@ class DeleteItem(ItemTest):
|
||||
def test_delete_static_page(self):
|
||||
# Add static tab
|
||||
resp = self.create_xblock(category='static_tab')
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
usage_key = self.response_usage_key(resp)
|
||||
|
||||
# Now delete it. There was a bug that the delete was failing (static tabs do not exist in draft modulestore).
|
||||
resp_content = json.loads(resp.content)
|
||||
resp = self.client.delete('/xblock/' + resp_content['locator'])
|
||||
resp = self.client.delete(reverse_usage_url('xblock_handler', usage_key))
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
|
||||
|
||||
@@ -199,36 +191,32 @@ class TestCreateItem(ItemTest):
|
||||
# create a chapter
|
||||
display_name = 'Nicely created'
|
||||
resp = self.create_xblock(display_name=display_name, category='chapter')
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# get the new item and check its category and display_name
|
||||
chap_locator = self.response_locator(resp)
|
||||
new_obj = self.get_item_from_modulestore(chap_locator)
|
||||
chap_usage_key = self.response_usage_key(resp)
|
||||
new_obj = self.get_item_from_modulestore(chap_usage_key)
|
||||
self.assertEqual(new_obj.scope_ids.block_type, 'chapter')
|
||||
self.assertEqual(new_obj.display_name, display_name)
|
||||
self.assertEqual(new_obj.location.org, self.course.location.org)
|
||||
self.assertEqual(new_obj.location.course, self.course.location.course)
|
||||
|
||||
# get the course and ensure it now points to this one
|
||||
course = self.get_item_from_modulestore(self.unicode_locator)
|
||||
self.assertIn(self.get_old_id(chap_locator).url(), course.children)
|
||||
course = self.get_item_from_modulestore(self.usage_key)
|
||||
self.assertIn(chap_usage_key, course.children)
|
||||
|
||||
# use default display name
|
||||
resp = self.create_xblock(parent_locator=chap_locator, category='vertical')
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
vert_locator = self.response_locator(resp)
|
||||
resp = self.create_xblock(parent_usage_key=chap_usage_key, category='vertical')
|
||||
vert_usage_key = self.response_usage_key(resp)
|
||||
|
||||
# create problem w/ boilerplate
|
||||
template_id = 'multiplechoice.yaml'
|
||||
resp = self.create_xblock(
|
||||
parent_locator=vert_locator,
|
||||
parent_usage_key=vert_usage_key,
|
||||
category='problem',
|
||||
boilerplate=template_id
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
prob_locator = self.response_locator(resp)
|
||||
problem = self.get_item_from_modulestore(prob_locator, True)
|
||||
prob_usage_key = self.response_usage_key(resp)
|
||||
problem = self.get_item_from_modulestore(prob_usage_key, True)
|
||||
# ensure it's draft
|
||||
self.assertTrue(problem.is_draft)
|
||||
# check against the template
|
||||
@@ -248,8 +236,8 @@ class TestCreateItem(ItemTest):
|
||||
def test_create_with_future_date(self):
|
||||
self.assertEqual(self.course.start, datetime(2030, 1, 1, tzinfo=UTC))
|
||||
resp = self.create_xblock(category='chapter')
|
||||
locator = self.response_locator(resp)
|
||||
obj = self.get_item_from_modulestore(locator)
|
||||
usage_key = self.response_usage_key(resp)
|
||||
obj = self.get_item_from_modulestore(usage_key)
|
||||
self.assertEqual(obj.start, datetime(2030, 1, 1, tzinfo=UTC))
|
||||
|
||||
|
||||
@@ -261,35 +249,35 @@ class TestDuplicateItem(ItemTest):
|
||||
""" Creates the test course structure and a few components to 'duplicate'. """
|
||||
super(TestDuplicateItem, self).setUp()
|
||||
# Create a parent chapter (for testing children of children).
|
||||
resp = self.create_xblock(parent_locator=self.unicode_locator, category='chapter')
|
||||
self.chapter_locator = self.response_locator(resp)
|
||||
resp = self.create_xblock(parent_usage_key=self.usage_key, category='chapter')
|
||||
self.chapter_usage_key = self.response_usage_key(resp)
|
||||
|
||||
# create a sequential containing a problem and an html component
|
||||
resp = self.create_xblock(parent_locator=self.chapter_locator, category='sequential')
|
||||
self.seq_locator = self.response_locator(resp)
|
||||
resp = self.create_xblock(parent_usage_key=self.chapter_usage_key, category='sequential')
|
||||
self.seq_usage_key = self.response_usage_key(resp)
|
||||
|
||||
# create problem and an html component
|
||||
resp = self.create_xblock(parent_locator=self.seq_locator, category='problem', boilerplate='multiplechoice.yaml')
|
||||
self.problem_locator = self.response_locator(resp)
|
||||
resp = self.create_xblock(parent_usage_key=self.seq_usage_key, category='problem', boilerplate='multiplechoice.yaml')
|
||||
self.problem_usage_key = self.response_usage_key(resp)
|
||||
|
||||
resp = self.create_xblock(parent_locator=self.seq_locator, category='html')
|
||||
self.html_locator = self.response_locator(resp)
|
||||
resp = self.create_xblock(parent_usage_key=self.seq_usage_key, category='html')
|
||||
self.html_usage_key = self.response_usage_key(resp)
|
||||
|
||||
# Create a second sequential just (testing children of children)
|
||||
self.create_xblock(parent_locator=self.chapter_locator, category='sequential2')
|
||||
self.create_xblock(parent_usage_key=self.chapter_usage_key, category='sequential2')
|
||||
|
||||
def test_duplicate_equality(self):
|
||||
"""
|
||||
Tests that a duplicated xblock is identical to the original,
|
||||
except for location and display name.
|
||||
"""
|
||||
def duplicate_and_verify(source_locator, parent_locator):
|
||||
locator = self._duplicate_item(parent_locator, source_locator)
|
||||
self.assertTrue(check_equality(source_locator, locator), "Duplicated item differs from original")
|
||||
def duplicate_and_verify(source_usage_key, parent_usage_key):
|
||||
usage_key = self._duplicate_item(parent_usage_key, source_usage_key)
|
||||
self.assertTrue(check_equality(source_usage_key, usage_key), "Duplicated item differs from original")
|
||||
|
||||
def check_equality(source_locator, duplicate_locator):
|
||||
original_item = self.get_item_from_modulestore(source_locator, draft=True)
|
||||
duplicated_item = self.get_item_from_modulestore(duplicate_locator, draft=True)
|
||||
def check_equality(source_usage_key, duplicate_usage_key):
|
||||
original_item = self.get_item_from_modulestore(source_usage_key, draft=True)
|
||||
duplicated_item = self.get_item_from_modulestore(duplicate_usage_key, draft=True)
|
||||
|
||||
self.assertNotEqual(
|
||||
original_item.location,
|
||||
@@ -309,22 +297,16 @@ class TestDuplicateItem(ItemTest):
|
||||
"Duplicated item differs in number of children"
|
||||
)
|
||||
for i in xrange(len(original_item.children)):
|
||||
source_locator = loc_mapper().translate_location(
|
||||
self.course.location.course_id, Location(original_item.children[i]), False, True
|
||||
)
|
||||
duplicate_locator = loc_mapper().translate_location(
|
||||
self.course.location.course_id, Location(duplicated_item.children[i]), False, True
|
||||
)
|
||||
if not check_equality(source_locator, duplicate_locator):
|
||||
if not check_equality(original_item.children[i], duplicated_item.children[i]):
|
||||
return False
|
||||
duplicated_item.children = original_item.children
|
||||
|
||||
return original_item == duplicated_item
|
||||
|
||||
duplicate_and_verify(self.problem_locator, self.seq_locator)
|
||||
duplicate_and_verify(self.html_locator, self.seq_locator)
|
||||
duplicate_and_verify(self.seq_locator, self.chapter_locator)
|
||||
duplicate_and_verify(self.chapter_locator, self.unicode_locator)
|
||||
duplicate_and_verify(self.problem_usage_key, self.seq_usage_key)
|
||||
duplicate_and_verify(self.html_usage_key, self.seq_usage_key)
|
||||
duplicate_and_verify(self.seq_usage_key, self.chapter_usage_key)
|
||||
duplicate_and_verify(self.chapter_usage_key, self.usage_key)
|
||||
|
||||
def test_ordering(self):
|
||||
"""
|
||||
@@ -332,74 +314,72 @@ class TestDuplicateItem(ItemTest):
|
||||
(if duplicate and source share the same parent), else at the
|
||||
end of the children of the parent.
|
||||
"""
|
||||
def verify_order(source_locator, parent_locator, source_position=None):
|
||||
locator = self._duplicate_item(parent_locator, source_locator)
|
||||
parent = self.get_item_from_modulestore(parent_locator)
|
||||
def verify_order(source_usage_key, parent_usage_key, source_position=None):
|
||||
usage_key = self._duplicate_item(parent_usage_key, source_usage_key)
|
||||
parent = self.get_item_from_modulestore(parent_usage_key)
|
||||
children = parent.children
|
||||
if source_position is None:
|
||||
self.assertFalse(source_locator in children, 'source item not expected in children array')
|
||||
self.assertFalse(source_usage_key in children, 'source item not expected in children array')
|
||||
self.assertEqual(
|
||||
children[len(children) - 1],
|
||||
self.get_old_id(locator).url(),
|
||||
usage_key,
|
||||
"duplicated item not at end"
|
||||
)
|
||||
else:
|
||||
self.assertEqual(
|
||||
children[source_position],
|
||||
self.get_old_id(source_locator).url(),
|
||||
source_usage_key,
|
||||
"source item at wrong position"
|
||||
)
|
||||
self.assertEqual(
|
||||
children[source_position + 1],
|
||||
self.get_old_id(locator).url(),
|
||||
usage_key,
|
||||
"duplicated item not ordered after source item"
|
||||
)
|
||||
|
||||
verify_order(self.problem_locator, self.seq_locator, 0)
|
||||
verify_order(self.problem_usage_key, self.seq_usage_key, 0)
|
||||
# 2 because duplicate of problem should be located before.
|
||||
verify_order(self.html_locator, self.seq_locator, 2)
|
||||
verify_order(self.seq_locator, self.chapter_locator, 0)
|
||||
verify_order(self.html_usage_key, self.seq_usage_key, 2)
|
||||
verify_order(self.seq_usage_key, self.chapter_usage_key, 0)
|
||||
|
||||
# Test duplicating something into a location that is not the parent of the original item.
|
||||
# Duplicated item should appear at the end.
|
||||
verify_order(self.html_locator, self.unicode_locator)
|
||||
verify_order(self.html_usage_key, self.usage_key)
|
||||
|
||||
def test_display_name(self):
|
||||
"""
|
||||
Tests the expected display name for the duplicated xblock.
|
||||
"""
|
||||
def verify_name(source_locator, parent_locator, expected_name, display_name=None):
|
||||
locator = self._duplicate_item(parent_locator, source_locator, display_name)
|
||||
duplicated_item = self.get_item_from_modulestore(locator, draft=True)
|
||||
def verify_name(source_usage_key, parent_usage_key, expected_name, display_name=None):
|
||||
usage_key = self._duplicate_item(parent_usage_key, source_usage_key, display_name)
|
||||
duplicated_item = self.get_item_from_modulestore(usage_key, draft=True)
|
||||
self.assertEqual(duplicated_item.display_name, expected_name)
|
||||
return locator
|
||||
return usage_key
|
||||
|
||||
# Display name comes from template.
|
||||
dupe_locator = verify_name(self.problem_locator, self.seq_locator, "Duplicate of 'Multiple Choice'")
|
||||
dupe_usage_key = verify_name(self.problem_usage_key, self.seq_usage_key, "Duplicate of 'Multiple Choice'")
|
||||
# Test dupe of dupe.
|
||||
verify_name(dupe_locator, self.seq_locator, "Duplicate of 'Duplicate of 'Multiple Choice''")
|
||||
verify_name(dupe_usage_key, self.seq_usage_key, "Duplicate of 'Duplicate of 'Multiple Choice''")
|
||||
|
||||
# Uses default display_name of 'Text' from HTML component.
|
||||
verify_name(self.html_locator, self.seq_locator, "Duplicate of 'Text'")
|
||||
verify_name(self.html_usage_key, self.seq_usage_key, "Duplicate of 'Text'")
|
||||
|
||||
# The sequence does not have a display_name set, so category is shown.
|
||||
verify_name(self.seq_locator, self.chapter_locator, "Duplicate of sequential")
|
||||
verify_name(self.seq_usage_key, self.chapter_usage_key, "Duplicate of sequential")
|
||||
|
||||
# Now send a custom display name for the duplicate.
|
||||
verify_name(self.seq_locator, self.chapter_locator, "customized name", display_name="customized name")
|
||||
verify_name(self.seq_usage_key, self.chapter_usage_key, "customized name", display_name="customized name")
|
||||
|
||||
def _duplicate_item(self, parent_locator, source_locator, display_name=None):
|
||||
def _duplicate_item(self, parent_usage_key, source_usage_key, display_name=None):
|
||||
data = {
|
||||
'parent_locator': parent_locator,
|
||||
'duplicate_source_locator': source_locator
|
||||
'parent_locator': unicode(parent_usage_key),
|
||||
'duplicate_source_locator': unicode(source_usage_key)
|
||||
}
|
||||
if display_name is not None:
|
||||
data['display_name'] = display_name
|
||||
|
||||
resp = self.client.ajax_post('/xblock', json.dumps(data))
|
||||
resp_content = json.loads(resp.content)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
return resp_content['locator']
|
||||
resp = self.client.ajax_post(reverse('contentstore.views.xblock_handler'), json.dumps(data))
|
||||
return self.response_usage_key(resp)
|
||||
|
||||
|
||||
class TestEditItem(ItemTest):
|
||||
@@ -412,18 +392,18 @@ class TestEditItem(ItemTest):
|
||||
# create a chapter
|
||||
display_name = 'chapter created'
|
||||
resp = self.create_xblock(display_name=display_name, category='chapter')
|
||||
chap_locator = self.response_locator(resp)
|
||||
resp = self.create_xblock(parent_locator=chap_locator, category='sequential')
|
||||
self.seq_locator = self.response_locator(resp)
|
||||
self.seq_update_url = '/xblock/' + self.seq_locator
|
||||
chap_usage_key = self.response_usage_key(resp)
|
||||
resp = self.create_xblock(parent_usage_key=chap_usage_key, category='sequential')
|
||||
self.seq_usage_key = self.response_usage_key(resp)
|
||||
self.seq_update_url = reverse_usage_url("xblock_handler", self.seq_usage_key)
|
||||
|
||||
# create problem w/ boilerplate
|
||||
template_id = 'multiplechoice.yaml'
|
||||
resp = self.create_xblock(parent_locator=self.seq_locator, category='problem', boilerplate=template_id)
|
||||
self.problem_locator = self.response_locator(resp)
|
||||
self.problem_update_url = '/xblock/' + self.problem_locator
|
||||
resp = self.create_xblock(parent_usage_key=self.seq_usage_key, category='problem', boilerplate=template_id)
|
||||
self.problem_usage_key = self.response_usage_key(resp)
|
||||
self.problem_update_url = reverse_usage_url("xblock_handler", self.problem_usage_key)
|
||||
|
||||
self.course_update_url = '/xblock/' + self.unicode_locator
|
||||
self.course_update_url = reverse_usage_url("xblock_handler", self.usage_key)
|
||||
|
||||
def test_delete_field(self):
|
||||
"""
|
||||
@@ -433,45 +413,45 @@ class TestEditItem(ItemTest):
|
||||
self.problem_update_url,
|
||||
data={'metadata': {'rerandomize': 'onreset'}}
|
||||
)
|
||||
problem = self.get_item_from_modulestore(self.problem_locator, True)
|
||||
problem = self.get_item_from_modulestore(self.problem_usage_key, True)
|
||||
self.assertEqual(problem.rerandomize, 'onreset')
|
||||
self.client.ajax_post(
|
||||
self.problem_update_url,
|
||||
data={'metadata': {'rerandomize': None}}
|
||||
)
|
||||
problem = self.get_item_from_modulestore(self.problem_locator, True)
|
||||
problem = self.get_item_from_modulestore(self.problem_usage_key, True)
|
||||
self.assertEqual(problem.rerandomize, 'never')
|
||||
|
||||
def test_null_field(self):
|
||||
"""
|
||||
Sending null in for a field 'deletes' it
|
||||
"""
|
||||
problem = self.get_item_from_modulestore(self.problem_locator, True)
|
||||
problem = self.get_item_from_modulestore(self.problem_usage_key, True)
|
||||
self.assertIsNotNone(problem.markdown)
|
||||
self.client.ajax_post(
|
||||
self.problem_update_url,
|
||||
data={'nullout': ['markdown']}
|
||||
)
|
||||
problem = self.get_item_from_modulestore(self.problem_locator, True)
|
||||
problem = self.get_item_from_modulestore(self.problem_usage_key, True)
|
||||
self.assertIsNone(problem.markdown)
|
||||
|
||||
def test_date_fields(self):
|
||||
"""
|
||||
Test setting due & start dates on sequential
|
||||
"""
|
||||
sequential = self.get_item_from_modulestore(self.seq_locator)
|
||||
sequential = self.get_item_from_modulestore(self.seq_usage_key)
|
||||
self.assertIsNone(sequential.due)
|
||||
self.client.ajax_post(
|
||||
self.seq_update_url,
|
||||
data={'metadata': {'due': '2010-11-22T04:00Z'}}
|
||||
)
|
||||
sequential = self.get_item_from_modulestore(self.seq_locator)
|
||||
sequential = self.get_item_from_modulestore(self.seq_usage_key)
|
||||
self.assertEqual(sequential.due, datetime(2010, 11, 22, 4, 0, tzinfo=UTC))
|
||||
self.client.ajax_post(
|
||||
self.seq_update_url,
|
||||
data={'metadata': {'start': '2010-09-12T14:00Z'}}
|
||||
)
|
||||
sequential = self.get_item_from_modulestore(self.seq_locator)
|
||||
sequential = self.get_item_from_modulestore(self.seq_usage_key)
|
||||
self.assertEqual(sequential.due, datetime(2010, 11, 22, 4, 0, tzinfo=UTC))
|
||||
self.assertEqual(sequential.start, datetime(2010, 9, 12, 14, 0, tzinfo=UTC))
|
||||
|
||||
@@ -482,24 +462,24 @@ class TestEditItem(ItemTest):
|
||||
# Create 2 children of main course.
|
||||
resp_1 = self.create_xblock(display_name='child 1', category='chapter')
|
||||
resp_2 = self.create_xblock(display_name='child 2', category='chapter')
|
||||
chapter1_locator = self.response_locator(resp_1)
|
||||
chapter2_locator = self.response_locator(resp_2)
|
||||
chapter1_usage_key = self.response_usage_key(resp_1)
|
||||
chapter2_usage_key = self.response_usage_key(resp_2)
|
||||
|
||||
course = self.get_item_from_modulestore(self.unicode_locator)
|
||||
self.assertIn(self.get_old_id(chapter1_locator).url(), course.children)
|
||||
self.assertIn(self.get_old_id(chapter2_locator).url(), course.children)
|
||||
course = self.get_item_from_modulestore(self.usage_key)
|
||||
self.assertIn(chapter1_usage_key, course.children)
|
||||
self.assertIn(chapter2_usage_key, course.children)
|
||||
|
||||
# Remove one child from the course.
|
||||
resp = self.client.ajax_post(
|
||||
self.course_update_url,
|
||||
data={'children': [chapter2_locator]}
|
||||
data={'children': [unicode(chapter2_usage_key)]}
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Verify that the child is removed.
|
||||
course = self.get_item_from_modulestore(self.unicode_locator)
|
||||
self.assertNotIn(self.get_old_id(chapter1_locator).url(), course.children)
|
||||
self.assertIn(self.get_old_id(chapter2_locator).url(), course.children)
|
||||
course = self.get_item_from_modulestore(self.usage_key)
|
||||
self.assertNotIn(chapter1_usage_key, course.children)
|
||||
self.assertIn(chapter2_usage_key, course.children)
|
||||
|
||||
def test_reorder_children(self):
|
||||
"""
|
||||
@@ -507,39 +487,39 @@ class TestEditItem(ItemTest):
|
||||
"""
|
||||
# Create 2 child units and re-order them. There was a bug about @draft getting added
|
||||
# to the IDs.
|
||||
unit_1_resp = self.create_xblock(parent_locator=self.seq_locator, category='vertical')
|
||||
unit_2_resp = self.create_xblock(parent_locator=self.seq_locator, category='vertical')
|
||||
unit1_locator = self.response_locator(unit_1_resp)
|
||||
unit2_locator = self.response_locator(unit_2_resp)
|
||||
unit_1_resp = self.create_xblock(parent_usage_key=self.seq_usage_key, category='vertical')
|
||||
unit_2_resp = self.create_xblock(parent_usage_key=self.seq_usage_key, category='vertical')
|
||||
unit1_usage_key = self.response_usage_key(unit_1_resp)
|
||||
unit2_usage_key = self.response_usage_key(unit_2_resp)
|
||||
|
||||
# The sequential already has a child defined in the setUp (a problem).
|
||||
# Children must be on the sequential to reproduce the original bug,
|
||||
# as it is important that the parent (sequential) NOT be in the draft store.
|
||||
children = self.get_item_from_modulestore(self.seq_locator).children
|
||||
self.assertEqual(self.get_old_id(unit1_locator).url(), children[1])
|
||||
self.assertEqual(self.get_old_id(unit2_locator).url(), children[2])
|
||||
children = self.get_item_from_modulestore(self.seq_usage_key).children
|
||||
self.assertEqual(unit1_usage_key, children[1])
|
||||
self.assertEqual(unit2_usage_key, children[2])
|
||||
|
||||
resp = self.client.ajax_post(
|
||||
self.seq_update_url,
|
||||
data={'children': [self.problem_locator, unit2_locator, unit1_locator]}
|
||||
data={'children': [unicode(self.problem_usage_key), unicode(unit2_usage_key), unicode(unit1_usage_key)]}
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
children = self.get_item_from_modulestore(self.seq_locator).children
|
||||
self.assertEqual(self.get_old_id(self.problem_locator).url(), children[0])
|
||||
self.assertEqual(self.get_old_id(unit1_locator).url(), children[2])
|
||||
self.assertEqual(self.get_old_id(unit2_locator).url(), children[1])
|
||||
children = self.get_item_from_modulestore(self.seq_usage_key).children
|
||||
self.assertEqual(self.problem_usage_key, children[0])
|
||||
self.assertEqual(unit1_usage_key, children[2])
|
||||
self.assertEqual(unit2_usage_key, children[1])
|
||||
|
||||
def test_make_public(self):
|
||||
""" Test making a private problem public (publishing it). """
|
||||
# When the problem is first created, it is only in draft (because of its category).
|
||||
with self.assertRaises(ItemNotFoundError):
|
||||
self.get_item_from_modulestore(self.problem_locator, False)
|
||||
self.get_item_from_modulestore(self.problem_usage_key, False)
|
||||
self.client.ajax_post(
|
||||
self.problem_update_url,
|
||||
data={'publish': 'make_public'}
|
||||
)
|
||||
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_locator, False))
|
||||
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_usage_key, False))
|
||||
|
||||
def test_make_private(self):
|
||||
""" Test making a public problem private (un-publishing it). """
|
||||
@@ -548,14 +528,14 @@ class TestEditItem(ItemTest):
|
||||
self.problem_update_url,
|
||||
data={'publish': 'make_public'}
|
||||
)
|
||||
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_locator, False))
|
||||
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_usage_key, False))
|
||||
# Now make it private
|
||||
self.client.ajax_post(
|
||||
self.problem_update_url,
|
||||
data={'publish': 'make_private'}
|
||||
)
|
||||
with self.assertRaises(ItemNotFoundError):
|
||||
self.get_item_from_modulestore(self.problem_locator, False)
|
||||
self.get_item_from_modulestore(self.problem_usage_key, False)
|
||||
|
||||
def test_make_draft(self):
|
||||
""" Test creating a draft version of a public problem. """
|
||||
@@ -564,7 +544,7 @@ class TestEditItem(ItemTest):
|
||||
self.problem_update_url,
|
||||
data={'publish': 'make_public'}
|
||||
)
|
||||
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_locator, False))
|
||||
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_usage_key, False))
|
||||
# Now make it draft, which means both versions will exist.
|
||||
self.client.ajax_post(
|
||||
self.problem_update_url,
|
||||
@@ -575,9 +555,9 @@ class TestEditItem(ItemTest):
|
||||
self.problem_update_url,
|
||||
data={'metadata': {'due': '2077-10-10T04:00Z'}}
|
||||
)
|
||||
published = self.get_item_from_modulestore(self.problem_locator, False)
|
||||
published = self.get_item_from_modulestore(self.problem_usage_key, False)
|
||||
self.assertIsNone(published.due)
|
||||
draft = self.get_item_from_modulestore(self.problem_locator, True)
|
||||
draft = self.get_item_from_modulestore(self.problem_usage_key, True)
|
||||
self.assertEqual(draft.due, datetime(2077, 10, 10, 4, 0, tzinfo=UTC))
|
||||
|
||||
def test_make_public_with_update(self):
|
||||
@@ -589,7 +569,7 @@ class TestEditItem(ItemTest):
|
||||
'publish': 'make_public'
|
||||
}
|
||||
)
|
||||
published = self.get_item_from_modulestore(self.problem_locator, False)
|
||||
published = self.get_item_from_modulestore(self.problem_usage_key, False)
|
||||
self.assertEqual(published.due, datetime(2077, 10, 10, 4, 0, tzinfo=UTC))
|
||||
|
||||
def test_make_private_with_update(self):
|
||||
@@ -607,8 +587,8 @@ class TestEditItem(ItemTest):
|
||||
}
|
||||
)
|
||||
with self.assertRaises(ItemNotFoundError):
|
||||
self.get_item_from_modulestore(self.problem_locator, False)
|
||||
draft = self.get_item_from_modulestore(self.problem_locator, True)
|
||||
self.get_item_from_modulestore(self.problem_usage_key, False)
|
||||
draft = self.get_item_from_modulestore(self.problem_usage_key, True)
|
||||
self.assertEqual(draft.due, datetime(2077, 10, 10, 4, 0, tzinfo=UTC))
|
||||
|
||||
def test_create_draft_with_update(self):
|
||||
@@ -618,7 +598,7 @@ class TestEditItem(ItemTest):
|
||||
self.problem_update_url,
|
||||
data={'publish': 'make_public'}
|
||||
)
|
||||
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_locator, False))
|
||||
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_usage_key, False))
|
||||
# Now make it draft, which means both versions will exist.
|
||||
self.client.ajax_post(
|
||||
self.problem_update_url,
|
||||
@@ -627,9 +607,9 @@ class TestEditItem(ItemTest):
|
||||
'publish': 'create_draft'
|
||||
}
|
||||
)
|
||||
published = self.get_item_from_modulestore(self.problem_locator, False)
|
||||
published = self.get_item_from_modulestore(self.problem_usage_key, False)
|
||||
self.assertIsNone(published.due)
|
||||
draft = self.get_item_from_modulestore(self.problem_locator, True)
|
||||
draft = self.get_item_from_modulestore(self.problem_usage_key, True)
|
||||
self.assertEqual(draft.due, datetime(2077, 10, 10, 4, 0, tzinfo=UTC))
|
||||
|
||||
def test_create_draft_with_multiple_requests(self):
|
||||
@@ -641,7 +621,7 @@ class TestEditItem(ItemTest):
|
||||
self.problem_update_url,
|
||||
data={'publish': 'make_public'}
|
||||
)
|
||||
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_locator, False))
|
||||
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_usage_key, False))
|
||||
# Now make it draft, which means both versions will exist.
|
||||
self.client.ajax_post(
|
||||
self.problem_update_url,
|
||||
@@ -649,8 +629,8 @@ class TestEditItem(ItemTest):
|
||||
'publish': 'create_draft'
|
||||
}
|
||||
)
|
||||
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_locator, False))
|
||||
draft_1 = self.get_item_from_modulestore(self.problem_locator, True)
|
||||
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_usage_key, False))
|
||||
draft_1 = self.get_item_from_modulestore(self.problem_usage_key, True)
|
||||
self.assertIsNotNone(draft_1)
|
||||
|
||||
# Now check that when a user sends request to create a draft when there is already a draft version then
|
||||
@@ -661,7 +641,7 @@ class TestEditItem(ItemTest):
|
||||
'publish': 'create_draft'
|
||||
}
|
||||
)
|
||||
draft_2 = self.get_item_from_modulestore(self.problem_locator, True)
|
||||
draft_2 = self.get_item_from_modulestore(self.problem_usage_key, True)
|
||||
self.assertIsNotNone(draft_2)
|
||||
self.assertEqual(draft_1, draft_2)
|
||||
|
||||
@@ -675,7 +655,7 @@ class TestEditItem(ItemTest):
|
||||
self.problem_update_url,
|
||||
data={'publish': 'make_public'}
|
||||
)
|
||||
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_locator, False))
|
||||
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_usage_key, False))
|
||||
|
||||
# Now make it private, and check that its published version not exists
|
||||
resp = self.client.ajax_post(
|
||||
@@ -686,8 +666,8 @@ class TestEditItem(ItemTest):
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
with self.assertRaises(ItemNotFoundError):
|
||||
self.get_item_from_modulestore(self.problem_locator, False)
|
||||
draft_1 = self.get_item_from_modulestore(self.problem_locator, True)
|
||||
self.get_item_from_modulestore(self.problem_usage_key, False)
|
||||
draft_1 = self.get_item_from_modulestore(self.problem_usage_key, True)
|
||||
self.assertIsNotNone(draft_1)
|
||||
|
||||
# Now check that when a user sends request to make it private when it already is private then
|
||||
@@ -700,8 +680,8 @@ class TestEditItem(ItemTest):
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
with self.assertRaises(ItemNotFoundError):
|
||||
self.get_item_from_modulestore(self.problem_locator, False)
|
||||
draft_2 = self.get_item_from_modulestore(self.problem_locator, True)
|
||||
self.get_item_from_modulestore(self.problem_usage_key, False)
|
||||
draft_2 = self.get_item_from_modulestore(self.problem_usage_key, True)
|
||||
self.assertIsNotNone(draft_2)
|
||||
self.assertEqual(draft_1, draft_2)
|
||||
|
||||
@@ -714,13 +694,13 @@ class TestEditItem(ItemTest):
|
||||
self.problem_update_url,
|
||||
data={'publish': 'make_public'}
|
||||
)
|
||||
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_locator, False))
|
||||
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_usage_key, False))
|
||||
|
||||
# Now make a draft
|
||||
resp = self.client.ajax_post(
|
||||
self.problem_update_url,
|
||||
data={
|
||||
'id': self.problem_locator,
|
||||
'id': unicode(self.problem_usage_key),
|
||||
'metadata': {},
|
||||
'data': "<p>Problem content draft.</p>",
|
||||
'publish': 'create_draft'
|
||||
@@ -728,39 +708,39 @@ class TestEditItem(ItemTest):
|
||||
)
|
||||
|
||||
# Both published and draft content should be different
|
||||
published = self.get_item_from_modulestore(self.problem_locator, False)
|
||||
draft = self.get_item_from_modulestore(self.problem_locator, True)
|
||||
published = self.get_item_from_modulestore(self.problem_usage_key, False)
|
||||
draft = self.get_item_from_modulestore(self.problem_usage_key, True)
|
||||
self.assertNotEqual(draft.data, published.data)
|
||||
|
||||
# Get problem by 'xblock_handler'
|
||||
view_url = '/xblock/{locator}/student_view'.format(locator=self.problem_locator)
|
||||
view_url = reverse_usage_url("xblock_view_handler", self.problem_usage_key, {"view_name": "student_view"})
|
||||
resp = self.client.get(view_url, HTTP_ACCEPT='application/json')
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Activate the editing view
|
||||
view_url = '/xblock/{locator}/studio_view'.format(locator=self.problem_locator)
|
||||
view_url = reverse_usage_url("xblock_view_handler", self.problem_usage_key, {"view_name": "studio_view"})
|
||||
resp = self.client.get(view_url, HTTP_ACCEPT='application/json')
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Both published and draft content should still be different
|
||||
published = self.get_item_from_modulestore(self.problem_locator, False)
|
||||
draft = self.get_item_from_modulestore(self.problem_locator, True)
|
||||
published = self.get_item_from_modulestore(self.problem_usage_key, False)
|
||||
draft = self.get_item_from_modulestore(self.problem_usage_key, True)
|
||||
self.assertNotEqual(draft.data, published.data)
|
||||
|
||||
def test_publish_states_of_nested_xblocks(self):
|
||||
""" Test publishing of a unit page containing a nested xblock """
|
||||
|
||||
resp = self.create_xblock(parent_locator=self.seq_locator, display_name='Test Unit', category='vertical')
|
||||
unit_locator = self.response_locator(resp)
|
||||
resp = self.create_xblock(parent_locator=unit_locator, category='wrapper')
|
||||
wrapper_locator = self.response_locator(resp)
|
||||
resp = self.create_xblock(parent_locator=wrapper_locator, category='html')
|
||||
html_locator = self.response_locator(resp)
|
||||
resp = self.create_xblock(parent_usage_key=self.seq_usage_key, display_name='Test Unit', category='vertical')
|
||||
unit_usage_key = self.response_usage_key(resp)
|
||||
resp = self.create_xblock(parent_usage_key=unit_usage_key, category='wrapper')
|
||||
wrapper_usage_key = self.response_usage_key(resp)
|
||||
resp = self.create_xblock(parent_usage_key=wrapper_usage_key, category='html')
|
||||
html_usage_key = self.response_usage_key(resp)
|
||||
|
||||
# The unit and its children should be private initially
|
||||
unit_update_url = '/xblock/' + unit_locator
|
||||
unit = self.get_item_from_modulestore(unit_locator, True)
|
||||
html = self.get_item_from_modulestore(html_locator, True)
|
||||
unit_update_url = reverse_usage_url('xblock_handler', unit_usage_key)
|
||||
unit = self.get_item_from_modulestore(unit_usage_key, True)
|
||||
html = self.get_item_from_modulestore(html_usage_key, True)
|
||||
self.assertEqual(compute_publish_state(unit), PublishState.private)
|
||||
self.assertEqual(compute_publish_state(html), PublishState.private)
|
||||
|
||||
@@ -770,8 +750,8 @@ class TestEditItem(ItemTest):
|
||||
data={'publish': 'make_public'}
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
unit = self.get_item_from_modulestore(unit_locator, True)
|
||||
html = self.get_item_from_modulestore(html_locator, True)
|
||||
unit = self.get_item_from_modulestore(unit_usage_key, True)
|
||||
html = self.get_item_from_modulestore(html_usage_key, True)
|
||||
self.assertEqual(compute_publish_state(unit), PublishState.public)
|
||||
self.assertEqual(compute_publish_state(html), PublishState.public)
|
||||
|
||||
@@ -779,14 +759,14 @@ class TestEditItem(ItemTest):
|
||||
resp = self.client.ajax_post(
|
||||
unit_update_url,
|
||||
data={
|
||||
'id': unit_locator,
|
||||
'id': unicode(unit_usage_key),
|
||||
'metadata': {},
|
||||
'publish': 'create_draft'
|
||||
}
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
unit = self.get_item_from_modulestore(unit_locator, True)
|
||||
html = self.get_item_from_modulestore(html_locator, True)
|
||||
unit = self.get_item_from_modulestore(unit_usage_key, True)
|
||||
html = self.get_item_from_modulestore(html_usage_key, True)
|
||||
self.assertEqual(compute_publish_state(unit), PublishState.draft)
|
||||
self.assertEqual(compute_publish_state(html), PublishState.draft)
|
||||
|
||||
@@ -802,7 +782,9 @@ class TestComponentHandler(TestCase):
|
||||
|
||||
self.descriptor = self.get_modulestore.return_value.get_item.return_value
|
||||
|
||||
self.usage_id = 'dummy_usage_id'
|
||||
self.usage_key_string = unicode(
|
||||
Location('dummy_org', 'dummy_course', 'dummy_run', 'dummy_category', 'dummy_name')
|
||||
)
|
||||
|
||||
self.user = UserFactory()
|
||||
|
||||
@@ -813,7 +795,7 @@ class TestComponentHandler(TestCase):
|
||||
self.descriptor.handle.side_effect = Http404
|
||||
|
||||
with self.assertRaises(Http404):
|
||||
component_handler(self.request, self.usage_id, 'invalid_handler')
|
||||
component_handler(self.request, self.usage_key_string, 'invalid_handler')
|
||||
|
||||
@ddt.data('GET', 'POST', 'PUT', 'DELETE')
|
||||
def test_request_method(self, method):
|
||||
@@ -829,7 +811,7 @@ class TestComponentHandler(TestCase):
|
||||
request = req_factory_method('/dummy-url')
|
||||
request.user = self.user
|
||||
|
||||
component_handler(request, self.usage_id, 'dummy_handler')
|
||||
component_handler(request, self.usage_key_string, 'dummy_handler')
|
||||
|
||||
@ddt.data(200, 404, 500)
|
||||
def test_response_code(self, status_code):
|
||||
@@ -838,4 +820,4 @@ class TestComponentHandler(TestCase):
|
||||
|
||||
self.descriptor.handle = create_response
|
||||
|
||||
self.assertEquals(component_handler(self.request, self.usage_id, 'dummy_handler').status_code, status_code)
|
||||
self.assertEquals(component_handler(self.request, self.usage_key_string, 'dummy_handler').status_code, status_code)
|
||||
|
||||
@@ -7,22 +7,24 @@ from django.test.client import RequestFactory
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
|
||||
from contentstore.views.preview import get_preview_fragment
|
||||
|
||||
|
||||
class GetPreviewHtmlTestCase(TestCase):
|
||||
"""
|
||||
Tests for get_preview_html.
|
||||
Tests for get_preview_fragment.
|
||||
|
||||
Note that there are other existing test cases in test_contentstore that indirectly execute
|
||||
get_preview_html via the xblock RESTful API.
|
||||
get_preview_fragment via the xblock RESTful API.
|
||||
"""
|
||||
|
||||
def test_preview_handler_locator(self):
|
||||
def test_preview_fragment(self):
|
||||
"""
|
||||
Test for calling get_preview_html when descriptor.location is a Locator.
|
||||
Test for calling get_preview_html.
|
||||
|
||||
This test used to be specifically about Locators (ensuring that they did not
|
||||
get translated to Locations). The test now has questionable value.
|
||||
"""
|
||||
course = CourseFactory.create()
|
||||
html = ItemFactory.create(
|
||||
@@ -31,25 +33,16 @@ class GetPreviewHtmlTestCase(TestCase):
|
||||
data={'data': "<html>foobar</html>"}
|
||||
)
|
||||
|
||||
locator = loc_mapper().translate_location(
|
||||
course.location.course_id, html.location, True, True
|
||||
)
|
||||
|
||||
# Change the stored location to a locator.
|
||||
html.location = locator
|
||||
html.save()
|
||||
|
||||
request = RequestFactory().get('/dummy-url')
|
||||
request.user = UserFactory()
|
||||
request.session = {}
|
||||
|
||||
# Must call get_preview_fragment directly, as going through xblock RESTful API will attempt
|
||||
# to use item.location as a Location.
|
||||
# Call get_preview_fragment directly.
|
||||
html = get_preview_fragment(request, html, {}).content
|
||||
# Verify student view html is returned, and there are no old locations in it.
|
||||
|
||||
# Verify student view html is returned, and the usage ID is as expected.
|
||||
self.assertRegexpMatches(
|
||||
html,
|
||||
'data-usage-id="MITx.999.Robot_Super_Course;_branch;_published;_block;_html_[0-9]*"'
|
||||
'data-usage-id="location:MITx\+999\+Robot_Super_Course\+html\+html_[0-9]*"'
|
||||
)
|
||||
self.assertRegexpMatches(html, '<html>foobar</html>')
|
||||
self.assertNotRegexpMatches(html, 'i4x')
|
||||
|
||||
@@ -6,8 +6,9 @@ from contentstore.tests.utils import CourseTestCase
|
||||
from django.test import TestCase
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from courseware.courses import get_course_by_id
|
||||
from xmodule.tabs import CourseTabList, WikiTab
|
||||
from contentstore.utils import reverse_course_url
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
class TabsPageTests(CourseTestCase):
|
||||
@@ -20,11 +21,11 @@ class TabsPageTests(CourseTestCase):
|
||||
super(TabsPageTests, self).setUp()
|
||||
|
||||
# Set the URL for tests
|
||||
self.url = self.course_locator.url_reverse('tabs')
|
||||
self.url = reverse_course_url('tabs_handler', self.course.id)
|
||||
|
||||
# add a static tab to the course, for code coverage
|
||||
self.test_tab = ItemFactory.create(
|
||||
parent_location=self.course_location,
|
||||
parent_location=self.course.location,
|
||||
category="static_tab",
|
||||
display_name="Static_1"
|
||||
)
|
||||
@@ -177,8 +178,7 @@ class TabsPageTests(CourseTestCase):
|
||||
"""
|
||||
Verify that the static tab renders itself with the correct HTML
|
||||
"""
|
||||
locator = loc_mapper().translate_location(self.course.id, self.test_tab.location)
|
||||
preview_url = '/xblock/{locator}/student_view'.format(locator=locator)
|
||||
preview_url = '/xblock/{}/student_view'.format(self.test_tab.location)
|
||||
|
||||
resp = self.client.get(preview_url, HTTP_ACCEPT='application/json')
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -224,5 +224,5 @@ class PrimitiveTabEdit(TestCase):
|
||||
"""Test course saving."""
|
||||
course = CourseFactory.create(org='edX', course='999')
|
||||
tabs.primitive_insert(course, 3, 'notes', 'aname')
|
||||
course2 = get_course_by_id(course.id)
|
||||
course2 = modulestore().get_course(course.id)
|
||||
self.assertEquals(course2.tabs[3], {'type': 'notes', 'name': 'aname'})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import json
|
||||
from unittest import TestCase
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from contentstore.utils import get_modulestore
|
||||
from contentstore.utils import reverse_course_url
|
||||
|
||||
from contentstore.views.course import (
|
||||
validate_textbooks_json, validate_textbook_json, TextbookValidationError)
|
||||
@@ -12,7 +12,7 @@ class TextbookIndexTestCase(CourseTestCase):
|
||||
def setUp(self):
|
||||
"Set the URL for tests"
|
||||
super(TextbookIndexTestCase, self).setUp()
|
||||
self.url = self.course_locator.url_reverse('textbooks')
|
||||
self.url = reverse_course_url('textbooks_list_handler', self.course.id)
|
||||
|
||||
def test_view_index(self):
|
||||
"Basic check that the textbook index page responds correctly"
|
||||
@@ -110,7 +110,8 @@ class TextbookCreateTestCase(CourseTestCase):
|
||||
def setUp(self):
|
||||
"Set up a url and some textbook content for tests"
|
||||
super(TextbookCreateTestCase, self).setUp()
|
||||
self.url = self.course_locator.url_reverse('textbooks')
|
||||
self.url = reverse_course_url('textbooks_list_handler', self.course.id)
|
||||
|
||||
self.textbook = {
|
||||
"tab_title": "Economics",
|
||||
"chapters": {
|
||||
@@ -177,7 +178,8 @@ class TextbookDetailTestCase(CourseTestCase):
|
||||
"url": "/a/b/c/ch1.pdf",
|
||||
}
|
||||
}
|
||||
self.url1 = self.course_locator.url_reverse("textbooks", "1")
|
||||
self.url1 = self.get_details_url("1")
|
||||
|
||||
self.textbook2 = {
|
||||
"tab_title": "Algebra",
|
||||
"id": 2,
|
||||
@@ -186,12 +188,22 @@ class TextbookDetailTestCase(CourseTestCase):
|
||||
"url": "/a/b/ch11.pdf",
|
||||
}
|
||||
}
|
||||
self.url2 = self.course_locator.url_reverse("textbooks", "2")
|
||||
self.url2 = self.get_details_url("2")
|
||||
self.course.pdf_textbooks = [self.textbook1, self.textbook2]
|
||||
# Save the data that we've just changed to the underlying
|
||||
# MongoKeyValueStore before we update the mongo datastore.
|
||||
self.save_course()
|
||||
self.url_nonexist = self.course_locator.url_reverse("textbooks", "20")
|
||||
self.url_nonexist = self.get_details_url("1=20")
|
||||
|
||||
def get_details_url(self, textbook_id):
|
||||
"""
|
||||
Returns the URL for textbook detail handler.
|
||||
"""
|
||||
return reverse_course_url(
|
||||
'textbooks_detail_handler',
|
||||
self.course.id,
|
||||
kwargs={'textbook_id': textbook_id}
|
||||
)
|
||||
|
||||
def test_get_1(self):
|
||||
"Get the first textbook"
|
||||
@@ -233,7 +245,7 @@ class TextbookDetailTestCase(CourseTestCase):
|
||||
"url": "supercool.pdf",
|
||||
"id": "1supercool",
|
||||
}
|
||||
url = self.course_locator.url_reverse("textbooks", "1supercool")
|
||||
url = self.get_details_url("1supercool")
|
||||
resp = self.client.post(
|
||||
url,
|
||||
data=json.dumps(textbook),
|
||||
|
||||
@@ -12,15 +12,14 @@ from django.core.urlresolvers import reverse
|
||||
from django.test.utils import override_settings
|
||||
from django.conf import settings
|
||||
|
||||
from xmodule.video_module import transcripts_utils
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from cache_toolbox.core import del_cached_content
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.django import contentstore, _CONTENTSTORE
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
from xmodule.modulestore.locator import BlockUsageLocator
|
||||
from xmodule.modulestore.keys import UsageKey
|
||||
from xmodule.video_module import transcripts_utils
|
||||
|
||||
from contentstore.tests.modulestore_config import TEST_MODULESTORE
|
||||
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
|
||||
@@ -31,15 +30,11 @@ TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().
|
||||
class Basetranscripts(CourseTestCase):
|
||||
"""Base test class for transcripts tests."""
|
||||
|
||||
org = 'MITx'
|
||||
number = '999'
|
||||
|
||||
def clear_subs_content(self):
|
||||
"""Remove, if transcripts content exists."""
|
||||
for youtube_id in self.get_youtube_ids().values():
|
||||
filename = 'subs_{0}.srt.sjson'.format(youtube_id)
|
||||
content_location = StaticContent.compute_location(
|
||||
self.org, self.number, filename)
|
||||
content_location = StaticContent.compute_location(self.course.id, filename)
|
||||
try:
|
||||
content = contentstore().find(content_location)
|
||||
contentstore().delete(content.get_id())
|
||||
@@ -49,38 +44,35 @@ class Basetranscripts(CourseTestCase):
|
||||
def setUp(self):
|
||||
"""Create initial data."""
|
||||
super(Basetranscripts, self).setUp()
|
||||
self.unicode_locator = unicode(loc_mapper().translate_location(
|
||||
self.course.location.course_id, self.course.location, False, True
|
||||
))
|
||||
|
||||
# Add video module
|
||||
data = {
|
||||
'parent_locator': self.unicode_locator,
|
||||
'parent_locator': unicode(self.course.location),
|
||||
'category': 'video',
|
||||
'type': 'video'
|
||||
}
|
||||
resp = self.client.ajax_post('/xblock', data)
|
||||
self.item_locator, self.item_location = self._get_locator(resp)
|
||||
resp = self.client.ajax_post('/xblock/', data)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
self.item = modulestore().get_item(self.item_location)
|
||||
self.video_usage_key = self._get_usage_key(resp)
|
||||
self.item = modulestore().get_item(self.video_usage_key)
|
||||
# hI10vDNYz4M - valid Youtube ID with transcripts.
|
||||
# JMD_ifUUfsU, AKqURZnYqpk, DYpADpL7jAY - valid Youtube IDs without transcripts.
|
||||
self.item.data = '<video youtube="0.75:JMD_ifUUfsU,1.0:hI10vDNYz4M,1.25:AKqURZnYqpk,1.50:DYpADpL7jAY" />'
|
||||
modulestore().update_item(self.item, self.user.id)
|
||||
|
||||
self.item = modulestore().get_item(self.item_location)
|
||||
self.item = modulestore().get_item(self.video_usage_key)
|
||||
# Remove all transcripts for current module.
|
||||
self.clear_subs_content()
|
||||
|
||||
def _get_locator(self, resp):
|
||||
""" Returns the locator and old-style location (as a string) from the response returned by a create operation. """
|
||||
locator = json.loads(resp.content).get('locator')
|
||||
return locator, loc_mapper().translate_locator_to_location(BlockUsageLocator(locator)).url()
|
||||
def _get_usage_key(self, resp):
|
||||
""" Returns the usage key from the response returned by a create operation. """
|
||||
usage_key_string = json.loads(resp.content).get('locator')
|
||||
return UsageKey.from_string(usage_key_string)
|
||||
|
||||
def get_youtube_ids(self):
|
||||
"""Return youtube speeds and ids."""
|
||||
item = modulestore().get_item(self.item_location)
|
||||
item = modulestore().get_item(self.video_usage_key)
|
||||
|
||||
return {
|
||||
0.75: item.youtube_id_0_75,
|
||||
@@ -142,7 +134,7 @@ class TestUploadtranscripts(Basetranscripts):
|
||||
link = reverse('upload_transcripts')
|
||||
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
|
||||
resp = self.client.post(link, {
|
||||
'locator': self.item_locator,
|
||||
'locator': self.video_usage_key,
|
||||
'transcript-file': self.good_srt_file,
|
||||
'video_list': json.dumps([{
|
||||
'type': 'html5',
|
||||
@@ -153,11 +145,11 @@ class TestUploadtranscripts(Basetranscripts):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(json.loads(resp.content).get('status'), 'Success')
|
||||
|
||||
item = modulestore().get_item(self.item_location)
|
||||
item = modulestore().get_item(self.video_usage_key)
|
||||
self.assertEqual(item.sub, filename)
|
||||
|
||||
content_location = StaticContent.compute_location(
|
||||
self.org, self.number, 'subs_{0}.srt.sjson'.format(filename))
|
||||
self.course.id, 'subs_{0}.srt.sjson'.format(filename))
|
||||
self.assertTrue(contentstore().find(content_location))
|
||||
|
||||
def test_fail_data_without_id(self):
|
||||
@@ -168,7 +160,7 @@ class TestUploadtranscripts(Basetranscripts):
|
||||
|
||||
def test_fail_data_without_file(self):
|
||||
link = reverse('upload_transcripts')
|
||||
resp = self.client.post(link, {'locator': self.item_locator})
|
||||
resp = self.client.post(link, {'locator': self.video_usage_key})
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertEqual(json.loads(resp.content).get('status'), 'POST data without "file" form data.')
|
||||
|
||||
@@ -192,7 +184,7 @@ class TestUploadtranscripts(Basetranscripts):
|
||||
link = reverse('upload_transcripts')
|
||||
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
|
||||
resp = self.client.post(link, {
|
||||
'locator': '{0}_{1}'.format(self.item_locator, 'BAD_LOCATOR'),
|
||||
'locator': '{0}_{1}'.format(self.video_usage_key, 'BAD_LOCATOR'),
|
||||
'transcript-file': self.good_srt_file,
|
||||
'video_list': json.dumps([{
|
||||
'type': 'html5',
|
||||
@@ -206,13 +198,13 @@ class TestUploadtranscripts(Basetranscripts):
|
||||
def test_fail_for_non_video_module(self):
|
||||
# non_video module: setup
|
||||
data = {
|
||||
'parent_locator': self.unicode_locator,
|
||||
'parent_locator': unicode(self.course.location),
|
||||
'category': 'non_video',
|
||||
'type': 'non_video'
|
||||
}
|
||||
resp = self.client.ajax_post('/xblock', data)
|
||||
item_locator, item_location = self._get_locator(resp)
|
||||
item = modulestore().get_item(item_location)
|
||||
resp = self.client.ajax_post('/xblock/', data)
|
||||
usage_key = self._get_usage_key(resp)
|
||||
item = modulestore().get_item(usage_key)
|
||||
item.data = '<non_video youtube="0.75:JMD_ifUUfsU,1.0:hI10vDNYz4M" />'
|
||||
modulestore().update_item(item, self.user.id)
|
||||
|
||||
@@ -221,7 +213,7 @@ class TestUploadtranscripts(Basetranscripts):
|
||||
link = reverse('upload_transcripts')
|
||||
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
|
||||
resp = self.client.post(link, {
|
||||
'locator': item_locator,
|
||||
'locator': unicode(usage_key),
|
||||
'transcript-file': self.good_srt_file,
|
||||
'video_list': json.dumps([{
|
||||
'type': 'html5',
|
||||
@@ -239,7 +231,7 @@ class TestUploadtranscripts(Basetranscripts):
|
||||
link = reverse('upload_transcripts')
|
||||
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
|
||||
resp = self.client.post(link, {
|
||||
'locator': self.item_locator,
|
||||
'locator': unicode(self.video_usage_key),
|
||||
'transcript-file': self.good_srt_file,
|
||||
'video_list': json.dumps([{
|
||||
'type': 'html5',
|
||||
@@ -256,7 +248,7 @@ class TestUploadtranscripts(Basetranscripts):
|
||||
link = reverse('upload_transcripts')
|
||||
filename = os.path.splitext(os.path.basename(self.bad_data_srt_file.name))[0]
|
||||
resp = self.client.post(link, {
|
||||
'locator': self.item_locator,
|
||||
'locator': unicode(self.video_usage_key),
|
||||
'transcript-file': self.bad_data_srt_file,
|
||||
'video_list': json.dumps([{
|
||||
'type': 'html5',
|
||||
@@ -271,7 +263,7 @@ class TestUploadtranscripts(Basetranscripts):
|
||||
link = reverse('upload_transcripts')
|
||||
filename = os.path.splitext(os.path.basename(self.bad_name_srt_file.name))[0]
|
||||
resp = self.client.post(link, {
|
||||
'locator': self.item_locator,
|
||||
'locator': unicode(self.video_usage_key),
|
||||
'transcript-file': self.bad_name_srt_file,
|
||||
'video_list': json.dumps([{
|
||||
'type': 'html5',
|
||||
@@ -298,7 +290,7 @@ class TestUploadtranscripts(Basetranscripts):
|
||||
link = reverse('upload_transcripts')
|
||||
filename = os.path.splitext(os.path.basename(srt_file.name))[0]
|
||||
resp = self.client.post(link, {
|
||||
'locator': self.item_locator,
|
||||
'locator': self.video_usage_key,
|
||||
'transcript-file': srt_file,
|
||||
'video_list': json.dumps([{
|
||||
'type': 'html5',
|
||||
@@ -326,8 +318,7 @@ class TestDownloadtranscripts(Basetranscripts):
|
||||
mime_type = 'application/json'
|
||||
filename = 'subs_{0}.srt.sjson'.format(subs_id)
|
||||
|
||||
content_location = StaticContent.compute_location(
|
||||
self.org, self.number, filename)
|
||||
content_location = StaticContent.compute_location(self.course.id, filename)
|
||||
content = StaticContent(content_location, filename, mime_type, filedata)
|
||||
contentstore().save(content)
|
||||
del_cached_content(content_location)
|
||||
@@ -349,7 +340,7 @@ class TestDownloadtranscripts(Basetranscripts):
|
||||
self.save_subs_to_store(subs, 'JMD_ifUUfsU')
|
||||
|
||||
link = reverse('download_transcripts')
|
||||
resp = self.client.get(link, {'locator': self.item_locator, 'subs_id': "JMD_ifUUfsU"})
|
||||
resp = self.client.get(link, {'locator': self.video_usage_key, 'subs_id': "JMD_ifUUfsU"})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.content, """0\n00:00:00,100 --> 00:00:00,200\nsubs #1\n\n1\n00:00:00,200 --> 00:00:00,240\nsubs #2\n\n2\n00:00:00,240 --> 00:00:00,380\nsubs #3\n\n""")
|
||||
|
||||
@@ -376,7 +367,7 @@ class TestDownloadtranscripts(Basetranscripts):
|
||||
self.save_subs_to_store(subs, subs_id)
|
||||
|
||||
link = reverse('download_transcripts')
|
||||
resp = self.client.get(link, {'locator': self.item_locator, 'subs_id': subs_id})
|
||||
resp = self.client.get(link, {'locator': self.video_usage_key, 'subs_id': subs_id})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(
|
||||
resp.content,
|
||||
@@ -401,20 +392,20 @@ class TestDownloadtranscripts(Basetranscripts):
|
||||
|
||||
# Test for raising `ItemNotFoundError` exception.
|
||||
link = reverse('download_transcripts')
|
||||
resp = self.client.get(link, {'locator': '{0}_{1}'.format(self.item_locator, 'BAD_LOCATOR')})
|
||||
resp = self.client.get(link, {'locator': '{0}_{1}'.format(self.video_usage_key, 'BAD_LOCATOR')})
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
def test_fail_for_non_video_module(self):
|
||||
# Video module: setup
|
||||
data = {
|
||||
'parent_locator': self.unicode_locator,
|
||||
'parent_locator': unicode(self.course.location),
|
||||
'category': 'videoalpha',
|
||||
'type': 'videoalpha'
|
||||
}
|
||||
resp = self.client.ajax_post('/xblock', data)
|
||||
item_locator, item_location = self._get_locator(resp)
|
||||
resp = self.client.ajax_post('/xblock/', data)
|
||||
usage_key = self._get_usage_key(resp)
|
||||
subs_id = str(uuid4())
|
||||
item = modulestore().get_item(item_location)
|
||||
item = modulestore().get_item(usage_key)
|
||||
item.data = textwrap.dedent("""
|
||||
<videoalpha youtube="" sub="{}">
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4"/>
|
||||
@@ -436,7 +427,7 @@ class TestDownloadtranscripts(Basetranscripts):
|
||||
self.save_subs_to_store(subs, subs_id)
|
||||
|
||||
link = reverse('download_transcripts')
|
||||
resp = self.client.get(link, {'locator': item_locator})
|
||||
resp = self.client.get(link, {'locator': unicode(usage_key)})
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
def test_fail_nonyoutube_subs_dont_exist(self):
|
||||
@@ -450,7 +441,7 @@ class TestDownloadtranscripts(Basetranscripts):
|
||||
modulestore().update_item(self.item, self.user.id)
|
||||
|
||||
link = reverse('download_transcripts')
|
||||
resp = self.client.get(link, {'locator': self.item_locator})
|
||||
resp = self.client.get(link, {'locator': self.video_usage_key})
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
def test_empty_youtube_attr_and_sub_attr(self):
|
||||
@@ -464,7 +455,7 @@ class TestDownloadtranscripts(Basetranscripts):
|
||||
modulestore().update_item(self.item, self.user.id)
|
||||
|
||||
link = reverse('download_transcripts')
|
||||
resp = self.client.get(link, {'locator': self.item_locator})
|
||||
resp = self.client.get(link, {'locator': self.video_usage_key})
|
||||
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
@@ -489,7 +480,7 @@ class TestDownloadtranscripts(Basetranscripts):
|
||||
self.save_subs_to_store(subs, 'JMD_ifUUfsU')
|
||||
|
||||
link = reverse('download_transcripts')
|
||||
resp = self.client.get(link, {'locator': self.item_locator})
|
||||
resp = self.client.get(link, {'locator': self.video_usage_key})
|
||||
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
@@ -503,8 +494,7 @@ class TestChecktranscripts(Basetranscripts):
|
||||
mime_type = 'application/json'
|
||||
filename = 'subs_{0}.srt.sjson'.format(subs_id)
|
||||
|
||||
content_location = StaticContent.compute_location(
|
||||
self.org, self.number, filename)
|
||||
content_location = StaticContent.compute_location(self.course.id, filename)
|
||||
content = StaticContent(content_location, filename, mime_type, filedata)
|
||||
contentstore().save(content)
|
||||
del_cached_content(content_location)
|
||||
@@ -533,7 +523,7 @@ class TestChecktranscripts(Basetranscripts):
|
||||
self.save_subs_to_store(subs, subs_id)
|
||||
|
||||
data = {
|
||||
'locator': self.item_locator,
|
||||
'locator': unicode(self.video_usage_key),
|
||||
'videos': [{
|
||||
'type': 'html5',
|
||||
'video': subs_id,
|
||||
@@ -577,7 +567,7 @@ class TestChecktranscripts(Basetranscripts):
|
||||
self.save_subs_to_store(subs, 'JMD_ifUUfsU')
|
||||
link = reverse('check_transcripts')
|
||||
data = {
|
||||
'locator': self.item_locator,
|
||||
'locator': unicode(self.video_usage_key),
|
||||
'videos': [{
|
||||
'type': 'youtube',
|
||||
'video': 'JMD_ifUUfsU',
|
||||
@@ -633,7 +623,7 @@ class TestChecktranscripts(Basetranscripts):
|
||||
|
||||
# Test for raising `ItemNotFoundError` exception.
|
||||
data = {
|
||||
'locator': '{0}_{1}'.format(self.item_locator, 'BAD_LOCATOR'),
|
||||
'locator': '{0}_{1}'.format(self.video_usage_key, 'BAD_LOCATOR'),
|
||||
'videos': [{
|
||||
'type': '',
|
||||
'video': '',
|
||||
@@ -647,14 +637,14 @@ class TestChecktranscripts(Basetranscripts):
|
||||
def test_fail_for_non_video_module(self):
|
||||
# Not video module: setup
|
||||
data = {
|
||||
'parent_locator': self.unicode_locator,
|
||||
'parent_locator': unicode(self.course.location),
|
||||
'category': 'not_video',
|
||||
'type': 'not_video'
|
||||
}
|
||||
resp = self.client.ajax_post('/xblock', data)
|
||||
item_locator, item_location = self._get_locator(resp)
|
||||
resp = self.client.ajax_post('/xblock/', data)
|
||||
usage_key = self._get_usage_key(resp)
|
||||
subs_id = str(uuid4())
|
||||
item = modulestore().get_item(item_location)
|
||||
item = modulestore().get_item(usage_key)
|
||||
item.data = textwrap.dedent("""
|
||||
<not_video youtube="" sub="{}">
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4"/>
|
||||
@@ -676,7 +666,7 @@ class TestChecktranscripts(Basetranscripts):
|
||||
self.save_subs_to_store(subs, subs_id)
|
||||
|
||||
data = {
|
||||
'locator': item_locator,
|
||||
'locator': unicode(usage_key),
|
||||
'videos': [{
|
||||
'type': '',
|
||||
'video': '',
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
Tests for contentstore/views/user.py.
|
||||
"""
|
||||
import json
|
||||
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from contentstore.utils import reverse_course_url
|
||||
from django.contrib.auth.models import User
|
||||
from student.models import CourseEnrollment
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
from student.roles import CourseStaffRole, CourseInstructorRole
|
||||
from student import auth
|
||||
|
||||
@@ -24,12 +25,16 @@ class UsersTestCase(CourseTestCase):
|
||||
self.inactive_user.is_staff = False
|
||||
self.inactive_user.save()
|
||||
|
||||
self.location = loc_mapper().translate_location(self.course.location.course_id, self.course.location, False, True)
|
||||
self.index_url = self.course_team_url()
|
||||
self.detail_url = self.course_team_url(email=self.ext_user.email)
|
||||
self.inactive_detail_url = self.course_team_url(email=self.inactive_user.email)
|
||||
self.invalid_detail_url = self.course_team_url(email='nonexistent@user.com')
|
||||
|
||||
self.index_url = self.location.url_reverse('course_team', '')
|
||||
self.detail_url = self.location.url_reverse('course_team', self.ext_user.email)
|
||||
self.inactive_detail_url = self.location.url_reverse('course_team', self.inactive_user.email)
|
||||
self.invalid_detail_url = self.location.url_reverse('course_team', "nonexistent@user.com")
|
||||
def course_team_url(self, email=None):
|
||||
return reverse_course_url(
|
||||
'course_team_handler', self.course.id,
|
||||
kwargs={'email': email} if email else {}
|
||||
)
|
||||
|
||||
def test_index(self):
|
||||
resp = self.client.get(self.index_url, HTTP_ACCEPT='text/html')
|
||||
@@ -38,7 +43,7 @@ class UsersTestCase(CourseTestCase):
|
||||
self.assertNotContains(resp, self.ext_user.email)
|
||||
|
||||
def test_index_member(self):
|
||||
auth.add_users(self.user, CourseStaffRole(self.course_locator), self.ext_user)
|
||||
auth.add_users(self.user, CourseStaffRole(self.course.id), self.ext_user)
|
||||
|
||||
resp = self.client.get(self.index_url, HTTP_ACCEPT='text/html')
|
||||
self.assertContains(resp, self.ext_user.email)
|
||||
@@ -71,8 +76,8 @@ class UsersTestCase(CourseTestCase):
|
||||
# reload user from DB
|
||||
ext_user = User.objects.get(email=self.ext_user.email)
|
||||
# no content: should not be in any roles
|
||||
self.assertFalse(auth.has_access(ext_user, CourseStaffRole(self.course_locator)))
|
||||
self.assertFalse(auth.has_access(ext_user, CourseInstructorRole(self.course_locator)))
|
||||
self.assertFalse(auth.has_access(ext_user, CourseStaffRole(self.course.id)))
|
||||
self.assertFalse(auth.has_access(ext_user, CourseInstructorRole(self.course.id)))
|
||||
self.assert_not_enrolled()
|
||||
|
||||
def test_detail_post_staff(self):
|
||||
@@ -85,12 +90,12 @@ class UsersTestCase(CourseTestCase):
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
# reload user from DB
|
||||
ext_user = User.objects.get(email=self.ext_user.email)
|
||||
self.assertTrue(auth.has_access(ext_user, CourseStaffRole(self.course_locator)))
|
||||
self.assertFalse(auth.has_access(ext_user, CourseInstructorRole(self.course_locator)))
|
||||
self.assertTrue(auth.has_access(ext_user, CourseStaffRole(self.course.id)))
|
||||
self.assertFalse(auth.has_access(ext_user, CourseInstructorRole(self.course.id)))
|
||||
self.assert_enrolled()
|
||||
|
||||
def test_detail_post_staff_other_inst(self):
|
||||
auth.add_users(self.user, CourseInstructorRole(self.course_locator), self.user)
|
||||
auth.add_users(self.user, CourseInstructorRole(self.course.id), self.user)
|
||||
|
||||
resp = self.client.post(
|
||||
self.detail_url,
|
||||
@@ -101,13 +106,13 @@ class UsersTestCase(CourseTestCase):
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
# reload user from DB
|
||||
ext_user = User.objects.get(email=self.ext_user.email)
|
||||
self.assertTrue(auth.has_access(ext_user, CourseStaffRole(self.course_locator)))
|
||||
self.assertFalse(auth.has_access(ext_user, CourseInstructorRole(self.course_locator)))
|
||||
self.assertTrue(auth.has_access(ext_user, CourseStaffRole(self.course.id)))
|
||||
self.assertFalse(auth.has_access(ext_user, CourseInstructorRole(self.course.id)))
|
||||
self.assert_enrolled()
|
||||
# check that other user is unchanged
|
||||
user = User.objects.get(email=self.user.email)
|
||||
self.assertTrue(auth.has_access(user, CourseInstructorRole(self.course_locator)))
|
||||
self.assertFalse(CourseStaffRole(self.course_locator).has_user(user))
|
||||
self.assertTrue(auth.has_access(user, CourseInstructorRole(self.course.id)))
|
||||
self.assertFalse(CourseStaffRole(self.course.id).has_user(user))
|
||||
|
||||
def test_detail_post_instructor(self):
|
||||
resp = self.client.post(
|
||||
@@ -119,8 +124,8 @@ class UsersTestCase(CourseTestCase):
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
# reload user from DB
|
||||
ext_user = User.objects.get(email=self.ext_user.email)
|
||||
self.assertTrue(auth.has_access(ext_user, CourseInstructorRole(self.course_locator)))
|
||||
self.assertFalse(CourseStaffRole(self.course_locator).has_user(ext_user))
|
||||
self.assertTrue(auth.has_access(ext_user, CourseInstructorRole(self.course.id)))
|
||||
self.assertFalse(CourseStaffRole(self.course.id).has_user(ext_user))
|
||||
self.assert_enrolled()
|
||||
|
||||
def test_detail_post_missing_role(self):
|
||||
@@ -144,12 +149,12 @@ class UsersTestCase(CourseTestCase):
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
# reload user from DB
|
||||
ext_user = User.objects.get(email=self.ext_user.email)
|
||||
self.assertTrue(auth.has_access(ext_user, CourseStaffRole(self.course_locator)))
|
||||
self.assertFalse(auth.has_access(ext_user, CourseInstructorRole(self.course_locator)))
|
||||
self.assertTrue(auth.has_access(ext_user, CourseStaffRole(self.course.id)))
|
||||
self.assertFalse(auth.has_access(ext_user, CourseInstructorRole(self.course.id)))
|
||||
self.assert_enrolled()
|
||||
|
||||
def test_detail_delete_staff(self):
|
||||
auth.add_users(self.user, CourseStaffRole(self.course_locator), self.ext_user)
|
||||
auth.add_users(self.user, CourseStaffRole(self.course.id), self.ext_user)
|
||||
|
||||
resp = self.client.delete(
|
||||
self.detail_url,
|
||||
@@ -158,10 +163,10 @@ class UsersTestCase(CourseTestCase):
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
# reload user from DB
|
||||
ext_user = User.objects.get(email=self.ext_user.email)
|
||||
self.assertFalse(auth.has_access(ext_user, CourseStaffRole(self.course_locator)))
|
||||
self.assertFalse(auth.has_access(ext_user, CourseStaffRole(self.course.id)))
|
||||
|
||||
def test_detail_delete_instructor(self):
|
||||
auth.add_users(self.user, CourseInstructorRole(self.course_locator), self.ext_user, self.user)
|
||||
auth.add_users(self.user, CourseInstructorRole(self.course.id), self.ext_user, self.user)
|
||||
|
||||
resp = self.client.delete(
|
||||
self.detail_url,
|
||||
@@ -170,10 +175,10 @@ class UsersTestCase(CourseTestCase):
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
# reload user from DB
|
||||
ext_user = User.objects.get(email=self.ext_user.email)
|
||||
self.assertFalse(auth.has_access(ext_user, CourseInstructorRole(self.course_locator)))
|
||||
self.assertFalse(auth.has_access(ext_user, CourseInstructorRole(self.course.id)))
|
||||
|
||||
def test_delete_last_instructor(self):
|
||||
auth.add_users(self.user, CourseInstructorRole(self.course_locator), self.ext_user)
|
||||
auth.add_users(self.user, CourseInstructorRole(self.course.id), self.ext_user)
|
||||
|
||||
resp = self.client.delete(
|
||||
self.detail_url,
|
||||
@@ -184,10 +189,10 @@ class UsersTestCase(CourseTestCase):
|
||||
self.assertIn("error", result)
|
||||
# reload user from DB
|
||||
ext_user = User.objects.get(email=self.ext_user.email)
|
||||
self.assertTrue(auth.has_access(ext_user, CourseInstructorRole(self.course_locator)))
|
||||
self.assertTrue(auth.has_access(ext_user, CourseInstructorRole(self.course.id)))
|
||||
|
||||
def test_post_last_instructor(self):
|
||||
auth.add_users(self.user, CourseInstructorRole(self.course_locator), self.ext_user)
|
||||
auth.add_users(self.user, CourseInstructorRole(self.course.id), self.ext_user)
|
||||
|
||||
resp = self.client.post(
|
||||
self.detail_url,
|
||||
@@ -199,14 +204,14 @@ class UsersTestCase(CourseTestCase):
|
||||
self.assertIn("error", result)
|
||||
# reload user from DB
|
||||
ext_user = User.objects.get(email=self.ext_user.email)
|
||||
self.assertTrue(auth.has_access(ext_user, CourseInstructorRole(self.course_locator)))
|
||||
self.assertTrue(auth.has_access(ext_user, CourseInstructorRole(self.course.id)))
|
||||
|
||||
def test_permission_denied_self(self):
|
||||
auth.add_users(self.user, CourseStaffRole(self.course_locator), self.user)
|
||||
auth.add_users(self.user, CourseStaffRole(self.course.id), self.user)
|
||||
self.user.is_staff = False
|
||||
self.user.save()
|
||||
|
||||
self_url = self.location.url_reverse('course_team', self.user.email)
|
||||
self_url = self.course_team_url(email=self.user.email)
|
||||
|
||||
resp = self.client.post(
|
||||
self_url,
|
||||
@@ -218,7 +223,7 @@ class UsersTestCase(CourseTestCase):
|
||||
self.assertIn("error", result)
|
||||
|
||||
def test_permission_denied_other(self):
|
||||
auth.add_users(self.user, CourseStaffRole(self.course_locator), self.user)
|
||||
auth.add_users(self.user, CourseStaffRole(self.course.id), self.user)
|
||||
self.user.is_staff = False
|
||||
self.user.save()
|
||||
|
||||
@@ -232,20 +237,20 @@ class UsersTestCase(CourseTestCase):
|
||||
self.assertIn("error", result)
|
||||
|
||||
def test_staff_can_delete_self(self):
|
||||
auth.add_users(self.user, CourseStaffRole(self.course_locator), self.user)
|
||||
auth.add_users(self.user, CourseStaffRole(self.course.id), self.user)
|
||||
self.user.is_staff = False
|
||||
self.user.save()
|
||||
|
||||
self_url = self.location.url_reverse('course_team', self.user.email)
|
||||
self_url = self.course_team_url(email=self.user.email)
|
||||
|
||||
resp = self.client.delete(self_url)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
# reload user from DB
|
||||
user = User.objects.get(email=self.user.email)
|
||||
self.assertFalse(auth.has_access(user, CourseStaffRole(self.course_locator)))
|
||||
self.assertFalse(auth.has_access(user, CourseStaffRole(self.course.id)))
|
||||
|
||||
def test_staff_cannot_delete_other(self):
|
||||
auth.add_users(self.user, CourseStaffRole(self.course_locator), self.user, self.ext_user)
|
||||
auth.add_users(self.user, CourseStaffRole(self.course.id), self.user, self.ext_user)
|
||||
self.user.is_staff = False
|
||||
self.user.save()
|
||||
|
||||
@@ -255,7 +260,7 @@ class UsersTestCase(CourseTestCase):
|
||||
self.assertIn("error", result)
|
||||
# reload user from DB
|
||||
ext_user = User.objects.get(email=self.ext_user.email)
|
||||
self.assertTrue(auth.has_access(ext_user, CourseStaffRole(self.course_locator)))
|
||||
self.assertTrue(auth.has_access(ext_user, CourseStaffRole(self.course.id)))
|
||||
|
||||
def test_user_not_initially_enrolled(self):
|
||||
# Verify that ext_user is not enrolled in the new course before being added as a staff member.
|
||||
@@ -300,13 +305,13 @@ class UsersTestCase(CourseTestCase):
|
||||
def assert_not_enrolled(self):
|
||||
""" Asserts that self.ext_user is not enrolled in self.course. """
|
||||
self.assertFalse(
|
||||
CourseEnrollment.is_enrolled(self.ext_user, self.course.location.course_id),
|
||||
CourseEnrollment.is_enrolled(self.ext_user, self.course.id),
|
||||
'Did not expect ext_user to be enrolled in course'
|
||||
)
|
||||
|
||||
def assert_enrolled(self):
|
||||
""" Asserts that self.ext_user is enrolled in self.course. """
|
||||
self.assertTrue(
|
||||
CourseEnrollment.is_enrolled(self.ext_user, self.course.location.course_id),
|
||||
CourseEnrollment.is_enrolled(self.ext_user, self.course.id),
|
||||
'User ext_user should have been enrolled in the course'
|
||||
)
|
||||
|
||||
@@ -6,7 +6,6 @@ import json
|
||||
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from contentstore.views.helpers import xblock_studio_url
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
from xmodule.modulestore.tests.factories import ItemFactory
|
||||
|
||||
|
||||
@@ -26,7 +25,7 @@ class StudioPageTestCase(CourseTestCase):
|
||||
"""
|
||||
Returns the HTML for the page representing the xblock.
|
||||
"""
|
||||
url = xblock_studio_url(xblock, self.course)
|
||||
url = xblock_studio_url(xblock)
|
||||
self.assertIsNotNone(url)
|
||||
resp = self.client.get_html(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -36,8 +35,7 @@ class StudioPageTestCase(CourseTestCase):
|
||||
"""
|
||||
Returns the HTML for the xblock when shown within a unit or container page.
|
||||
"""
|
||||
locator = loc_mapper().translate_location(self.course.id, xblock.location, published=False)
|
||||
preview_url = '/xblock/{locator}/{view_name}'.format(locator=locator, view_name=view_name)
|
||||
preview_url = '/xblock/{usage_key}/{view_name}'.format(usage_key=xblock.location, view_name=view_name)
|
||||
resp = self.client.get_json(preview_url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
resp_content = json.loads(resp.content)
|
||||
|
||||
@@ -17,14 +17,16 @@ from django.contrib.auth.decorators import login_required
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from opaque_keys import InvalidKeyError
|
||||
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.modulestore.django import modulestore, loc_mapper
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.keys import UsageKey
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError, InsufficientSpecificationError
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
from util.json_request import JsonResponse
|
||||
from xmodule.modulestore.locator import BlockUsageLocator
|
||||
|
||||
from xmodule.video_module.transcripts_utils import (
|
||||
generate_subs_from_source,
|
||||
@@ -32,7 +34,6 @@ from xmodule.video_module.transcripts_utils import (
|
||||
download_youtube_subs, get_transcripts_from_youtube,
|
||||
copy_or_rename_transcript,
|
||||
manage_video_subtitles_save,
|
||||
TranscriptsGenerationException,
|
||||
GetTranscriptsFromYouTubeException,
|
||||
TranscriptsRequestValidationException
|
||||
)
|
||||
@@ -84,7 +85,7 @@ def upload_transcripts(request):
|
||||
|
||||
try:
|
||||
item = _get_item(request, request.POST)
|
||||
except (ItemNotFoundError, InvalidLocationError, InsufficientSpecificationError):
|
||||
except (InvalidKeyError, ItemNotFoundError):
|
||||
return error_response(response, "Can't find item by locator.")
|
||||
|
||||
if 'transcript-file' not in request.FILES:
|
||||
@@ -149,7 +150,7 @@ def download_transcripts(request):
|
||||
|
||||
try:
|
||||
item = _get_item(request, request.GET)
|
||||
except (ItemNotFoundError, InvalidLocationError, InsufficientSpecificationError):
|
||||
except (InvalidKeyError, ItemNotFoundError):
|
||||
log.debug("Can't find item by locator.")
|
||||
raise Http404
|
||||
|
||||
@@ -163,9 +164,7 @@ def download_transcripts(request):
|
||||
raise Http404
|
||||
|
||||
filename = 'subs_{0}.srt.sjson'.format(subs_id)
|
||||
content_location = StaticContent.compute_location(
|
||||
item.location.org, item.location.course, filename
|
||||
)
|
||||
content_location = StaticContent.compute_location(item.location.course_key, filename)
|
||||
try:
|
||||
sjson_transcripts = contentstore().find(content_location)
|
||||
log.debug("Downloading subs for %s id", subs_id)
|
||||
@@ -227,9 +226,7 @@ def check_transcripts(request):
|
||||
transcripts_presence['status'] = 'Success'
|
||||
|
||||
filename = 'subs_{0}.srt.sjson'.format(item.sub)
|
||||
content_location = StaticContent.compute_location(
|
||||
item.location.org, item.location.course, filename
|
||||
)
|
||||
content_location = StaticContent.compute_location(item.location.course_key, filename)
|
||||
try:
|
||||
local_transcripts = contentstore().find(content_location).data
|
||||
transcripts_presence['current_item_subs'] = item.sub
|
||||
@@ -243,9 +240,7 @@ def check_transcripts(request):
|
||||
|
||||
# youtube local
|
||||
filename = 'subs_{0}.srt.sjson'.format(youtube_id)
|
||||
content_location = StaticContent.compute_location(
|
||||
item.location.org, item.location.course, filename
|
||||
)
|
||||
content_location = StaticContent.compute_location(item.location.course_key, filename)
|
||||
try:
|
||||
local_transcripts = contentstore().find(content_location).data
|
||||
transcripts_presence['youtube_local'] = True
|
||||
@@ -276,9 +271,7 @@ def check_transcripts(request):
|
||||
html5_subs = []
|
||||
for html5_id in videos['html5']:
|
||||
filename = 'subs_{0}.srt.sjson'.format(html5_id)
|
||||
content_location = StaticContent.compute_location(
|
||||
item.location.org, item.location.course, filename
|
||||
)
|
||||
content_location = StaticContent.compute_location(item.location.course_key, filename)
|
||||
try:
|
||||
html5_subs.append(contentstore().find(content_location).data)
|
||||
transcripts_presence['html5_local'].append(html5_id)
|
||||
@@ -438,7 +431,7 @@ def _validate_transcripts_data(request):
|
||||
|
||||
try:
|
||||
item = _get_item(request, data)
|
||||
except (ItemNotFoundError, InvalidLocationError, InsufficientSpecificationError):
|
||||
except (InvalidKeyError, ItemNotFoundError):
|
||||
raise TranscriptsRequestValidationException(_("Can't find item by locator."))
|
||||
|
||||
if item.category != 'video':
|
||||
@@ -503,7 +496,7 @@ def save_transcripts(request):
|
||||
|
||||
try:
|
||||
item = _get_item(request, data)
|
||||
except (ItemNotFoundError, InvalidLocationError, InsufficientSpecificationError):
|
||||
except (InvalidKeyError, ItemNotFoundError):
|
||||
return error_response(response, "Can't find item by locator.")
|
||||
|
||||
metadata = data.get('metadata')
|
||||
@@ -538,14 +531,13 @@ def _get_item(request, data):
|
||||
|
||||
Returns the item.
|
||||
"""
|
||||
locator = BlockUsageLocator(data.get('locator'))
|
||||
old_location = loc_mapper().translate_locator_to_location(locator)
|
||||
usage_key = UsageKey.from_string(data.get('locator'))
|
||||
|
||||
# This is placed before has_course_access() to validate the location,
|
||||
# because has_course_access() raises InvalidLocationError if location is invalid.
|
||||
item = modulestore().get_item(old_location)
|
||||
# because has_course_access() raises r if location is invalid.
|
||||
item = modulestore().get_item(usage_key)
|
||||
|
||||
if not has_course_access(request.user, locator):
|
||||
if not has_course_access(request.user, usage_key.course_key):
|
||||
raise PermissionDenied()
|
||||
|
||||
return item
|
||||
|
||||
@@ -7,15 +7,15 @@ from django.views.decorators.http import require_POST
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from edxmako.shortcuts import render_to_response
|
||||
|
||||
from xmodule.modulestore.django import modulestore, loc_mapper
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.keys import CourseKey
|
||||
from util.json_request import JsonResponse, expect_json
|
||||
from student.roles import CourseRole, CourseInstructorRole, CourseStaffRole, GlobalStaff
|
||||
from student.roles import CourseInstructorRole, CourseStaffRole
|
||||
from course_creators.views import user_requested_access
|
||||
|
||||
from .access import has_course_access
|
||||
|
||||
from student.models import CourseEnrollment
|
||||
from xmodule.modulestore.locator import BlockUsageLocator
|
||||
from django.http import HttpResponseNotFound
|
||||
from student import auth
|
||||
|
||||
@@ -37,7 +37,7 @@ def request_course_creator(request):
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(("GET", "POST", "PUT", "DELETE"))
|
||||
def course_team_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None, email=None):
|
||||
def course_team_handler(request, course_key_string=None, email=None):
|
||||
"""
|
||||
The restful handler for course team users.
|
||||
|
||||
@@ -49,51 +49,49 @@ def course_team_handler(request, tag=None, package_id=None, branch=None, version
|
||||
DELETE:
|
||||
json: remove a particular course team member from the course team (email is required).
|
||||
"""
|
||||
location = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block)
|
||||
if not has_course_access(request.user, location):
|
||||
course_key = CourseKey.from_string(course_key_string) if course_key_string else None
|
||||
if not has_course_access(request.user, course_key):
|
||||
raise PermissionDenied()
|
||||
|
||||
if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'):
|
||||
return _course_team_user(request, location, email)
|
||||
return _course_team_user(request, course_key, email)
|
||||
elif request.method == 'GET': # assume html
|
||||
return _manage_users(request, location)
|
||||
return _manage_users(request, course_key)
|
||||
else:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
|
||||
def _manage_users(request, locator):
|
||||
def _manage_users(request, course_key):
|
||||
"""
|
||||
This view will return all CMS users who are editors for the specified course
|
||||
"""
|
||||
old_location = loc_mapper().translate_locator_to_location(locator)
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_course_access(request.user, locator):
|
||||
if not has_course_access(request.user, course_key):
|
||||
raise PermissionDenied()
|
||||
|
||||
course_module = modulestore().get_item(old_location)
|
||||
instructors = CourseInstructorRole(locator).users_with_role()
|
||||
course_module = modulestore().get_course(course_key)
|
||||
instructors = CourseInstructorRole(course_key).users_with_role()
|
||||
# the page only lists staff and assumes they're a superset of instructors. Do a union to ensure.
|
||||
staff = set(CourseStaffRole(locator).users_with_role()).union(instructors)
|
||||
staff = set(CourseStaffRole(course_key).users_with_role()).union(instructors)
|
||||
|
||||
return render_to_response('manage_users.html', {
|
||||
'context_course': course_module,
|
||||
'staff': staff,
|
||||
'instructors': instructors,
|
||||
'allow_actions': has_course_access(request.user, locator, role=CourseInstructorRole),
|
||||
'allow_actions': has_course_access(request.user, course_key, role=CourseInstructorRole),
|
||||
})
|
||||
|
||||
|
||||
@expect_json
|
||||
def _course_team_user(request, locator, email):
|
||||
def _course_team_user(request, course_key, email):
|
||||
"""
|
||||
Handle the add, remove, promote, demote requests ensuring the requester has authority
|
||||
"""
|
||||
# check that logged in user has permissions to this item
|
||||
if has_course_access(request.user, locator, role=CourseInstructorRole):
|
||||
if has_course_access(request.user, course_key, role=CourseInstructorRole):
|
||||
# instructors have full permissions
|
||||
pass
|
||||
elif has_course_access(request.user, locator, role=CourseStaffRole) and email == request.user.email:
|
||||
elif has_course_access(request.user, course_key, role=CourseStaffRole) and email == request.user.email:
|
||||
# staff can only affect themselves
|
||||
pass
|
||||
else:
|
||||
@@ -102,6 +100,7 @@ def _course_team_user(request, locator, email):
|
||||
}
|
||||
return JsonResponse(msg, 400)
|
||||
|
||||
|
||||
try:
|
||||
user = User.objects.get(email=email)
|
||||
except Exception:
|
||||
@@ -119,7 +118,7 @@ def _course_team_user(request, locator, email):
|
||||
"role": None,
|
||||
}
|
||||
# what's the highest role that this user has? (How should this report global staff?)
|
||||
for role in [CourseInstructorRole(locator), CourseStaffRole(locator)]:
|
||||
for role in [CourseInstructorRole(course_key), CourseStaffRole(course_key)]:
|
||||
if role.has_user(user):
|
||||
msg["role"] = role.ROLE
|
||||
break
|
||||
@@ -134,11 +133,11 @@ def _course_team_user(request, locator, email):
|
||||
|
||||
if request.method == "DELETE":
|
||||
try:
|
||||
try_remove_instructor(request, locator, user)
|
||||
try_remove_instructor(request, course_key, user)
|
||||
except CannotOrphanCourse as oops:
|
||||
return JsonResponse(oops.msg, 400)
|
||||
|
||||
auth.remove_users(request.user, CourseStaffRole(locator), user)
|
||||
auth.remove_users(request.user, CourseStaffRole(course_key), user)
|
||||
return JsonResponse()
|
||||
|
||||
# all other operations require the requesting user to specify a role
|
||||
@@ -146,27 +145,26 @@ def _course_team_user(request, locator, email):
|
||||
if role is None:
|
||||
return JsonResponse({"error": _("`role` is required")}, 400)
|
||||
|
||||
old_location = loc_mapper().translate_locator_to_location(locator)
|
||||
if role == "instructor":
|
||||
if not has_course_access(request.user, locator, role=CourseInstructorRole):
|
||||
if not has_course_access(request.user, course_key, role=CourseInstructorRole):
|
||||
msg = {
|
||||
"error": _("Only instructors may create other instructors")
|
||||
}
|
||||
return JsonResponse(msg, 400)
|
||||
auth.add_users(request.user, CourseInstructorRole(locator), user)
|
||||
auth.add_users(request.user, CourseInstructorRole(course_key), user)
|
||||
# auto-enroll the course creator in the course so that "View Live" will work.
|
||||
CourseEnrollment.enroll(user, old_location.course_id)
|
||||
CourseEnrollment.enroll(user, course_key)
|
||||
elif role == "staff":
|
||||
# add to staff regardless (can't do after removing from instructors as will no longer
|
||||
# be allowed)
|
||||
auth.add_users(request.user, CourseStaffRole(locator), user)
|
||||
auth.add_users(request.user, CourseStaffRole(course_key), user)
|
||||
try:
|
||||
try_remove_instructor(request, locator, user)
|
||||
try_remove_instructor(request, course_key, user)
|
||||
except CannotOrphanCourse as oops:
|
||||
return JsonResponse(oops.msg, 400)
|
||||
|
||||
# auto-enroll the course creator in the course so that "View Live" will work.
|
||||
CourseEnrollment.enroll(user, old_location.course_id)
|
||||
CourseEnrollment.enroll(user, course_key)
|
||||
|
||||
return JsonResponse()
|
||||
|
||||
@@ -180,13 +178,14 @@ class CannotOrphanCourse(Exception):
|
||||
Exception.__init__(self)
|
||||
|
||||
|
||||
def try_remove_instructor(request, locator, user):
|
||||
def try_remove_instructor(request, course_key, user):
|
||||
|
||||
# remove all roles in this course from this user: but fail if the user
|
||||
# is the last instructor in the course team
|
||||
instructors = CourseInstructorRole(locator)
|
||||
instructors = CourseInstructorRole(course_key)
|
||||
if instructors.has_user(user):
|
||||
if instructors.users_with_role().count() == 1:
|
||||
msg = {"error":_("You may not remove the last instructor from a course")}
|
||||
msg = {"error": _("You may not remove the last instructor from a course")}
|
||||
raise CannotOrphanCourse(msg)
|
||||
else:
|
||||
auth.remove_users(request.user, instructors, user)
|
||||
|
||||
@@ -9,8 +9,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from contentstore.utils import get_modulestore, course_image_url
|
||||
from models.settings import course_grading
|
||||
from xmodule.fields import Date
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
class CourseDetails(object):
|
||||
def __init__(self, org, course_id, run):
|
||||
@@ -31,61 +30,60 @@ class CourseDetails(object):
|
||||
self.course_image_asset_path = "" # URL of the course image
|
||||
|
||||
@classmethod
|
||||
def fetch(cls, course_locator):
|
||||
def fetch(cls, course_key):
|
||||
"""
|
||||
Fetch the course details for the given course from persistence and return a CourseDetails model.
|
||||
"""
|
||||
course_old_location = loc_mapper().translate_locator_to_location(course_locator)
|
||||
descriptor = get_modulestore(course_old_location).get_item(course_old_location)
|
||||
course = cls(course_old_location.org, course_old_location.course, course_old_location.name)
|
||||
descriptor = modulestore('direct').get_course(course_key)
|
||||
course_details = cls(course_key.org, course_key.course, course_key.run)
|
||||
|
||||
course.start_date = descriptor.start
|
||||
course.end_date = descriptor.end
|
||||
course.enrollment_start = descriptor.enrollment_start
|
||||
course.enrollment_end = descriptor.enrollment_end
|
||||
course.course_image_name = descriptor.course_image
|
||||
course.course_image_asset_path = course_image_url(descriptor)
|
||||
course_details.start_date = descriptor.start
|
||||
course_details.end_date = descriptor.end
|
||||
course_details.enrollment_start = descriptor.enrollment_start
|
||||
course_details.enrollment_end = descriptor.enrollment_end
|
||||
course_details.course_image_name = descriptor.course_image
|
||||
course_details.course_image_asset_path = course_image_url(descriptor)
|
||||
|
||||
temploc = course_old_location.replace(category='about', name='syllabus')
|
||||
temploc = course_key.make_usage_key('about', 'syllabus')
|
||||
try:
|
||||
course.syllabus = get_modulestore(temploc).get_item(temploc).data
|
||||
course_details.syllabus = get_modulestore(temploc).get_item(temploc).data
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
|
||||
temploc = course_old_location.replace(category='about', name='short_description')
|
||||
temploc = course_key.make_usage_key('about', 'short_description')
|
||||
try:
|
||||
course.short_description = get_modulestore(temploc).get_item(temploc).data
|
||||
course_details.short_description = get_modulestore(temploc).get_item(temploc).data
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
|
||||
temploc = temploc.replace(name='overview')
|
||||
temploc = course_key.make_usage_key('about', 'overview')
|
||||
try:
|
||||
course.overview = get_modulestore(temploc).get_item(temploc).data
|
||||
course_details.overview = get_modulestore(temploc).get_item(temploc).data
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
|
||||
temploc = temploc.replace(name='effort')
|
||||
temploc = course_key.make_usage_key('about', 'effort')
|
||||
try:
|
||||
course.effort = get_modulestore(temploc).get_item(temploc).data
|
||||
course_details.effort = get_modulestore(temploc).get_item(temploc).data
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
|
||||
temploc = temploc.replace(name='video')
|
||||
temploc = course_key.make_usage_key('about', 'video')
|
||||
try:
|
||||
raw_video = get_modulestore(temploc).get_item(temploc).data
|
||||
course.intro_video = CourseDetails.parse_video_tag(raw_video)
|
||||
course_details.intro_video = CourseDetails.parse_video_tag(raw_video)
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
|
||||
return course
|
||||
return course_details
|
||||
|
||||
@classmethod
|
||||
def update_about_item(cls, course_old_location, about_key, data, course, user):
|
||||
def update_about_item(cls, course_key, about_key, data, course, user):
|
||||
"""
|
||||
Update the about item with the new data blob. If data is None, then
|
||||
delete the about item.
|
||||
"""
|
||||
temploc = Location(course_old_location).replace(category='about', name=about_key)
|
||||
temploc = course_key.make_usage_key('about', about_key)
|
||||
store = get_modulestore(temploc)
|
||||
if data is None:
|
||||
store.delete_item(temploc)
|
||||
@@ -98,12 +96,12 @@ class CourseDetails(object):
|
||||
store.update_item(about_item, user.id)
|
||||
|
||||
@classmethod
|
||||
def update_from_json(cls, course_locator, jsondict, user):
|
||||
def update_from_json(cls, course_key, jsondict, user):
|
||||
"""
|
||||
Decode the json into CourseDetails and save any changed attrs to the db
|
||||
"""
|
||||
course_old_location = loc_mapper().translate_locator_to_location(course_locator)
|
||||
descriptor = get_modulestore(course_old_location).get_item(course_old_location)
|
||||
module_store = modulestore('direct')
|
||||
descriptor = module_store.get_course(course_key)
|
||||
|
||||
dirty = False
|
||||
|
||||
@@ -153,19 +151,19 @@ class CourseDetails(object):
|
||||
dirty = True
|
||||
|
||||
if dirty:
|
||||
get_modulestore(course_old_location).update_item(descriptor, user.id)
|
||||
module_store.update_item(descriptor, user.id)
|
||||
|
||||
# NOTE: below auto writes to the db w/o verifying that any of the fields actually changed
|
||||
# to make faster, could compare against db or could have client send over a list of which fields changed.
|
||||
for about_type in ['syllabus', 'overview', 'effort', 'short_description']:
|
||||
cls.update_about_item(course_old_location, about_type, jsondict[about_type], descriptor, user)
|
||||
cls.update_about_item(course_key, about_type, jsondict[about_type], descriptor, user)
|
||||
|
||||
recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video'])
|
||||
cls.update_about_item(course_old_location, 'video', recomposed_video_tag, descriptor, user)
|
||||
cls.update_about_item(course_key, 'video', recomposed_video_tag, descriptor, user)
|
||||
|
||||
# Could just return jsondict w/o doing any db reads, but I put the reads in as a means to confirm
|
||||
# it persisted correctly
|
||||
return CourseDetails.fetch(course_locator)
|
||||
return CourseDetails.fetch(course_key)
|
||||
|
||||
@staticmethod
|
||||
def parse_video_tag(raw_video):
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from datetime import timedelta
|
||||
from contentstore.utils import get_modulestore
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xblock.fields import Scope
|
||||
|
||||
|
||||
@@ -18,25 +17,21 @@ class CourseGradingModel(object):
|
||||
self.grace_period = CourseGradingModel.convert_set_grace_period(course_descriptor)
|
||||
|
||||
@classmethod
|
||||
def fetch(cls, course_locator):
|
||||
def fetch(cls, course_key):
|
||||
"""
|
||||
Fetch the course grading policy for the given course from persistence and return a CourseGradingModel.
|
||||
"""
|
||||
course_old_location = loc_mapper().translate_locator_to_location(course_locator)
|
||||
descriptor = get_modulestore(course_old_location).get_item(course_old_location)
|
||||
|
||||
descriptor = modulestore('direct').get_course(course_key)
|
||||
model = cls(descriptor)
|
||||
return model
|
||||
|
||||
@staticmethod
|
||||
def fetch_grader(course_location, index):
|
||||
def fetch_grader(course_key, index):
|
||||
"""
|
||||
Fetch the course's nth grader
|
||||
Returns an empty dict if there's no such grader.
|
||||
"""
|
||||
course_old_location = loc_mapper().translate_locator_to_location(course_location)
|
||||
descriptor = get_modulestore(course_old_location).get_item(course_old_location)
|
||||
|
||||
descriptor = modulestore('direct').get_course(course_key)
|
||||
index = int(index)
|
||||
if len(descriptor.raw_grader) > index:
|
||||
return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
|
||||
@@ -52,33 +47,31 @@ class CourseGradingModel(object):
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def update_from_json(course_locator, jsondict, user):
|
||||
def update_from_json(course_key, jsondict, user):
|
||||
"""
|
||||
Decode the json into CourseGradingModel and save any changes. Returns the modified model.
|
||||
Probably not the usual path for updates as it's too coarse grained.
|
||||
"""
|
||||
course_old_location = loc_mapper().translate_locator_to_location(course_locator)
|
||||
descriptor = get_modulestore(course_old_location).get_item(course_old_location)
|
||||
descriptor = modulestore('direct').get_course(course_key)
|
||||
|
||||
graders_parsed = [CourseGradingModel.parse_grader(jsonele) for jsonele in jsondict['graders']]
|
||||
|
||||
descriptor.raw_grader = graders_parsed
|
||||
descriptor.grade_cutoffs = jsondict['grade_cutoffs']
|
||||
|
||||
get_modulestore(course_old_location).update_item(descriptor, user.id)
|
||||
modulestore('direct').update_item(descriptor, user.id)
|
||||
|
||||
CourseGradingModel.update_grace_period_from_json(course_locator, jsondict['grace_period'], user)
|
||||
CourseGradingModel.update_grace_period_from_json(course_key, jsondict['grace_period'], user)
|
||||
|
||||
return CourseGradingModel.fetch(course_locator)
|
||||
return CourseGradingModel.fetch(course_key)
|
||||
|
||||
@staticmethod
|
||||
def update_grader_from_json(course_location, grader, user):
|
||||
def update_grader_from_json(course_key, grader, user):
|
||||
"""
|
||||
Create or update the grader of the given type (string key) for the given course. Returns the modified
|
||||
grader which is a full model on the client but not on the server (just a dict)
|
||||
"""
|
||||
course_old_location = loc_mapper().translate_locator_to_location(course_location)
|
||||
descriptor = get_modulestore(course_old_location).get_item(course_old_location)
|
||||
descriptor = modulestore('direct').get_course(course_key)
|
||||
|
||||
# parse removes the id; so, grab it before parse
|
||||
index = int(grader.get('id', len(descriptor.raw_grader)))
|
||||
@@ -89,33 +82,31 @@ class CourseGradingModel(object):
|
||||
else:
|
||||
descriptor.raw_grader.append(grader)
|
||||
|
||||
get_modulestore(course_old_location).update_item(descriptor, user.id)
|
||||
modulestore('direct').update_item(descriptor, user.id)
|
||||
|
||||
return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
|
||||
|
||||
@staticmethod
|
||||
def update_cutoffs_from_json(course_location, cutoffs, user):
|
||||
def update_cutoffs_from_json(course_key, cutoffs, user):
|
||||
"""
|
||||
Create or update the grade cutoffs for the given course. Returns sent in cutoffs (ie., no extra
|
||||
db fetch).
|
||||
"""
|
||||
course_old_location = loc_mapper().translate_locator_to_location(course_location)
|
||||
descriptor = get_modulestore(course_old_location).get_item(course_old_location)
|
||||
descriptor = modulestore('direct').get_course(course_key)
|
||||
descriptor.grade_cutoffs = cutoffs
|
||||
|
||||
get_modulestore(course_old_location).update_item(descriptor, user.id)
|
||||
modulestore('direct').update_item(descriptor, user.id)
|
||||
|
||||
return cutoffs
|
||||
|
||||
@staticmethod
|
||||
def update_grace_period_from_json(course_location, graceperiodjson, user):
|
||||
def update_grace_period_from_json(course_key, graceperiodjson, user):
|
||||
"""
|
||||
Update the course's default grace period. Incoming dict is {hours: h, minutes: m} possibly as a
|
||||
grace_period entry in an enclosing dict. It is also safe to call this method with a value of
|
||||
None for graceperiodjson.
|
||||
"""
|
||||
course_old_location = loc_mapper().translate_locator_to_location(course_location)
|
||||
descriptor = get_modulestore(course_old_location).get_item(course_old_location)
|
||||
descriptor = modulestore('direct').get_course(course_key)
|
||||
|
||||
# Before a graceperiod has ever been created, it will be None (once it has been
|
||||
# created, it cannot be set back to None).
|
||||
@@ -126,15 +117,14 @@ class CourseGradingModel(object):
|
||||
grace_timedelta = timedelta(**graceperiodjson)
|
||||
descriptor.graceperiod = grace_timedelta
|
||||
|
||||
get_modulestore(course_old_location).update_item(descriptor, user.id)
|
||||
modulestore('direct').update_item(descriptor, user.id)
|
||||
|
||||
@staticmethod
|
||||
def delete_grader(course_location, index, user):
|
||||
def delete_grader(course_key, index, user):
|
||||
"""
|
||||
Delete the grader of the given type from the given course.
|
||||
"""
|
||||
course_old_location = loc_mapper().translate_locator_to_location(course_location)
|
||||
descriptor = get_modulestore(course_old_location).get_item(course_old_location)
|
||||
descriptor = modulestore('direct').get_course(course_key)
|
||||
|
||||
index = int(index)
|
||||
if index < len(descriptor.raw_grader):
|
||||
@@ -142,24 +132,22 @@ class CourseGradingModel(object):
|
||||
# force propagation to definition
|
||||
descriptor.raw_grader = descriptor.raw_grader
|
||||
|
||||
get_modulestore(course_old_location).update_item(descriptor, user.id)
|
||||
modulestore('direct').update_item(descriptor, user.id)
|
||||
|
||||
@staticmethod
|
||||
def delete_grace_period(course_location, user):
|
||||
def delete_grace_period(course_key, user):
|
||||
"""
|
||||
Delete the course's grace period.
|
||||
"""
|
||||
course_old_location = loc_mapper().translate_locator_to_location(course_location)
|
||||
descriptor = get_modulestore(course_old_location).get_item(course_old_location)
|
||||
descriptor = modulestore('direct').get_course(course_key)
|
||||
|
||||
del descriptor.graceperiod
|
||||
|
||||
get_modulestore(course_old_location).update_item(descriptor, user.id)
|
||||
modulestore('direct').update_item(descriptor, user.id)
|
||||
|
||||
@staticmethod
|
||||
def get_section_grader_type(location):
|
||||
old_location = loc_mapper().translate_locator_to_location(location)
|
||||
descriptor = get_modulestore(old_location).get_item(old_location)
|
||||
descriptor = modulestore('direct').get_item(location)
|
||||
return {
|
||||
"graderType": descriptor.format if descriptor.format is not None else 'notgraded',
|
||||
"location": unicode(location),
|
||||
@@ -174,7 +162,7 @@ class CourseGradingModel(object):
|
||||
del descriptor.format
|
||||
del descriptor.graded
|
||||
|
||||
get_modulestore(descriptor.location).update_item(descriptor, user.id)
|
||||
modulestore('direct').update_item(descriptor, user.id)
|
||||
return {'graderType': grader_type}
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -4,8 +4,6 @@ XBlock runtime implementations for edX Studio
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from lms.lib.xblock.runtime import quote_slashes
|
||||
|
||||
|
||||
def handler_url(block, handler_name, suffix='', query='', thirdparty=False):
|
||||
"""
|
||||
@@ -16,7 +14,7 @@ def handler_url(block, handler_name, suffix='', query='', thirdparty=False):
|
||||
raise NotImplementedError("edX Studio doesn't support third-party xblock handler urls")
|
||||
|
||||
url = reverse('component_handler', kwargs={
|
||||
'usage_id': quote_slashes(unicode(block.scope_ids.usage_id).encode('utf-8')),
|
||||
'usage_key_string': unicode(block.scope_ids.usage_id).encode('utf-8'),
|
||||
'handler': handler_name,
|
||||
'suffix': suffix,
|
||||
}).rstrip('/')
|
||||
|
||||
@@ -13,7 +13,6 @@ class TestHandlerUrl(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.block = Mock()
|
||||
self.course_id = "org/course/run"
|
||||
|
||||
def test_trailing_charecters(self):
|
||||
self.assertFalse(handler_url(self.block, 'handler').endswith('?'))
|
||||
|
||||
@@ -20,11 +20,12 @@ define ["jquery", "underscore", "gettext", "xblock/runtime.v1",
|
||||
createItem: (parent, payload, callback=->) ->
|
||||
payload.parent_locator = parent
|
||||
$.postJSON(
|
||||
@model.urlRoot
|
||||
@model.urlRoot + '/'
|
||||
payload
|
||||
(data) =>
|
||||
@model.set(id: data.locator)
|
||||
@$el.data('locator', data.locator)
|
||||
@$el.data('courseKey', data.courseKey)
|
||||
@render()
|
||||
).success(callback)
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ require(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape"],
|
||||
'run': run
|
||||
});
|
||||
|
||||
$.postJSON('/course', {
|
||||
$.postJSON('/course/', {
|
||||
'org': org,
|
||||
'number': number,
|
||||
'display_name': display_name,
|
||||
|
||||
@@ -7,7 +7,7 @@ define(['js/utils/module'],
|
||||
});
|
||||
describe('getUpdateUrl ', function () {
|
||||
it('can take no arguments', function () {
|
||||
expect(ModuleUtils.getUpdateUrl()).toBe('/xblock');
|
||||
expect(ModuleUtils.getUpdateUrl()).toBe('/xblock/');
|
||||
});
|
||||
it('appends a locator', function () {
|
||||
expect(ModuleUtils.getUpdateUrl("locator")).toBe('/xblock/locator');
|
||||
|
||||
@@ -218,10 +218,15 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers
|
||||
clickDelete(componentIndex);
|
||||
create_sinon.respondWithJson(requests, {});
|
||||
|
||||
// expect request URL to contain given component's id
|
||||
expect(lastRequest().url).toMatch(
|
||||
// first request contains given component's id (to delete the component)
|
||||
expect(requests[requests.length - 2].url).toMatch(
|
||||
new RegExp("locator-component-" + GROUP_TO_TEST + (componentIndex + 1))
|
||||
);
|
||||
|
||||
// second request contains parent's id (to remove as child)
|
||||
expect(lastRequest().url).toMatch(
|
||||
new RegExp("locator-group-" + GROUP_TO_TEST)
|
||||
);
|
||||
};
|
||||
|
||||
deleteComponentWithSuccess = function(componentIndex) {
|
||||
@@ -311,7 +316,7 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers
|
||||
|
||||
// verify content of request
|
||||
request = lastRequest();
|
||||
expect(request.url).toEqual("/xblock");
|
||||
expect(request.url).toEqual("/xblock/");
|
||||
expect(request.method).toEqual("POST");
|
||||
expect(JSON.parse(request.requestBody)).toEqual(
|
||||
JSON.parse(
|
||||
|
||||
@@ -94,7 +94,7 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers
|
||||
verifyXBlockRequest = function (requests, expectedJson) {
|
||||
var request = requests[requests.length - 1],
|
||||
actualJson = JSON.parse(request.requestBody);
|
||||
expect(request.url).toEqual("/xblock");
|
||||
expect(request.url).toEqual("/xblock/");
|
||||
expect(request.method).toEqual("POST");
|
||||
expect(actualJson).toEqual(expectedJson);
|
||||
};
|
||||
|
||||
@@ -12,10 +12,10 @@ define(["underscore"], function (_) {
|
||||
|
||||
var getUpdateUrl = function (locator) {
|
||||
if (_.isUndefined(locator)) {
|
||||
return urlRoot;
|
||||
return urlRoot + '/';
|
||||
}
|
||||
else {
|
||||
return urlRoot + "/" + locator;
|
||||
return urlRoot + '/' + locator;
|
||||
}
|
||||
};
|
||||
return {
|
||||
@@ -23,4 +23,3 @@ define(["underscore"], function (_) {
|
||||
getUpdateUrl: getUpdateUrl
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -41,12 +41,12 @@ define(["jquery", "underscore", "js/views/xblock", "js/utils/module", "gettext",
|
||||
// avoid creating an orphan if the addition fails.
|
||||
if (newParent) {
|
||||
removeFromParent = oldParent;
|
||||
self.reorder(newParent, function () {
|
||||
self.reorder(removeFromParent, hideSaving);
|
||||
self.updateChildren(newParent, function () {
|
||||
self.updateChildren(removeFromParent, hideSaving);
|
||||
});
|
||||
} else {
|
||||
// No new parent, only reordering within same container.
|
||||
self.reorder(oldParent, hideSaving);
|
||||
self.updateChildren(oldParent, hideSaving);
|
||||
}
|
||||
|
||||
oldParent = undefined;
|
||||
@@ -79,7 +79,7 @@ define(["jquery", "underscore", "js/views/xblock", "js/utils/module", "gettext",
|
||||
});
|
||||
},
|
||||
|
||||
reorder: function (targetParent, successCallback) {
|
||||
updateChildren: function (targetParent, successCallback) {
|
||||
var children, childLocators;
|
||||
|
||||
// Find descendants with class "studio-xblock-wrapper" whose parent === targetParent.
|
||||
|
||||
@@ -14,7 +14,8 @@ function(BaseView, _, MetadataModel, AbstractEditor, FileUpload, UploadDialog, V
|
||||
initialize : function() {
|
||||
var self = this,
|
||||
counter = 0,
|
||||
locator = self.$el.closest('[data-locator]').data('locator');
|
||||
locator = self.$el.closest('[data-locator]').data('locator'),
|
||||
courseKey = self.$el.closest('[data-course-key]').data('course-key');
|
||||
|
||||
this.template = this.loadTemplate('metadata-editor');
|
||||
this.$el.html(this.template({numEntries: this.collection.length}));
|
||||
@@ -23,6 +24,7 @@ function(BaseView, _, MetadataModel, AbstractEditor, FileUpload, UploadDialog, V
|
||||
function (model) {
|
||||
var data = {
|
||||
el: self.$el.find('.metadata_entry')[counter++],
|
||||
courseKey: courseKey,
|
||||
locator: locator,
|
||||
model: model
|
||||
},
|
||||
@@ -528,7 +530,7 @@ function(BaseView, _, MetadataModel, AbstractEditor, FileUpload, UploadDialog, V
|
||||
upload: function (event) {
|
||||
var self = this,
|
||||
target = $(event.currentTarget),
|
||||
url = /assets/ + this.options.locator,
|
||||
url = '/assets/' + this.options.courseKey + '/',
|
||||
model = new FileUpload({
|
||||
title: gettext('Upload File'),
|
||||
}),
|
||||
|
||||
@@ -191,6 +191,7 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal",
|
||||
xblockElement = xblockWrapperElement.find('.xblock');
|
||||
xblockInfo = new XBlockInfo({
|
||||
id: xblockWrapperElement.data('locator'),
|
||||
courseKey: xblockWrapperElement.data('course-key'),
|
||||
category: xblockElement.data('block-type')
|
||||
});
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification",
|
||||
requestData = _.extend(template, {
|
||||
parent_locator: parentLocator
|
||||
});
|
||||
return $.postJSON(this.getURLRoot(), requestData,
|
||||
return $.postJSON(this.getURLRoot() + '/', requestData,
|
||||
_.bind(this.onNewXBlock, this, placeholderElement, scrollOffset));
|
||||
},
|
||||
|
||||
@@ -135,7 +135,7 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification",
|
||||
duplicate_source_locator: xblockElement.data('locator'),
|
||||
parent_locator: parentElement.data('locator')
|
||||
};
|
||||
return $.postJSON(self.getURLRoot(), requestData,
|
||||
return $.postJSON(self.getURLRoot() + '/', requestData,
|
||||
_.bind(self.onNewXBlock, self, placeholderElement, scrollOffset));
|
||||
});
|
||||
},
|
||||
@@ -152,9 +152,12 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification",
|
||||
type: 'DELETE',
|
||||
url: self.getURLRoot() + "/" +
|
||||
xblockElement.data('locator') + "?" +
|
||||
$.param({recurse: true, all_versions: true})
|
||||
$.param({recurse: true, all_versions: false})
|
||||
}).success(function() {
|
||||
// get the parent so we can remove this component from its parent.
|
||||
var parent = self.findXBlockElement(xblockElement.parent());
|
||||
xblockElement.remove();
|
||||
self.xblockView.updateChildren(parent);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,7 +59,7 @@ main_xblock_info = {
|
||||
<small class="navigation navigation-parents">
|
||||
% for ancestor in ancestor_xblocks:
|
||||
<%
|
||||
ancestor_url = xblock_studio_url(ancestor, context_course)
|
||||
ancestor_url = xblock_studio_url(ancestor)
|
||||
%>
|
||||
% if ancestor_url:
|
||||
<a href="${ancestor_url}"
|
||||
@@ -83,7 +83,7 @@ main_xblock_info = {
|
||||
<section class="content-area">
|
||||
|
||||
<article class="content-primary window">
|
||||
<section class="wrapper-xblock level-page is-hidden studio-xblock-wrapper" data-locator="${xblock_locator}">
|
||||
<section class="wrapper-xblock level-page is-hidden studio-xblock-wrapper" data-locator="${xblock_locator}" data-course-key="${xblock_locator.course_key}">
|
||||
</section>
|
||||
<div class="no-container-content is-hidden">
|
||||
<p>${_("This page has no content yet.")}</p>
|
||||
|
||||
@@ -4,7 +4,7 @@ from contentstore.views.helpers import xblock_studio_url
|
||||
%>
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
<section class="wrapper-xblock xblock-type-container level-element" data-locator="${locator}">
|
||||
<section class="wrapper-xblock xblock-type-container level-element" data-locator="${xblock.location}" data-course-key="${xblock.location.course_key}">
|
||||
<header class="xblock-header">
|
||||
<div class="header-details">
|
||||
${xblock.display_name_with_default}
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
"xmodule", "coffee/src/main", "xblock/cms.runtime.v1"],
|
||||
function (TabsModel, TabsEditView) {
|
||||
var model = new TabsModel({
|
||||
id: "${course_locator}",
|
||||
explicit_url: "${course_locator.url_reverse('tabs')}"
|
||||
id: "${context_course.location}",
|
||||
explicit_url: "${reverse('contentstore.views.tabs_handler', kwargs={'course_key_string': context_course.id})}"
|
||||
});
|
||||
|
||||
new TabsEditView({
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="main-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<div class="main-column">
|
||||
<article class="subsection-body window" data-locator="${locator}">
|
||||
<article class="subsection-body window" data-locator="${locator}" data-course-key="${locator.course_key}">
|
||||
<div class="subsection-name-input">
|
||||
<label>${_("Display Name:")}</label>
|
||||
<input type="text" value="${subsection.display_name_with_default | h}" class="subsection-display-name-input" data-metadata-name="display_name"/>
|
||||
@@ -32,7 +32,7 @@
|
||||
</div>
|
||||
|
||||
<div class="sidebar">
|
||||
<div class="unit-settings window id-holder" data-locator="${locator}">
|
||||
<div class="unit-settings window id-holder" data-locator="${locator}" data-course-key="${locator.course_key}">
|
||||
<h4 class="header">${_("Subsection Settings")}</h4>
|
||||
<div class="window-contents">
|
||||
<div class="scheduled-date-input row">
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
<%!
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
import json
|
||||
%>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
% else:
|
||||
<ul class="list-actions">
|
||||
<li class="item-action">
|
||||
<a class="action action-export-git"" action-primary" href="${reverse('export_git', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}?action=push">
|
||||
<a class="action action-export-git"" action-primary" href="${reverse('export_git', kwargs=dict(course_key_string=unicode(context_course.id)))}?action=push">
|
||||
<i class="icon-download"></i>
|
||||
<span class="copy">${_("Export to Git")}</span>
|
||||
</a>
|
||||
|
||||
@@ -1 +1 @@
|
||||
<div class="xblock-editor" data-locator="<%= xblockInfo.get('id') %>"></div>
|
||||
<div class="xblock-editor" data-locator="<%= xblockInfo.get('id') %>" data-course-key="<%= xblockInfo.get('courseKey') %>"></div>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%! from student.roles import CourseInstructorRole %>
|
||||
<%! from xmodule.modulestore.django import loc_mapper %>
|
||||
<%inherit file="base.html" />
|
||||
<%def name="online_help_token()"><% return "team" %></%def>
|
||||
<%block name="title">${_("Course Team Settings")}</%block>
|
||||
@@ -61,12 +60,16 @@
|
||||
%endif
|
||||
|
||||
<ol class="user-list">
|
||||
<% new_location = loc_mapper().translate_location(context_course.location.course_id, context_course.location, False, True) %>
|
||||
% for user in staff:
|
||||
<% course_team_url = reverse(
|
||||
'contentstore.views.course_team_handler',
|
||||
kwargs={'course_key_string': unicode(context_course.id), 'email': user.email}
|
||||
)
|
||||
%>
|
||||
|
||||
<li class="user-item" data-email="${user.email}" data-url="${new_location.url_reverse('course_team/', user.email) }">
|
||||
<li class="user-item" data-email="${user.email}" data-url="${course_team_url}">
|
||||
|
||||
<% is_instuctor = CourseInstructorRole(context_course.location).has_user(user) %>
|
||||
<% is_instuctor = CourseInstructorRole(context_course.id).has_user(user) %>
|
||||
% if is_instuctor:
|
||||
<span class="wrapper-ui-badge">
|
||||
<span class="flag flag-role flag-role-admin is-hanging">
|
||||
@@ -121,7 +124,7 @@
|
||||
% endfor
|
||||
</ol>
|
||||
|
||||
<% user_is_instuctor = CourseInstructorRole(context_course.location).has_user(request.user) %>
|
||||
<% user_is_instuctor = CourseInstructorRole(context_course.id).has_user(request.user) %>
|
||||
% if user_is_instuctor and len(staff) == 1:
|
||||
<div class="notice notice-incontext notice-create has-actions">
|
||||
<div class="msg">
|
||||
@@ -164,9 +167,7 @@ require(["jquery", "underscore", "gettext", "js/views/feedback_prompt"],
|
||||
function($, _, gettext, PromptView) {
|
||||
|
||||
var staffEmails = ${json.dumps([user.email for user in staff])};
|
||||
var tplUserURL = "${loc_mapper().\
|
||||
translate_location(context_course.location.course_id, context_course.location, False, True).\
|
||||
url_reverse('course_team/', "@@EMAIL@@")}";
|
||||
var tplUserURL = "${reverse('contentstore.views.course_team_handler', kwargs={'course_key_string': unicode(context_course.id), 'email': '@@EMAIL@@'})}"
|
||||
|
||||
var unknownErrorMessage = gettext("Unknown");
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
import logging
|
||||
from util.date_utils import get_default_time_display
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.core.urlresolvers import reverse
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
from contentstore.utils import reverse_usage_url
|
||||
%>
|
||||
<%block name="title">${_("Course Outline")}</%block>
|
||||
<%block name="bodyclass">is-signedin course view-outline</%block>
|
||||
@@ -57,7 +56,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
|
||||
<h3 class="section-name">
|
||||
<form class="section-name-form">
|
||||
<input type="text" value="${_('New Section Name')}" class="new-section-name" />
|
||||
<input type="submit" class="new-section-name-save" data-parent="${parent_locator}"
|
||||
<input type="submit" class="new-section-name-save" data-parent="${context_course.location}"
|
||||
data-category="${new_section_category}" value="${_('Save')}" />
|
||||
<input type="button" class="new-section-name-cancel" value="${_('Cancel')}" />
|
||||
</form>
|
||||
@@ -77,7 +76,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
|
||||
<span class="section-name-span">${_('Add a new section name')}</span>
|
||||
<form class="section-name-form">
|
||||
<input type="text" value="${_('New Section Name')}" class="new-section-name" />
|
||||
<input type="submit" class="new-section-name-save" data-parent="${parent_locator}"
|
||||
<input type="submit" class="new-section-name-save" data-parent="${context_course.id}"
|
||||
data-category="${new_section_category}" value="${_('Save')}" />
|
||||
<input type="button" class="new-section-name-cancel" value="$(_('Cancel')}" /></h3>
|
||||
</form>
|
||||
@@ -149,16 +148,12 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
|
||||
|
||||
<div class="wrapper-dnd">
|
||||
<%
|
||||
course_locator = loc_mapper().translate_location(
|
||||
context_course.location.course_id, context_course.location, False, True
|
||||
)
|
||||
course_locator = context_course.location
|
||||
%>
|
||||
<article class="courseware-overview" data-locator="${course_locator}">
|
||||
<article class="courseware-overview" data-locator="${course_locator}" data-course-key="${course_locator.course_key}">
|
||||
% for section in sections:
|
||||
<%
|
||||
section_locator = loc_mapper().translate_location(
|
||||
context_course.location.course_id, section.location, False, True
|
||||
)
|
||||
section_locator = section.location
|
||||
%>
|
||||
<section class="courseware-section is-collapsible is-draggable" data-parent="${course_locator}"
|
||||
data-locator="${section_locator}">
|
||||
@@ -208,9 +203,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
|
||||
<ol class="sortable-subsection-list">
|
||||
% for subsection in section.get_children():
|
||||
<%
|
||||
subsection_locator = loc_mapper().translate_location(
|
||||
context_course.location.course_id, subsection.location, False, True
|
||||
)
|
||||
subsection_locator = subsection.location
|
||||
%>
|
||||
<li class="courseware-subsection collapsed id-holder is-draggable is-collapsible "
|
||||
data-parent="${section_locator}" data-locator="${subsection_locator}">
|
||||
@@ -220,7 +213,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
|
||||
<div class="section-item">
|
||||
<div class="details">
|
||||
<a href="#" data-tooltip="${_('Expand/collapse this subsection')}" class="action expand-collapse expand"><i class="icon-caret-down ui-toggle-expansion"></i><span class="sr">${_('Expand/collapse this subsection')}</span></a>
|
||||
<a href="${subsection_locator.url_reverse('subsection')}">
|
||||
<a href="${reverse_usage_url('subsection_handler', subsection_locator)}">
|
||||
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name_with_default}</span></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -118,7 +118,7 @@ require(["jquery", "jquery.cookie"], function($) {
|
||||
data: submit_data,
|
||||
headers: {'X-CSRFToken': $.cookie('csrftoken')},
|
||||
success: function(json) {
|
||||
location.href = "/course";
|
||||
location.href = "/course/";
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
json = $.parseJSON(jqXHR.responseText);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
from contentstore import utils
|
||||
%>
|
||||
|
||||
<%block name="header_extras">
|
||||
@@ -306,9 +306,9 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
|
||||
<div class="bit">
|
||||
% if context_course:
|
||||
<%
|
||||
course_team_url = course_locator.url_reverse('course_team/', '')
|
||||
grading_config_url = course_locator.url_reverse('settings/grading/')
|
||||
advanced_config_url = course_locator.url_reverse('settings/advanced/')
|
||||
course_team_url = utils.reverse_course_url('course_team_handler', context_course.id)
|
||||
grading_config_url = utils.reverse_course_url('grading_handler', context_course.id)
|
||||
advanced_config_url = utils.reverse_course_url('advanced_settings_handler', context_course.id)
|
||||
%>
|
||||
<h3 class="title-3">${_("Other Course Settings")}</h3>
|
||||
<nav class="nav-related">
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
from contentstore import utils
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
%>
|
||||
<%block name="title">${_("Advanced Settings")}</%block>
|
||||
<%block name="bodyclass">is-signedin course advanced view-settings</%block>
|
||||
@@ -90,11 +89,9 @@ require(["domReady!", "jquery", "js/models/settings/advanced", "js/views/setting
|
||||
<div class="bit">
|
||||
% if context_course:
|
||||
<%
|
||||
ctx_loc = context_course.location
|
||||
location = loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True)
|
||||
details_url = location.url_reverse('settings/details/')
|
||||
grading_url = location.url_reverse('settings/grading/')
|
||||
course_team_url = location.url_reverse('course_team/', '')
|
||||
details_url = utils.reverse_course_url('settings_handler', context_course.id)
|
||||
grading_url = utils.reverse_course_url('grading_handler', context_course.id)
|
||||
course_team_url = utils.reverse_course_url('course_team_handler', context_course.id)
|
||||
%>
|
||||
<h3 class="title-3">${_("Other Course Settings")}</h3>
|
||||
<nav class="nav-related">
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<%!
|
||||
from contentstore import utils
|
||||
from django.utils.translation import ugettext as _
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
%>
|
||||
|
||||
<%block name="header_extras">
|
||||
@@ -139,9 +138,9 @@ require(["domReady!", "jquery", "js/views/settings/grading", "js/models/settings
|
||||
<div class="bit">
|
||||
% if context_course:
|
||||
<%
|
||||
course_team_url = course_locator.url_reverse('course_team/')
|
||||
advanced_settings_url = course_locator.url_reverse('settings/advanced/')
|
||||
detailed_settings_url = course_locator.url_reverse('settings/details/')
|
||||
detailed_settings_url = utils.reverse_course_url('settings_handler', context_course.id)
|
||||
course_team_url = utils.reverse_course_url('course_team_handler', context_course.id)
|
||||
advanced_settings_url = utils.reverse_course_url('advanced_settings_handler', context_course.id)
|
||||
%>
|
||||
<h3 class="title-3">${_("Other Course Settings")}</h3>
|
||||
<nav class="nav-related">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user